DNS Enumeration
DNS is the only directory service on the network that answers without credentials, and on an Active Directory network it is also the inventory: every server, workstation and service is registered somewhere in a zone.
DNS offers six distinct surfaces, and they differ in what they cost you rather than in what they return. One table, because the order you try them in is a judgement call the reader should make from cost and exposure:
| Surface | What it reveals | Credential | What it leaves behind |
|---|---|---|---|
| A / PTR records | Hostnames and the naming convention you can extrapolate from | None | One query per address; a /24 loop looks like a scan |
| SRV records | DCs, global catalogues, KDCs, PDC emulator, AD sites | None | Standard queries; indistinguishable from a workstation |
| Zone transfer | Every record in the zone, if the server permits it | None | A logged refusal, or an obvious full-zone pull |
| ADIDNS read (LDAP) | The same records, plus tombstoned names nobody cleaned up | Any domain account | One LDAP search — quieter than bruteforcing |
| ADIDNS write | Whether Authenticated Users can create records — a MITM path |
Any domain account | A directory change (5137/5136) that survives you |
| Cache snooping | Which external services staff have been resolving | None | A normal query with the recursion bit cleared |
Basic DNS Queries
Start by finding out which server answers, because on an AD network the answer is usually a domain
controller and that tells you where the zone lives. Then query by record type with intent: A and AAAA
map names to hosts, MX names the mail path, TXT carries SPF and DKIM and occasionally notes an
administrator left behind, NS names the authoritative servers, and SOA names the primary and the
serial. ANY is the least useful of the set on a modern server — RFC 8482 lets a resolver
answer it with a minimal response instead of every record, so a thin ANY reply is normal rather than a
sign that the zone is empty.
Record Types
# A records (IPv4)
dig @DC_IP corp.local A
nslookup -type=A corp.local DC_IP
# AAAA records (IPv6)
dig @DC_IP corp.local AAAA
# MX records
dig @DC_IP corp.local MX
# TXT records (SPF, DKIM, etc)
dig @DC_IP corp.local TXT
# NS records
dig @DC_IP corp.local NS
# SOA record
dig @DC_IP corp.local SOA
# All records
dig @DC_IP corp.local ANYActive Directory SRV Records
Tip
These lookups are the cheapest DC inventory available and they need no credential. Each service has
its own record: _ldap._tcp.dc._msdcs lists the domain controllers,
_gc._tcp the global catalogues, _kerberos._tcp the KDCs and
_ldap._tcp.pdc._msdcs the PDC emulator — the DC that holds the lockout authority and the
one to be careful with during a spray. Site-specific records
(_ldap._tcp.<site>._sites) tell you the site topology, which is how you learn that
a second datacentre exists before anyone mentions it. The SRV target and port fields are
both useful: the port confirms the service, the target names the host.
# Domain Controllers
dig @DC_IP _ldap._tcp.dc._msdcs.corp.local SRV
nslookup -type=SRV _ldap._tcp.dc._msdcs.corp.local DC_IP
# Global Catalogs
dig @DC_IP _gc._tcp.corp.local SRV
dig @DC_IP _ldap._tcp.gc._msdcs.corp.local SRV
# Kerberos servers
dig @DC_IP _kerberos._tcp.corp.local SRV
dig @DC_IP _kerberos._udp.corp.local SRV
# KDC (Key Distribution Center)
dig @DC_IP _kdc._tcp.corp.local SRV
# PDC Emulator
dig @DC_IP _ldap._tcp.pdc._msdcs.corp.local SRV
# Sites
dig @DC_IP _ldap._tcp.sitename._sites.corp.local SRVZone Transfer
Warning
A successful transfer is unmistakable: dig prints the SOA first, then every record in the
zone, and you have the complete inventory in one response. A refused transfer is equally clear —
; Transfer failed. — and it is the normal answer on a modern AD-integrated zone, where
zone transfers are restricted to named servers by default. Do not loop on it: one attempt per
nameserver is enough to record the configuration. If it succeeds, say so plainly in the report, because
a zone that will hand its full contents to any host on the network is a significant exposure.
# Attempt zone transfer
dig @DC_IP corp.local AXFR
# Using host
host -l corp.local DC_IP
# Using nslookup
nslookup
> server DC_IP
> set type=AXFR
> corp.local
# DNSrecon zone transfer
dnsrecon -d corp.local -n DC_IP -t axfr
# Fierce
fierce --domain corp.local --dns-servers DC_IPReverse DNS Lookup
Reverse records are the cheapest way to turn a list of IP addresses into hostnames, and hostnames carry
the naming convention. Once you can see srv-sql-01, wks-fin-114 and
prt-floor3, you can generate the names of hosts you have not found yet — which is worth
more than the lookups themselves. Each lookup is one query against the DNS server, so a /24 loop is 254
queries from one source address and is visible as a scan; if you have a capture or a cache dump, read
the PTR records from there instead. A missing PTR usually means a statically addressed device or a
host that was never registered, not that nothing is there.
# Single IP reverse lookup
dig @DC_IP -x 10.0.0.1
# Reverse zone transfer
dig @DC_IP 0.0.10.in-addr.arpa AXFR
# Bulk reverse lookup (bash)
for ip in $(seq 1 254); do
dig @DC_IP -x 10.0.0.$ip +short
done
# DNSrecon reverse bruteforce
dnsrecon -d corp.local -n DC_IP -r 10.0.0.0/24
# Using nmap
nmap -sL 10.0.0.0/24 --dns-servers DC_IPDNS Bruteforce
Brute forcing names is the noisy fallback for when nothing else gives you the zone. The trap is the
wildcard record: if the zone answers every name, every candidate looks like a hit and the results are
worthless. Check before you start by querying a name that cannot exist — if that resolves, the zone has
a wildcard and you must discard all results that share its address. Also keep the wordlist specific to
the environment: internal naming follows the company's convention (dc01,
srv-*, *-prod), and a generic subdomain list finds almost nothing inside a
corporate zone.
# DNSrecon bruteforce
dnsrecon -d corp.local -n DC_IP -D wordlist.txt -t brt
# Fierce
fierce --domain corp.local --dns-servers DC_IP --subdomain-file subdomains.txt
# Gobuster DNS
gobuster dns -d corp.local -r DC_IP -w wordlist.txt
# dnscan
dnscan.py -d corp.local -w wordlist.txt -r DC_IP
# Common internal subdomains
mail, webmail, owa, autodiscover
vpn, remote, gateway
dc, dc01, dc02, ad
sql, db, database
web, www, intranet
file, files, share
print, printer
backup, bakADIDNS Enumeration
Tip
ADIDNS is the same zone data, but stored as objects in the directory partitions rather than in a text
file, which means two things: it can be read over LDAP with any account, and it can be written to by
whatever the object's ACL permits. Read the zone list first to confirm the partition layout —
DomainDnsZones for domain zones, ForestDnsZones for forest-wide ones — and
then read the dnsNode objects for the records. This route is quieter than a DNS bruteforce
because it is one LDAP search rather than thousands of queries.
Enumerate DNS Records via LDAP
# List DNS zones
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "CN=MicrosoftDNS,DC=DomainDnsZones,DC=corp,DC=local" "(objectClass=dnsZone)"
# List DNS records in a zone
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "DC=corp.local,CN=MicrosoftDNS,DC=DomainDnsZones,DC=corp,DC=local" "(objectClass=dnsNode)" \
name dnsRecord
# Forest DNS zones
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "CN=MicrosoftDNS,DC=ForestDnsZones,DC=corp,DC=local" "(objectClass=dnsZone)"adidnsdump
adidnsdump exists because the LDAP query above cannot resolve every record: some
dnsNode objects have no readable dnsRecord attribute for your account, so the
tool falls back to querying the DNS server for the names it found. Its most useful output is the set of
records it could not resolve — those are the entries whose host is gone, which is exactly the
list of stale or tombstoned names that keeps working as an injection target. --include-tombstoned
adds the deleted records, which is where you find the old server names nobody removed.
# Dump all DNS records
adidnsdump -u corp.local\user -p password ldap://DC_IP
# Output: records.csv with all ADIDNS records
# Also resolves 'YOURNAME' tombstoned records
# Dump specific zone
adidnsdump -u corp.local\user -p password ldap://DC_IP --zone corp.local
# Include deleted/tombstoned records
adidnsdump -u corp.local\user -p password ldap://DC_IP --include-tombstonedDNSUpdate (Record Injection)
Danger
Authenticated Users create child objects, so
the technical control is open and the boundary has to come from the rules of engagement: name the record
you will add, use a name that does not exist rather than shadowing a live host, and delete it with the
same tool before you leave. A wildcard record catches every failed lookup in the zone, so it is a
separately agreed demonstration with a stated rollback — not a default step.
The default ACL on an AD-integrated zone lets Authenticated Users create child objects,
which is why a domain user can add an A record without any special delegation. That is a write action:
it changes the directory, it survives your engagement, and it can break name resolution for real users.
Get it into the rules of engagement before you test it, use a name that does not exist rather than
shadowing a live host, and record the exact record you added so it can be removed — with
--action delete on the same tool — before you leave. A wildcard record is the most
disruptive variant: it catches every failed lookup in the zone, so it is a demonstration to be agreed
in advance, not a default step.
# Check permissions with Powermad
Import-Module Powermad
Get-ADIDNSZone -TargetDC DC_IP
# Add A record
Invoke-DNSUpdate -DNSType A -DNSName attacker -DNSData 10.0.0.100
# Add wildcard record (catch-all for MITM)
Invoke-DNSUpdate -DNSType A -DNSName '*' -DNSData 10.0.0.100
# Using nsupdate (create update.txt first)
# Contents: server DC_IP, zone corp.local, update add attacker.corp.local 86400 A 10.0.0.100, send
nsupdate update.txt
# Krbrelayx - ADIDNS poisoning
dnstool.py -u 'corp.local\user' -p password --action add --record 'attacker' --data 10.0.0.100 --type A DC_IPDNS Cache Snooping
A recursive resolver that answers non-recursive queries is leaking its cache: the answer means someone
on the network recently asked for that name, and the cached record is the evidence.
+norecurse tells dig not to ask upstream, so a response can only come from
the cache. This is the one DNS technique that produces information about user behaviour rather than
infrastructure — which external services staff use, and therefore what a phishing pretext should
reference. If every +norecurse query returns nothing, cache snooping is disabled and the
question is closed.
# Check if DNS server allows cache snooping
# Non-recursive query for cached records
dig @DC_IP www.google.com A +norecurse
# If it returns results, cache snooping is possible
# This reveals what domains users have been accessing
# Check common domains
for domain in github.com dropbox.com pastebin.com; do
echo "Checking $domain"
dig @DC_IP $domain A +norecurse +short
donePowerShell DNS Enumeration
From a Windows host the client-side commands need no privilege: Get-DnsClientCache is the
local resolver cache and shows which internal names this machine has recently resolved, which is a free
map of the services it uses. Resolve-DnsName is the client query tool and works for SRV
records as well as A records. The Get-DnsServer* cmdlets are different — they require DNS
administrator rights on the server, so treat them as a post-escalation step rather than as enumeration
you can plan around.
# Get DNS servers
Get-DnsClientServerAddress
# DNS cache
Get-DnsClientCache
# Resolve-DnsName
Resolve-DnsName -Name corp.local -Type ALL
Resolve-DnsName -Name _ldap._tcp.dc._msdcs.corp.local -Type SRV
# Get all DNS zones (requires DNS admin)
Get-DnsServerZone -ComputerName DC
# Get DNS records (requires DNS admin)
Get-DnsServerResourceRecord -ZoneName corp.local -ComputerName DC
# Test zone transfer
$records = @()
$ns = Resolve-DnsName -Name corp.local -Type NS
$records = Resolve-DnsName -Name corp.local -Type AXFR -Server $ns[0].NameHost -DnsOnlyAutomated Tools
DNSrecon
dnsrecon is worth using for the modes you would otherwise script badly: -t axfr
tries a transfer against every nameserver, -t snoop tests cache snooping against a domain
list, and -a runs the standard set in one pass with one output file. Use it to produce the
evidence artefact rather than as your primary enumeration — its default wordlists are generic
internet subdomains and will miss an internal naming convention entirely.
# Standard enumeration
dnsrecon -d corp.local -n DC_IP
# All enumeration types
dnsrecon -d corp.local -n DC_IP -a
# Cache snooping
dnsrecon -d corp.local -n DC_IP -t snoop -D domains.txt
# Output to XML
dnsrecon -d corp.local -n DC_IP --xml output.xmlnxc
NetExec's DNS coverage is thin — it is not a resolver and does not walk zones. Where it earns its place
in this phase is the ADIDNS route: an LDAP bind lets you query the dnsNode objects, and
the -M dns module is at best a summary of what the LDAP queries above already return. If
you need the record set, use adidnsdump or plain ldapsearch; if you need to
know whether an account can write to a zone, use dnstool.py.
Common Discovery Targets
Use this list to interpret what you find rather than as a wordlist to spray. A name that resolves and
looks infrastructural is worth following: adfs and sts mean federation, which
is a path into the cloud tenant; ca and pki mean a certificate authority,
which is the target of the ADCS attacks; sccm and wsus mean software
distribution with high privileges on every client; backup and veeam mean a
credential store that usually holds domain-wide access. Those four categories are worth more than
another hundred workstations.
# High-value DNS records to find
_msdcs.corp.local # AD forest root
_sites.corp.local # AD sites
_tcp.corp.local # Service records
gc._msdcs.corp.local # Global Catalogs
_kerberos._tcp.corp.local # Kerberos servers
_ldap._tcp.corp.local # LDAP servers
# Common hostnames
dc01, dc02, dc1, dc2 # Domain Controllers
sql, sql01, database # SQL servers
exchange, mail, cas # Exchange servers
adfs, sts # Federation services
ca, pki, cert # Certificate Authority
sccm, wsus # Management servers
backup, veeam, dpm # Backup serversWhat the Defender Sees
DNS is usually the least-logged part of an AD network, which makes this phase hard to detect after the fact and easy to detect in aggregate:
- Query volume: the DNS server's debug log is off by default. Where it is on, a single source issuing hundreds of PTR or A queries per minute is the signature of a sweep — the reason bruteforcing is louder than the LDAP route.
- Zone transfer: an AXFR request against a zone that does not permit it is a logged refusal, and one that succeeds is a configuration finding the blue team can act on immediately.
- ADIDNS writes: a new
dnsNodeobject is a directory change — 5137 for creation and 5136 for modification, if the DNS partition is audited. A record pointing at an address outside the corporate ranges is the high-confidence indicator. - Cache snooping: indistinguishable from normal resolution in most telemetry, because it is a normal query with the recursion bit cleared. Treat it as an authorisation question rather than a detection problem.
Evidence Collection
DNS output is only useful if it names the server that answered and the query that was asked, because the same name can resolve differently from a different resolver.
Resolver identity:
The server you queried for every record set (@DC_IP), and whether it is the client's own resolver or a forwarder. A record set from a third-party resolver is not evidence about the internal zone.
Zone transfer result: The command, the nameserver, and either the transferred zone or the exact refusal. "AXFR blocked" without the nameserver does not tell the client which server to fix.
SRV inventory: DCs, GCs, KDCs and PDC emulator with their targets and ports, plus any site-specific records that reveal a second datacentre.
ADIDNS dump:
The records.csv from adidnsdump, including tombstoned names, with the zone and the account used.
Any record you wrote: Name, type, value, zone, timestamp and the delete command you ran — or the note that authorisation was refused. This is the row that proves cleanup.
False Positive Identification
- A wildcard makes everything resolve: If a name that cannot exist resolves, the zone has a wildcard and every bruteforce hit is meaningless. Discard all candidates sharing the wildcard address before reporting a discovered host.
-
A thin <code>ANY</code> response:
RFC 8482 lets a resolver answer
ANYwith a minimal response. An empty ANY result means the query type is unsupported, not that the zone is empty. - A missing PTR: Statically addressed devices and never-registered hosts have no reverse record. A missing PTR is a documentation gap, not evidence the host is absent.
- A tombstoned name is not a live target: A stale record that resolves is not a host — it is an injection target. Do not report it as a discovered server.
Testing Checklist
DNS is where a wildcard silently invalidates a whole bruteforce run, so the checks are ordered to catch that first.
Confirm the resolver and the zone
- ☐ Every record set names the resolver it was queried from (
@DC_IP), so a forwarder's answer is not mistaken for the zone's. - ☐ The
_ldap._tcp.dc._msdcs,_kerberos._tcpand_gc._tcpSRV records list every DC and site the client expects. - ☐ AXFR is attempted against each nameserver, and the result is either the transferred zone or the exact refusal.
- ☐ A random-name test in the zone is run before any bruteforce, and a wildcard answer is recorded as such.
Confirm the results mean something
- ☐ Bruteforced names resolving to the wildcard address are discarded rather than reported as discovered hosts.
- ☐
adidnsdumpproduces its CSV, and tombstoned names in it are labelled as injection targets, not servers. - ☐ Any record you created resolves (or fails) as expected and is deleted; name, type, value and delete command are in the notes.
- ☐ Missing PTR records are reported as documentation gaps; a missing reverse record is not an absent host.
Remediation & Defense
The DNS hardening here is small, well documented, and rarely applied.
Restrict the zone
- • Restrict zone transfers to named secondaries, and verify from a non-authoritative host
- • Disable cache snooping on resolvers that do not need to answer non-recursive queries
- • Remove stale and tombstoned records on a schedule, and scan for wildcard records
Control ADIDNS writes
- • Change the default ACL so <code>Authenticated Users</code> cannot create arbitrary records
- • Require secure dynamic updates, and alert on new <code>dnsNode</code> objects with external addresses
Detect the enumeration
- • Turn on DNS server debug logging to a central collector, at least on the DCs
- • Alert on a single source issuing hundreds of PTR queries or any AXFR attempt
- • Audit 5137/5136 on the DNS partitions so record creation is visible