L08 · DNS and How It Gets Attacked
Module 2: Networking for Defenders · ~20 min · Lesson 8 of 71
🎯 What you'll learn
- How a name like
api.example.comturns into an IP address, step by step - How cache poisoning works, and why source-port randomization and DNSSEC make it harder
- What DNSSEC protects (integrity and authenticity) and what it doesn't (confidentiality)
- How DNS tunneling smuggles data out, and what it looks like in logs
- How a forgotten DNS record lets someone else take over your subdomain
🔥 Why this matters
In April 2018, attackers used a BGP routing hijack to send some traffic meant for Amazon Route 53's
DNS servers to a server they controlled. For about two hours, that server gave forged answers for
myetherwallet.com, sending users to a look-alike site. Around 215 ether (about $150,000 at the time)
was stolen. Route 53 itself wasn't hacked. The attackers only faked the answer to "where is this site?".
Every call a service makes (a gateway such as Kong to an upstream, a pod to MongoDB, Airflow to AWS) starts with a DNS lookup. If the lookup lies, everything after it goes to the wrong place.
1. How a name gets resolved
When your laptop calls https://api.example.com, it doesn't know the IP address. It asks for help,
and the question can travel through several servers:
laptop / pod recursive resolver authoritative
(stub resolver) (does the legwork) servers
│ │ │
│ 1 A? api.example.com │ │
│─────────────────────────▶│ 2 ask root ──────────▶│ .
│ │◀── "ask .com" ────────│
│ │ 3 ask .com TLD ──────▶│ com.
│ │◀── "ask example's NS"─│
│ │ 4 ask example NS ────▶│ example.com.
│ │◀── A 203.0.113.10 ────│
│◀── 5 A 203.0.113.10 ─────│ │
│ (TTL 300) │ caches it 300 s │
There are two very different jobs here:
- A recursive resolver does the legwork for clients: it asks other servers until it has an
answer, then caches it. A company's internal DNS servers, a home router, and
8.8.8.8are recursive resolvers. - An authoritative server holds the real records for a zone (a domain and its subdomains) and answers only for it. It doesn't look anything up for you.
The stub resolver is the small client in your OS or container. It asks the recursive resolver and trusts the reply.
In the example stack: a pod in EKS asks CoreDNS (the cluster DNS service, usually reached as
kube-dns). CoreDNS answers cluster names like policy-svc.default.svc.cluster.local itself and
forwards everything else to the Route 53 Resolver, the recursive resolver AWS runs in every VPC.
Separately, a Route 53 public hosted zone is the authoritative server for a company's
public domains. Same product name, two different jobs.
2. TTLs and caching
Every DNS record carries a TTL (time to live): how many seconds a resolver may cache it before asking again. Caching is why DNS is fast. Most lookups never leave your resolver.
Caching also cuts the other way. A resolver that caches a wrong answer hands it to every client that asks, for the whole TTL. That's what makes the next attack worth doing.
3. Cache poisoning
Classic DNS runs over UDP, which has no handshake (L07). The resolver sends a question and accepts the first answer that looks like it matches. In classic DNS, "looks like it matches" means the same question, from the right server's IP, to the right port, with the same transaction ID, a 16-bit number the resolver picks per query.
Cache poisoning is getting a resolver to accept and cache a forged answer. The attacker doesn't need to see the traffic. They race the real server by flooding the resolver with guesses:
attacker resolver real ns.bank.example
│ 1 "A? x1.bank.example"│ │
│──────────────────────▶│ 2 query, ID=?, port=? ──▶│
│ 3 flood of forged │ │
│ answers, guessing │ │
│ ID (and port) │ │
│═════════════════════▶ │ │
│ │◀── 4 real answer ────────│
│ │ (if it wins the race) │
│ │ │
If a forgery lands first with the right ID and port,
the resolver caches it, and every client gets the lie.
In 2008, Dan Kaminsky showed how to make this practical. Asking for random, nonexistent names
(x1.bank.example, x2.bank.example, ...) means there's never a cached answer, so the attacker gets
unlimited fresh races. Each forged answer also includes a fake "the name server for bank.example is
mine" record. Win once, and the attacker owns the whole domain in that resolver's cache. With only
16 bits of ID to guess, that could take seconds to minutes.
The defenses, in layers:
- Source-port randomization (RFC 5452): the resolver sends each query from a random UDP port, so the attacker must guess the port and the ID. That multiplies the guesses needed by up to tens of thousands. It's a probability defense, and later research (SAD DNS, 2020) found side channels for guessing the port. NAT devices that rewrite ports can also undo it.
- DNSSEC: the zone owner signs the records, and a validating resolver checks the signatures. A forged answer has no valid signature, so it gets rejected no matter how the race goes.
⚠️ What DNSSEC does and doesn't protect
This is the part people mix up, so it's worth being precise, using L01's terms:
| Property (L01) | Question | DNSSEC? |
|---|---|---|
| Integrity | Was the answer changed? | ✅ Yes: a changed record fails validation |
| Authenticity | Did it really come from the zone owner? | ✅ Yes: only the owner's key can sign |
| Confidentiality | Can anyone read my query or the answer? | ❌ No: everything is still plaintext |
| Availability | Will I get an answer? | ❌ No, and broken signatures can even cause outages |
DNSSEC is a signature, not an envelope: anyone on the path can still see what you looked up. RFC 4033 says outright that DNSSEC doesn't provide confidentiality. Hiding queries takes an encrypted connection to your resolver (DoT or DoH, the second deep dive).
4. DNS tunneling: data out through the lookup
Here's a trick that works even from a locked-down pod with no internet access. The attacker owns a
domain, exfil.example, and runs its authoritative server. Malware inside your network encodes data
into the name it looks up:
pod (no internet egress)
│ A? MFRGG4DFMVZXI2LO.chunk0001.exfil.example
▼
CoreDNS ──▶ Route 53 Resolver ──▶ root ─▶ .example ─▶
attacker's ns
(logs the label,
decodes the data)
◀─────── answer (TXT can carry commands back) ──────
Your own resolver does the delivery; the pod never connects out directly. This is DNS tunneling: carrying arbitrary data inside DNS queries and answers. Each label holds at most 63 characters, so data leaks slowly but steadily.
What it looks like in logs (Route 53 Resolver query logging, simplified, made-up values):
10:02:11 10.0.42.17 A MFRGG4DFMVZXI2LO.chunk0001.exfil.example
10:02:11 10.0.42.17 A ON2HE2LOM4QHE33P.chunk0002.exfil.example
10:02:12 10.0.42.17 TXT cmd.exfil.example
10:02:12 10.0.42.17 A NB2HI4DTHIXS6ZLY.chunk0003.exfil.example
The red flags: long random-looking labels, hundreds of unique names under one unknown domain, lots of TXT queries, all from one source IP. Normal traffic repeats the same few names.
Controls: let workloads use only your resolver (block outbound port 53 elsewhere), turn on resolver query logging, and use Route 53 Resolver DNS Firewall or GuardDuty, which has findings for DNS-based exfiltration. These only see queries that go through the VPC resolver, though.
5. Subdomain takeover
The last attack needs no network tricks. It exploits a record you forgot. A CNAME record says "this name is really that other name". A dangling record points at a cloud resource that no longer exists. If anyone can create a resource with that name, they now serve content on your domain. That's a subdomain takeover.
🧪 Worked example: the deleted statements bucket
Your team hosted a static help page from S3 under statements.example.com. In Route 53:
statements.example.com. CNAME
statements.example.com.s3-website-us-east-1.amazonaws.com.
The project ends. Someone deletes the bucket to save money. Nobody touches DNS.
1. The record still resolves, to nothing.
$ curl -s http://statements.example.com/ | grep -o 'Code: [A-Za-z]*'
Code: NoSuchBucket
That's the warning sign. S3 bucket names are global, and website hosting needs the bucket name to match the host name.
2. An attacker creates a bucket named statements.example.com in us-east-1 in their own AWS
account and uploads a "Verify your account" page. Your DNS now sends your customers to it.
3. Why it's worse than a defaced page: it's your real domain, so users and email filters trust
it. Cookies set for .example.com are sent to it. If a Kong CORS config allows *.example.com, the
attacker's page can call your APIs from a logged-in customer's browser.
The fix, and the habit that prevents it:
- Remove or change the DNS record first, then delete the resource. Put both in the same Terraform or CloudFormation stack so they're created and destroyed together.
- Never allow-list
*.example.comin CORS or redirect rules; list the exact hosts. - Regularly check your zones for records that point at missing resources (first deep dive).
🛠 Try it (optional, ~5 min)
dig ships with macOS (this was written with DiG 9.10.6). Run:
dig +trace example.com
+trace makes dig do the recursion itself, from the root down, so you see steps 2 to 4 from the
diagram. On a home network you'll get four blocks: root (.), .com (gtld-servers.net),
example.com's name servers, and the final A records.
On a corporate network it may stop after the first block, like this (trimmed):
. 475334 IN NS a.root-servers.net.
. 475334 IN RRSIG NS 8 0 518400 ...
;; Received 1109 bytes from 192.0.2.53#53(192.0.2.53)
;; connection timed out; no servers could be reached
That's not a bug. The corporate resolver answered the first question, but direct DNS to the root servers is blocked: section 4's anti-tunneling control, working on you. Walk the chain through your resolver instead:
$ dig com NS +short | head -1
d.gtld-servers.net.
$ dig example.com NS +short
hera.ns.cloudflare.com.
elliott.ns.cloudflare.com.
$ dig example.com A +dnssec +noall +answer
example.com. 183 IN A 104.20.23.154
example.com. 183 IN A 172.66.147.243
example.com. 183 IN RRSIG A 13 2 300 20261107125946 ...
What to look for: the second column is the remaining TTL. Run the last command again a few
seconds later and it counts down, because you're reading your resolver's cache. The RRSIG line is
example.com's DNSSEC signature (algorithm 13, ECDSA P-256; original TTL 300). It's a signature, sent
in plaintext: integrity and authenticity, not confidentiality.
🧠 Key takeaways
- Recursive resolvers do the lookups and cache; authoritative servers hold the real records.
- A poisoned cache serves a lie to every client for the whole TTL.
- Source-port randomization makes forging harder; DNSSEC makes forgeries detectable.
- DNSSEC gives integrity and authenticity, not confidentiality: queries are still readable.
- DNS tunneling uses your own resolver to carry data out; watch for long, unique names.
- Delete the DNS record before the cloud resource, or someone else may claim your subdomain.
📖 Words to know
| Term | Meaning |
|---|---|
| Stub resolver | The DNS client in an OS or container that asks a recursive resolver |
| Recursive resolver | A server that looks names up on clients' behalf and caches answers |
| Authoritative server | A server that holds and answers for a zone's real records |
| TTL | How many seconds a resolver may cache a record |
| Transaction ID | A 16-bit number that matches a DNS answer to its question |
| Cache poisoning | Getting a resolver to cache a forged answer |
| Source-port randomization | Sending each query from a random port so forgers must guess it too |
| DNSSEC | Signatures on DNS records that prove they're unchanged and from the zone owner |
| DNS tunneling | Hiding data in DNS queries and answers to get it past network controls |
| CNAME | A record that says one name is an alias for another |
| Dangling record | A DNS record pointing at a resource that no longer exists |
| Subdomain takeover | Claiming the resource a dangling record points at, to serve content on someone else's domain |
⏭ Next time
L09: HTTP under the microscope, where you'll take apart a real request and see which parts an attacker controls (spoiler: almost all of it).
Subdomain takeover: finding your own dangling records
The kinds of dangling records
The S3 example is the classic one, but anything that points outside your control can dangle:
| Record | Points at | Dangles when |
|---|---|---|
| CNAME | S3 website, CloudFront, a SaaS app (help desk, status page), another cloud's app service | The bucket, distribution, or SaaS account is deleted |
| A | An Elastic IP or other cloud IP | The IP is released back to the provider's pool, and someone else may get it |
| NS | A delegated zone, e.g. dev.example.com to another Route 53 hosted zone |
The delegated hosted zone is deleted |
| MX | A mail provider | The mail account is closed, so someone may receive your mail |
NS dangling is the worst case. If someone can recreate the delegated zone on a provider that lets them, they control every name under it, not just one. Whether that's possible depends on the provider. Treat any NS record pointing at a zone you no longer own as urgent.
A safe audit workflow
Only check zones and records your team owns. Everything here reads your own data.
1 export ──▶ 2 filter ──▶ 3 resolve ──▶ 4 fingerprint ──▶ 5 fix
records CNAME/A/NS does target what does the delete
per zone to outside still exist? target say? record
1. Export the records from a hosted zone you own (read-only API call):
aws route53 list-resource-record-sets \
--hosted-zone-id Z0EXAMPLE12345 \
--query "ResourceRecordSets[?Type=='CNAME']" \
--output json > cnames.json
2. Filter for targets outside your control: *.amazonaws.com, *.cloudfront.net, SaaS
domains. Internal-only targets matter less.
3. Resolve each target. A CNAME target that returns NXDOMAIN (no such name) is a strong
signal. Some services keep resolving even when the resource is gone, so step 4 is needed too:
$ dig +short legacy-docs.example.com
legacy-docs.example.com.s3-website-us-east-1.amazonaws.com.
s3-website-us-east-1.amazonaws.com.
52.218.0.17
It resolves, because the S3 website endpoint always exists. The DNS layer looks healthy.
4. Fingerprint the response. Ask the HTTP layer what's really there:
$ curl -s -o /dev/null -w '%{http_code}\n' \
http://legacy-docs.example.com/
404
$ curl -s http://legacy-docs.example.com/ | grep -m1 Code
<li>Code: NoSuchBucket</li>
Common fingerprints: S3 says NoSuchBucket; many SaaS apps show a "no such app" or "this site is
not configured" page. Open-source scanners keep lists of these, but a hand check on your own records
is often enough.
5. Fix it: delete or update the record, then find out how it happened.
Finding records you forgot you had
Not every subdomain is in the zone you're looking at. Old teams, other accounts, other DNS providers. Two public, read-only sources help:
- Certificate transparency logs (L11): every public TLS certificate is logged. Searching
crt.shfor%.example.comlists subdomains someone once got a certificate for. - Your own IaC repos: search GitHub for your domain in Terraform files and Kubernetes ingress
manifests (
host:lines).
Preventing it
- Lifecycle together: the DNS record lives in the same IaC stack as the resource, so
destroyremoves both. - Order of operations: in a manual decommission runbook, DNS comes off first.
- Prefer Route 53 alias records to AWS resources (CloudFront, load balancers) where you can, and keep Elastic IPs you still reference in DNS. Aliases reduce some dangling cases, but don't assume they remove all of them.
- Schedule the audit: a weekly job (an Airflow DAG works) that runs steps 1 to 4 and alerts on
NXDOMAINor known fingerprints. Cloud security tools such as Prowler also ship dangling-record checks.
🧠 Extra takeaways
- CNAME, A, NS, and MX records can all dangle; NS dangling can hand over a whole subtree.
- "It resolves" doesn't mean "it's ours": check the HTTP response too.
- Certificate transparency and IaC repos reveal subdomains that zone exports miss.
- Tie DNS records to the resource's lifecycle, and audit on a schedule.
| Term | Meaning |
|---|---|
| NXDOMAIN | The DNS answer meaning "this name does not exist" |
| Alias record | A Route 53 record type that points at an AWS resource instead of a name |
| Fingerprint | A response pattern showing a target resource is unclaimed |
| Certificate transparency | Public logs of issued TLS certificates, searchable on crt.sh |
DoH, DoT and DNSSEC: what each one actually protects
Three tools, three different jobs
People often say "secure DNS" as if it's one thing. It's three, and they protect different hops and different properties. Here's a lookup with each hop labeled:
laptop ──hop A──▶ recursive ──hop B──▶ authoritative
(stub) resolver servers (signer)
DoT / DoH : encrypt hop A only. Resolver sees everything.
DNSSEC : signs the records themselves, checked by a
validating resolver. Readable on every hop.
- DoT (DNS over TLS, RFC 7858) wraps DNS in a TLS connection, normally on port 853.
- DoH (DNS over HTTPS, RFC 8484) sends DNS queries as HTTPS requests on port 443, so they look like ordinary web traffic.
- DNSSEC doesn't touch the connection. It adds signatures to the data, so a validator can check them no matter which hop or cache the answer came through.
The property table
Using L01's properties again:
| Confidentiality | Integrity | Authenticity of the data | |
|---|---|---|---|
| DoT / DoH | ✅ hop A only | ✅ hop A only | ❌ only "this is my resolver", not "this is the zone owner's record" |
| DNSSEC | ❌ none | ✅ end to end | ✅ signed by the zone owner's key |
The gaps line up neatly. DoT and DoH stop the café Wi-Fi from reading or changing your queries, but if the resolver itself has a poisoned cache, they deliver the poison over a perfectly encrypted channel. DNSSEC catches the poison, but anyone on the path can still read which names you looked up.
🧪 Worked example: three attackers
You're on hotel Wi-Fi, calling api.example.com, a DNSSEC-signed zone.
1. Someone on the Wi-Fi reads your queries to learn which internal tools you use. Confidentiality. DoT or DoH stops them. DNSSEC does nothing, since the signed answer is plaintext.
2. Someone on the Wi-Fi forges answers to send you to their server. Integrity and authenticity. DoT or DoH stops them on hop A, because they can't inject into the TLS session. DNSSEC also stops them, but only if validation happens on your side of the attacker. If your laptop just trusts the resolver's "validated" flag (the AD bit) over plain UDP, an attacker on hop A can forge that flag too. That's why validation on a trusted resolver plus an encrypted hop A is the strong pairing.
3. The resolver's cache was poisoned by a Kaminsky-style race on hop B. Integrity. DoT and DoH don't help: the lie is already inside the resolver. A validating resolver with DNSSEC rejects the forged answer before it's cached, so it never gets there.
And for all three, TLS on the final HTTPS connection (L10) is your last line: a forged IP still
can't present a valid certificate for api.example.com unless the attacker can also get one
issued. In the 2018 MyEtherWallet attack, the fake site reportedly used a certificate browsers
didn't trust, and the users who lost money clicked through the warning.
Why your company may block DoH
DoH is great for privacy at a café, but awkward for an enterprise. If a browser or a piece of
malware sends DNS straight to a public DoH server over port 443, it bypasses the company resolver,
the DNS Firewall, the query logs, and GuardDuty's DNS findings. That's the tunneling blind spot from
the main lesson, encrypted. Many companies disable browser DoH by policy or point it at their own
resolver. Firefox checks a "canary" domain, use-application-dns.net, and turns off its default DoH
if the network's resolver says it doesn't exist.
In AWS
- Signing: Route 53 can DNSSEC-sign public hosted zones. The signing key is a KMS asymmetric key (ECC_NIST_P256) that AWS's docs say must be in us-east-1. Signing also means adding a DS record at the parent zone, and a mistake here can make the whole domain fail to resolve for validating resolvers. That's DNSSEC's availability risk; roll it out carefully.
- Validating: Route 53 Resolver can validate DNSSEC for a VPC, so pods behind CoreDNS get validated answers for signed public names. Per AWS's docs, if the Resolver forwards to another resolver, that one has to validate instead.
🧠 Extra takeaways
- DoT and DoH encrypt the hop to your resolver: confidentiality and integrity for that hop only.
- DNSSEC signs the data: integrity and authenticity end to end, no confidentiality.
- Neither replaces TLS certificate checks on the real connection.
- DoH can bypass enterprise DNS controls, which is why companies often manage or block it.
| Term | Meaning |
|---|---|
| DoT | DNS over TLS: DNS inside a TLS connection, usually port 853 |
| DoH | DNS over HTTPS: DNS queries sent as HTTPS requests on port 443 |
| Validating resolver | A resolver that checks DNSSEC signatures and rejects bad answers |
| AD bit | A flag a resolver sets to say "I validated this answer with DNSSEC" |
| DS record | A record in the parent zone that links it to the child zone's DNSSEC key |
The quiz for this lesson and the Tests section need JavaScript.