技術

2026年10月22日

PHPの緊急脆弱性に、WordPressサーバーを丸ごと入れ替えて1日で対応した記録

2026年10月21日夜に公表された PHP のリモートコード実行の脆弱性「php2shell」に対して、弊社が運用する WordPress サイト5台を、翌22日の1日で新しいサーバーへ丸ごと入れ替えました。古いサーバーには修正版の PHP を入れる手段がなかったためです。切り替えは固定 IP の付け替えで行い、DNS は変えず、サイトの停止は数分のメンテナンス表示だけで済みました。あわせて、サーバーの堅牢化と、不正侵入の痕跡を毎時見張る仕組みも入れています。この記事は、その判断と手順、つまずいた点をまとめた記録です。

この記事は、サーバー名・IP アドレス・お客様名は伏せています。

php2shell とは

php2shell は、PHP 本体に見つかった2件のリモートコード実行(RCE)の脆弱性、CVE-2026-109618 と CVE-2026-109619 の通称です。CVSS v4.0 はどちらも 9.2 です。ある HTTP リクエストが残した不完全なメモリの状態を、別のリクエストで悪用する仕組みです。

  • 状態を汚す入口として知られているのは、既定で有効な Session Upload Progress 機能です
  • mod_php・php-fpm、WordPress・Laravel・Symfony など、よくある構成で再現したと報告されています
  • 修正版は 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 で、日本時間の10月21日 22時40分ごろに公開されました
  • 8.1 以前はサポートが終わっており、修正版は出ません

公開されている Web サイトならどこからでも狙え、突かれればサーバーそのものを乗っ取られます。そのため「次の定期メンテナンスで」ではなく、修正版が出たその日のうちに当てると決めました。(参考: GMO Flatt Security Blog、PHP 公式の告知)

当夜の暫定対策

修正版が出る前の夜は、「攻撃の入口を狭める」ことだけに絞りました。どれも根本対策ではなく、修正版を当てるまでの時間稼ぎです。

  1. 全台のスナップショットを取る(いつでも戻れる状態にしてから触る)
  2. session.upload_progress.enabled = 0 と session.use_strict_mode = 1 を設定する
  3. Apache で multipart/form-data の POST を、自社の VPN 以外からは拒否する

3 は、お問い合わせフォームの添付や管理画面からの画像アップロードも止まる強い手です。管理画面を使うのは社内と一部の関係者だけなので、VPN の IP だけを例外にして一晩だけ使いました。

確認と適用は、サーバーごとに同じスクリプトを check と apply の2段で流しました。サーバーの構成によって設定ファイルの場所が違うため、スクリプト側で場所を判定させています。

パッチではなく入れ替えを選んだ理由

5台のうち4台は、サーバーの中で PHP だけを更新する手段がありませんでした。PHP や Web サーバーが一式で組み込まれた構成で、OS を更新しても PHP は新しくなりません。修正版の PHP を入れるには、サーバーを作り直すしかありませんでした。

サイトサーバー内での PHP 更新対応
お客様A(EC あり)本番できない新サーバーへ入れ替え
お客様A ステージングできない新サーバーへ入れ替え
お客様Bできない新サーバーへ入れ替え
個人サイトできない新サーバーへ入れ替え
自社サイトできるその場で PHP 8.4 に更新

移り先は、OS のパッケージで PHP を更新できる Debian 12 のサーバーです。PHP は Sury リポジトリ の 8.4 を使い、unattended-upgrades でセキュリティ更新が自動で入るようにしました。次に同じことが起きても、修正版が出れば一晩で勝手に当たります。「更新できない構成」そのものをやめることが、今回いちばん大きな成果です。

止めずに入れ替える手順

要は、新サーバーを完成させてから固定 IP を付け替えることです。固定 IP はサーバーの間で数秒で付け替えられるので、DNS の浸透を待つ必要がありません。毎回同じ順番で進められるよう、各段階をスクリプトのサブコマンドにしました。

  1. prep: 新サーバーに PHP 8.4(php-fpm)、必要なパッケージ、監視エージェントを入れる
  2. sync: 旧から新へ wp-content と運用スクリプトを rsync、DB をダンプして取り込む。差分だけなので何度でも流せる
  3. config: wp-config・Web サーバー・cron を新サーバー用に書き換え、旧サーバー固有のパスを DB 内も含めて置換する
  4. verify: curl --resolve で本番ドメイン名のまま新サーバーの仮 IP につなぎ、主要ページ・カート・管理画面を旧と比べる
  5. freeze: 旧をメンテナンス表示にし、cron と監視を止める(二重実行・二重通知を防ぐ)
  6. 最後の sync と config → 固定 IP を新へ付け替える
  7. enable: 新で証明書を取り直し、cron・常駐監視・メトリクス送信を始める

切り戻しは unfreeze(旧のメンテナンス表示を外し、cron と監視を戻す)と IP の付け戻しだけです。旧サーバーは停止して残し、切り替え直前のスナップショットも取っています。

