🔑 重要なポイント
✦ AI·GEN筆者が定期的な性能チェックの最中に、自宅サーバーへ仕込まれたマイニングマルウェアに偶然行き当たった一部始終を、誰でも走らせられる怪しいプロセスの自己点検リストとしてまとめた記録。始まりはあってはならない数字、16 スレッドのマシンが暇なのに load average 10.65。top と docker stats で 10 コアを占有するランダム名のプロセスを引き出し、Linux の /proc 三点セット(exe が削除済みの /tmp ファイルを指す、cmdline、environ から Docker コンテナへ遡る)でマイナーだと確定し、攻撃チェーンを再構成する。0.0.0.0 にバインドされ nginx を越えて露出した Next.js コンテナが、CVE-2025-66478(Next.js 16.0.6 の満点 RCE、通称 React2Shell)で撃ち抜かれていた。駆除(kill に sudo は不要、親を殺してゾンビを回収)、永続化チェック(cron/bashrc/tmp)、根本対策(127.0.0.1 に戻す・アップグレード・コンテナ上限)を経て、6 行の自己点検で締める。教訓は、侵害されて一番怖いのは CPU を食われることではなく、気づいていないこと自体だ。
サーバーの定期ヘルスチェックをしようとしただけの朝、最初の指標からもうおかしかった。。16 スレッドのマシンが普段は暇なのに、load が 10.65 に張り付いている。つまり CPU の 3 分の 2 を、何かがずっと食っていた。
誰かが私の知らないところでマシンを使っていた。この記事はそれを見つけ出し、駆除し、入口を塞ぐまでの一部始終だ。そしてできるだけ、自分でも走らせられるチェックリストとして書いた。自分のマシンに、こういう「こっそり動いている怪しいプロセス」がいないかを確認する方法として。
第 1 歩:誰が 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.41GiB3 つの数字がどれも正常ではない。16 スレッドのマシンが暇なのに load は 10.65。XXAkjjBB という、ランダムな大文字小文字のファイル名のものが 996% CPU(10 コア占有)と 2.4GB のメモリを食っている。そして Next.js のフロントエンドコンテナ nas-frontend まで 1017% CPU を示している。
XXigEEFC のほうは だが、CPU 欄は 429 になっている。すでに死んだプロセスに CPU の数字があるのはおかしく、これは集計の残りかすだ。
まっとうなサービスはこんな名前を付けない。ランダムな大文字小文字の無意味なファイル名は、マイナーの最も典型的な見た目であり、プロセス一覧の中で一目では見分けられないように作られている。
第 2 歩:/proc の三点セットで正体を割る#
怪しい PID を見つけても、すぐ殺す前に 3 つ質問する。お前は何者か、どう起動されたか、どこから来たか。Linux の /proc/<pid>/ はこの 3 つの答えをすべてそこに広げてくれる。追加ツールは何もいらない。そのとき私が叩いた 3 行と、返ってきたもの:
$ 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
...3 行それぞれがパズルの 1 ピースを差し出す:
exeが指すのは 。実行ファイルは/tmpにあり、しかもすでにディスクから削除されているのにプロセスは走り続けている。まっとうなサービスはこうならない。「着地して即自己削除」の教科書どおりの動きだ。environのNODE_VERSION、HOSTNAME=0.0.0.0、PORT=3001は、私のログインシェルが持つような変数ではなく、どこかの Node.js Docker コンテナの内部の環境に見える。このマイナーは、私のコンテナの一つから生み出されたものだ。
第 3 歩:攻撃チェーンを引き出す#
親プロセスをたどっていく(ps -o ppid= -p <pid>、または htop のツリー表示)と、全体像がはっきりする:
親プロセスは next-server、私の Next.js フロントエンドコンテナの一つだ。侵害された後、順に sh(シェル取得)、base64(同梱された payload のデコード)を spawn し、最後に XXAkjjBB を起動してマイニングを始めた。だからそのコンテナが docker stats で 1017% CPU を示していた。CPU を食っていたのは、実際にはそれが fork したマイナーだったわけだ。
第 4 歩:駆除#
朗報として、このマイナーは 私の一般ユーザー権限(root ではない)で走っていた。私の権限で起動したコンテナから生み出され、私の権限を引き継いだからだ。だから 殺すのに sudo はいらない:
# マイナーを殺す(自分の権限で走っているプロセスは自分の権限で殺せる)
kill -9 2714011
# ゾンビは殺せない、もう死んでいるから。回収するには親プロセスを殺す
docker stop <侵害されたコンテナ>殺しながら load average が下がっていくのを見る:10.65 → 8.80 → 6.30、CPU が冷めていく。ゾンビは親コンテナを止めたあとに init に回収されてはじめて本当に消える。ゾンビを kill しても意味がない、もう死んでいるのだから。処理すべきはまだ生きている親プロセスのほうだ。
第 5 歩:バックドアを残していないか確認する#
いま動いているプロセスを殺すのは応急処置にすぎない。本当に厄介なのは**永続化(persistence)**だ。攻撃者がどこかの起動スクリプトやスケジューラに「再ダウンロードして実行」を仕込んでいたら、殺した 1 分後にまた生き返る。マイナーが最も好んで隠れる 3 か所を、一つずつ確認する:
# 1. スケジューラ:cron が最も一般的な再生ルート
crontab -l
cat /etc/crontab
ls -la /etc/cron.d/ /etc/cron.*/
# 2. シェル起動ファイル:ログイン時に自動実行される
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 を回す項目があれば、攻撃者はすでにあなたのスケジューラへの書き込み権限を得ている。プロセスを殺すだけでは全然足りない。今回は 3 か所とも綺麗だった(/tmp は空、cron は無傷、シェル起動ファイルも正常)ので、まだ根を張っていないと確信でき、応急処置で十分だった。だがこの手順は飛ばせない。永続化を確認せずに「駆除完了」と宣言するのは、インシデント対応で最もよくある間違いだ。
根本原因:閉め忘れた扉と、期限切れの錠#
止血のあとの本当の問題は、それがどうやって入ってきたかだ。2 つの要因が重なっていて、片方だけでは成立しない。
第一に、そのフロントエンドコンテナのポートが 0.0.0.0 に直接バインドされていた。 本来は nginx リバースプロキシ経由でのみアクセスされるべきなのに、docker-compose には 127.0.0.1:13001:3001 ではなく 13001:3001 と書かれていた。この差が致命的だ:
。前者は誰でも「私の公開 IP:13001」に直接叩き込め、nginx と Cloudflare を完全に回避できる。nginx のアクセスログを調べても、このコンテナの記録は一切なかった。攻撃トラフィックが nginx を通らず、晒されたコンテナポートを直接叩いていたからだ。
第二に、そのコンテナが動かしていた Next.js は 16.0.6、既知の の穴を抱えたバージョンだった。 扉が開いていて(ポート露出)、錠が壊れている(古いフレームワーク)。攻撃者は公開されたエクスプロイト一つでシェルを取り、あとはマイナーをダウンロードする定型作業だった。
この RCE には名前がある。CVE-2025-66478(上流の React Server Components では CVE-2025-55182、通称 React2Shell)、CVSS は満点の 10.0。Next.js 16.0.0 から 16.0.6 まですべて影響を受け、攻撃者は細工した Next-Action ヘッダー付きのリクエストを 1 本送るだけで、認証情報なしに任意コードを実行できる。修正は 16.0.7 で入った。そしてこれが公開されたのは 2025 年 12 月、私が侵害に気づいたのは 2026 年 4 月。その間の数か月が、まさに私がアップグレードを怠っていた空白期間だ。扉は開き、錠は壊れたまま、そこに放置されていた。
対策:マシン中のすべての扉を点検する#
この穴を塞いだあと、最初にやったのは「この扉が開いていたなら、他の扉も開いていないか」と疑うことだった。マシン全体のポートバインドをスキャンした:
# 0.0.0.0 で待ち受けている(露出した)ポートをすべて列挙
ss -tlnp | grep '0.0.0.0'
# Docker なら各コンテナのマッピングも見る
docker ps --format '{{.Names}}\t{{.Ports}}'結果、本来リバースプロキシ経由のみのはずが 0.0.0.0 に裸で晒されているサービスがずらりと出てきた。最も背筋が凍ったのは、PostgreSQL データベースがインターネットに直接開いていたこと。マイニングよりはるかに深刻で、データベース丸ごといつ持ち去られてもおかしくない。すべて 127.0.0.1 に戻し、nginx だけが入れるようにした。
締めくくりに 3 つ:
「nginx 経由でのみアクセスされるべき」コンテナには、ポートに 127.0.0.1: 接頭辞を付ける。公開の入口は nginx 一つだけに絞り、他のサービスはインターネットに対して不可視にする。今回のインシデントで最も低コスト・最高効率の一手だ。
RCE を抱えた Next.js を 16.0.6 から当時の最新安定版に上げる。扉を閉め(127.0.0.1 にバインド)、錠も新しくする(既知の穴を塞ぐ)、両方やってはじめて、次の脆弱性で同じ扉から入られずに済む。
すべてのコンテナに mem_limit と cpus を付ける。これは侵入を防がないが、爆発半径を限定する。万一また別のコンテナにマイナーを仕込まれても、食えるのはせいぜい自分の割り当て分だけで、今回のように 1 プロセスが 10 コアを占有してマシン全体を引きずり倒すことはない。
いますぐ走らせられるチェックリスト#
もしあなたもインターネットに面したマシン(VPS、家庭用サーバー、あるいはポート開放しただけの家庭用 PC でも)を管理しているなら、5 分でこれを一通り走らせてみてほしい:
怪しいプロセスの自己点検(1 行ずつ実行)
- 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でインターネットに待ち受けているものをすべて列挙し、一つずつ「これは本当に公開が必要か」と問う。データベース、管理画面、内部 API はほぼ不要。
今回の請求書:10 コアを占有するマイナー、インターネットに裸で晒されたデータベース、RCE を抱えるほど古いフレームワーク、そのすべてが同じ 1 回のヘルスチェックで掘り出された。直すのは難しくなかった。難しいのは、見に行くと知っていることだ。
- Next.js CVE-2025-66478 —— 今回撃ち抜かれた RCE の公式アドバイザリnextjs.org
- Linux /proc ファイルシステム —— プロセス鑑識の一次情報man proc
- Docker —— localhost のみにバインドするポートマッピング構文Docker Docs
- OWASP —— サーバー堅牢化と最小露出の原則OWASP
まだコメントがありません
✨ 最初のコメントを残しませんか