3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PHPの脆弱性「php2shell」が来たので、Amazonの公式パッケージを待たずにPHPを自分でビルドして本番に入れた話

3
Posted at

ChatGPT 画像 2026年11月2日 19_57_48.png

はじめに

2026年10月に公表された PHPの脆弱性「php2shell」に、公表当日の夜に対応しましたので、備忘録としてやったことを記載していきます!

いろんなPHPを使っている企業がメンテになって、中には、まる1日メンテになってる企業もありましたね、、

僕たちのプロジェクトは大型のイベントがあったので、メンテをせずパッチを当てました。

その時、実際にやったことや躓いたことを記載していきます。

ざっくりやったこと

本番は Amazon Linux 2023の EC2で、PHPはOSのパッケージ(php8.4-*)として入れています。
ふだんなら dnf upgrade で済む話ですが、修正版の PHP 8.4.27 が php.net で公開された時点では、Amazon Linux の修正パッケージがまだ出ていませんでした。

そこで、Amazon のソースRPMを使い、中身だけphp.net 8.4.27 に差し替えて自前でRPMをビルドし、dev サーバーで試してから本番へ入れました。

devでRPMをビルドして動作確認 → RPMをS3にアップロード → 本番のEC2でダウンロードして入れ替え

環境

  • AWS EC2(Amazon Linux 2023)
  • Apache httpd + php-fpm
  • Laravel
  • 本番には SSM でコマンドを送り、dev には SSH で入っています

用語

この記事でよく出てくる言葉です。

  • RPM:Amazon Linux などの Red Hat 系 Linux で使うソフトのパッケージ形式です(拡張子は .rpm)。アプリのインストールファイルのようなものです
  • ソースRPM:完成品ではなく、「材料(ソースコード)」と「作り方(spec ファイル)」のセットです
  • dnf:RPM をリポジトリから取ってきて、入れたり更新したりするコマンドです

やったことのタイムライン(日本時間)

  • 10/20:php.net が「10/21 23:00 にセキュリティリリースを出す」と予告
  • 10/21 夜:本番に緩和策を入れる
  • 同 夜:本番のPHPを、その時点で Amazon Linux に出ていた最新の PHP パッケージまで上げておく
  • 23:00:php.net が 8.4.27 を公開。Amazon Linux の修正パッケージは出ていない
  • 23:21:dev で 8.4.27 のビルドを開始
  • 23:59:ビルド成功。dev に入れて確認
  • 10/22 0:06:本番に自前ビルドの 8.4.27 を入れる
  • 翌朝:Amazon 公式の 8.4.27 が出たので、dnf upgrade で置き換える

1. 脆弱性について

きっかけは Flatt Security さんの記事でした。

  • PHPで外部から任意のコードを実行される脆弱性
  • php-fpm でも再現し、Laravel 構成も対象
  • 10/21 23:00(日本時間)に php.net が修正版を出す

修正版が出ると、修正前後のソースの差分から「どこを直したか=脆弱性の場所」が誰でも分かるようになります。
つまり、23:00 を境に攻撃できる人が一気に増えるということです。

総理も注意を呼びかけていました。

2. 緩和策を入れる(サイトは止めない)

パッチが出るまでのつなぎとして、攻撃の入口と見られている「Session Upload Progress」という機能を止めました。

ファイルをアップロードしている最中に、進み具合を PHP がセッションに書き込んでいく機能です。外から送られた値を、アプリのコードより先に PHP 自身が処理するので、アプリ側でいくら入力チェックをしていても防げません。

アプリでこの機能を使っていないことを確認してから止めました(Laravel は PHP 標準のセッションを使わないので、だいたい使っていないはずです)。

php.ini(PHPのメイン設定ファイル)本体は触らず、/etc/php.d/(PHP の追加の設定ファイルを置くフォルダ)にファイルを1つ足すだけにしています。消せば元に戻せます。

echo "session.upload_progress.enabled = 0" > /etc/php.d/99-php2shell-mitigation.ini
php-fpm -t                    # 設定の構文チェック
systemctl reload php-fpm      # reload なら処理中のリクエストは切れない
php-fpm -i | grep "^session.upload_progress.enabled"
session.upload_progress.enabled => Off => Off

CLI(php -i)と php-fpm は別々に設定を読むので、サイトを動かしている php-fpm 側(php-fpm -i)でも Off になっているかを確認します。設定を変えたら systemctl reload php-fpm で読み直させるのも忘れずに。