EC のあるお客様Aの本番は、切り替える時刻を事前に先方へ伝え、その時刻どおりに実施しました。メンテナンス表示が出ていたのは freeze から IP 付け替えまでの数分です。切り替え後は、新旧の設定とコンテンツの差分を別のスクリプトで洗い出し、漏れがないか確かめています。

つまずいたところ

いちばん大きかったのは、切り替えたばかりのお客様Bのサイトが朝に約10分、応答しなくなったことです。特定の IP から接続を開けたままにする Slowloris 型のアクセスが来ていて、ログには 408(タイムアウト)が並んでいました。その時点の新サーバーは Apache prefork + mod_php で、1接続に1プロセスを占有するため、開けっ放しの接続で枠が尽きます。

最初はネットワーク側の問題と見誤り、同時接続数(MaxRequestWorkers)を下げるという逆効果の手も打っています。最終的には、接続を少ないスレッドで持てる mpm_event + php-fpm に切り替え、mod_reqtimeout でヘッダーや本文を遅く送る接続を早めに切るようにしました。このあとは全台を同じ構成にしています。

そのほかは、イメージの違いや古い設定の名残りが原因でした。

起きたこと原因対処
cron が動かない新しいイメージに cron パッケージが入っていないprep で cron を入れて有効化
初期の PHP が未パッチイメージ作成時点の版のまま最初に apt upgrade、そのあと 8.4 へ
テーマ内の監視スクリプトが落ちる旧サーバーのパスを決め打ちしていたcron 実行時だけ auto_prepend_file で先に定義(テーマは触らない)
旧サーバーでメンテナンス表示にならない旧の wp-cli が何も言わず失敗.maintenance を直接書く
DB の 3306 番が外から見えるMariaDB が systemd のソケット起動で、bind-address が効かないmariadb.socket の待ち受けを 127.0.0.1 に上書き
バージョン表示とディレクトリ一覧が消えないDebian 標準の security.conf と apache2.conf が自分の設定より優先される設定ファイル名を zz- 始まりにして最後に読ませ、apache2.conf の Indexes も外す
大きなプラグインの削除で wp-cli が固まる削除時にプラグイン自身の処理が走る止めて、ディレクトリごと退避
ステージングの同期が途中で止まる旧に rsync がない、入れ子の ssh が標準入力を食う、wp-config がシンボリックリンクrsync を入れる(入らなければ tar)、</dev/null、rsync -L

入れ替えのついでに固めたこと

入れ替えで空のサーバーから作り直すので、何年も手を付けていなかった設定も一緒に見直しました。どれも、攻撃者が最初に測る「入口」を減らすためのものです。

対象やったこと
PHP8.4(php-fpm)。セキュリティ更新は自動適用
Apacheバージョン非表示、ディレクトリ一覧なし、.git・ログ・バックアップ・readme へのアクセス拒否、xmlrpc.php 拒否、uploads 内での PHP 実行禁止
WordPress未ログイン時のユーザー一覧 API と ?author= を隠す、アプリケーションパスワード無効、管理画面からのテーマ・プラグイン編集禁止(DISALLOW_FILE_EDIT)。これらは mu-plugin 1枚にまとめて全サイト共通
DB・phpMyAdminMariaDB は 127.0.0.1 のみ。phpMyAdmin はサーバー内からのみ(使うときは SSH トンネル)
アカウントイメージが作る初期管理者を削除。今年ログインのない管理者は権限を下げた
プラグイン・テーマ無効のまま残っていたもの、移行ツール、使っていない標準テーマを削除。開発が止まったプラグインは代わりのものに置き換え、記事も直した
SSH接続元を自社 VPN に限定

削除したプラグインやファイルはすぐに消さず、数日はサーバー内の退避先に移しておきます。何かが壊れても、元に戻すのは mv 一つで済みます。

侵入の痕跡を見張る仕組み

修正版を当てる前にすでに入られていたら、パッチでは消えません。そこで全台に、「正常な状態」を記録して毎時そこからの差分を見る小さなシェルスクリプトを入れました。新しいツールを入れず、find・sha256sum・wp-cli・systemd のタイマーだけで動きます。

見ているのは、侵入者が居座るためにほぼ必ず触る場所です。

  • OS: ログインできるユーザー、sudo 権限、SSH 鍵、cron、systemd のサービス、待ち受けポート、SUID ファイル
  • WordPress: 管理者、重要な設定(siteurl・admin_email・登録可否など)、すべての PHP と .htaccess のハッシュ、uploads に置かれた PHP
  • その場の状態: /tmp などから動いているプロセス、許可 IP 以外からの SSH ログイン、WordPress 標準以外の PHP への POST、本体ファイルの改ざん

