
はじめに
2026年10月21日、PHPプロジェクトはPHPコアに存在する2件の深刻な認証不要のリモートコード実行の脆弱性に対する緊急修正をリリースしました。CVE-2026-109618(メモリ不足による状態破損を介したRCE)とCVE-2026-109619(古いグローバル状態を介したRCE)で、いずれもCVSS v4.0のスコアは9.2です。どちらもアプリケーションコード内の問題ではなく、アプリケーションのコードが実行される前に、単一のHTTPリクエストから到達できます。また、パッチ公開前から実環境で悪用されていました。
修正版はPHP 8.5.12、8.4.27、8.3.36、8.2.35です。サポート終了済みの7.4、8.0、8.1系向けにはソースコードのバックポートも提供されています。PHPを運用している場合は、まずパッチを適用してワーカーを再起動し、その理由を以下で確認してください。
本記事では、PHP2Shellと総称される2件のPHPコアの脆弱性について、脆弱なコードパス、公式の修正、影響を受けるバージョンと修正版、公開状態を調べるスキャン方法を技術的に解説します。
「PHP2Shell」が初期の解説とは異なる理由
協調的な情報公開に先立って詳細が流出した際、流通した分析はPHPのセッションアップロード進捗ハンドラー(ext/session/session.cのphp_session_rfc1867_callback)にある解放後使用を原因としていました。この経路は実際にトリガーとなりますが、根本原因ではなく症状であることが判明しました。独立したラボでの検証(rdwr-namikbの検証ラボ)でも、広く流通したsession.upload_progressのUAF解説を根本原因として再現できなかったことが明記されています。
公式のGitHub Security Advisoriesは、リクエストのライフサイクル自体に存在する、別々の2件のメモリ安全性の欠陥を示しています。どちらもアップロード進捗処理から到達できますが、それ以外の初期化前の経路からも到達可能です。このため、アップロード進捗の無効化では露出を一部軽減するにとどまり、確実な修正にはパッチの適用が必要です。
2件のアドバイザリは以下のとおりです。
CVE-2026-109618:メモリ不足による状態破損を介したRCE
PHPは長年、メモリ不足(OOM)が発生するとzend_bailout()を呼び出し、longjmp()によって保存済みの回復地点に戻る仕組みを使用していました。このジャンプでは、途中のクリーンアップ処理を実行せずにCスタックを巻き戻します。
問題は、リクエストが所有するメモリが完全に初期化される前に、到達可能な状態になる場合があることです。その間にメモリ割り当てが失敗すると(例えば、マルチパートパーサーがリクエスト状態を構築している途中など)、longjmp()によってメモリが初期化不完全な無効な状態のまま残ります。その後のリクエスト終了処理は、この無効な状態を読み取ったり、それに基づいて分岐したりします。攻撃者は、細工したリクエストボディとmemory_limitを調整することで、どの割り当てを失敗させるかを誘導できます。このため「無効な状態」は攻撃者の影響を受けるようになり、破損のプリミティブとなります。マルチパート解析がトリガーになることは確認されており、これがアップロード進捗に関する説との接点です。
脆弱なコードパス(PHP 8.4.4以前、抜粋)
修正前のアロケーターでは、ソフトリミットを超過するとzend_mm_safe_error()に処理が移り、致命的エラーを発生させた後、longjmpで脱出していました。
危険なのは、リクエストのメモリが部分的にしか設定されていないスタック上でzend_bailout()が実行されることです。このジャンプにより、そのメモリの初期化や検証を完了するはずのコードが飛ばされ、その後の終了処理が未完成のメモリを使用します。
修正内容(PHP 8.4.27 / 8.3.36 / 8.2.35 / 8.5.12)
コミット3584a98bda0f2c0d367255b046798b0d3a221a19「Avoid state corruption via bailout.」のパッチでは、OOMの経路からlongjmpを完全に取り除いています。代わりにアロケーターは次の2つの処理を行います。
- ソフトリミット:遅延OOMエラーをキューに入れ、VMが次の安全な地点でエラーを発生させます。これにより、処理の途中でエンジンが放棄されることを防ぎます。
- ハードリミット:使用量が2段階目のより高いしきい値を超えると、
zend_mm_hard_limit_errorを呼び出します。この関数は回復を試みる代わりに_exit(137)で直ちに終了します。
以下は8.4.27のソフトリミット/ハードリミット検査をそのまま掲載したものです。
遅延エラーはEG(delayed_oom)に保存され、EG(vm_interrupt)を通じて通知されます。
ハードリミット時の脱出処理は以下のとおりです。
結果として、PHPはlongjmpによって部分的に初期化されたリクエストを放棄しなくなり、終了処理が破損した構築途中の状態を使用することを防ぎます。
CVE-2026-109619:古いグローバル状態を介したRCE
PHPワーカーは永続的で、1つのOSプロセスが多数のリクエストを処理します。エンジンはその存続期間を通じて、エラー、セッション、オブジェクト、拡張機能などのグローバル状態を保持します。その一部は、現在のリクエストが所有するメモリを指しています。維持すべき条件は単純で、リクエストヒープが解放される前に、この状態をリセットしなければなりません。
この条件が崩れる原因は2つあります。
- 致命的エラーによってリセットが飛ばされる:OOMやタイムアウトにより、グローバル状態をリセットしない経路を通ってリクエストが中断されることがあります。
- 終了処理によって、消去後に状態が再設定される:終了処理中の非致命的な診断によって、リセット済みのエラー状態が再び書き込まれることがあります。
どちらの場合も、グローバル状態が参照を保持したまま、リクエストヒープが解放されます。その後、同じワーカーの次のリクエストで、通常の初期化より前に動く処理(INI診断、セッションアップロード進捗処理、PHP 8.5のfatal_error_backtraces経路など)が古いポインターを参照します。これは典型的な解放後使用(CWE-416)です。検証で特に明確に確認されたのは、終了時にバックトレースの状態がリセットされないPHP 8.5のfatal_error_backtraces経路でした。
GitHubのアドバイザリによると、具体的な脆弱性は以下の条件で発生します。
- 到達可能なPHPの処理が、リクエストの所有する状態をグローバルにアクセスできる場所へ保存し、producerとして機能します。
- その状態がリセットされない、または通常のリセット後に別の処理によって再設定されます。
- 永続ワーカーが参照元のグローバル状態を保持したまま、PHPがリクエストのメモリを解放します。
- 同じワーカーが別のリクエストを処理します。
- 通常のリクエスト初期化で状態がリセットされる前に、PHPの処理が古い状態を読み取り、解放し、それに基づいて分岐するなどしてconsumerとして機能します。
修正内容
修正版では包括的な変更が行われています。エンジンが広範なグローバル状態(エラー、セッション、オブジェクト、拡張機能)をリセットし、通常の初期化前に動く処理まで状態が残らないようにします。アドバイザリは、設定変更ではこれを完全に代替できず、更新のみが確実な緩和策であると明記しています。
直ちに修正を適用できない場合は、到達可能な攻撃面を減らす次の対策が推奨されます。ただし、これらは不具合そのものを修正するものではありません。
- PHP 8.5:
fatal_error_backtraces=0に設定し、ワーカーを再起動します。 - セッションの経路:
session.upload_progress.enabled=0に設定し、ワーカーを再起動します。
パッチの適用
CVE-2026-109618
修正は公開前の非公開期間中にLinuxディストリビューションとLTSベンダーへ共有されていたため、公開時またはその前後にディストリビューションのパッケージが利用可能になりました。ディストリビューションはバージョン番号を変えずにバックポートするため、php -vの文字列ではなく、パッケージの変更履歴とCVE番号で修正を確認してください。
DebianとUbuntu(ondrej PPAおよびpackages.sury.orgを含む):
RHEL、AlmaLinux、Rocky:
注:ボランティア運営のrpms.remirepo.netでは、公開当日に更新が提供されていませんでした。これに依存している場合は、ディストリビューションのパッケージに切り替えるか、ソースからビルドしてください。
CVE-2026-109619
パッチ適用後はワーカーを再起動してください。ディスク上のファイルが修正されていても、すでに実行中のワーカーは保護されません。まずサービスを再起動し、その後で各ワーカーを再起動します。
古いPHPが稼働していないことを確認します。
公開状態のスキャン
この攻撃では、システムで稼働しているPHPのバージョンが重要になるため、公開状態のスキャンは容易です。X-Powered-ByやServerなどのヘッダーを調べることで、脆弱なサイトの検出に役立ちます。ただし、バージョンが脆弱でも、事前の対策によりリスクが下がっている場合があります。このため、防御目的の公開アプローチが3つ登場しています。いずれも動作するエクスプロイトを必要としません。
1. 無害なプリミティブによるフィンガープリント(Masriyan/php2shell)
このツールは対象の特徴を調べ、PHPのバージョンとサポート系統を検出した後、1回だけ無害なプローブを送ります。プローブは、カナリアとしてPHPコメント(実行コードではない)を含むマルチパートリクエストです。コードの実行は行いません。クライアント指定のセッションIDに対してPHPがセッションCookieを発行するかどうかをシグナルとし、初期化前のセッション経路に到達可能かを示します。
処理は7段階です。対象の解析、CMSのフィンガープリントと設定の読み取り(session.upload_progress.enabled、cleanup、memory_limitとpost_max_sizeの比較、SAPI)、PHPのバージョンと系統、確認ゲート、無害なプローブ、公開状態の判定、そして任意の読み取り専用include-sinkおよびpearcmd.phpスキャンです。
2. ワーカー終了の差分スキャン(0xrsydn/php2shell-scan)
このスキャナーは、仕様に準拠した約24種類のマルチパートリクエストをPOSTルートへ送り、HTTPステータスコードを信用するのではなくワーカーの終了回数を数えます。静的なパスには応答があるのに5xxまたはハングが発生する場合を、ワーカーの終了として扱います。注意点として「PHP-FPMは子プロセスを再生成するため、クラッシュが正常な200応答に見える場合がある」としており、ステータス行よりも終了回数のカウンターが重要です。
修正前のベースラインを保存して再テストと比較し、mitigated、exposed、inconclusive、improved、changed、unchanged、no_signalなどの判定を出力します。読み取り専用で、レート制限と上限が設けられています。
3. ローカル差分検証ラボ(rdwr-namikb)
確かな結果を求める防御担当者向けに、この検証ラボはPHP 8.4.4(脆弱版)と8.4.27(修正版)をAddressSanitizer付きでビルドし、2×2の組み合わせで検証します。
verify_109619.py:php-cgiに対してupload_progressの有効/無効を切り替えます。8.4.4でアップロード進捗が有効な場合、セッションが破壊されて$_SESSIONが漏えいしました。8.4.27では起動時の診断が出ず、セッションは維持されました。verify_109618.py:memory_limitを8M(トリガー)と-1(対照)にしてOOMテストを実行します。8.4.4の8Mでは、応答が空のまま接続が閉じられ、部分的なセッション記録が残りました。8.4.27では正常な500応答となり、セッション記録は残りませんでした。- 上流の回帰テストでも確認されています。
ext/session/tests/rfc1867_no_user_code.phptは8.4.4で3回中3回失敗し、8.4.27で3回中3回成功します。Zend/tests/oom_after_main_script.phptでも同様の結果になります。
注意すべき点として、Zendのアロケーターはチャンク内の解放後使用をASANから隠します。そのため109618のハーネスは、サニタイザーのクラッシュを待つのではなく、観測できる状態(空のまま接続が閉じられ、部分的なセッション記録が残ること)を確認します。
ローカルでの検知と脅威ハンティング
公開時点および執筆時点で、確認済みのファイルハッシュIOCは公表されていません。そのため、挙動に基づくハンティングを推奨します。
プロセスレベルの検知
PHPワーカー(php-fpm、php-cgi、httpd、apache2)が以下を起動する挙動がシグナルになります。
- シェル:
sh、bash、cmd.exe、powershell.exe - インタープリター:
python、perl - ダウンロードツール:
curl、wget、nc、certutil.exe - Webサーバーのユーザーとしての偵察:
whoami、id、uname -a、ipconfig
アラート対象として検討すべきコマンドライン文字列:/dev/tcp/、fsockopen、mkfifo。
PHP-FPMワーカーには通常子プロセスがないため、簡単な初期調査には次のコマンドを使用します。
ネットワークレベルの検知
PHPワーカーへのPOSTルートで5xx(多くは502)が短時間に集中し、その後200が続く挙動を確認します。特にアップロード進捗フィールドを含むリクエストに注目してください。マルチパートパーサーは厳密でないヘッダーも許容するため、文字列の完全一致シグネチャだけに依存しないでください。少なくとも2026年10月20日以降のログを確認し、可能であれば2026年6月まで遡って、Webシェル、新しいファイルやユーザー、想定外の外向き接続を調査してください。
影響を受けるバージョンと修正版
Webリクエストを処理するすべてのSAPIが影響を受けます。PHP-FPM、Apache mod_php、php-cgi、FrankenPHP、LiteSpeedです。mod_phpは最もリスクが高いと評価されており、動作するエクスプロイトが実証されています。
ExaPHP2Shell
本記事をきっかけに、応答ヘッダーを解析して脆弱なPHPがインストールされたページを調べるツール、ExaPHP2Shellを作成しました。このツールはHTML/CSS/JSのフロントエンドとAWS Lambdaのバックエンドで構成されます。ブラウザーの同一オリジンポリシーにより、S3だけでは別サイトのヘッダーを読み取れないため、Lambdaがサイトをスキャンして結果を返します。
フロントエンドは、Webサイトとして設定したAWS S3バケットでホストでき、Lambda関数を呼び出せます。
Lambda関数にはURLを設定する必要があります。そのURLをフロントエンドのBackend settingsに入力するか、フロントエンドのJSコード内のBACKEND変数に固定値として設定します。
バックエンドのコードはServerとX-Powered-Byの値、およびそれらが返すバージョンを調べます。
出力はフロントエンドのWebページに渡され、脆弱かどうかの判定が表示されます。
まとめ
CVE-2026-109618とCVE-2026-109619は、プラグインの不具合や設定ミスではありません。PHPランタイムの中核にある2件のメモリ安全性の欠陥であり、認証前の単一リクエストでトリガーされ、パッチ公開前から悪用されていました。初期の「セッションアップロード進捗の解放後使用」という説明は、トリガーとしては有用でしたが、根本原因とは異なります。実際の修正では、危険なOOMのlongjmpを取り除き(109618)、グローバル状態を完全にリセットして、リクエスト所有のポインターがリクエストヒープより長く残らないようにしています(109619)。
8.5.12、8.4.27、8.3.36、8.2.35へ更新し、すべてのワーカーを再起動して、変更履歴で修正を確認し、過去のログを調査してください。サポート終了済みの系統にとどまる場合は、ソースのバックポートを適用するか、その系統から移行してください。
参考資料と関連情報
- PHPアドバイザリGHSA-w8hw-w9fx-j79w(CVE-2026-109618)、php/php-src
- PHPアドバイザリGHSA-6wmf-4rv5-p3rp(CVE-2026-109619)、php/php-src
- 修正コミット
3584a98bda0f2c0d367255b046798b0d3a221a19:「Avoid state corruption via bailout」 - Help Net Security:PHP2Shellのパッチ公開、今すぐ更新
- Mattias Geniar:パッチの適用方法
- Brandefense:CVE-2026-109618の解説
- Sansec:PHP2Shellの調査
- Patchstack:進行中の攻撃
- rdwr-namikb検証ラボ(8.4.4と8.4.27の比較)
- Masriyan/php2shell
- 0xrsydn/php2shell-scan
- PHP 8公式変更履歴