3. パッチを当てられる状態かを確認する

Amazon Linux 2023 は、リリース(releasever)を固定していると新しいパッケージが降ってきません。そこで、固定されていないか、修正パッケージが来ているかを確認しました。

cat /etc/dnf/vars/releasever 2>/dev/null || echo "(ファイルなし = 作成時の版に固定)"
dnf -q --releasever=latest list --available "php8.4" "php8.4-fpm"
dnf -q --releasever=latest updateinfo list --available | grep -i php   # Amazon のセキュリティ告知

本番の PHP はしばらく更新できていなかったので、23:00 を待つ間に、その時点で Amazon Linux に出ていた最新の PHP パッケージまで先に上げておきました。

いきなり上げるのは怖いので、まず --assumeno(全部の質問に自動で No と答えるオプション) で何が入れ替わるかだけを見ます。

dnf upgrade --assumeno "php8.4*" php-pear   # 見るだけ。何も入れない

入れ替わるのが php8.4 系のパッケージだけだったので、そのまま更新しました。

dnf -y upgrade "php8.4*" php-pear
php-fpm -t && systemctl restart php-fpm

万一のときに dnf downgrade(古いパッケージに戻せるコマンド) で戻せるよう、古い版のパッケージがリポジトリに残っていることも事前に確認しています。

4. 23:00、php.net は出た。でも Amazon Linux には来ない

php.net は予告どおり 23:00 に 8.4.27 を公開しました。ところが、Amazon Linux のリポジトリには来ていません。

# dnf はリポジトリの一覧をしばらく使い回すので、--refresh で取り直す
dnf -q --refresh --showduplicates list --available "php8.4-fpm"

php.net が出す時間と、Amazon がパッケージを出す時間は別なんですよね。冷静に考えれば当たり前なんですが、その場では「23時に出たらすぐ当てられる」と思い込んでいました。

このとき、「php.net からダウンロードして PR で上げられないの?」「composer で上げられないの?」とも考えましたが、どちらも無理でした。PHP 本体はアプリのリポジトリにも vendor/ にも入っておらず、サーバーの OS にパッケージとして入っているからです。composer.json の "php": "^8.2" は動作条件を書いているだけです。

選択肢は「Amazon を待つ」か「自分でビルドする」かの2つで、今夜中に上げるために自分でビルドすることにしました。

いつAmazonが対応してくれるかわからないし、その間に攻撃を受ける可能性があるから早めの方が良いという判断です。

5. 方針:Amazon のソースRPMを使って、中身だけ差し替える

ソースから ./configure && make install で入れる方法もありますが、

  • 今の php8.4-* パッケージと二重管理になる
  • Amazon が当てているパッチや、設定ファイルの置き場所が変わる
  • Amazon の公式版が出たときに、きれいに戻せない

ので、やめました。代わりに次のようにしています。

  1. 今入っている版の Amazon のソースRPM を取ってくる
  2. 中の spec ファイル(作り方の手順書)のバージョンを 8.4.27 に書き換え、材料を php.net の 8.4.27 のソースに差し替える
  3. Release 番号を 0.1.php2shell にしてビルドする

ポイントは 3 です。Amazon の公式版は 8.4.27-1... になるはずなので、自前ビルドの 8.4.27-0.1... より新しい版として扱われます。公式版が出たら dnf upgrade するだけで自然に置き換わります。 名前に php2shell が入るので、どちらが入っているかも rpm -q で一目で分かります。

ビルドは本番ではなく dev で

最初は「本番はスペックに余裕があるので、本番でビルドだけしておこう」と考えました。ところが、ビルドに必要なパッケージを入れる前に dry run してみると…

Upgrading:
 httpd   ...
 ...(systemd、pam なども)

httpd や systemd などの、本番で動いているパッケージまで一緒に更新されそうになりました。 さすがに怖いので本番でのビルドはやめ、dev でビルドしてできた RPM を本番へ運ぶことにしました。

6. dev でビルドする

dev で実行したスクリプトです(要点のみ)。

# 本番と同じ版のソースRPMを取ってくる
dnf -q --releasever=latest download --source php8.4-8.4.26

