背景
自分用のサイトをいくつか動かしているとはいえ、サーバー周りは仕事とは無関係で、いつまで経ってもド素人だ。
ここ数ヶ月、マイニングプログラムを仕込まれる被害が続いていた。でも当時はTencent Cloudのサーバーセキュリティお試しカードがまだ有効で、通知が来たら手動で修復をクリックするだけで済んでいた。
春節休みから戻って確認したら、お試しカードは期限切れ。終わった。古くてショボいサーバーが、マイニングで何日もフル稼働していた。
春節が明けてから、プロジェクトのコード管理をセルフホストのGitサーバーで本格的に運用し始めたので、この感染問題もちゃんと調べることにした。実はテンセント(🐕ペンギン)にカネを払いたくないだけ。
要するに、現状は、ブログのフロントエンドのポートがそのままインターネットに露出していて、next.js の何らかの特性(詳しく調べていない)を悪用されて侵入され、フロントエンドのコンテナが汚染されてしまった。とりあえずイメージをpullし直して再デプロイし、docker-compose.yml の image と同じレベルに read_only: true を追加して、侵入者にマイニングプログラムをダウンロードされないようにする。これでひとまず凌ぐ。
調査手順
とにかく止血
いちばんCPUを食っているプロセスを探す。
top -o %CPU
root に切り替える。
whoami
id
sudo -n true; echo $?
ぶっ殺す。
たとえば今回のマイニングプロセスのPIDが53075だったら、sudo kill -STOP 53075 を実行する。
sudo kill -STOP <PID>
プロセスを再確認する。
top -b -n 1 | head -n 15
誰の💩だ
実際の実行ファイルを探す。パスが得られるので、後で掃除したければそのパスを使って処理する。
sudo readlink -f /proc/<PID>/exe
起動コマンドラインを確認する。あくまで補助的な調査で、結果が「空」なら、ユーザーが手動でコマンドラインから起動したプロセスではないことがわかる。
sudo sh -c 'tr “\0“ ” ” < /proc/<PID>/cmdline; echo’
プロセス起動時の環境変数を確認する。見覚えのある文字列があれば、この時点でどのアプリが侵入されたのか特定できる。
sudo cat /proc/<PID>/environ | tr “\0“ “\n“ | head -n 60
ここまでの調査で原因アプリが特定できなければ、さらに深掘りする。以下のコマンドの出力が判別できなければ、GPTに投げて解析してもらい、次の指示をもらおう。
プロセスのcgroupを確認する。たとえば自分の環境では 0::/system.slice/docker-9cffb5dca4c53cf437c1499d7df3725132406b42b3e4873837ec1487a7aa2b46.scope と返ってきた。これで、CONTAINER ID がこの文字列であるコンテナが侵入されたと特定できる。
sudo cat /proc/<PID>/cgroup
プロセスの基本情報を確認する。
sudo ps -o pid,ppid,user,cmd -p <PID>
親プロセスの情報を確認する。
sudo ps -o pid,ppid,user,cmd -p $(ps -o ppid= -p <PID>)
プロセスのネットワーク接続状況を確認する。
sudo ss -antup | grep -E "pid=<PID>," || true
プロセスが開いているファイルとネットワーク接続を詳しく一覧表示する。
sudo lsof -nP -p <PID> -i 2>/dev/null | head -n 50
[!NOTE]
上のコマンドの出力が読めなくて調査を進められないなら、そのままGPTに丸投げして、次の手順を教えてもらえばいい。
自分の対応
調べた結果、自分の環境ではDockerコンテナ 9cffb5dca4c53cf437c1499d7df3725132406b42b3e4873837ec1487a7aa2b46 が原因だとわかった。あとはこいつが何者かを直接調べるだけだ。
sudo docker inspect 9cffb5dca4c53cf437c1499d7df3725132406b42b3e4873837ec1487a7aa2b46 --format 'ID={{.Id}}
Name={{.Name}}
Image={{.Config.Image}}
Created={{.Created}}
Entrypoint={{json .Config.Entrypoint}}
Cmd={{json .Config.Cmd}}
WorkingDir={{.Config.WorkingDir}}
EnvCount={{len .Config.Env}}
Mounts={{json .Mounts}}
Ports={{json .NetworkSettings.Ports}}'
返ってきたのは次のとおり。
ID=9cffb5dca4c53cf437c1499d7df3725132406b42b3e4873837ec1487a7aa2b46
Name=/shiroi
Image=ghcr.io/stbanana/shiroi:latest
Created=2026-01-26T07:33:07.293181432Z
......
また、先ほどの親プロセスの調査でも次のことがわかっていた。
[lighthouse@VM-20-5-opencloudos ~]$ sudo ps -o pid,ppid,user,cmd -p $(ps -o ppid= -p 53075)
PID PPID USER CMD
2206 1954 root next-server (v
もう一つの傍証として、フロントエンドのイメージバックアップが2GBに膨らんでいた。保存してあった安定版バックアップの400MBとはえらい違いだ。
というわけで、原因はブログのフロントエンドである next.js を直接公開していたことだと確認できた。でも直し方はわからないし、フロントエンドを最新版に更新する気もない(in神が作り直したので、バックエンドの更新と移行を同時にやるのが面倒)。
なら、read_only でデプロイすればいい。docker-compose.yml の image と同じレベルに read_only: true を追加する。現在の設定はこれ。
version: '3'
services:
shiro:
container_name: shiroi
image: shiroi:local
volumes:
- ./.env:/app/.env
read_only: true
restart: always
ports:
- 2323:2323
ローカルDockerイメージのtarアーカイブの食べ方
ところで、さっき image: shiroi:local とローカルイメージを使ったが、これは pull せずに済む。保存してあった安定版のtarアーカイブから作ったイメージだからだ。
この操作は何度もやっているのに、毎回コマンドを忘れて調べ直している。tarアーカイブのある場所へcd → docker load でイメージを読み込む → 自分にわかるタグを付ける の流れだ。
こんな感じ。
[root@VM-20-5-opencloudos ~]# cd /root/mx-space/Shiroi
[root@VM-20-5-opencloudos Shiroi]# sudo docker load -i shiroi.tar
b0f89ac7b966: Loading layer 148.4MB/148.4MB
d3b1ea8ff6e6: Loading layer 5.39MB/5.39MB
2497eefc5264: Loading layer 3.584kB/3.584kB
769a089b3661: Loading layer 30.95MB/30.95MB
8a9366fbc004: Loading layer 1.536kB/1.536kB
556c774c2fcc: Loading layer 8.075MB/8.075MB
55405dd49280: Loading layer 4.096kB/4.096kB
66b48a972c24: Loading layer 24.75MB/24.75MB
7f1566c30993: Loading layer 24.29MB/24.29MB
911870b37b57: Loading layer 27.33MB/27.33MB
14c1060772f2: Loading layer 6.655MB/6.655MB
7be455708435: Loading layer 12.18MB/12.18MB
6fdc0a038ef1: Loading layer 5.213MB/5.213MB
ba8c6d828d78: Loading layer 2.56kB/2.56kB
5c557125ffd5: Loading layer 160.3kB/160.3kB
77abd848d7fc: Loading layer 110.1kB/110.1kB
e7f72ced6a18: Loading layer 111.2MB/111.2MB
0d71f84d6130: Loading layer 23.76MB/23.76MB
dade50077aed: Loading layer 22.65MB/22.65MB
Loaded image ID: sha256:8c476da1052b3d1e26bdaae875b0ac026b6c26869224732f2deee58a8349911a
[root@VM-20-5-opencloudos Shiroi]# sudo docker tag sha256:8c476da1052b3d1e26bdaae875b0ac026b6c26869224732f2deee58a8349911a shiroi:local
[root@VM-20-5-opencloudos Shiroi]#
pull せずにデプロイできる compose コマンドで起動する。
docker compose up -d