CyberNugget

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.com turns 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.8 are 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.com in 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).

Deep dive 1

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.sh for %.example.com lists 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 destroy removes 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 NXDOMAIN or 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
Deep dive 2

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.