# php.net から 8.4.27 のソースを取ってきて、公式の sha256 と照合する
curl -sSfLO https://www.php.net/distributions/php-8.4.27.tar.xz
echo "dcf93923b0e38e0d5252b25b796520b5a47761a08b3488b6d0b5d1115764d3f0  php-8.4.27.tar.xz" | sha256sum -c -

# ビルドに必要なパッケージを入れる
dnf -y --releasever=latest builddep php8.4-*.src.rpm

# ソースRPMを展開して、spec を書き換える
rpm -i php8.4-*.src.rpm
cp php-8.4.27.tar.xz* ~/rpmbuild/SOURCES/
SPEC=~/rpmbuild/SPECS/php.spec
sed -i "1,/^%changelog/ s/8\.4\.26/8.4.27/g" "$SPEC"
sed -i -E "0,/^Release:/ s/^Release:.*/Release: 0.1.php2shell%{?dist}/" "$SPEC"
sed -i "/gpgverify/d" "$SPEC"   # 理由は後述

# dev のサイトを止めないよう、優先度を最低にしてバックグラウンドでビルド
nohup nice -n 19 rpmbuild -bb --nocheck "$SPEC" > build.log 2>&1 &

Amazon の spec はバージョンを %global upver の1か所で持っていたので、書き換えは実質1行でした。

PHP のソースコード自体には手を入れていません。php.net が配布している 8.4.27 をそのまま使い、Amazon がいつも当てているパッチは spec の手順どおりに当たります。一番の不安は「Amazon のパッチが 8.4.27 にもそのまま当たるか」でしたが、パッチリリース同士で差分が小さいからか、問題なく通りました。

ビルドは 約40分かかりました。PHP を用途別(CGI 用・Apache 用・FPM 用・組み込み用)に4回組み立てるので、思ったより時間がかかります。

結構長いのでこの間に攻撃を受けないかハラハラしました。

つまずいたところ

1. gnupg2 が入らない

ツールとして gnupg2(暗号化と署名をするソフト) を入れようとしたら、最初から入っている軽量版の gnupg2-minimal と衝突して入りませんでした。外してそのまま進めたら、次の問題が起きました。

2. ビルドの最初で gpgv2: command not found

Amazon の spec は、ビルドの最初にソースの GPG 署名をチェックします。ところが軽量版の gnupg には、その道具(gpgv2)が入っていません。

ソースは事前に php.net 公式の sha256 と照合していたので、署名チェックの行を消して進めました。ただ、sha256 の照合は「php.net に載っている値と同じか」しか見ていないので、ちゃんとやるなら署名チェックも通すべきでした。ここは妥協した点です。

もう1つ、ビルドのコマンドに付けた --nocheck は、PHP 自体のテストを飛ばすオプションです。時間を優先して飛ばしたので、ここも妥協した点です。

3. dev のソースRPMが本番と違う版だった

dev は OS のリリースが古い版で固定されていたため、最初は古い版のソースRPMが落ちてきました。--releasever=latest を付けてバージョンも明示し、本番と同じものを使うようにしました。本番と違う作り方でビルドしても意味がないので、気づけてよかったです。

7. dev に入れて確認する

今入っているのと同じ名前のパッケージだけを選んで入れます。ビルドすると使っていない拡張もたくさんできますが、それは入れません。

for n in $(rpm -qa --qf "%{NAME}\n" "php8.4*" | grep -v pecl); do
  P="$P $(ls ~/rpmbuild/RPMS/x86_64/$n-8.4.27-*.rpm)"
done
dnf -y install $P
php-fpm -t && systemctl restart php-fpm
php-fpm -v
php -m      # 拡張がちゃんと読み込めているか
PHP 8.4.27 (fpm-fcgi) ...

dev の各環境のサイトがすべて 200 を返すことも確認しました。戻すときに使う dnf downgrade のスクリプトも、先に用意しておきました。

8. 本番へ運ぶ

本番のサーバーに余計な権限を足したくなかったので、S3 の署名付き URL で運びました。

  1. dev でできた RPM を手元に持ってくる
  2. S3 にアップロードし、30分だけ有効なダウンロード URL を作る
  3. 本番がその URL からダウンロードし、sha256 が一致するかを確認する
# 手元で:アップロードして、期限付き URL を作る
aws s3 cp php8.4-fpm-8.4.27-0.1.php2shell.amzn2023.x86_64.rpm s3://<bucket>/temp/
aws s3 presign s3://<bucket>/temp/php8.4-fpm-8.4.27-0.1.php2shell.amzn2023.x86_64.rpm --expires-in 1800

