Sixty Defects in Fourteen DHCP Implementations

DHCP is the first protocol a device speaks, before it has an address and before any credential exists. We tested fourteen independent implementations and uncovered 60 defects, on both the client and the server side.

DHCP runs before anything else. A device joining a network has no address, no route, no name and no credential, so it broadcasts to whoever is listening and believes the first plausible answer. There is no authentication in the protocol and nothing to authenticate with yet.

We tested fourteen independent DHCP implementations, servers, clients and relays, across both DHCPv4 and DHCPv6, and uncovered 60 defects. One of the fourteen came back clean.

Both directions matter here, and that is unusual. A server is exposed to any device that can broadcast on the link, which on a corporate network is every meeting room socket and every guest access point. A client is exposed to whichever server answers first, which on any network you do not own is whoever wants to.

The option area

Almost everything after the fixed header is a list of options, each a type byte, a length byte, and that much data. Nine of the fourteen mishandle that walk.

Product B never checks a declared option length against the buffer that remains. Product C runs the walk past the end of the receive buffer before checking the length at all. Product G parses the option area past the end of the datagram, and copies before validating. Product L skips the walker past the end of the buffer, and separately reads the message-type length and value without first checking either exists.

Product E and Product N carry the DHCPv6 versions. One has option decoders that read past the end of the datagram, the other a copy routine that bounds nothing at all, reached three separate ways: a server writing a client-controlled identifier, a rogue server against a client, and one full-size datagram against a relay.

Product L has the subtlest variant. Its guard on an address-list length has a hole when the remainder is zero, which is the one case the check was written to catch.

Lengths that size a copy

The fixed header carries a hardware address length alongside a 16-byte hardware address field. Four implementations let the declared value drive something.

Product E reads it as a signed byte and uses it as an array length. Product J lets it drive an unbounded hex conversion into a 64-byte buffer, and separately lets it run a name builder past its destination and a string copy past a 65-byte heap allocation. Product K walks the 16-byte address field off the end of the heap object that holds it.

Product C shows the same shape in a different field. Its MAC derivation reads seven bytes of the client identifier option regardless of what the option's length says, so bytes from a previous client become the hardware address recorded against a lease.

Checks that run backwards

The most striking cluster in this set is not a missing check. It is a check that does the opposite of what it was written to do.

Product G has RFC 5227 address conflict detection inverted, and the defence rate limiting inverted as well, in the same client. Product H uses its DUID comparison with inverted sense at all three call sites.

Code with no check at all fails obviously under a conforming test. Code whose check is backwards passes every test where the answer should be yes, and the failure only shows when something is genuinely wrong, which is precisely when it matters. These do not look like absent validation in review. They look like validation.

Identity taken at face value

DHCP identifies clients by values the client chose. Several implementations act on that identity without noticing it is unverified.

Product L honours a DHCPRELEASE for any lease, from any hardware address, so one datagram returns somebody else's address to the pool. Product J authenticates its replication peer by UDP source address alone. Product C selects a lease using one attacker-supplied identity and writes the ARP entry using another. Product M lets a DHCP client choose its own name in the DNS zone.

Product H turns the same looseness into a denial of service that outlives the attack: a 64-byte client DUID makes the daemon abort on its next startup, so the damage lands after a reboot rather than during the request.

State that only grows

Product A permanently consumes a delegated prefix on every SOLICIT, never reclaims an address in its range plugin, and can be ended entirely by one 85-byte SOLICIT. Product E consumes as many leases as a single datagram carries address options. Product L adds a permanent entry for every unseen hardware address. Product K leaks a packet object on every read. Product B makes its address search cost the range multiplied by the lease count, per datagram.

Product D has the version that rewards patience: relay nesting that is cheap to parse and very expensive to print, and uncapped DNS-style compression pointers inside a quadratic string builder.

Where the data goes after it is parsed

The parser is rarely the last stop, and this family shows that more clearly than most.

Product C runs a string length off the end of the packet and puts roughly 65 KB of its own receive buffer into a log line, and separately stores a client-supplied host name and renders it into its administrative web page without escaping it. Product I lets control characters and terminal escape sequences survive its option escaper. Product J builds an HTTP request line into a 256-byte global that reaches a file write.

Product L has the one that needs no commentary: every DISCOVER forks a shell on the receive thread.

Finding these with Penzzer

Select INET4 or INET6 and choose the target that matches the side you are testing. There are three on v4, a generic client, a targeted client and a server, and two on v6, plus BOOTP. The client targets matter here, because roughly a third of the findings above are reached by a hostile server answering a client that had no way to tell.

Protocol-aware fuzzing walks the message as fields: the fixed header and its hardware address length, the magic cookie, and each option as a type, a declared length and that much data, with the declared length varied against the bytes that follow it. Seventeen attack modules run alongside, ten for DHCPv4 and seven for DHCPv6:

| Module | The shape it provokes | | --- | --- | | Option TLV walk abuse | A declared option length against the buffer that remains | | Hlen and chaddr bounds | The hardware address length driving a copy | | Short datagram field reads | A header field read before it has arrived | | Unterminated sname and file fields | Fixed string fields with no terminator | | Release spoofing | A release for a lease the sender does not hold | | Rogue offer spoofing | A hostile answer to a client that cannot verify it | | Client FQDN and hostname injection | A client-chosen name reaching a name service or a page | | Reply builder field residue | Bytes from an earlier request in a new reply | | Per message resource leak | State retained after the message is answered | | Discover flood | Cost per request against a server with no cap | | Client identifier handling | A DUID length against what the option carries | | IA option count and hint abuse | Address options consumed per datagram | | Option count and request list cost | Repeated options against a single budget | | Reply and forward builder bounds | A relay or server building a reply into a fixed buffer | | Reconfigure spoofing | An unsolicited reconfigure a client should refuse | | Rogue advertise spoofing | A hostile advertise against a soliciting client | | Solicit flood | Prefix and lease consumption per solicit |

‍

What this article is, and is not

The 60 defects above are a sample of what this kind of testing surfaces, not a description of Penzzer's DHCP coverage. They are what fourteen 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. One of the fourteen here came back clean, and that is what a clean result means.

If your product gets its address from DHCP

It does, and the client is almost certainly something you inherited rather than chose.

Three questions. Does the option walk bound every declared length against the bytes actually remaining, before any copy rather than after? Is every value the client supplied as its identity treated as a claim rather than a fact, including on release and on name registration? And does anything derived from a DHCP option reach a log, a web page, a DNS zone or a shell without being escaped for where it lands?

The last one is where this family differs from most. A DHCP parser that is entirely correct still hands attacker-chosen strings to whatever is downstream of it, and on the evidence here, downstream is usually not ready.

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.