How a zero-value GetBlockHeaders request underflows to serve go-ethereum's entire header chain (CVE-2024-32972)
A go-ethereum (geth) peer that sends an eth-protocol GetBlockHeaders request with amount
(the wire's count field) set to 0 triggers an integer underflow in the header-serving path:
count-1 wraps from 0 to UINT64_MAX, bypassing the node's maxHeadersServe cap and causing
it to serve headers from the requested block all the way back to genesis. In versions before
1.13.15 this drives the responding node to build an oversized in-memory response — a
memory-exhaustion denial of service (CVE-2024-32972, GHSA-4xc9-8hmq-j652, CVSS 7.5 High). This is
a publicly disclosed go-ethereum vulnerability, reported by DongHan Kim via the Ethereum bug
bounty program and fixed upstream in 1.13.15 — NullRabbit did not discover it. NullRabbit
reproduced the wire-level attack request against a self-owned lab node and shipped it as a
labelled detection-training bundle; we did not independently measure the resulting memory growth.
Why this matters
Any go-ethereum execution-layer node with P2P networking enabled — full nodes, RPC-serving
infrastructure, and the execution-client half of a post-merge validator pair — accepts inbound
eth protocol connections from any peer that completes the standard devp2p Hello + eth Status
handshake. No authentication, elevated privilege, or user interaction is required (CVSS vector
AV:N/AC:L/PR:N/UI:N): the request is a normal-shaped protocol message with one field set to a
value (0) that a legitimate client has no reason to send.
Because the trigger is a single field in a single request rather than a volumetric flood, the
attack does not need sustained traffic to reach the vulnerable code path — one connection,
one malformed GetBlockHeaders message, is enough to start the oversized serving loop described
below. The GHSA states plainly that "no workarounds have been made public" for versions prior to
the fix; the only documented remedy is upgrading past 1.13.15.
How the attack works
- Entry point. The
ethprotocol'sGetBlockHeadersrequest (devp2p message, eth code0x03), sent over a standard RLPx connection after a completed devp2pHelloandethStatushandshake. Remote, unauthenticated, reachable by any connecting peer. - Preconditions. A target running go-ethereum before v1.13.15 with P2P networking reachable (standard geth default: TCP/UDP 30303). No prior state or special peer standing is needed beyond completing the handshake.
- Processing path. The handler resolves the request through
BlockChain.GetHeadersFrom(number, count uint64)(core/blockchain_reader.go), which is called withcount-1as its count argument (per the GHSA:chain.GetHeadersFrom(num+count-1, count-1)). When the attacker setsamount(count) to0, the subtractioncount-1underflows the unsigned 64-bit type, wrapping from0to18,446,744,073,709,551,615(UINT64_MAX). That oversized count bypasses the node'smaxHeadersServelimit, and the read range extends from the requested origin back to the genesis block. - Resource / trust boundary crossed. Server-side memory. The fix (go-ethereum PR #29534,
merged for v1.13.15) confirms the code-level gap directly:
core/rawdb/accessors_chain.go'sReadHeaderRangepreviously calleddb.AncientRange(ChainFreezerHeaderTable, i+1-count, count, 0)— the trailing0is the response's byte-size ceiling, i.e. none. v1.13.15 changes that argument to2*1024*1024(a 2 MB sanity cap). Before the fix, a single peer-supplied integer controlled how much of the freezer/ancient store the node would read into memory for one response, with no independent size bound. - Resulting behaviour. The responding node accumulates an oversized set of headers in memory while serving the request. Per the GHSA/CVSS, the consequence is that "a vulnerable node can be made to consume very large amounts of memory" — an availability/DoS effect. This is not memory corruption, not remote code execution, and not a consensus fault.
Affected systems
- Chain. Ethereum (execution layer, devp2p/
ethprotocol P2P surface). - Implementation. go-ethereum (geth). No other execution-client implementation is asserted here — this brief is scoped to the geth codebase named in the GHSA.
- Component / method.
ethprotocolGetBlockHeadersrequest handler ->BlockChain.GetHeadersFrom->core/rawdb.ReadHeaderRange/AncientRange. - Affected versions. go-ethereum prior to 1.13.15 (per the GHSA and the NVD record).
- Fixed version. 1.13.15 and onwards, via go-ethereum PR #29534 (
ReadHeaderRange'sAncientRangecall gains a 2 MB /2*1024*1024-byte response-size cap). - Severity. CVSS 3.1 base score 7.5 (High) — vector
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H(NVD); CWE-400 (Uncontrolled Resource Consumption), also per the NVD record. The GHSA itself rates the advisory "High" and lists no CWE. Published 6 May 2024. - Credit. "This issue was disclosed responsibly by DongHan Kim via the Ethereum bug bounty program" (GHSA).
- NullRabbit's reproduction target. The lab reproduction runs against a self-owned,
privately-provisioned geth node reachable over devp2p/RLPx (
target_env: privnet-geth-devp2p). Per the reproducer's own documented design, the malicious request is emitted and captured as wire traffic regardless of whether the target build is patched — a patched node simply disconnects rather than serving the oversized range. NullRabbit's bundles do not pin, or make any claim about, a specific vulnerable-vs-patched geth build; the affected/fixed version boundary above is the GHSA's, not an independent NullRabbit finding.
Evidence
- Publicly documented (primary source). CVE-2024-32972 / GHSA-4xc9-8hmq-j652: mechanism
(
GetBlockHeadersamount=0->count-1underflow toUINT64_MAX->maxHeadersServebypass -> serve-to-genesis -> memory exhaustion), affected versions (< 1.13.15), fixed version (1.13.15, PR #29534), CVSS 7.5 High / CWE-400 (NVD), credit to DongHan Kim via the Ethereum bug bounty program, and the statement that no workaround exists pre-upgrade. - Independently reproduced by NullRabbit (lab). A hand-rolled RLPx/ECIES client
(
chains/ethereum/lab/drivers/known_class_geth.py) completes a genuine devp2pHello+ethStatushandshake against a live geth node, then emits the exact malicious shape: devp2p message0x13(ethGetBlockHeaders) with payload[reqID, [origin, amount=0, skip=0, reverse=1]]. The resulting wire traffic — handshake plus the malformed request — was captured as 12 pcap-only attack bundles across four traffic postures (distributed: 8 workers, ~224-248 connections;saturating: 16 workers, ~215-240 connections;mimicry: 3 workers, ~68-70 connections;low-volume: 2 workers, ~64-65 connections; each over 8 s, 3 repetitions per posture), against a self-owned lab target (fidelity: lab,target_authorisation: self-owned). These bundles carry packet capture only (packets_pcap: true; nohost_parquet/responses_parquet) — they confirm the malicious request was constructed and delivered over a real handshake, not that the target's memory usage was observed to spike. - Measured under controlled conditions. Not established. NullRabbit's reproduction did not instrument or record the target node's memory consumption; the memory-exhaustion impact is asserted by the public disclosure, not independently measured here.
- Inferred but not reproduced. None beyond the above.
Operator actions
- The documented fix. Upgrade go-ethereum to 1.13.15 or later. This is the only remedy the GHSA names; per the advisory, "no workarounds have been made public" for earlier versions.
- Exposure check. Query a node's client version (
admin_nodeInfoor theGeth/vX.Y.Z...string exposed to peers during handshake) and confirm it is>= 1.13.15. Any go-ethereum node below that version, reachable over P2P, is exposed as described. - Detection idea (NullRabbit's own, not a vendor recommendation). A
GetBlockHeadersrequest withamount(count)= 0has no legitimate purpose — a well-behaved client never asks for zero headers. An operator or IDS able to inspect devp2peth-protocol traffic could flag this exact field value as a strong signal of an attempted exploitation, independent of whether the target node is patched. This is a detection heuristic derived from the mechanism, not a mitigation documented by the vendor.
Canonical references
- NRDAX technique. NRDAX-T0038 — count-underflow-header-serving-amplification
- Evidence bundle (Hugging Face). NullRabbit/nr-bundles-public — primitive
geth_getblockheaders_count_zero(12 bundles, pcap capture,labfidelity) - Reproducer. Internal to NullRabbit's known-class corpus pipeline; not separately published as standalone code — the Hugging Face bundle above is the public artefact (packet capture of the attack traffic).
- Upstream primary sources. CVE-2024-32972 · GHSA-4xc9-8hmq-j652 (go-ethereum security advisory) · go-ethereum PR #29534 (the fix).
Related attacks
- NRDAX-T0122 on Ethereum —
geth_les_skip_negative. A different geth protocol (LES, noteth) and a different public CVE (CVE-2018-12018, "EPoD"), but the same shape of bug: a header-serving request handler (GetBlockHeadersMsg) that does not validate an attacker-supplied integer field (Skip = -1, i.e.UINT64_MAXas an unsigned value) before using it, there causing an out-of-range access and a crash rather than an oversized serve. Fixed in geth 1.8.11. - NRDAX-T0071 on Ethereum —
geth_ethash_memory_exhaustion_dos. Samememory_ampfamily and chain, a different public CVE (CVE-2021-42219): an excessive volume of block/header messages drives repeated ethash cache generation (consensus/ethash/algorithm.go) to the same end state — node memory exhaustion — via a different mechanism (repeated cache regeneration rather than a single oversized header-range read).
Track attacks affecting go-ethereum
New go-ethereum and Ethereum execution-layer findings are catalogued as they are reproduced. Track attacks affecting go-ethereum on NRDAX and in NullRabbit research.
Analysis plus reproducer. No weaponised proof-of-concept code.
Related Posts
How an exposed debug_traceCall lets one unauthenticated request burn seconds of go-ethereum CPU
debug_traceCall re-runs an eth_call-style message opcode-by-opcode with a full EVM tracer attached, so a single small request converts into up to seconds of single-goroutine CPU on the target — and with no per-source rate or concurrency cap, an unauthenticated caller drives many such traces in parallel against an exposed go-ethereum node.
How a wide-range eth_getLogs query turns a few hundred bytes into an unbounded response on exposed go-ethereum RPC nodes
eth_getLogs returns every log matching a filter across the requested block span in one JSON-RPC response, so a tiny wide-range or broad-topic request forces an exposed go-ethereum node to scan the range and serialise an unbounded matched-log set — a response-amplification DoS payable by an unauthenticated caller.
One prior-knowledge h2c connection multiplexes N eth_getLogs past an L4 per-connection cap on go-ethereum's JSON-RPC port
go-ethereum's JSON-RPC port terminates cleartext HTTP/2 (h2c) by prior knowledge, so one TCP connection can multiplex N eth_getLogs streams past an L4 edge that only caps connections per IP (nginx stream limit_conn) — measured 20/20 streams and ~39 MB pulled through a single connection the edge counts as one, an amplification-multiplexing bypass of a connection cap, not of an L7 request rate limit.