# 本番で:ダウンロードして照合する
curl -sSf -o php8.4-fpm-....rpm "<署名付きURL>"
echo "<sha256>  php8.4-fpm-....rpm" | sha256sum -c -

URL さえあれば、本番側に S3 の権限がなくてもダウンロードできます。作業が終わったら、S3 に置いた RPM は消しています。

9. 本番で dry run → 入れ替え

いきなり入れずに、まず何が変わるかだけを見ます。

dnf --assumeno install ./rpms/*.rpm

スクリプトには「php8.4 以外のパッケージが1つでも変わるなら止める」という条件を入れておきました。php8.4 のパッケージだけが 8.4.27 になることを確認してから、本番を入れ替えました。

dnf -y install ./rpms/*.rpm
php-fpm -t && systemctl restart php-fpm
php-fpm -v
rpm -q php8.4-fpm
PHP 8.4.27 (fpm-fcgi) ...
php8.4-fpm-8.4.27-0.1.php2shell.amzn2023.x86_64

0:06、本番が 8.4.27 になりました。 止まったのは php-fpm の再起動の一瞬だけで、サイトは止めていません。

ちょうど、大型のイベントとかぶっていたので、サイトを止めずにアップデートできたのは大きかったです。

10. Amazon の公式版へ置き換える

公式版を待つ間に何度でも叩けるよう、更新スクリプトは次の2点を直してから使いました。

  • 更新がなければ php-fpm を再起動しない
  • 毎回 --refresh でリポジトリの一覧を取り直す(出た直後の新しい版を見落とさないため)
before=$(rpm -q php8.4-fpm)
dnf -y --refresh upgrade "php8.4*" php-pear
after=$(rpm -q php8.4-fpm)
if [ "$before" = "$after" ]; then
  echo "更新なし。再起動しない"; exit 0
fi
php-fpm -t && systemctl restart php-fpm

翌朝実行したところ、Amazon 公式の 8.4.27-1.amzn2023.0.1 に置き換わりました。狙いどおり、自前ビルドより新しい版として扱われ、dnf upgrade だけで済みました。

11. パッチ前に侵入されていないかを確認する

パッチは「これから」の攻撃を防ぐものなので、当てる前に入り込まれていた場合は防げません。そこで、公表日以降を対象に、読み取りだけで次のようなことを一通り確認しました(調べた範囲では、形跡は見つかりませんでした)。

  • アプリのディレクトリに、Git 管理外のファイルや書き換えられたファイルがないか
  • 公表日以降に作られた・変更された PHP ファイルがないか
  • Web シェルによくある書き方(eval(base64_decode(...)) など)がないか
  • Web サーバーのユーザーで、見慣れないプロセスや外向きの通信がないか
  • cron・ユーザー・SSH 鍵が書き換えられていないか
  • php-fpm の異常終了がないか

まとめ

段取りでよかったこと

  • とりあえず緩和策を先に入れた:サイトを止めずに入れられて、ファイル1つ消せば戻せるので、迷わず入れてよかったです
  • 何をするにも、先に --assumeno で dry run した:本番でビルドしようとして httpd まで巻き込みそうになったのも、これで気づけました
  • Release 番号を公式より小さくした:自前ビルドは放っておくと Amazon の更新の流れから外れますが、このおかげで公式版が出たら自然に置き換わりました

もっとこうすればよかったと思うこと

  • 普段から OS のパッケージを更新しておくべきだった:いざという時に、まず追いつく作業から始めることになりました
  • 緊急時の手順と権限を事前に整えておくべきだった:バックアップの取り方なども含め、慌てずに動ける状態にしておくべきでした
  • AI に任せた部分を、その場で理解しきれていなかった:スクリプトの多くは AI(Claude Code)が書いてくれて本当に速かったのですが、細かいミスもけっこうありました。dry run と「止める条件」を入れていたので事故にはなりませんでしたが、緊急時ほど、後から必ず理解し直すのが大事だと思いました。この記事はその復習も兼ねています

メンテナンスせずに、素早くパッチを当てられたことがよかったです。

今回の作業をしてみて、dnf upgrade で直るだろうと鷹を括っていたのですが、Bプランを用意しておくべきでした。
あと、やってみて、理解できている部分と理解できてない部分があったので、内部の構造をしっかり理解していこうと思いました。

参考

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?