那天我只是想顺手帮自己的家用伺服器做个健检,结果第一个指标就不对劲:。这是一台 16 执行绪的机器,平常闲着没事,load 却卡在 10.65,等于三分之二的 CPU 被某个东西持续吃着。

有东西在偷用我的机器。这篇是我把它揪出来、清掉、堵住入口的全程,而且我尽量写成一份你可以照着跑一遍的检查清单:怎么确认自己的机器上有没有这种偷跑的可疑程序。

第一步:谁在吃我的 CPU#

load 高就先看是谁占的。从 uptime 一路追到揪出元凶,当时的终端记录是这样(数字都是那天实际跑出来的):

console
$ uptime
 03:14  up 12 days,  load average: 10.65, 9.90, 7.40

$ top -bn1 -o %CPU | head
    PID USER     %CPU  %MEM  COMMAND
2714011 alice   996.0   3.9  XXAkjjBB
 525303 alice   429.0   0.0  XXigEEFC

$ docker stats --no-stream
NAME             CPU %      MEM USAGE
nas-frontend     1017.32%   2.41GiB

三个数字没一个对:一台 16 执行绪的机器闲着,load 却是 10.65;一个叫 XXAkjjBB、随机大小写档名的东西吃 996% CPU(占满 10 个核心)、2.4GB 记忆体;连跑 Next.js 的前端容器 nas-frontend 都显示 1017% CPU。

那个 XXigEEFC 是个 ,CPU 栏却是 429,一个已经死掉的程序不该有 CPU 数字,这是计数残留。

一个正常的服务不会叫这种名字。随机大小写、无意义的档名,是挖矿程式最典型的长相,它就是要让你在 process 列表里一眼认不出来。

第二步:用 /proc 三件套鉴定它#

看到可疑 PID 先别急着杀,先问它三个问题:你是什么、你怎么被启动的、你从哪来。Linux 的 /proc/<pid>/ 把答案都摊在那里,不需要任何额外工具。当时我跑的三条,和它们吐回来的东西:

console
$ ls -la /proc/2714011/exe
lrwxrwxrwx 1 alice alice 0 ... /proc/2714011/exe -> '/tmp/XXAkjjBB (deleted)'

$ cat /proc/2714011/cmdline | tr '\0' ' '
/tmp/XXAkjjBB

$ cat /proc/2714011/environ | tr '\0' '\n'
NODE_VERSION=20.11.0
HOSTNAME=0.0.0.0
PORT=3001
...

三条各供出一块拼图:

  • exe 指向 :执行档放在 /tmp,而且已经从磁碟删掉了,程序却还在跑。合法服务不会这样,这是恶意程式「落地即自删」的标准动作。
  • environ 那串 NODE_VERSIONHOSTNAME=0.0.0.0PORT=3001,不像我登入 shell 会有的变数,比较像某个 Node.js Docker 容器内部的环境。这只挖矿程式,是从我其中一个容器里被生出来的。

第三步:把攻击链拉出来#

顺着父程序往上追(ps -o ppid= -p <pid>,或 htop 开 tree view),整条链就清楚了:

父程序是 next-server,也就是我一个跑 Next.js 的前端容器。它被入侵后,依序 spawn 了 sh(拿到 shell)、base64(解码夹带的 payload),最后跑起 XXAkjjBB 挖矿。这就是为什么那个容器在 docker stats 里显示 1017% CPU:吃 CPU 的其实是它 fork 出来的挖矿程式。

第四步:清除#

好消息是,这只挖矿程式是用我的普通使用者身分(不是 root)在跑的:它从一个以我身分启动的容器里被生出来,继承了我的权限。所以杀它不需要 sudo:

bash
# 杀挖矿程序(用你自己的身分就能杀掉你身分跑的程序)
kill -9 2714011

# zombie 杀不掉,因为它已经死了;要回收它得杀它的父程序
docker stop <被入侵的容器>

杀完盯着 load average 看它往下掉:10.65 → 8.80 → 6.30,CPU 开始退烧。zombie 在父容器停掉后才会被 init 收走、真正消失。直接 kill 一个 zombie 没有用,它已经死了,你要处理的是还活着的父程序

第五步:确认它没有留后门#

杀掉当下的程序只是止血。真正要命的是持久化:如果攻击者在某个开机脚本或排程里埋了「重新下载并执行」,你杀完它下一分钟又活过来。挖矿程式最爱藏的三个地方,逐一检查:

bash
# 1. 排程:cron 是最常见的重生管道
crontab -l
cat /etc/crontab
ls -la /etc/cron.d/ /etc/cron.*/

# 2. shell 启动档:登入时自动执行
cat ~/.bashrc ~/.profile ~/.bash_profile 2>/dev/null | grep -iE 'curl|wget|base64|/tmp|http'

# 3. /tmp 里还有没有其他可执行档
find /tmp -type f -executable -ls 2>/dev/null

WARNING

