mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
926 字
3 分钟
React2Shell 学习
2026-08-04

参考#

React Flight Protocol (RFC)#

分块的前后端通讯协议,不过可以使用特殊的标记:

ExpressionValue
$0, $1Refers to the value of another chunk
$1:foo:barRefers to a property of another chunk. Here, it is chunk1.value.foo.bar
$Q1Creates a Map instance from another chunk. Here, it is new Map(chunk1.value)
$@1Refers to the chunk instance, rather than the chunk value
$BRefers to blob (binary) data
$FRefers to an exported value of an already loaded module

这个主要在react/packages/react-server/src/ReactFlightReplyServer.js:916function parseModelString函数里,这是一个巨长的switch状态机,

分析#

找个有缺陷的源码,https://github.com/react/react/security/advisories/GHSA-fv66-9v8q-g76r

The vulnerability is present in versions 19.0, 19.1.0, 19.1.1, and 19.2.0 of:
react-server-dom-webpack
react-server-dom-parcel
react-server-dom-turbopack

这几个react-server-dom-*似乎PoC还不一样,先用最基础的webpack看看。

git clone -b v19.2.0 https://github.com/facebook/react.git --depth 1

Okay直接来看PoC吧:

Chunk 0

{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then":"$B1337"}',
"_response": {
"_prefix": "<any Javascript code>",
"_chunks": "$Q2",
"_formData": {
"get": "$1:constructor:constructor"
}
}
}

Chunk 1

"$@0"

Chunk 2

[]

这个Chunk 0实际上是个假的Chunk,目标是Chunk1在$@0的时候自动解包,实现可控Chunk的效果。之前提到,$@<id>是Promise引用,他会返回Chunk本身。(如果是$<id>,则返回的是chunk的.value)

$1:foo:bar表达式存在原型链污染,危险的属性也能被遍历穿透。

react/packages/react-client/src/ReactFlightClient.js:1920:2019

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

如果传入的是"then": "$1:__proto__:then",那么实际上能污染Chunk的then方法(Chunk.prototype.then)。正常应该是什么?比如传入$1:name,那么会变成chunk1.name;但是如果传入$1:__proto__:then,那么会发生三次循环,第一次value = chunk1,第二次value = chunk1.__proto__,也就是value = Chunk.prototype,第三轮value = Chunk.prototype.then,此时value就已经变成了Chunk原型链上的then方法。由于Chunk 2是$@0,这会让chunk1.value = chunk0,所以chunk1.value.then = Chunk.prototype.then

这时又涉及到JavaScript中另一个特性:resolve(value)的value是Promise或Thenable时,Promise会自动”解包”它。具体来说,JavaScript会调用该Promise的.then()方法,递归地等待其完成,直到得到一个非Promise的值。这意味着,当上一个 then 返回的是普通数据时,JavaScript将其直接交给下一个 then方法;当上一个then返回的是一个Promise时,JavaScript会自动解包这个Promise,将最终的结果值交给下一个then方法。

Flight第一次解析$@0获得的是真实的Chunk,将其传给resolve()时进行了第一次解包,解包后的value是攻击者构造的“假Chunk”。但JavaScript发现这个对象存在then方法,是一个Thenable对象,由于递归机制的存在,于是会继续对这个对象进行解包。

第二次解包时,Chunk就完全变成了用户控制的对象,这里就开始混淆了数据和逻辑对象,最终导致了漏洞。总结来说,React Flight在解析用户输入的时候,混淆了数据和Chunk对象,导致攻击者可以伪造Chunk对象,接着利用后续的逻辑造成任意代码执行。

利用#

ReactFlightReplyServer.js#L1059-L1068

case 'B': {
// Blob
const id = parseInt(value.slice(2), 16);
const prefix = response._prefix;
const blobKey = prefix + id;
// We should have this backingEntry in the store already because we emitted
// it before referencing it. It should be a Blob.
const backingEntry: Blob = (response._formData.get(blobKey): any);
return backingEntry;
}

这里的id虽然可控但不可用,他只是数字。但是response是可控的(Chunk 0里伪造了这个Chunk)。只要让response._formData.get变成一个恶意函数,比如eval,而response._prefix是恶意代码即可。

在JavaScript中,某个对象的.constructor 属性是这个对象的构造函数,而.constructor.constructor就是这个对象的构造函数的构造函数,而任何函数的构造函数都是Function。

// 立刻弹窗
eval('alert(1)')
// f调用后才会弹窗
var f = Function('alert(1)')
f()
// xx.constructor.constructor()也一样
var f = {}.constructor.constructor('alert(1)')
f()

Function和eval类似,只不过它其中的代码不是立刻执行,而是需要再次调用才能执行,所以,$B1337的返回值最后被控制成chunk1.constructor.constructor('<any Javascript code>'),这是一个函数,将这个函数赋值给then属性后,在下一次resolve()的时候触发,最终完成任意代码执行。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

React2Shell 学习
https://blog.chaomixian.top/posts/react2shell/
作者
炒米线
发布于
2026-08-04
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录