Skip to content
Exploit for Unchecked Return Value in Google Brotli

Exploit for Unchecked Return Value in Google Brotli

Sploitus • October 30, 2026

The two-stage local validation tool for CVE-2026-111497 (node2shell). It generates a full stream of the public PoC structure, feeding it to a pure C Brotli decoder, Node, Bun, and Python respectively; this stream is then analyzed for error semantics or memory corruption. **Scope**: Only validates trigger behavior, error semantics, and memory corruption; does not cover RCE, ROP, callbacks, or weaponization. Usage is restricted to isolated VMs, local files, or 127.0.0.1. ## Background

An out-of-bounds write vulnerability related to Brotli in the Node.js security patch cycle for October 2026. The public PoC splits the attack flow into two HTTP chunks:

- Stage one: 23,069,720 bytes, responsible for exhausting distance block counters and inducing “impossible block transitions”;

- Stage two: 27 bytes (poison) or 127 bytes (groom), continuing the same bitstream to influence decoder states. When only Stage one is fed, both the pure C ASAN and bare Node remain clean; writing occurs only after adding Stage two. This repository compiles the generation of both streams, C-level checks, and runtime validation into a reproducible script. ## Quick Start

This command produces `groom_full.bro` and `poison_full.bro` (each approximately 23MB), along with their corresponding SHA256 hashes. The stream originates from the `generate_stream`, `rce_stage_one`, and `rce_stage_two` functions within the public PoC `dimasma0305/CVE-2026-111497-POC`. The proof payload has been replaced with pattern data lacking any executable semantics. ### 2. Compiling the C harness

Brotli versions 1.2.0 (vulnerable) and 1.2.1 (fixed) source code are required:

bash build_harness.sh /path/to/brotli-1.2.0 /path/to/brotli-1.2.1

- `build-v120/harness_two_stage_plain_asan`

- `build-v121/harness_two_stage_plain_asan`

# Vulnerable version: Expected SEGV (uninitialized memory write via memmove16)

ASAN_OPTIONS=detect_leaks=0 ./build-v120/harness_two_stage_plain_asan poison_full.bro 27 16384 0x330

ASAN_OPTIONS=detect_leaks=0 ./build-v121/harness_two_stage_plain_asan poison_full.bro 27 16384 0x330

node verify_two_stage.js poison_full.bro 27

bun verify_two_stage_bun.js poison_full.bro 27

python3 verify_two_stage.py poison_full.bro 27

Single-stage comparison: use `poison_stream.bro` (only stage one) and pass `0`:

node verify_two_stage.js poison_stream.bro 0

python3 verify_two_stage.py poison_stream.bro 0

## Expected results (2026-10-30, Ubuntu 24.04 x86_64)

| Runtime | Version | Single-stage | Two-stage poison 27B |

| brotli C ASAN | 1.2.0 | Clean, NEEDS_MORE_INPUT | **SEGV in memmove16 (write)** |

| Node.js | 24.22.0 | Clean | **SIGSEGV, rc=139** |

| Bun | 1.4.3 | Z_BUF_ERROR, alive | Z_BUF_ERROR, alive (out 2048) |

| Python brotli | 1.1.0 | Silent, 1024B, is_finished=False | **SIGSEGV, rc=139** |

| Python brotli | 1.2.1 | Error | Error (decoder failed) |

Bun does not crash under two-stage streams, but the error semantics remain that of a vulnerable Z_BUF_ERROR; Python 1.1.0 directly suffers from a segmentation fault. Both indicate that the trigger key serves as an override for the second stage regarding the shared C library state; the actual behavior depends on the allocated memory and error handling mechanisms. ## Files

gen_rce_full.py # Two-stage flow generator

harness_two_stage.c # C-level two-stage feeding mechanism

build_harness.sh # Compilation of vulnerable/patched versions of the harness

verify_two_stage.js # Node.js validation script

verify_two_stage_bun.js # Bun validation script

verify_two_stage.py # Python validation script

├── stage2_trace.txt # Byte-by-byte APPEND/STATE logs

├── stage2_len_sweep.txt # Scanning of length N for the second stage

├── copy_trace_matrix.txt # Matrix of write addresses and number of iterations

- `BrotliDecoderState.buffer.u8` is 8 bytes; after invalid block switching triggers a NEEDS_MORE_INPUT condition, the second stage appends data byte by byte. This overwrites `buffer_length`, which in turn affects `pos`, `max_backward_distance`, and `max_distance`.

- A total of 27 bytes plus one byte at the end of Stage One results in exactly `max_distance`; only when N ≥ 23 can `max_backward_distance` be increased, thereby enabling distance checks via LZ77 copying mechanisms.

- The write address is computed as `ringbuffer + staged_pos − N×1024`; N is determined by the output flushing rate (0x330→8, ≥0x1000→20, public PoC Express→10, i.e., 0x2800).

- In version 1.2.1, the patched version returns -17 at the point of invalid block switching, preventing further appending cycles.

- The trigger flow structure originates from a public PoC:

- Node.js security advisory:

- Raw data was collected on an isolated VM on 2026-10-30; the ASLR address changes each time it runs. The address within “data/” is used solely for comparing the “pos” and “ringbuffer” structures during the same execution. This is neither a general detector nor a exploitation tool; do not use it on unauthorized targets. ## License

Extracted Entities