The Protocol Everyone Depends On and Nobody Secures#

Almost every connection on the internet starts with a DNS lookup, which makes DNS both an attractive target and one of the most valuable sources of security telemetry you have. The protocol was designed in the 1980s for a network of trusted hosts: answers arrive over UDP, unauthenticated and in plaintext. Every security feature since has been layered on top. This lesson assumes you know how resolution works; if not, start with the DNS lesson.

Cache Poisoning#

A recursive resolver caches answers so it does not have to ask the authoritative servers every time. In cache poisoning, an attacker gets a forged answer into that cache, and the resolver hands the malicious IP to every client that asks until the TTL expires.

The classic off-path attack races the real answer. The resolver sends a query over UDP; the attacker floods it with forged responses, hoping one arrives first with the right transaction ID and destination port. Originally, only the 16-bit ID had to be guessed. In 2008, Dan Kaminsky showed that asking for random, non-existent subdomains (a1.bank.example, a2.bank.example, and so on) gives the attacker unlimited fresh races, and that a forged referral in one of them could take over the whole domain. The emergency fix was source port randomisation, which adds roughly another 16 bits for the attacker to guess. Resolvers also use techniques such as 0x20 encoding (random letter case in the query name, which the answer must echo exactly).

Randomisation makes forging answers hard. It does not make it impossible, and it does nothing if the attacker is on the path or controls a server. Only cryptography solves that.

DNSSEC: Authenticating Answers#

DNSSEC signs DNS data so a resolver can prove an answer came from the zone owner, unmodified. It adds new record types:

  • DNSKEY: the zone's public keys. Usually a key-signing key (KSK) signs the key set, and a zone-signing key (ZSK) signs everything else.
  • RRSIG: a signature over each set of records.
  • DS: published in the parent zone, a hash of the child's KSK. This is the link in the chain of trust: the root vouches for .com, .com vouches for example.com, and so on down.
  • NSEC / NSEC3: signed proof that a name does not exist, so an attacker cannot forge "no such domain" either.

A validating resolver checks the whole chain from the root's trust anchor. If validation fails, it returns SERVFAIL rather than a possibly forged answer. You can watch this happen with dig:

dig +dnssec example.com A        # look for the "ad" flag and RRSIG records
dig dnssec-failed.org A          # deliberately broken test domain: status SERVFAIL
dig +cd dnssec-failed.org A      # checking disabled: the unvalidated answer appears
delv example.com A               # BIND's validating tool prints "; fully validated"

The ad (authenticated data) flag in the header means the resolver you queried validated the answer. Note that this tells you nothing if the path between you and the resolver is itself untrusted, which is why validation belongs on a resolver you control or reach over an encrypted channel.

DNSSEC's limitations are operational. Many domains do not sign their zones, key rollovers and DS updates at the registrar are easy to get wrong, and a mistake makes your domain fail for everyone behind a validating resolver. That is an outage, not a warning. NSEC records allow zone walking (listing every name in a zone); NSEC3 hashes the names, but weak hashes can still be cracked offline. DNSSEC also encrypts nothing. It guarantees integrity, not privacy.

Encrypted DNS: DoT and DoH#

Plain DNS on UDP port 53 is visible to anyone on the path, and on-path devices can tamper with it. Two transports encrypt the hop between client and resolver:

DNS over TLS (DoT) DNS over HTTPS (DoH)
Port 853 443, shared with web traffic
Easy for networks to identify Yes, a dedicated port Hard: looks like any HTTPS connection
Typical client OS resolvers, Android Private DNS Browsers, applications, some OSes
Enterprise impact Easy to allow or block by policy Can bypass corporate DNS filtering unless managed on the client

Here is DoH from Python using a public resolver's JSON interface. do=true asks the resolver to perform DNSSEC processing:

import json
import urllib.parse
import urllib.request

def doh_lookup(name, rtype="A", endpoint="https://cloudflare-dns.com/dns-query"):
    url = f"{endpoint}?{urllib.parse.urlencode({'name': name, 'type': rtype, 'do': 'true'})}"
    req = urllib.request.Request(url, headers={"Accept": "application/dns-json"})
    with urllib.request.urlopen(req, timeout=5) as resp:
        data = json.load(resp)
    answers = [a["data"] for a in data.get("Answer", []) if a["type"] != 46]  # skip RRSIGs
    return data["Status"], data.get("AD", False), answers

for name in ["example.com", "dnssec-failed.org"]:
    status, validated, answers = doh_lookup(name)
    print(f"{name:<20} status={status} AD={validated} answers={answers}")
example.com          status=0 AD=True answers=['104.20.23.154', '172.66.147.243']
dnssec-failed.org    status=2 AD=False answers=[]

Status 0 is NOERROR and 2 is SERVFAIL, so the broken signature is rejected rather than served. Encrypted DNS moves trust rather than eliminating it: the DoH provider now sees every query. It also only protects the client-to-resolver hop, while the resolver's queries to authoritative servers usually remain plaintext.

