16 min read

Enumeration

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

record-types.sh
# 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 ANY

Active Directory SRV Records

Tip

AD uses standard SRV records to advertise services. These reveal DCs, GCs, and Kerberos servers.

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.

srv-records.sh
# 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 SRV

Zone Transfer

Warning

Zone transfers (AXFR) expose the entire DNS zone. Test carefully as this may trigger alerts.

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.

zone-transfer.sh
# 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_IP

Reverse 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.

reverse-lookup.sh
# 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_IP

DNS 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.

dns-bruteforce.sh
# 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, bak

ADIDNS Enumeration

Tip

Active Directory Integrated DNS stores records in AD. Any authenticated user can query (and potentially add) records.

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

adidns-ldap.sh
# 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.

adidnsdump.sh
# 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-tombstoned

DNSUpdate (Record Injection)

Danger

This is the only write action in the phase, and it changes the client's directory. The default ACL on an AD-integrated zone lets 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.

adidns-injection.ps1
# 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_IP

DNS 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.

cache-snooping.sh
# 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
done

PowerShell 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.

powershell-dns.ps1
# 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 -DnsOnly

Automated 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.

dnsrecon.sh
# 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.xml

nxc

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.

dns-targets.txt
# 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 servers

What 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 dnsNode object 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 ANY with 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._tcp and _gc._tcp SRV 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.
  • ☐ adidnsdump produces 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