RedRoom Recon Category : DNS Enumeration


What is DNS?
DNS (Domain Name System) acts like the internet’s phonebook. It translates human-readable domain names, like youtube.com
or google.com
, into machine-readable IP addresses that computers use to communicate.
So what is DNS Enumeration?
DNS Enumeration is the process of extracting DNS records from a target domain to gather valuable information. This can include discovering linked subdomains, mail servers, name servers, and other DNS entries. Essentially, it helps map the domain’s structure and associated services.
DNS Enumeration Main Handler – What’s Happening Here?
import ipaddress
from .methods_recon.dns_resolve.resolve_lookup import Lookup
from Essentials.utils import handle_scan_output,print_zone_transfer_results,print_whois_results,print_asn_results
import traceback
DNS_RECORDS = {
'A': (Lookup.forward_lookup, True),
'AAAA': (Lookup.forward_lookup_aaaa, True),
'NS': (Lookup.get_ns_records, True),
'MX': (Lookup.get_mx_records, True),
'CNAME': (Lookup.get_cname, False),
'TXT': (Lookup.get_txt_records, True),
'SOA': (Lookup.get_soa_record, False),
'SRV': (Lookup.get_srv_records, True)
}
MODES = {
'min': ['A', 'AAAA', 'NS'],
'average': ['A', 'AAAA', 'MX', 'NS', 'CNAME'],
'full': list(DNS_RECORDS.keys()),
}
def parse_ips(ip_range):
try:
net = ipaddress.ip_network(ip_range, strict=False)
return [str(ip) for ip in net.hosts()]
except ValueError:
return [ip_range]
def run(args):
if not args.domain:
print("[!] Error: No domain specified")
return
domain = args.domain
ips = parse_ips(args.range)
if args.whois:
if domain:
results = Lookup.domain_whois_server_lookup(domain)
elif ips:
results = Lookup.ips_whois_server_lookup(ips)
print_whois_results(results)
elif args.asn:
results = Lookup.ip_asn_lookup(ips)
print_asn_results(results)
handle_scan_output(dns_results, scantype="dnsenum", filename=args.output, ftype=args.format)
if args.zonetransfer:
try:
results = Lookup.attempt_zone_transfer(domain)
except Exception:
print("[!] Unexpected error during scan:")
traceback.print_exc()
if results:
for ns, recs in results.items():
print(f" - Zone transfer successful from {ns}:")
for r in recs:
print(f" - {r}")
else:
print(" - Zone transfer unsuccessful or denied")
return
print_zone_transfer_results(results)
handle_scan_output(results, scantype="dnsenum", filename=args.output, ftype=args.format)
return
if args.min:
records_to_query = MODES['min']
elif args.full:
records_to_query = MODES['full']
else:
records_to_query = MODES['average']
dns_results = {}
for record in records_to_query:
lookup_func, is_list = DNS_RECORDS[record]
print(f"\n[+] {record} Record(s) for {domain}")
try:
results = lookup_func(domain)
dns_results[record] = results
except Exception as e:
print(f" [!] Error querying {record} records: {e}")
dns_results[record] = None
continue
if results is None or (is_list and len(results) == 0):
print(" - None")
dns_results[record] = None
continue
if not results:
print(" - None")
continue
if is_list:
if record == 'MX':
for pref, exch in results:
print(f" - {exch} (priority {pref})")
else:
for item in results:
print(f" - {item}")
else:
if record == 'SOA':
print(f" - MNAME: {results.get('mname')}")
print(f" - RNAME: {results.get('rname')}")
print(f" - Serial: {results.get('serial')}")
else:
print(f" - {results}")
if 'A' in records_to_query:
ips = Lookup.forward_lookup(domain)
ptr_results = {}
for ip in ips:
rev = Lookup.reverse_lookup(ip)
ptr_results[ip] = rev if rev else "N/A"
print(f" - {ip} => {rev if rev else 'N/A'}")
dns_results["PTR"] = ptr_results
handle_scan_output(dns_results, scantype="dnsenum", filename=args.output, ftype=args.format)
This is the brain of the DNS Enumeration tool. Here’s what’s going on step by step:
DNS Records Setup – At the top, we define what records we want to query (A, AAAA, MX, NS, etc.) and organize them into three modes:
min – Only the essentials (A, AAAA, NS).
average – Adds MX and CNAME on top.
full – Pulls everything we can.
IP Parsing – If the user provides an IP range,
parse_ips()
validates it and turns it into a list of IPs we can actually use.Handler Logic (
run()
) – This is where the magic starts:Checks if the user gave us a domain (because, well, DNS without a domain makes no sense).
Handles WHOIS lookups for the domain or IPs if the
--whois
flag is used.Handles ASN lookups if the
--asn
flag is used.Handles Zone Transfer attempts (this is the part that tries to pull the entire zone file if misconfigured).
Main Enumeration – After the special cases, it picks the correct mode (min, average, or full) and loops through all the DNS records we want:
Calls the right lookup function for each record type.
Prints the results in a readable format.
Stores everything in
dns_results
so we can later export it.
Reverse Lookup – If we have A records, it runs a PTR lookup on each IP to see if we can resolve hostnames back from IPs.
Output Handling – At the end, it saves everything in the output file using
handle_scan_output()
so you can store the results for later.
Inside Resolve_Lookup.py
from ipwhois import IPWhois
import dns.resolver
import dns.query
import dns.zone
import dns.exception
import socket
import re
class Lookup:
@staticmethod
def ip_asn_lookup(ip):
try:
obj = IPWhois(ip)
result = obj.lookup_rdap()
return {
"ip": ip,
"asn": result.get("asn"),
"asn_description": result.get("asn_description"),
"asn_country_code": result.get("asn_country_code"),
"network_name": result.get("network", {}).get("name"),
"network_cidr": result.get("network", {}).get("cidr"),
}
except Exception as e:
print(f"[-] ASN lookup failed for {ip}: {e}")
return None
@staticmethod
def ips_whois_server_lookup(ips):
try:
for ip in ips:
obj = IPWhois(ip)
result = obj.lookup_rdap()
return {
"asn": result.get("asn"),
"asn_description": result.get("asn_description"),
"asn_country_code": result.get("asn_country_code"),
"network_name": result.get("network", {}).get("name"),
"network_range": result.get("network", {}).get("cidr"),
"org": result.get("network", {}).get("org"),
"country": result.get("network", {}).get("country"),
}
except Exception as e:
return {"error": str(e)}
@staticmethod
def domain_whois_server_lookup(domain):
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("whois.iana.org", 43))
s.send((domain + "\r\n").encode())
response = b""
while True:
data = s.recv(4096)
if not data:
break
response += data
s.close()
match = re.search(r"refer:\s*(\S+)", response.decode())
if not match:
return {"error": "Could not find WHOIS server"}
whois_server = match.group(1)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((whois_server, 43))
s.send((domain + "\r\n").encode())
response = b""
while True:
data = s.recv(4096)
if not data:
break
response += data
s.close()
decoded = response.decode(errors="ignore")
return {"whois_raw": decoded}
except Exception as e:
return {"error": str(e)}
@staticmethod
def forward_lookup(domain):
try:
return [ip.address for ip in dns.resolver.resolve(domain, 'A')]
except Exception:
return []
@staticmethod
def forward_lookup_aaaa(domain):
try:
return [ip.address for ip in dns.resolver.resolve(domain, 'AAAA')]
except Exception:
return []
@staticmethod
def get_srv_records(domain):
try:
answers = dns.resolver.resolve(domain, 'SRV')
return [(r.priority, r.weight, r.port, r.target.to_text()) for r in answers]
except Exception:
return []
@staticmethod
def attempt_zone_transfer(domain):
results = {}
try:
ns_records = [ns.to_text() for ns in dns.resolver.resolve(domain, 'NS')]
except Exception:
return results
for ns in ns_records:
try:
ns_ip_list = Lookup.forward_lookup(ns)
for ns_ip in ns_ip_list:
zone = dns.zone.from_xfr(dns.query.xfr(ns_ip, domain, timeout=5))
if zone is None:
continue
results[ns] = []
for name, node in zone.nodes.items():
results[ns].append(name.to_text())
except dns.exception.DNSException:
continue
return results
@staticmethod
def get_ns_records(domain):
try:
return [ns.to_text() for ns in dns.resolver.resolve(domain, 'NS')]
except Exception:
return []
@staticmethod
def get_mx_records(domain):
try:
return sorted([(r.preference, r.exchange.to_text()) for r in dns.resolver.resolve(domain, 'MX')])
except Exception:
return []
@staticmethod
def get_txt_records(domain):
try:
return [r.to_text().strip('"') for r in dns.resolver.resolve(domain, 'TXT')]
except Exception:
return []
@staticmethod
def get_cname(domain):
try:
return dns.resolver.resolve(domain, 'CNAME')[0].to_text()
except Exception:
return None
@staticmethod
def get_soa_record(domain):
try:
r = dns.resolver.resolve(domain, 'SOA')[0]
return {
'mname': r.mname.to_text(),
'rname': r.rname.to_text(),
'serial': r.serial
}
except Exception:
return {}
@staticmethod
def reverse_lookup(ip):
try:
return socket.gethostbyaddr(ip)[0]
except Exception:
return None
So this file is basically the toolbox of DNS enumeration, and I structured it so that each function handles one specific record or lookup type. Let’s break it down:
ASN and WHOIS Records
ip_asn_lookup(ip)
Takes an IP and usesIPWhois
to pull ASN (Autonomous System Number) details like ASN name, country code, and network info. Super useful for figuring out who owns that IP and where it belongs.ips_whois_server_lookup(ips)
Same idea but for a list of IPs. It returns ASN details and network info for the given IP range.domain_whois_server_lookup(domain)
Connects to WHOIS servers manually via a socket (raw style), grabs the WHOIS server from IANA, then queries the correct server to return raw WHOIS data for the domain. Basically, this is your domain registration info.
Forward Lookups
forward_lookup(domain)
Gets all A records (IPv4 addresses) for a domain.forward_lookup_aaaa(domain)
Gets all AAAA records (IPv6 addresses).
DNS Record Fetching
get_ns_records(domain)
Pulls all NS (Name Server) records for the domain.get_mx_records(domain)
Gets Mail Exchange (MX) records sorted by priority.get_txt_records(domain)
Fetches TXT records, often used for SPF, DKIM, or domain verification.get_cname(domain)
Finds CNAME (Canonical Name) for the domain (if any).get_soa_record(domain)
Gets the SOA (Start of Authority) record with details like primary NS, admin email, and serial number.get_srv_records(domain)
Fetches SRV records, usually for VoIP or chat services.
Zone Transfer Attempt
attempt_zone_transfer(domain)
Tries to pull the entire DNS zone from the domain’s name servers if they allow it (big info leak if misconfigured).
Reverse Lookup
reverse_lookup(ip)
Takes an IP and does a PTR lookup to get the hostname tied to it.
Conclusion
DNS Enumeration is a core part of recon because it helps uncover the hidden structure behind a domain. By pulling DNS records, WHOIS info, and even attempting zone transfers, you can find subdomains, mail servers, name servers, and ownership details that might otherwise stay off your radar. Whether you’re mapping an attack surface or doing legit security testing, DNS enumeration is a goldmine for intel and in this article we unraveled how we can achieve it.
Next up : Sub-Domain Enumeration
Subscribe to my newsletter
Read articles from Ektoras Kalantzis directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