DNS Tunnelling and Exfiltration#

Firewalls almost always let DNS out, and a query for anything.attacker-domain.example travels, via your own resolver, to a server the attacker controls. That makes DNS a covert channel: data can be encoded in query names (<base32 chunk>.exfil.example) and commands returned in TXT or other records. Malware uses this for command-and-control and data theft.

The traffic has recognisable features: long, random-looking subdomains, a very large number of unique subdomains under one domain, unusual record types such as TXT or NULL in bulk, and steady query rates from a single host. A starting point for detecting it in your own resolver logs:

import math
from collections import Counter, defaultdict

def entropy(s):
    counts = Counter(s)
    return -sum(n / len(s) * math.log2(n / len(s)) for n in counts.values())

def base_domain(qname):
    return ".".join(qname.rstrip(".").split(".")[-2:])   # naive; use a public-suffix list in production

def suspicious(queries, min_len=30, min_entropy=3.5, min_unique=50):
    per_domain = defaultdict(set)
    flagged = []
    for q in queries:
        labels = q.rstrip(".").split(".")
        sub = "".join(labels[:-2])
        per_domain[base_domain(q)].add(sub)
        if len(sub) >= min_len and entropy(sub) >= min_entropy:
            flagged.append((q, len(sub), round(entropy(sub), 2)))
    noisy = {d: len(s) for d, s in per_domain.items() if len(s) >= min_unique}
    return flagged, noisy

log = [
    "www.example.com",
    "mail.google.com",
    "cdn.jsdelivr.net",
    "a3f9c2e17b0d44e8a1c6b2f09e7d3a51.x7k2q9.data-sync.net",
    "mzxw6ytboi3tmmrtgq2dkmzsgm4tsnrsha3dgnbv.data-sync.net",
] + [f"{i:04x}chunk{i*7919 % 65521:x}q.data-sync.net" for i in range(60)]

flagged, noisy = suspicious(log)
for q, length, h in flagged:
    print(f"long/high-entropy: {q}  (len={length}, H={h})")
print("many unique subdomains:", noisy)
long/high-entropy: a3f9c2e17b0d44e8a1c6b2f09e7d3a51.x7k2q9.data-sync.net  (len=38, H=4.13)
long/high-entropy: mzxw6ytboi3tmmrtgq2dkmzsgm4tsnrsha3dgnbv.data-sync.net  (len=40, H=4.29)
many unique subdomains: {'data-sync.net': 62}

The two checks catch different styles: long encoded labels, and exfiltration split into many short chunks. Expect false positives. CDNs, anti-virus lookups, and some telemetry services legitimately use long hashed subdomains, so maintain an allowlist and tune thresholds against a week of your own traffic before alerting. Feed the results into your SIEM alongside host data.

Other DNS Attacks and Their Fixes#

  • Subdomain takeover. shop.example.com is a CNAME to a cloud resource (a storage bucket, an app platform hostname) that has since been deleted. An attacker claims that resource name and now serves content on your subdomain. Fix: remove DNS records as part of decommissioning, and regularly scan your zones for CNAMEs pointing at unclaimed targets.
  • Registrar and account hijacking. Whoever controls your registrar account controls your nameservers. Use strong MFA, restrict access, and enable registry lock for critical domains.
  • Open resolvers and amplification. A resolver that answers anyone on the internet can be used to reflect large responses at a victim with spoofed source addresses. Restrict recursion to your own networks and use response rate limiting on authoritative servers; see DDoS.
  • Random-subdomain floods. Queries for millions of non-existent names bypass caches and hammer authoritative servers. Mitigations include aggressive negative caching and rate limiting at resolvers.
  • Zone transfer leaks. Test that dig axfr example.com @ns1.example.com against your own servers is refused, unless the request comes from your secondary servers.
  • Mis-issued certificates. A CAA record lists which certificate authorities may issue for your domain, closing off one path to fraudulent certificates for the TLS you rely on.

DNS as a Security Control#

DNS also works well as a defence:

  • DNS filtering and Response Policy Zones (RPZ) block known-malicious and newly registered domains before any connection is made.
  • Sinkholing points a malicious domain at a server you control, so infected machines reveal themselves by connecting to it.
  • Query logging is some of the cheapest and most useful breach telemetry available: which host looked up which domain, when. Keep it long enough to search back through an incident.

Hardening Checklist#

  • Validate DNSSEC on your recursive resolvers, and sign zones you control once you have monitoring for signature expiry and a tested key-rollover process.
  • Force clients to use your resolvers: block outbound port 53 and 853 to anything else, and manage browser DoH settings on corporate devices.
  • Log queries centrally and alert on tunnelling patterns and newly registered domains.
  • Lock down registrar accounts, publish CAA records, and audit zones for dangling CNAMEs.
  • Never run an open resolver; restrict recursion and zone transfers.