A comprehensive DNS toolkit (dns module) for performing lookups, zone transfers, and record manipulation from Python — the go-to library for DNS reconnaissance.
dnspython provides both high-level resolver helpers and low-level message construction for every common record type (A, AAAA, MX, NS, TXT, CNAME, SOA, PTR). In offensive work it powers subdomain brute-forcers, mail-security checks (SPF/DMARC), reverse-DNS mapping, and zone-transfer tests. It talks directly to resolvers or authoritative servers, giving precise control over queries during authorized footprinting.
pip install dnspythonimport dns.resolver
answers = dns.resolver.resolve("example.com", "A")
for record in answers:
print(record.to_text())
print(answers.rrset.ttl)resolve() raises rather than returning empty: NXDOMAIN when the name does not exist, NoAnswer when it exists but has no record of that type.
| API | Purpose |
|---|---|
dns.resolver.resolve(name, rdtype) |
Query a record type |
dns.resolver.Resolver() |
Custom resolver — set .nameservers, .timeout, .lifetime |
answers.rrset.ttl |
TTL of the answer set |
record.to_text() |
Record value as a string |
dns.resolver.NXDOMAIN |
Name does not exist |
dns.resolver.NoAnswer |
Name exists, no record of that type |
dns.resolver.Timeout |
Resolver did not respond |
dns.resolver.NoNameservers |
SERVFAIL from every nameserver |
dns.zone.from_xfr(dns.query.xfr(ns, domain)) |
Attempt an AXFR zone transfer |
dns.reversename.from_address(ip) |
Build a PTR lookup name |
Note
The pip package is dnspython; the import name is dns. This mismatch is a frequent source of confusion.
Resolve common record types for a domain:
import dns.resolver
domain = "example.com"
for rtype in ("A", "MX", "NS", "TXT"):
try:
answers = dns.resolver.resolve(domain, rtype)
for r in answers:
print(f"{rtype:4} {r.to_text()}")
except dns.resolver.NoAnswer:
print(f"{rtype:4} (no record)")A 93.184.216.34
NS a.iana-servers.net.
NS b.iana-servers.net.
Subdomain discovery with a wordlist (authorized target):
import dns.resolver
domain = "example.com"
wordlist = ["www", "mail", "dev", "vpn", "admin"]
for sub in wordlist:
fqdn = f"{sub}.{domain}"
try:
answers = dns.resolver.resolve(fqdn, "A")
print(f"[+] {fqdn} -> {answers[0].to_text()}")
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
pass[+] www.example.com -> 93.184.216.34
Attempt an AXFR zone transfer against a name server (misconfiguration check):
import dns.query
import dns.zone
ns = "ns1.zonetransfer.me" # known public test target
domain = "zonetransfer.me"
try:
zone = dns.zone.from_xfr(dns.query.xfr(ns, domain, timeout=5))
for name, node in list(zone.nodes.items())[:3]:
print(name.to_text(), "->", node.to_text(name))
except Exception as exc:
print("AXFR refused:", exc)@ -> @ 7200 IN SOA nsztm1.digi.ninja. robin.digi.ninja. ...
info -> info 7200 IN TXT "ZoneTransfer.me service..."
- DNS enumeration — map A/AAAA/MX/NS/TXT records to understand an organization's infrastructure and mail setup.
- Subdomain brute-forcing — resolve wordlists against a domain to discover hidden hosts and expand attack surface.
- Zone-transfer testing — check whether name servers wrongly allow AXFR, leaking the entire zone.
- Mail-security review — parse TXT records for SPF, DKIM, and DMARC policies during a posture assessment.
- Reverse DNS mapping — resolve PTR records across an in-scope IP range to name hosts.
- Installing the wrong name —
pip install dnspython, thenimport dns. - Treating
NXDOMAINandNoAnsweras the same — they are different findings: the name does not exist versus the name exists without that record type. - Not catching
Timeout— a slow or unreachable resolver raises rather than returning nothing. - Assuming the system resolver — set
Resolver().nameserversexplicitly when results must be reproducible. - Expecting AXFR to succeed — refusal is correct behaviour; success is a misconfiguration finding.
- Ignoring TTLs when caching results, producing stale data.
- Not stripping the trailing dot from
to_text()output when comparing names.
[!warning] Authorized use only Enumerate only domains you own or that are explicitly in scope for a signed engagement.
- A successful zone transfer is a real finding. AXFR to an arbitrary client exposes the entire internal naming of an organisation. Report it with clear remediation: restrict AXFR to authorised secondaries.
- Attempting AXFR is more intrusive than a normal query and is logged by the nameserver. Only do it within an authorized engagement.
- Bulk querying is noisy and rate-limited. Public resolvers throttle or block aggressive clients, which affects everyone sharing your address.
- DNS answers are untrusted input. A hostile or poisoned resolver can return arbitrary data; validate before using a result in a filesystem path, a shell command, or a connection target.
- Discovered names are sensitive — an internal hostname inventory is a target list.
- Prefer DNSSEC-validating resolvers where integrity matters, and note that plain DNS is unauthenticated and observable in transit.
- Set
dns.resolver.Resolver().timeout/lifetimeso lookups fail fast in bulk scans. - Handle
NXDOMAIN,NoAnswer, andTimeoutexplicitly; they are normal in enumeration loops. - Point at a specific resolver (
resolver.nameservers = ["1.1.1.1"]) for consistent results. - Rate-limit brute-force queries to avoid tripping resolver protections.
- Only test zone transfers against name servers you are authorized to assess.
- [[Scapy]] — for hand-crafted DNS packets and lower-level control
- [[requests]] — combine DNS recon with HTTP probing of discovered hosts
- [[Readme|Python for Security Professionals]] — course home