Mozilla reported HTTP2Shell with proof of RCE in April
(6 files, 4 external files, 1 obsolete file)
以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。
Attachments
(6 files, 4 external files, 1 obsolete file)
test.html
6 months ago Bugmon [:jkratzer for issues]
225 bytes, text/html
Details
fix.patch
6 months ago Bugmon [:jkratzer for issues]
2.40 KB, patch
Details | Diff | Splinter Review
server.py
6 months ago Bugmon [:jkratzer for issues]
5.77 KB, text/plain
Details
gen_stream.py
6 months ago Bugmon [:jkratzer for issues]
6.08 KB, text/plain
Details
gen_stream.cpython-312.pyc
6 months ago Bugmon [:jkratzer for issues]
6.71 KB, application/octet-stream
Details
README.md
6 months ago Bugmon [:jkratzer for issues]
2.13 KB, text/plain
Details
crash_stack.txt
6 months ago Bugmon [:jkratzer for issues]
3.07 KB, text/plain
Details
(secure)
6 months ago Ryan VanderMeulen [:RyanVM]
48 bytes, text/x-phabricator-request
tjr
:
sec-approval+
Details | Review
(secure)
6 months ago Ryan VanderMeulen [:RyanVM]
48 bytes, text/x-phabricator-request
phab-bot
:
approval-mozilla-beta+
Details | Review
(secure)
6 months ago Ryan VanderMeulen [:RyanVM]
48 bytes, text/x-phabricator-request
phab-bot
:
approval-mozilla-esr140+
Details | Review
(secure)
6 months ago Ryan VanderMeulen [:RyanVM]
48 bytes, text/x-phabricator-request
phab-bot
:
approval-mozilla-esr115+
Details | Review
Show External Show Obsolete
Bottom ↓
Tags ▾
Reset
Timeline ▾
Reset
Collapse All
Expand All
Comments Only
Bugmon [:jkratzer for issues]
Reporter
Description
• 6 months ago
The brotli decoder vendored in Firefox contains a heap buffer overflow that can be triggered by a malicious HTTP server serving a crafted ~22MB brotli-compressed response. The decoder initializes block_length[2] to BROTLI_BLOCK_SIZE_CAP (1<<24 = 16,777,216) under the assumption that no metablock can issue more distance codes than its declared output size. However, the dictionary transform path at decode.c:2247 permits zero-output transforms when distance_code > 120 — each such command decrements block_length[2] without consuming any of meta_block_remaining_len. After 16M such commands, block_length[2] reaches zero while the metablock is still active.
When block_length[2]==0 and num_block_types[2]==1 (the default), DecodeBlockTypeAndLength returns BROTLI_FALSE at decode.c:1189 because there is no block-type tree to read. The BROTLI_SAFE macro at decode.c:2139 misinterprets this BROTLI_FALSE as BROTLI_DECODER_NEEDS_MORE_INPUT and jumps to saveStateAndReturn. The streaming retry loop at decode.c:2444-2455 then assumes one more input byte will allow progress: it writes that byte to s->buffer.u8[s->buffer_length++] and retries. Since the bug is structural rather than input-shortage, the retry hits the same BROTLI_FALSE indefinitely, and buffer_length grows past 8 with no bound check (BROTLI_DCHECK is compiled out in release builds).
The BrotliDecoderState struct places buffer_length (uint64_t), pos (int), max_backward_distance (int), max_distance (int), ringbuffer_size, ringbuffer_mask, dist_rb_idx, dist_rb[4], error_code, meta_block_remaining_len, and the ringbuffer pointer immediately after the 8-byte buffer.u8[] array. The attacker controls every byte written, allowing them to set buffer_length carefully (e.g., u8[8]=0x08 to keep the loop sequential), then overwrite pos with an arbitrary 32-bit offset and max_distance/max_backward_distance with values that bypass bounds checks. Once the unsafe ProcessCommands path activates, memmove16(&s->ringbuffer[pos], ...) at decode.c:2283 writes 16 bytes at an attacker-chosen heap offset. With further bytes the attacker can also corrupt the ringbuffer pointer itself for arbitrary write-what-where. This is a remote, no-interaction memory corruption — visiting any webpage that fetches a brotli resource from an attacker-controlled origin (or a MitM on plain HTTP) is sufficient.
File: modules/brotli/dec/state.c, line 112-119
void BrotliDecoderStateMetablockBegin(BrotliDecoderState* s) { s->meta_block_remaining_len = 0; s->block_length[0] = BROTLI_BLOCK_SIZE_CAP; s->block_length[1] = BROTLI_BLOCK_SIZE_CAP; s->block_length[2] = BROTLI_BLOCK_SIZE_CAP; /* Assumes max 16M distance codes / s->num_block_types[0] = 1; s->num_block_types[1] = 1; s->num_block_types[2] = 1; / Single type → no block-switch tree */
File: modules/brotli/dec/state.h, line 264-280
/* Temporary storage for remaining input. Brotli stream format is designed in a way, that 64 bits are enough to make progress in decoding. / union { uint64_t u64; uint8_t u8[8]; } buffer; brotli_reg_t buffer_length; / uint64_t on 64-bit — first thing overwritten */
int pos; /* used as ringbuffer index */ int max_backward_distance; int max_distance; int ringbuffer_size; int ringbuffer_mask; int dist_rb_idx; int dist_rb[4]; int error_code; int meta_block_remaining_len;
uint8_t* ringbuffer; /* eventually overwritable too */
File: modules/brotli/dec/state.h, line 388-390
/* Literal/Command/Distance block size maximum; same as maximum metablock size; used as block size when there is no block switching. */ #define BROTLI_BLOCK_SIZE_CAP (1U << 24)
File: modules/brotli/dec/decode.c, line 2241-2253
} else { len = BrotliTransformDictionaryWord(&s->ringbuffer[pos], word, len, transforms, transform_idx); BROTLI_LOG(("[ProcessCommandsInternal] dictionary word: [%.*s]," " transform_idx = %d, transformed: [%.s]\n", i, word, transform_idx, len, &s->ringbuffer[pos])); if (len == 0 && s->distance_code <= 120) { / BUG: distance_code > 120 escapes / BROTLI_LOG(("Invalid length-0 dictionary word after transform\n")); return BROTLI_FAILURE(BROTLI_DECODER_ERROR_FORMAT_TRANSFORM); } } pos += len; / len==0 → no progress / s->meta_block_remaining_len -= len; / len==0 → not consumed */
File: modules/brotli/dec/decode.c, line 1854
--s->block_length[2]; /* Decremented unconditionally per distance code */
File: modules/brotli/dec/decode.c, line 1179-1191
static BROTLI_INLINE BROTLI_BOOL DecodeBlockTypeAndLength( int safe, BrotliDecoderState* s, int tree_type) { brotli_reg_t max_block_type = s->num_block_types[tree_type]; const HuffmanCode* type_tree = &s->block_type_trees[ tree_type * BROTLI_HUFFMAN_MAX_SIZE_258]; const HuffmanCode* len_tree = &s->block_len_trees[ tree_type * BROTLI_HUFFMAN_MAX_SIZE_26]; BrotliBitReader* br = &s->br; brotli_reg_t* ringbuffer = &s->block_type_rb[tree_type * 2]; brotli_reg_t block_type; if (max_block_type <= 1) { return BROTLI_FALSE; /* BUG: returned to BROTLI_SAFE which treats it as NEEDS_MORE_INPUT */ }
File: modules/brotli/dec/decode.c, line 1943-1953
#define BROTLI_SAFE(METHOD)
{
if (safe) {
if (!Safe##METHOD) {
result = BROTLI_DECODER_NEEDS_MORE_INPUT; \ /* Misinterprets structural FALSE */
goto saveStateAndReturn;
}
} else {
METHOD;
}
}
File: modules/brotli/dec/decode.c, line 2138-2141
if (BROTLI_PREDICT_FALSE(s->block_length[2] == 0)) { BROTLI_SAFE(DecodeDistanceBlockSwitch(s)); /* → BROTLI_FALSE → NEEDS_MORE_INPUT */ } BROTLI_SAFE(ReadDistance(s, br));
File: modules/brotli/dec/decode.c, line 2444-2456
} else if (available_in != 0) { / Not enough data in buffer, but can take one more byte from input stream. / result = BROTLI_DECODER_SUCCESS; BROTLI_DCHECK(s->buffer_length < 8); / COMPILED OUT in release */ s->buffer.u8[s->buffer_length] = *next_in; / OOB WRITE: index 8, 9, 10... */ s->buffer_length++; BrotliBitReaderSetInput(br, &s->buffer.u8[0], s->buffer_length); (*next_in)++; (available_in)--; / Retry with more data in buffer. / continue; / Loops forever — bug never clears */ }
File: modules/brotli/common/platform.h, line 464-470
#if defined(BROTLI_DEBUG) || defined(BROTLI_ENABLE_LOG) #define BROTLI_ENABLE_DUMP_DEFAULT 1 #define BROTLI_DCHECK(x) assert(x) #else #define BROTLI_ENABLE_DUMP_DEFAULT 0 #define BROTLI_DCHECK(x) /* No-op in release — no protection */ #endif
File: modules/brotli/dec/decode.c, line 2270-2283
} else { int src_start = (pos - s->distance_code) & s->ringbuffer_mask; uint8_t* copy_dst = &s->ringbuffer[pos]; /* pos is ATTACKER-CONTROLLED via overflow / uint8_t copy_src = &s->ringbuffer[src_start]; int dst_end = pos + i; int src_end = src_start + i; /* Update the recent distances cache. / s->dist_rb[s->dist_rb_idx & 3] = s->distance_code; ++s->dist_rb_idx; s->meta_block_remaining_len -= i; / There are 32+ bytes of slack in the ring-buffer allocation. Also, we have 16 short codes, that make these 16 bytes irrelevant in the ring-buffer. Let's copy over them as a first guess. / memmove16(copy_dst, copy_src); / 16-byte heap write at attacker offset */
The vulnerability arises from an interaction between four pieces of code. (1) BrotliDecoderStateMetablockBegin in state.c assumes block_length[2]=16M is sufficient because a metablock cannot output more than 16M bytes — but it failed to account for commands that produce zero output. (2) The dictionary transform path explicitly permits len==0 when distance_code>120, creating the zero-output gadget. (3) DecodeBlockTypeAndLength returns BROTLI_FALSE when there is only one block type, which is semantically 'no block switch needed' but is indistinguishable from 'not enough input bits' to the BROTLI_SAFE macro. (4) The streaming retry loop trusts that NEEDS_MORE_INPUT means progress is possible with more bytes, and has only a debug-build assertion guarding the array bound. The BrotliDecoderState struct layout places critical state (buffer_length, pos, max_distance, ringbuffer pointer) immediately after the overflowed 8-byte buffer, turning a linear overflow into full decoder state corruption and ultimately a controlled heap write.
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。