那天我只是想順手幫自己的家用伺服器做個健檢,結果第一個指標就不對勁:。這是一台 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