🔑 关键洞察
✦ AI·GEN这篇记录了作者在一次例行效能健检中,意外撞见自己家用伺服器被植入挖矿程式的完整抓贼过程,并整理成一份人人可照做的可疑进程自检清单。起点是一个不该出现的数字:一台 16 执行绪的机器 load average 却卡在 10.65。作者用 top 与 docker stats 揪出一个吃满 10 核、随机大小写档名的可疑程序,再靠 Linux 的 /proc 三件套(exe 指向已删除的 /tmp 档、cmdline、environ 溯源到某个 Docker 容器)确认它是挖矿程式,并把攻击链拉出来:一个 port 直接绑在 0.0.0.0、绕过 nginx 对外裸露的 Next.js 容器,被 CVE-2025-66478(Next.js 16.0.6 的满分 RCE、绰号 React2Shell)打穿。文中含清除(kill 不需 sudo、杀父程序回收 zombie)、查持久化(cron/bashrc/tmp)、堵根因(收回 127.0.0.1、升级框架、容器资源上限)的每一步命令,附一份六条的自检清单。作者的体悟是:被入侵最可怕的不是被吃 CPU,是你根本不知道自己被入侵了。
那天我只是想顺手帮自己的家用伺服器做个健检,结果第一个指标就不对劲:。这是一台 16 执行绪的机器,平常闲着没事,load 却卡在 10.65,等于三分之二的 CPU 被某个东西持续吃着。
有东西在偷用我的机器。这篇是我把它揪出来、清掉、堵住入口的全程,而且我尽量写成一份你可以照着跑一遍的检查清单:怎么确认自己的机器上有没有这种偷跑的可疑程序。
第一步:谁在吃我的 CPU#
load 高就先看是谁占的。从 uptime 一路追到揪出元凶,当时的终端记录是这样(数字都是那天实际跑出来的):
$ 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>/ 把答案都摊在那里,不需要任何额外工具。当时我跑的三条,和它们吐回来的东西:
$ 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_VERSION、HOSTNAME=0.0.0.0、PORT=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:
# 杀挖矿程序(用你自己的身分就能杀掉你身分跑的程序)
kill -9 2714011
# zombie 杀不掉,因为它已经死了;要回收它得杀它的父程序
docker stop <被入侵的容器>杀完盯着 load average 看它往下掉:10.65 → 8.80 → 6.30,CPU 开始退烧。zombie 在父容器停掉后才会被 init 收走、真正消失。直接 kill 一个 zombie 没有用,它已经死了,你要处理的是还活着的父程序。
第五步:确认它没有留后门#
杀掉当下的程序只是止血。真正要命的是持久化:如果攻击者在某个开机脚本或排程里埋了「重新下载并执行」,你杀完它下一分钟又活过来。挖矿程式最爱藏的三个地方,逐一检查:
# 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/nullWARNING
如果你在这几个地方发现了可疑的东西,问题比单一程序严重得多
一个挂在 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 绑定:
# 列出所有对外(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 进得去。
三件事收尾:
任何「只应该透过 nginx 存取」的容器,port 都加上 127.0.0.1: 前缀。对外的入口只留 nginx 一个,其余服务对公网一律隐形。这是这次事件成本最低、效益最高的一条。
把有 RCE 的 Next.js 从 16.0.6 升到当时的最新稳定版。门要关(绑 127.0.0.1),锁也要换新(修掉已知漏洞),两件都做,才不会下次换个漏洞又被同一扇门打进来。
替所有容器加 mem_limit 和 cpus。这不防入侵,但限制爆炸半径:万一再有一个容器被植入挖矿,它最多吃到自己那份配额,不会像这次一样一只程序吃满 10 核、把整台机器拖垮。
一份你可以现在就跑的自检清单#
如果你也管着一台对外的机器(VPS、家用伺服器、甚至只是开了 port forwarding 的家用电脑),花五分钟照这份跑一遍:
可疑进程自检清单(逐条照跑)
- 看 load 与 CPU:
uptime看 load average,top按 CPU 排序。闲置机器 load 却很高,或有你不认得的程序吃满 CPU,就是红旗。 - 可疑档名:
ps aux --sort=-%cpu | head,随机大小写、无意义的 COMMAND 名,高度可疑。 - 鉴定它的执行档:
ls -la /proc/<pid>/exe。指向/tmp、/dev/shm,或带(deleted)标记的,几乎确定是恶意的。 - 看它怎么启动、从哪来:
cat /proc/<pid>/cmdline | tr '\0' ' '和cat /proc/<pid>/environ | tr '\0' '\n',环境变数会告诉你它是从哪个容器/服务被生出来的。 - 查持久化:
crontab -l、cat /etc/crontab、ls /etc/cron.d/,以及~/.bashrc里有没有curl … | bash之类的东西。 - 查对外的门:
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
还没有留言
✨ 成为第一个留言的人吧