はじめに
個人サイト(静的 HTML + 自作アクセスカウンターの PHP)を、Vultr の小さな VPS(1vCPU / 1GB)で運用しています。
ある朝、AI(Claude Code)に「PHP のバージョン確認」と頼んだのがきっかけで、次の 3 つが分かりました。
-
php -vは 8.3.6 のまま。ところが中身には、今年だけで 5 回 セキュリティ修正が入っていた - PHP の修正は自動で効いていた。一方で カーネルの更新は 8 日間、再起動待ちのまま 放置されていた
- 再起動しようとしたら、サーバー丸ごとのバックアップが無い ことに気づいた
この記事は、その確認から再起動までの記録です。
前回の記事:1日数人の個人サイトが1か月で33万回攻撃されていた
1. 「8.3.6」は変わらないが、中身は変わっている
$ php -v
PHP 8.3.6 (cli) (built: Oct 7 2026 18:06:38) (NTS)
with Zend OPcache v8.3.6, Copyright (c), by Zend Technologies
Ubuntu 24.04 の PHP は、上流のバージョン番号(8.3.6)を固定したまま、修正だけを取り込む(バックポート)方式で更新されます。変わるのは、パッケージ版の末尾の数字だけです。
/var/log/apt/history.log を見ると、今年の更新は次の 5 回でした(すべて自動更新 unattended-upgrades による早朝の適用です。最後の 1 行は、記事の公開後に自分で行った更新で、末尾の追記で説明しています)。
| 適用日時(日本時間) | パッケージ版の変化 |
|---|---|
| 2026-01-14 06:44 |
0ubuntu0.24.04.5 → .6
|
| 2026-05-29 06:19 |
.6 → .9
|
| 2026-07-22 06:39 |
.9 → .10
|
| 2026-09-12 06:43 |
.10 → .11
|
| 2026-10-21 06:02 |
.11 → .12
|
| 2026-10-22 10:19 |
.12 → .13(自動更新を待たず、自分でコマンドを打って更新。首相が呼びかけた 2 件の修正。詳しくは末尾の追記) |
「PHP のバージョンは?」と聞かれて 8.3.6 とだけ答えると、パッチが当たっているかどうかは分かりません。 確認すべきなのはパッケージ版です。
dpkg -l | grep php8.3-fpm
# 修正内容は changelog に CVE 番号付きで書かれている
zcat /usr/share/doc/php8.3-common/changelog.Debian.gz | head -40
最新の .12 には 10 件の CVE 修正が入っていました(changelog より)。主なもの 3 件:
| CVE | 内容 |
|---|---|
| CVE-2026-92842 | ストリームフィルタの改行文字オプションに NUL を含めると、ヒープの範囲外を読む |
| CVE-2026-91768 | PHP-FPM の allowed_clients が、IPv6 アドレスの先頭 96 ビットしか照合していなかった |
| CVE-2026-91769 | OpenSSL の相手確認で、本来使わない CN(証明書の名前欄)に戻って照合してしまう |
2. PHP は自動で効いていた。カーネルは効いていなかった
更新されたファイルは、動いているプロセスが読み直すまで効きません。 確認したのは次の 2 点です。
# php-fpm がいつ起動したか(更新より後なら、新しい版で動いている)
systemctl show php8.3-fpm -p ActiveEnterTimestamp
# → Thu 2026-10-22 06:30:58 JST
# 再起動が必要なサービスとカーネルの一覧
needrestart -b
# NEEDRESTART-KCUR: 6.8.0-136-generic ← 動いているカーネル
# NEEDRESTART-KEXP: 6.8.0-147-generic ← 入っている新しいカーネル
# NEEDRESTART-KSTA: 3 ← 3 = 新しいカーネルが入っているのに使われていない
-
php-fpm:更新の後に起動し直しており、
needrestartの「再起動が必要な一覧」にも入っていませんでした。修正は効いています。 -
カーネル:
/var/run/reboot-requiredの日付は 10/14。このサーバーでは自動再起動(Unattended-Upgrade::Automatic-Reboot)を有効にしていないので、自動更新はカーネルを入れるところで止まります(有効にすることもできます)。そのため、8 日間、古いカーネルのまま動いていました。
稼働日数は 94 日でした。「落ちずに動き続けている」ことと「最新の修正が効いている」ことは、別の話です。
3. 再起動の前に:バックアップが「ある気がしていた」
再起動を頼むとき、私は「バックアップは充分ですね」と添えました。AI の答えは「充分とは言えません」でした。
| 対象 | 実際の状態 |
|---|---|
| サイトの HTML | 手元の作業フォルダと git にある → 戻せる |
| サーバー丸ごと | 無い(手元にあるのは 1 年以上前の移行時の zip だけ) |
| nginx・SSH の設定(7 月に固めた防御設定を含む) | 無い |
| アクセスカウンターのデータ・攻撃の月次記録 | 無い |
再起動そのものでデータが消えることはありません。怖いのは「新しいカーネルで起動しない」という、まれなケースです。それでも、「戻せる範囲でバックアップがあればよい」と判断し、再起動の前に必要なものだけ固めて手元に取りました。
# 出力先は /var/tmp(理由は後述)
tar czf /var/tmp/prereboot_20261022.tgz -C / \
etc var/www var/lib/tkurume-counter var/lib/attack-summary \
var/lib/fail2ban var/spool/cron usr/local root home
# 環境を再現するための一覧も別に取る
dpkg --get-selections > packages.txt
apt-mark showmanual > packages_manual.txt
systemctl list-unit-files --state=enabled > enabled_units.txt
約 355MB でした。手元に scp で取り、SHA-1 がサーバー側と一致する ことを確かめてから再起動しました。この一致は転送の確認で、復元できることの確認ではありません。完全なシステムイメージでもなく、復元テストは未実施です。
つまずき:tar が自分自身を固めていた
最初は出力先を /root/prereboot_20261022.tgz にし、--exclude=/root/prereboot_20261022.tgz を付けていました。
tar: root/prereboot_20261022.tgz: file changed as we read it
-rw-r--r-- 1 root root 709296016 ...
-C / で相対パス(root/...)として固めているため、先頭に / の付いた除外指定は一致しません。結果、書きかけの自分自身を取り込み、約 2 倍の 709MB に膨らんでいました。出力先を、固める対象の外(/var/tmp)に変えて解決しました。
4. 再起動と確認
# SSH のセッションが切れる前にコマンドを返すため、少し遅らせて再起動
nohup sh -c "sleep 2; systemctl reboot" >/dev/null 2>&1 &
つまずき:「復帰した」と早とちりする待ちループ
最初に書いた待ちループは、次のようなものでした。
ssh host 'cat /proc/uptime' | awk '{exit !($1<600)}' # 起動 600 秒未満なら復帰とみなす
再起動 5 秒後に「復帰した」と判定しましたが、実際にはまだ SSH がつながっていませんでした。ssh が失敗すると awk には 1 行も入力が来ません。行が無いと判定式は一度も実行されず、awk は終了コード 0 で終わります。 その結果、「成功」と判定されていました。
再起動の前後で boot_id(起動ごとに変わる ID)を比べるのが確実です。SSH がつながるだけでは、再起動前のサーバーにつながった場合と区別できません。
before=$(ssh host 'cat /proc/sys/kernel/random/boot_id') && [ -n "$before" ] || exit 1
ssh host 'nohup sh -c "sleep 2; systemctl reboot" >/dev/null 2>&1 &'
ok=0
for i in $(seq 1 60); do # 最大 60 回試行(5 秒間隔+SSH の待ち時間)
sleep 5
after=$(ssh -o ConnectTimeout=5 host 'cat /proc/sys/kernel/random/boot_id' 2>/dev/null) || continue
if [ -n "$after" ] && [ "$after" != "$before" ]; then echo "再起動完了($i 回目の試行)"; ok=1; break; fi
done
[ "$ok" -eq 1 ] || { echo "時間切れ"; exit 1; }
そのうえで、カーネル版・サービス・HTTP 応答を別に確認します(下の表)。
再起動後の確認結果
| 確認項目 | 結果 |
|---|---|
| カーネル |
6.8.0-136 → 6.8.0-147
|
/var/run/reboot-required |
消えた |
| nginx / php8.3-fpm / fail2ban / ssh / cron / ufw | すべて active。failed のサービスは 0 |
SSH の設定(sshd -T) |
passwordauthentication no のまま |
| サイト(トップ・主要ページ) | 200、応答 0.05 秒前後 |
| PHP のカウンター | 200 で正常に応答 |
サイトが止まったのは 1 分ほどでした。
なお、カウンターの PHP では、今も mb_* 関数を使えません(サーバーに mbstring が入っていないため。前の記事 で詳しく書きました)。PHP が更新されても、ここは変わりません。
まとめ
-
php -vの 8.3.6 だけでは、パッチの有無は分からない。 パッケージ版(0ubuntu0.24.04.12など)と changelog を見る。緊急時はapt-get updateで一覧を更新してからapt list --upgradableを見る -
自動更新は(自動再起動を有効にしていなければ)「入れる」まで。 php-fpm は再起動され修正が効いていたが、カーネルは再起動するまで古いまま。
needrestart -bと/var/run/reboot-requiredで確認できる - 再起動の前に「戻せるか」を確かめる。 「バックアップがある気がする」は、確かめると無いことがある
- tar の出力先は、固める対象の外に置く。
-C /を使うときは、除外指定に先頭の/を付けない - 待ちループは「失敗したときに何が返るか」まで考えて書く
今回のバックアップは手動で 1 回取っただけです。自動バックアップの仕組み作りは、次の宿題です。
追記(2026-10-22 10:19):首相が呼びかけた 2 件は、この時点ではまだ直っていなかった
この記事を公開した後、高市首相が X で「PHP のパッチを大至急適用して」と呼びかけていたことを知りました。対象は CVE-2026-109618 / 109619(外部からプログラムを実行される恐れ・CVSS 9.2)で、本文で紹介した .12 の 3 件とは別物です。
調べると、Ubuntu からさらに新しい 0ubuntu0.24.04.13 が出ていました。まだ入っていなかったので、すぐに入れました。
apt-get update # 一覧を更新(これを忘れると新しい修正版が見えない)
apt list --upgradable | grep php # .13 が出ていた
apt-get install --only-upgrade php8.3-cli php8.3-common php8.3-fpm php8.3-opcache php8.3-readline
dpkg -l php8.3-fpm # 入った版
systemctl show php8.3-fpm -p ActiveEnterTimestamp # サービスが更新後に起動し直したか
zcat /usr/share/doc/php8.3-common/changelog.Debian.gz | grep 109618 # 修正が入ったことを確認
PHP-FPM は自動で起動し直し、止まったのは 1 秒ほどでした。
「今朝、自動更新が効いていた」は「今日の最新の修正が入っている」と同じではありません。 大きな脆弱性のニュースを見たら、apt-get update で一覧を更新してから apt list --upgradable を見ます。入れたあとは、パッケージ版とサービスの起動時刻も確認します。
追記 2(2026-10-22 22:30):放っておいたらどうなっていたか — 12 時間後に CVE 番号を名乗るアクセスが来ていた
.13 を入れたあと、「自動更新に任せていたらどうなっていたか」を nginx のログで調べました。
-
10/22 22:06、User-Agent にこの脆弱性の CVE 番号を名乗るアクセス(
POST /index.php)が 1 件ありました。前日 19:25 に同じ発信元からトップページへのアクセスが 1 回あります(下見だったかどうかは推測です)。 - このサーバーの nginx は
index.phpを PHP-FPM に渡す設定なので、要求は PHP まで届いています。応答は通常のトップページ(200・38,261 バイト)でした。 - ただし、アクセスログだけでは、この要求が実際に脆弱性を突く内容だったか、修正によって阻止されたかまでは断定できません。言えるのは「その時点で修正版は適用済みだった」「確認したファイルとログの範囲では、侵入・改ざんを示す形跡は見つかっていない」ことです。
-
.13を自分で入れたのが 10:19。次の自動更新処理は 10/23 06:41 に実行予定(最大 1 時間のランダム遅延あり)でした。それまでに別の更新が走らなければ、未修正の状態でこのアクセスを受けていた可能性があります。
# CVE 番号を名乗るスキャナは、番号で grep すると拾える
zcat -f /var/log/nginx/access.log* | grep -iE "10961[89]"
修正版の公開から 1 日たらずで、CVE 番号を名乗るスキャナが「1 日数人の個人サイト」にまで届きます。緊急の修正は「出た当日に来る」前提で動くのがよさそうです。
あわせて確認したこと(侵入・改ざんの形跡なし):
- サーバー上の PHP 14 本すべてを、手元の作業フォルダと SHA-1 で照合(差分なし)
- 拡張子を偽装した PHP、画像の中に仕込まれた PHP などの不正ファイル:なし
- PHP-FPM の異常終了・nginx の upstream エラー:10/20 以降 0 件
これらは確認した範囲での結果で、あらゆる侵害を否定するものではありません。