如果你在这几个地方发现了可疑的东西,问题比单一程序严重得多 一个挂在 cron 里每分钟 curl … | bash 的项目,代表攻击者已经取得能写入你排程的权限,单纯杀程序完全不够。我这次三处都干净(/tmp 清空、cron 没被动、shell 启动档正常),所以确定它还没站稳脚跟,止血就足够。但这一步不能跳过:不查持久化就宣告清除完成,是资安事件处理最常见的错误。

根因:一扇没关的门,和一个过期的锁#

止血之后,真正的问题是:它怎么进来的?两个因素叠在一起,缺一不可。

第一,那个前端容器的 port 直接绑在 0.0.0.0 它本该只透过 nginx 反向代理对外,结果 docker-compose 里写的是 13001:3001,而不是 127.0.0.1:13001:3001。这个差别是致命的:

。前者让任何人都能直接打「我的公网 IP:13001」,完全绕过 nginx 和 Cloudflare 那层防护。我查 nginx 的 access log 时发现里面根本没有这只容器的纪录,就是因为攻击流量从来没经过 nginx,直接敲到裸露的 container port。

第二,那个容器跑的 Next.js 是 16.0.6,一个有已知 漏洞的版本。 门开着(port 裸露)加上锁是坏的(过期的框架),攻击者用一个公开的漏洞就取得了 shell,剩下的就是下载挖矿程式的例行公事。

这个 RCE 有名有姓:CVE-2025-66478(上游是 React Server Components 的 CVE-2025-55182,绰号 React2Shell),CVSS 满分 10.0。Next.js 16.0.0 到 16.0.6 都中招,攻击者送一个带特制 Next-Action header 的请求、不需要任何帐密就能执行任意程式码,16.0.7 才修掉。而它 2025 年 12 月就公开了,我却是 2026 年 4 月才发现自己中招。中间那几个月,就是我一直没升级、门开着锁也坏着、放给人打的空窗期。

加固:把整台机器的门都检查一遍#

补完这个洞,我做的第一件事是假设「如果这扇门开着,会不会还有别的门也开着」。扫了整台机器的 port 绑定:

bash
# 列出所有对外(0.0.0.0)监听的 port
ss -tlnp | grep '0.0.0.0'
# Docker 的话也看一下每个容器的映射
docker ps --format '{{.Names}}\t{{.Ports}}'

结果揪出一整排本该只走反代、却裸奔在 0.0.0.0 的服务。最让我背脊发凉的一个,是一个 PostgreSQL 资料库直接对公网开着,那比挖矿严重多了,是整个资料库随时可能被捞走。全部收回 127.0.0.1,只留 nginx 进得去。

三件事收尾:

把只走反代的服务全绑回 127.0.0.1

任何「只应该透过 nginx 存取」的容器,port 都加上 127.0.0.1: 前缀。对外的入口只留 nginx 一个,其余服务对公网一律隐形。这是这次事件成本最低、效益最高的一条。

升级那个过期的框架

把有 RCE 的 Next.js 从 16.0.6 升到当时的最新稳定版。门要关(绑 127.0.0.1),锁也要换新(修掉已知漏洞),两件都做,才不会下次换个漏洞又被同一扇门打进来。

给每个容器上资源上限

替所有容器加 mem_limitcpus。这不防入侵,但限制爆炸半径:万一再有一个容器被植入挖矿,它最多吃到自己那份配额,不会像这次一样一只程序吃满 10 核、把整台机器拖垮。

一份你可以现在就跑的自检清单#

如果你也管着一台对外的机器(VPS、家用伺服器、甚至只是开了 port forwarding 的家用电脑),花五分钟照这份跑一遍:

可疑进程自检清单(逐条照跑)
  1. 看 load 与 CPU:uptime 看 load average,top 按 CPU 排序。闲置机器 load 却很高,或有你不认得的程序吃满 CPU,就是红旗。
  2. 可疑档名:ps aux --sort=-%cpu | head,随机大小写、无意义的 COMMAND 名,高度可疑。
  3. 鉴定它的执行档:ls -la /proc/<pid>/exe。指向 /tmp/dev/shm,或带 (deleted) 标记的,几乎确定是恶意的。
  4. 看它怎么启动、从哪来:cat /proc/<pid>/cmdline | tr '\0' ' 'cat /proc/<pid>/environ | tr '\0' '\n',环境变数会告诉你它是从哪个容器/服务被生出来的。
  5. 查持久化:crontab -lcat /etc/crontabls /etc/cron.d/,以及 ~/.bashrc 里有没有 curl … | bash 之类的东西。
  6. 查对外的门:ss -tlnp | grep 0.0.0.0 列出所有对公网监听的 port,逐一问自己「这个真的需要对外吗」。资料库、管理面板、内部 API 几乎都不需要。

这次的帐算下来:一只吃满 10 核的挖矿程式、一个对公网裸奔的资料库、一个过期到有 RCE 的框架,全都在同一次健检里被翻出来。修起来不难,难的是知道要去看

參考連結
  • Next.js CVE-2025-66478 —— 这次被打穿的 RCE 官方公告nextjs.org
  • Linux /proc 档案系统 —— 程序鉴识的第一手资料man proc
  • Docker —— 只绑 localhost 的 port 映射语法Docker Docs
  • OWASP —— 伺服器加固与最小暴露面原则OWASP