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 uses IPWhois 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

0
Subscribe to my newsletter

Read articles from Ektoras Kalantzis directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ektoras Kalantzis
Ektoras Kalantzis