【漏洞分析】CVE-2025-55182 React2shell远程代码执行解析
嗯,用户让我帮忙总结一篇文章的内容,控制在一百个字以内,而且不需要用特定的开头。首先,我需要理解文章的主要内容。看起来这篇文章是关于React Server Components中的一个严重漏洞,这个漏洞被描述为“核弹级”,说明影响很大。 文章详细分析了漏洞的背景,包括React Server Components的工作原理,Flight协议的数据结构,以及漏洞的根本原因。漏洞的核心问题在于路径解析逻辑没有限制可访问的属性范围,导致攻击者可以访问敏感属性,比如__proto__和constructor。 然后文章详细描述了漏洞利用的过程,包括解析FormData、注册回调、触发代码执行等步骤。最后还提到了参考链接。 用户的需求是用中文总结,控制在100字以内。所以我要抓住关键点:React Server Components的漏洞、攻击者如何利用路径遍历和反序列化机制远程执行代码。 可能需要注意的是,用户可能是一个开发者或者安全研究人员,他们需要快速了解这个漏洞的核心问题和影响。因此,总结要简洁明了,突出漏洞的影响和利用方式。 现在我需要把这些信息浓缩到100字以内。确保涵盖主要技术点:React Server Components、路径遍历、反序列化、远程代码执行(RCE)。 最后检查一下是否符合要求:没有使用特定的开头词,直接描述内容。 </think> 文章分析了React Server Components(RSC)中的一个严重漏洞(被称为“核弹级”漏洞),该漏洞源于Flight协议解析逻辑未限制属性访问范围。攻击者可通过构造恶意FormData和引用路径,在服务端触发远程代码执行(RCE)。 2025-12-8 10:0:0 Author: keenlab.tencent.com(查看原文) 阅读量:0 收藏



这几天应该很多人在忙着应急这个“核弹级”漏洞,它已经成了安全圈里的“新顶流”。但网上还没有比较详细的分析文章,而我又对公开POC中的一些细节实在好奇,所以今天来一探究竟。
因为我对React不够了解,所以本文没有任何扩展内容,仅仅只是把漏洞利用过程调试了一遍,并且因为利用过程有点绕,所以配上了流程图方便理解。

背景知识

React Server Components (RSC)

RSC 是 React 19 推出的新渲染模型,核心思想是让部分组件在服务器端执行,仅将序列化后的”结果”传输给客户端。

主要特性:

  • 服务端执行:组件代码可直接在服务端执行业务逻辑与数据获取(如访问数据库、调用内部服务)
  • Flight 协议传输:客户端接收经过序列化的”React 树描述”,再在浏览器中反序列化为 React 元素
  • 流式渲染:支持增量传输,先发送已计算完成的部分,后续再补充剩余内容

与传统 SSR 的区别:

特性 传统 SSR RSC
输出格式 完整 HTML 字符串 组件树的数据格式(Flight stream)
客户端处理 仅做 Hydrate 作为完整 React 应用运行

这套机制已成为 Next.js App Router 的默认架构。

React Flight 协议

数据结构概述

FormData 与 Chunk 索引

在典型的 Server Action 请求中,Next.js / React 发送 multipart/form-data 请求,表单字段结构如下:

  • name="0": 主 payload(如参数列表)
  • name="1": 第 1 个模型块(model chunk)
  • name="2": 第 2 个模型块
  • ... : 更多块

$N 引用语法

Flight 协议使用 $ 前缀加数字表示对特定 chunk 的引用,冒号分隔的路径用于访问嵌套属性:

  • "$1": 引用 chunk1 本身
  • "$2:fruitName": 引用 chunk2 解析后对象的 fruitName 属性
  • "$3:user:email": 引用 chunk3 中的 .user.email

Flight 数据解析流程

假设客户端发送如下请求体:

1
2
3
4
5
6
7
8
9
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="0"

["$1:profile:name"]
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="1"

{"profile":{"name":"alice","age":18}}
------WebKitFormBoundaryABC123--

可以看作:

