通过路径遍历+原型链污染,伪造一个Thenable对象和自定义的Funtcion,利用Thenable的特性,执行自定义的Function
一些需要了解的知识
async await
js中的异步调用,比如这个例子
1 | function fetchData(){ |
1 | % node test.js |
await 执行的函数中,返回一个Thenable对象(包含then方法的对象),会自动执行then方法。
当然也可以直接返回一个普通的对象
1 | function fetchData(){ |
POC
回显
1 | POST /signin |
RSC
React Server Components(React 服务器组件)
RSC 允许某些组件在服务器上运行,并直接生成 HTML 或序列化的 React 元素树返回给客户端。这样可以减少客户端 JavaScript 包的大小,提高性能。
一种rpc框架,传入function id、参数,调用服务端的方法
React Flight Protocol
客户端通过form-data格式的请求,将包含了chunks的请求发送到服务端,
1 | files = { |
服务端会反序列化
1 | { object: 'fruit', name: 'cherry' } |
表单的0、1、2对应chuns的索引,通过$可以引用其他chunk,
通过:可以访问到嵌套的属性
1 | Chunk示例: "$0:users:0:name" |
code analyse
数据结构
Flight协议将表单的每个字段,都反序列化成一个chunk
对应的结构是
1 | function Chunk(status, value, reason, response) { |
Chunk也是一个Thenable对象,包含了then函数
1 | Chunk.prototype = Object.create(Promise.prototype); |
所有的chunk维护在一个Response对象中
1 | function createResponse( |
反序列化过程
每个chunk对应的value反序列化完成后,存在value字段中。
核心的代码在reviveModel函数,将所有string类型的value,用parseModelString来反序列化。其中关键的$引用相关的处理也在这里。
1 | function reviveModel(response, parentObj, parentKey, value, reference) { |
parseModelString的大概处理逻辑
- 字符串前缀检查:
- 如果字符串以
$开头,则根据第二个字符的值执行不同的解析逻辑。 - 如果字符串不以
$开头,则直接返回原始值。
- 如果字符串以
- 解析逻辑:
$开头的字符串表示特殊的编码格式,第二个字符决定了具体的解析方式:$$:返回去掉第一个$的字符串。$@:解析为一个Chunk对象。$F:解析为服务器引用的函数,并加载相关的服务器引用。$T:创建一个临时引用。$Q:解析为Map对象。$W:解析为Set对象。$K:解析为FormData对象。$i:解析为迭代器。$I:解析为正无穷大。$-:解析为负无穷大。$N:解析为NaN。$u:解析为undefined。$D:解析为Date对象。$n:解析为BigInt。
- 其他特殊前缀(如
$A,$O,$o等)用于解析不同类型的TypedArray。 $R,$r,$X,$x分别解析为ReadableStream或异步迭代器。- 如果第二个字符不匹配任何已知的前缀,则尝试解析为
OutlinedModel。
- 返回值:
- 根据解析结果返回相应的 JavaScript 数据类型或对象。
反序列化结束后
返回索引为0的chunk
1 | ...... |
从解析form-data表单,到反序列化,都是在decodeReplyFromBusboy函数中进行的。
并且这个函数通过await调用的。
1 | boundActionArguments = await decodeReplyFromBusboy( |
await的特性就是,会自动调用返回的Thenable中的then方法,但是这里正常情况下,返回的只是传入的普通的对象,不包含then方法。
虽然Chunk结构中,继承了Promise,但是返回的实际是Chunk.value,也就是从客户端中传入的json中反序列化出来的对象。
react2shell的漏洞利用中,就是巧妙的构造了一个Thenable对象,利用了这里的await调用。
漏洞分析
漏洞的根因,要从parseModelString说起。
在parseModelString中,如果传入的字符是$+数字索引,代表引用其他的chunk,会进入getOutlinedModel中。
1 | value = value.slice(1); |

通过id,获取到要引用的chunk后,在对应的chunk上,异步调用createModelResolver方法。
比如在chunk0上引用了chunk1,在反序列化chunk0时,chunk1还没有反序列化。所以在chunk1完成反序列化时,才会执行createModelResolver方法,来继续完成chunk0的反序列化。
漏洞的关键点就在createModelResolver中
这两行代码中,path、value、parentObject都是可控的,很明显的原型链污染。可以篡改parentObject中的任意字段,比如__proto__、then等
1 | for (var i = 1; i < path.length; i++) value = value[path[i]]; |
POC
https://github.com/msanft/CVE-2025-55182 这个版本的poc,是将chunk.value,构造成一个完整的chunk,包含then、value等,
最后反序列化结束时,通过await 调用then函数。触发构造的假chunk中的替换的Funcion,达到代码执行的效果。
伪造chunk
前面说过,反序列化结束后,会返回id为0的chunk,但默认情况下不是Thenable对象。
可以利用这里的原型链污染,给chunk添加一个then等方法,构造成一个完整的Chunk。
在poc中:
chunk0中"then": "$1:__proto__:then",
chunk1中"$@0"
chunk0的then指向chunk1的__proto__.then
chunk1的$@0,代表chunk0,相当于chunk0的then被赋值为自己的chunk0自己的then函数。
1 | chunk0.value.then = chunk0.__proto__then |
这时候chunk0.value已经是一个Thenable对象了
但是还不够,只是能构造在反序列化结束时,调用chunk本身的then,还不能代码执行。
继续看poc中的关于get的反序列化
1 | get= chunk1.constructor.constructor |
测试
将get替换成了Function函数
经过第一次反序列化,最后的chunk结构是:
Chunk.value,也被反序列化成了一个假的Chunk,并且get函数也被替换成了Function。
伪造Thenable, @B反序列化恶意函数
反序列化结束,经过await调用then,进入了Chunk.then :
如果chunk的status是resolved_model,会初始化chunk,也就是继续反序列化伪造的Chunk。
初始化完成后,resolve(this.value),如果value是Thenable对象,会继续执行then函数。

在伪造的Chunk中,value是{\"then\":\"$B\"},也就是继续在伪造的chunk中,反序列化出一个Thenable对象,不过这次不需要是一个完整的chunk,只要then函数。$B在parseModelString中对应的处理逻辑
1 | case "B": |
这里调用了response._formData.get,参数是response._prefix + obj.
在第一次反序列化时,将get替换为了Function,response._prefix是要执行的恶意代码。
相当于这里创建了一个执行恶意代码的函数Function("evil code"),并绑定到了then上。
最后在Chunk.then中,通过resolve执行Thenable.then,造成代码执行。
origin POC
漏洞的作者。放出了本人写的poc
1 | https://github.com/lachlan2k/React2Shell-CVE-2025-55182-original-poc |