Forty-Nine Defects in Fifteen DNS Implementations

DNS is the one protocol where receiving an answer is more dangerous than being asked a question. We tested fifteen independent implementations and uncovered 49 defects, including several that accept an answer from anybody who sends one.

Almost nothing on a network works without DNS, and almost nothing speaks only DNS. It is embedded in SIP stacks, in ad blockers, in embedded TCP/IP libraries, in gateway appliances, and in a long tail of resolvers written by people who needed one and wrote one.

We tested fifteen independent DNS implementations and uncovered 49 defects. Two of the fifteen came back clean.

The spread is worth noting before the findings. These are not fifteen DNS servers. Two of them are DNS parsers living inside a SIP stack, one is inside an embedded TCP/IP library, one is an ad blocker on a microcontroller, and several are resolvers or forwarders rather than authoritative servers. DNS parsing shows up in products that are not DNS products, and it is usually the part nobody thought to fuzz.

The answer is the attack surface

Most protocols put the risk on the side that receives requests. DNS inverts it. A resolver sends one question into the internet and then accepts an answer, and the answer is much larger, much more structured, and comes from somewhere it cannot verify.

Four of the implementations accept that answer far too readily.

Product C opens a forwarding socket that is never connected to the upstream server, so it accepts a response from any source address, straight into a 512-byte stack buffer. Product I accepts upstream replies from anyone, with any transaction ID at all. Product D accepts an upstream reply on the transaction ID alone, and separately answers DNS responses that arrive unsolicited, to anybody who sends them.

Nothing about those requires a race, a flood, or an off-path guessing attack. They are the cache-poisoning preconditions that the last thirty years of DNS hardening exists to prevent, present by omission.

Compression pointers, again

The pointer that lets a DNS message avoid repeating a name is the single most productive defect source in this set, as it was when we looked at multicast DNS. Six of the fifteen mishandle it.

Product A walks a compression chain with no bound on its length. Product C has no loop detection at all: the cursor is reassigned to an arbitrary offset in the message with no jump counter, no visited set and no iteration cap. Product H and Product L both fail to terminate on a pointer that refers to itself, a two-byte input.

Product E and Product O show the other shape. Both defend correctly against loops, and both re-walk the pointer chain from scratch for every name in the message with no memoisation, so cost grows with the square of the names rather than with the size of the datagram.

Product L has the sharpest version. The same walk that fails to terminate also wraps its counter and writes out of bounds.

The length field that describes a record

Every resource record carries an RDLENGTH saying how much data follows. Five implementations trust it, or skip it.

Product I dereferences NULL on an A record declaring RDLENGTH zero. Product N accepts the same zero length and produces a record that is simultaneously valid and broken. Product J reads A and AAAA record data without checking RDLENGTH at all, and whatever bytes are there become the address it returns to the caller, then reads the fixed fields of SRV and NAPTR records before its bounds check rather than after. Product K bounds RDLENGTH against the destination buffer but never against the source, and Product H walks records without bounding the walk by the datagram.

A resolver that returns an address assembled from out-of-bounds bytes is not crashing. It is answering, confidently, with something it read from memory that was not a DNS record.

Counts that disagree with the message

Product M is the clearest case of header fields and body disagreeing. Its answer count is byte-swapped twice, so every reply it sends claims 256 answers. The QNAME walk has no bound of any kind, a label index is not bounded against the array it indexes, and a buffer declared as 100 entries is read as a pointer that aliases the query type and class fields that follow it.

Product I returns blocked replies that carry records the header disclaims, and leaks the entire answer section of a reply it was supposed to block.

Product D limits name length to 127 rather than the 255 the protocol allows, and does not check label lengths at all. Its name matching compares a prefix rather than a label, so a lookup for a name that merely starts with a blocked string is treated as that name.

Work that grows faster than the input

Product A merges record sets with a linear scan per record, so a message with many records costs quadratic time. Product O turns one 65 KB response into gigabytes of resident memory. Product I has an unbounded job queue. Product E caches entries that are never given an expiration, so they are never freed. Product F lets repeated EDNS options inflate a byte budget that is only ever emitted once.

Product E also produces the single most disproportionate result in the set: one datagram inflating into a roughly 29 MB log line, with twenty of them retained.

One that leaves the protocol entirely

Product E passes raw DNS label bytes to an administrative web page without escaping them. The bytes arrive in a DNS query, from anyone who can send one, and are rendered as HTML to whoever opens the admin interface.

That is the finding to remember if you build something that parses DNS as a side activity. The parser is not the boundary. Whatever consumes the parsed name is.

Finding these with Penzzer

Select INET4 or INET6, choose the DNS target that matches what the product is, and point it at the device under test. There are three: a generic client, a targeted client, and a server. The client targets matter here, because more than half of the findings above are on the receiving side.

Protocol-aware fuzzing walks the fields of a DNS message as fields: the header counts, the opcode and flags, each label of a QNAME and the length that introduces it, the record type and class, the TTL, and the RDLENGTH set against the record data that follows it. Eight DNS attack modules run alongside, and they line up with the groups above:

| Module | The shape it provokes | | --- | --- | | Compression pointer loops | Self-referential and unbounded pointer chains | | QNAME label walk bounds | Label and name lengths against the datagram | | RDLENGTH and record bounds | A record length against the bytes that follow it | | Section count mismatch | Header counts against the records actually present | | Cross query buffer reuse | State left over from the previous request | | Per query allocation growth | Memory kept after the answer is sent | | EDNS option accounting | Repeated options against a single budget | | Oversized datagram stall | A message larger than the reader expects |

‍

What this article is, and is not

The 49 defects above are a sample of what this kind of testing surfaces, not a description of Penzzer's DNS coverage. They are what fifteen specific implementations happened to get wrong.

The attack library spans defect classes well beyond what this article shows: memory safety, information disclosure, resource exhaustion, availability, hangs and algorithmic complexity, input validation, protocol state logic, type and format handling, and error handling. Across the 107 protocols with a protocol-aware target, a run also generates its test cases from the structure of the protocol itself, walking each field through the boundaries where parsers break.

The same limit applies in reverse. What a test looks for is a property of the test, not a claim about your device. Two of the fifteen here came back clean, and that is what a clean result means.

If your product resolves names

It almost certainly does, and the resolver is probably not something you chose.

Three questions. Does your resolver bind its forwarding socket to the upstream server, so an answer from anywhere else is dropped before parsing? Does the name decompressor bound its total work per message rather than per name? And is every RDLENGTH compared against the bytes actually present before any record data is read?

The two clean implementations here show that none of this is unreasonable to get right. The other thirteen show how easy it is to ship a DNS parser that was only ever tested against answers from a server that meant well.

Other Post
Uncover Hidden Vulnerabilities

Identify security flaws before attackers do, automatically and at scale with Penzzer's intelligent fuzzing engine.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.