1
2
3
4
chunks = {
"0": '["$1:profile:name"]',
"1": '{"profile":{"name":"alice","age":18}}'
}

解析过程:

1
2
3
4
"$1:profile:name"
→ 查找 chunk1 并解析:{"profile":{"name":"alice","age":18}}
→ 访问路径 .profile.name
→ 返回 "alice"

漏洞根因

漏洞的核心问题在于:路径解析逻辑未通过 hasOwnProperty 限制可访问的属性范围,导致攻击者可以沿原型链访问任意属性,包括 __proto__constructor 等敏感属性。

流程图

漏洞代码分析

入口函数:decodeReplyFromBusboy

decodeReplyFromBusboy 是服务端解析客户端 FormData 的入口函数,位于 ReactFlightDOMServerNode.js

1
2
3
4
5
6
7
8
9
10
11
12

function decodeReplyFromBusboy<T>(
...
): Thenable<T> {
...
busboyStream.on('field', (name, value) => {
if (pendingFiles > 0) {
queuedFields.push(name, value);
} else {
resolveField(response, name, value);
}
});

调用链: resolveFieldresolveModelChunkinitializeModelChunk

字段解析:resolveField

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

export function resolveField(
response: Response,
key: string,
value: string,
): void {
response._formData.append(key, value);
const prefix = response._prefix;
if (key.startsWith(prefix)) {
const chunks = response._chunks;
const id = +key.slice(prefix.length);
const chunk = chunks.get(id);
if (chunk) {
resolveModelChunk(chunk, value, id);
}
}
}

核心解码逻辑:initializeModelChunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

function initializeModelChunk<T>(chunk: ResolvedModelChunk<T>): void {
...
const resolvedModel = chunk.value;
...
try {
const rawModel = JSON.parse(resolvedModel);

const value: T = reviveModel(
chunk._response,
{'': rawModel},
'',
rawModel,
rootReference,
);
...
}
}

关键步骤:

  • [1]chunk.value 进行 JSON 解析,得到如 {then: "$1:...", ...} 的对象
  • [2] reviveModel 处理 JSON,将字符串引用还原为实际对象

还原对象:reviveModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

function reviveModel(...): any {
...
if (typeof value === 'object' && value !== null) {
...
for (const key in value) {
if (hasOwnProperty.call(value, key)) {
const newValue = reviveModel(
response, value, key, value[key], childRef,
);
if (newValue !== undefined) {
value[key] = newValue;
}
}
}
}
return value;
}

POC 中 chunk[0].value 经 JSON 解析后为 object 类型,进入 [3] 处的循环,遍历所有 key-value 并递归调用 reviveModel。这是反序列化的核心过程——将数据还原为实际对象。

当遇到第一个 key then,其值为 $1:__proto__:then(字符串类型),会走不同分支:

1
2
3
4
5
6
function reviveModel(...): any {
if (typeof value === 'string') {
return parseModelString(response, parentObj, parentKey, value, reference);
}
...
}

字符串解析:parseModelString

parseModelString 包含一个大型 switch 表,根据前缀字符路由到不同处理逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

function parseModelString(...): any {
if (value[0] === '$') {
switch (value[1]) {
case '$': {
return value.slice(1);
}
case '@': {

const id = parseInt(value.slice(2), 16);
const chunk = getChunk(response, id);
return chunk;
}
...
}

const ref = value.slice(1);
return getOutlinedModel(response, ref, obj, key, createModel);
}
}

引用解析:getOutlinedModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

function getOutlinedModel<T>(...): T {
const path = reference.split(':');
const id = parseInt(path[0], 16);
const chunk = getChunk(response, id);
...
switch (chunk.status) {
...
case PENDING:
case BLOCKED:
case CYCLIC:
const parentChunk = initializingChunk;
chunk.then(
createModelResolver(...),
createModelReject(parentChunk),
);
return (null: any);
default:
throw chunk.reason;
}
}

漏洞利用过程

解析 chunk[0] 并注册回调

  1. **解析 $1:__proto__:then**:value[0]$value[1]1(chunk 1 的引用),进入 [5]
  2. **getOutlinedModel 处理 1:__proto__:then**:
    • : 分割得到 path = ["1", "__proto__", "then"]
    • 解析 path[0] 获取 chunk[1],此时状态为 PENDING
    • 进入 **[6]**,调用 Chunk.prototype.then(自定义实现)
    • createModelResolver 回调 push 到 chunk.value 中,待后续触发

图示:回调注册过程

  1. 继续处理 chunk[0] 的其他 key,完成后开始处理 chunk[1]

解析 chunk[1] 并触发回调

chunk[1] 的值为 "$@0",在 [4] 处解析,调用 getChunk 获取 chunk[0]。

一路向上返回到initializeModelChunk,之后返回至resolveModelChunk

1
2
3
4
5
6
7
8
9
10
11
12
13

function resolveModelChunk<T>(
chunk: SomeChunk<T>,
value: string,
id: number,
): void {
...
if (resolveListeners !== null) {
initializeModelChunk(resolvedChunk);

wakeChunkIfInitialized(chunk, resolveListeners, rejectListeners);
}
}

随后调用 wakeChunkIfInitialized

1
2
3
4
5
6
7
8
9
10
11
12
13

function wakeChunkIfInitialized<T>(
chunk: SomeChunk<T>,
resolveListeners: Array<...>,
rejectListeners: null | Array<...>,
): void {
switch (chunk.status) {
case INITIALIZED:
wakeChunk(resolveListeners, chunk.value, chunk);
break;
...
}
}

此时 chunk 状态为 INITIALIZED,进入 wakeChunk 调用之前注册的 resolveListeners

图示:resolveListeners 包含 thenget 两个 key 的回调

原型链遍历

回调中执行以下循环:

1
2
3
for (let i = 1; i < path.length; i++) {
value = value[path[i]];
}

图示:原型链遍历过程

这个循环类似于文件系统中的 ../../ 路径遍历,但作用于 JavaScript 原型链。

path = ["1", "__proto__", "then"] 为例,value 初始为 chunk[0](Chunk 实例):

迭代 操作 结果
1 value = chunk["__proto__"] Chunk.prototype
2 value = Chunk.prototype["then"] Chunk.prototype.then 函数

get 属性的处理类似,最终可获取 Chunk.constructor.constructor(即 Function 构造函数)。

构造伪造 Chunk

经过上述处理,chunk[0].value 被构造为:

1
2
3
4
5
6
7
8
9
10
11
12
{
then: Chunk.prototype.then,
status: "resolved_model",
reason: -1,
value: '{"then":"$B1337"}',
_response: {
_prefix: "debugger;throw new Error('test');",
_formData: {
get: Function
}
}
}

这是一个伪造的 thenable 对象,当 await decodeReplyFromBusboy 执行时会调用其 then 方法。

触发代码执行

then 方法中,由于 statusresolved_model,会调用 initializeModelChunkreviveModel

此时 value{"then":"$B1337"},解析 $B1337 进入 B 分支(Blob 处理):

1
2
3
4
5
6
7
case 'B': {
const id = parseInt(value.slice(2), 16);
const prefix = response._prefix;
const blobKey = prefix + id;
const backingEntry: Blob = (response._formData.get(blobKey): any);
return backingEntry;
}

[8] 处调用 response._formData.get(blobKey),而 _formData.get 已被替换为 Function 构造函数

因此实际执行:

1
Function("debugger;throw new Error('test');4919")

返回的函数被写回 then 属性:

1
2
3
{
"then": Function("debugger;throw new Error('test');4919")
}

后续 await decodeReplyFromBusboy 再次触发 then 函数时,恶意代码被执行,实现 RCE。

参考链接

https://gist.github.com/maple3142/48bc9393f45e068cf8c90ab865c0f5f3


文章来源: https://keenlab.tencent.com/zh/2025/12/08/2025-CVE-2025-55182/
如有侵权请联系:admin#unsafe.sh