設計でいちばん気を使ったのは、通知を少なくすることです。毎週のように誤報が来ると、いずれ本物も見過ごされます。

  • WordPress 本体とプラグインの更新は、wordpress.org の公式チェックサムと一致すれば正当とみなす
  • Git で管理しているテーマは、作業ツリーが clean(反映スクリプト経由の変更)なら正当とみなす
  • 同じ内容は二度通知しない。解消したら一度だけ知らせる
  • 毎週月曜の朝に「異常なし」を一度だけ送る。静かなのが「健全」なのか「止まっている」のかを区別するため

通知先は担当者だけの非公開 Slack チャンネルとメールです。正当な変更だと確かめたら intrusion-check --accept で記録し直します。

導入したその日の午後、さっそく2台から通報が来ました。「PHP ファイルが 74 件と 27 件変わった」「管理者メールアドレスが変わった」という内容です。調べると、標準テーマと翻訳ファイルの WordPress 自動更新が同じ分にまとまっていて、メールアドレスは担当者が自分で変えたものでした。テーマと翻訳ファイルには公式のチェックサムがないため、正当な更新でも鳴っていたのです。

そこで、除外を広げるのではなく、「正当だと確かめる方法」を足しました。

  • テーマ: 同じバージョンの公式 zip を wordpress.org から取ってきて、変わったファイルが完全に一致すれば正当
  • 翻訳ファイル(*.l10n.php): PHP のトークナイザーで解析し、文字列の配列を返すだけで関数呼び出しも変数もなければ正当
侵入検知の判定の流れ

差分があっても、公式の配布物や Git と照らして正当だと確かめられれば、基準を更新して黙ります。通報が来るのは、確かめられなかったときだけです。

改ざんした翻訳ファイルや、テーマに足された不審なファイルは、今も通報されます。一方で、WordPress が自動で更新しただけなら鳴りません。無視する範囲を広げると、そこがそのまま隠れ場所になります。「安全だと証明できたものだけ黙る」のが、誤報を減らすときの原則だと考えています。

運用の見直し

入れ替えを機に、「誰がどこに入れるか」を必要な分だけに絞りました。旧サーバーでは、外部のデザイナーも sudo できる共用ユーザーで本番とステージングに SSH できる状態でした。

誰本番ステージング
弊社担当者SSHSSH
外部のデザイナーなしテーマのフォルダだけに SFTP
お客様管理画面のみ管理画面のみ
  • 本番のテーマは、Git の production ブランチから反映スクリプトでしか変えない。スクリプトは反映前に全 PHP の構文を確かめ、反映後に主要ページが 200 を返さなければ自動で戻す
  • デザイナーはステージングにアップロードし、担当者が差分を Git に記録してから本番に出す
  • SFTP 専用アカウントはシェル不可・chroot で、テーマのフォルダだけが見える。bindfs で見せることで、アップロードされたファイルも実体は運用ユーザーの所有になり、反映スクリプトの所有者チェックもそのまま動く
  • SSH と SFTP は自社 VPN からのみ。個人の自宅 IP を許可リストに足していく運用をやめた

移行のとき、旧サーバーの共用ユーザーにあったデザイナーの公開鍵も、一度は新サーバーの管理ユーザーに引き継がれていました。「今までどおり入れるように」と移すと、権限もそのまま持ち込みます。鍵ごとに「この人には何が必要か」を判断し直し、専用アカウントに移しました。

まとめ

今回いちばん効いたのは、「修正版が出たら当てられる構成」に移ったことです。緊急の脆弱性はまた来ます。次は、自動更新と侵入検知の通知を待つだけで済むはずです。

同じように古い WordPress サーバーを運用している方向けに、確かめることを並べます。

  • PHP が OS とは別に同梱された構成になっていないか。その場合、OS を更新しても PHP は新しくならない
  • php -v が各ブランチの修正版(8.2.35 / 8.3.36 / 8.4.27 / 8.5.12)以上か。当てるまでは session.upload_progress.enabled = 0 と、不要なら multipart POST の遮断
  • セキュリティ更新が自動で入るか(unattended-upgrades と、PHP を入れたリポジトリがその対象になっているか)
  • DB(3306)や phpMyAdmin が外から見えていないか。MariaDB はソケット起動にも注意
  • イメージが作った初期管理者や、使っていない管理者が残っていないか
  • 共用ユーザーに、誰のものか分からない SSH 鍵がないか
  • 「入られたときに気づけるか」。何もなければ、まず管理者・SSH 鍵・cron・PHP ファイルの差分だけでも見る

弊社では、WordPress サイトの保守やサーバーの移行・堅牢化のご相談をお受けしています。

資料ダウンロード
資料ダウンロード
お問い合わせをご検討中の方は、ぜひ会社案内パンフレットもご覧ください。当社の特徴や実績集などをご紹介しています。
ダウンロードする
ただいま準備中です
お問い合わせ
お問い合わせ
WEB制作・システム構築・インフラ構築におけるご相談は以下まで。
お問い合わせする