クラウドストライク、PHP2Shellを介した脅威から顧客を保護
PHPコアに、CVE-2026-109618およびCVE-2026-109619として追跡されている、メモリ破損によるリモートコード実行 (RCE) を可能にする重大な脆弱性が2件公表されました。
2026年10月21日 • アミット・サーパー(Amit Serper) • サイバーセキュリティの最前線より
Webの大部分を支える基盤エンジンであるPHPコアに、PHP2Shellと総称される、メモリ破損によってリモートコード実行 (RCE) を可能にする重大な脆弱性が2件公表されました。CrowdStrike Falcon®プラットフォームは、両方の脆弱性の悪用後の活動に対する可視性と検知カバレッジを標準で提供します。
CVE-2026-109618およびCVE-2026-109619として追跡されている両方の脆弱性は、CVSS 4.0のスコアが9.2(Critical:緊急)で、CWE-824「初期化されていないポインターのアクセス」に分類されています。どちらの脆弱性も、認証されていないリモート攻撃者がHTTP経由で、影響を受けるウェブサーバー上の任意のシステムコマンドを実行できるようにします。
この2つの問題には共通の根本原因があります。PHPで致命的な状態(設定されたmemory_limitの枯渇、最大実行時間の超過、クライアント接続の切断など)が発生した場合、コールスタックを遡ってエラーを返すことはありません。致命的なエラーを発生させ、longjmp()を使用して現在の実行コンテキストを直ちに放棄し、リクエストを中止します。データ構造の構築途中で実行が中断されたC関数は、単に放棄されます。デストラクタは実行されず、管理情報はロールバックされず、データ構造が不完全であることも記録されません。その後、PHPは、すべてのデータ構造が整合性の取れた状態にあることを前提とする、通常のクリーンアップシーケンスに進みます。
この2つのCVEは、その前提がどこで崩れるかを示しています:
- CVE-2026-109618(メモリ不足によるRCE)は、リクエスト用メモリが初期化されないままになる状況を対象としています。データ構造では、エントリの領域となるメモリが割り当てられる前に、そのエントリが存在するとマークされます。その割り当てによってメモリ上限を超えると、エントリには古いヒープデータが残ります。失敗したリクエスト自身のクリーンアップルーチンは、その後それらのバイトを読み取り、その内容に基づいて分岐します。
- CVE-2026-109619(致命的なエラーによるRCE)は、グローバル状態が無効なままになる状況を対象としています。モジュールレベルのグローバル変数は、シャットダウン処理によってすでに解放されたリクエストごとのヒープを指したままで、その操作が実行中であると報告し続けます。同じワーカープロセスが処理する後続のリクエストが、その解放済みメモリを読み取ったり、その内容に基づいて分岐したりします。
どちらの場合も、攻撃者はトリガーを制御し、さらに重要なことに、最終的にPHPが誤って解釈するデータも制御します。クラウドストライクは、匿名のPOSTリクエスト1件によって、Apacheのmod_phpワーカープロセスがシェルプロセスを起動し、攻撃者が指定したコマンドを実行することをエンドツーエンドで検証しました。認証もセッションも、脆弱なアプリケーションコードも必要ありません。
これらの脆弱性の深刻さを高めているのは、実行スタックにおける位置です。PHPは、拡張機能の初期化前、そしてアプリケーションコードが1行もコンパイルされていない段階で、受信したリクエストデータを解析します。そのため、悪用は、サーバーがどのようなアプリケーションをホストしているかに依存しません。細心の注意を払って保守されているアプリケーションも、保守が不十分なアプリケーションと同程度に影響を受けます。この攻撃は、非常に発見されにくいという特徴もあります。ワーカープロセスはクラッシュせず、サーバーは通常のHTTP 200レスポンスを返し、アプリケーションログにも異常は記録されません。
当社のテストでは、x86-64とARMの両アーキテクチャで一貫して動作し、ワーカーの再起動後も安定して機能するエクスプロイトコードを開発しました。また、クラウド環境で普及が進むARMベースのホストでは、このコードはHTTPリクエストのみを使ってアドレス空間配置のランダム化 (ASLR) を突破しました。
技術的な詳細
共通する根本原因:スタックを巻き戻さない longjmp()
両方の脆弱性は、PHPの中核を担うエンジンであるZendに存在します。Zendは、PHPソースをバイトコードにコンパイルし、仮想マシン内で実行し、変数、配列、オブジェクト、参照カウント、メモリ割り当てなど、言語がランタイムで必要とするすべてを管理するC言語による実装です。Zeev Suraski氏とAndi Gutmans氏によって最初に作成され(Zendという名前は2人のファーストネームを組み合わせたもの)、PHP 4で導入されたZendは、オプションのコンポーネントやサードパーティ製のアドオンではありません。すべてのPHPビルドに同梱されており、そのソースがZend/ディレクトリに置かれており、同ディレクトリがphp-src内にあるのもそのためです。実際には、これはZendの欠陥がPHPエンジン自体の欠陥であることを意味します。そのため、コードがどのように記述されているかにかかわらず、PHPインタープリターで実行されるすべてのアプリケーション、フレームワーク、拡張機能に影響を与えます。
ここで説明する問題の中心となるのは、Zendの中核的な仕組みである2つの要素です。1つは、すべてのPHP配列を支えるHashTableであり、PHP配列には$_POSTスーパ―グローバルも含まれます。もう1つは、PHPとオペレーティングシステムの間に位置する、PHP独自のリクエスト単位のアロケーターであるZend Memory Manager(Zend MM)です。PHPの致命的エラー処理パスはzend_bailout()で終了し、LONGJMPを実行して現在のCコールスタックから抜け出します:
// Zend/zend.c
ZEND_API ZEND_COLD ZEND_NORETURN void _zend_bailout(const char *filename, uint32_t lineno)
{
...
CG(unclean_shutdown) = 1;
...
LONGJMP(*EG(bailout), FAILURE);
}
致命的エラーが発生してから、外側のzend_catch ブロックに到達するまでの間、何も実行されません。状態を変更し、自身のクリーンアップコードに到達することを前提としていた呼び出し元は、その状態を残したままになります。この方法が安全なのは、すべての呼び出し元が突然の終了を想定して記述されている場合に限られます。しかし、そうなっていないものも複数あります。
CVE-2026-109618:未初期化のリクエストメモリが同一リクエスト中に消費される
代表的な実装は、_zend_hash_str_add_or_update_i()です。これは、Zend/zend_hash.cにある、PHP配列への文字列キーによる挿入をすべて処理するヘルパーです。挿入処理では、両方の要素カウンターをインクリメントし、新しいバケットへのポインターを取得した後になって初めて、キーのコピー用メモリを割り当てます:
// Zend/zend_hash.c
add_to_hash:
idx = ht->nNumUsed++;
ht->nNumOfElements++;
p = ht->arData + idx;
p->key = key = zend_string_init(str, len, GC_FLAGS(ht) & IS_ARRAY_PERSISTENT);
zend_string_init()がmemory_limitを超えると、Zend MMは最終的にzend_bailout()に至る致命的なエラーを発生させます。実行が戻ってバケットの処理を完了することはありません。この時点で、テーブルは実際に含まれている数より1つ多いエントリを報告します。そのエントリの値、ハッシュ、キーへのポインターには、アロケーターがそのメモリに最後に残したバイト列がそのまま入っています。バケット配列は初期化されていないemalloc()で割り当てられ、ハッシュ領域だけが明示的にリセットされます。
解放処理中、php_request_shutdown()がスーパーグローバル変数を破棄し、zend_array_destroy()がnNumUsed個のバケットを反復処理します。残存エントリはi_zval_ptr_dtor()に渡されます。この関数は、残されたバイト列を参照カウント付きの値として扱います。つまり、そこに含まれるポインターをデリファレンスし、そのアドレスにある参照カウントを減らします。参照カウントが0になると、同じく残されたバイト列に由来する型タグをインデックスとして、関数ポインターテーブル経由で処理を呼び出します。
この条件を引き起こすのに必要なのは、URLエンコードされたPOSTボディだけです。PHPは$_POSTに値を設定しますが、これはphp_hash_environment()内で行われます。この関数は、php_request_startup()によって、zend_activate_modules()の前、さらにスクリプトがコンパイルされる前に呼び出されます。ZZZZ…[x]=1 のようなネストした名前を持つフィールドがあると、php_register_variable_ex()は外側の名前を新しい配列キーとして挿入します。そのため、攻撃者は割り当てが必要なキーの長さを直接制御できます。その名前をmemory_limitを超えるサイズにすると、キーの割り当て時に強制終了が正確に発生します。
残されたバイト列を制御できるのは、アロケーターの動作が決定論的だからです。Zend MMは、小さなメモリ割り当てをサイズごとの空きリストから厳密な後入れ先出し (LIFO) 順で処理し、再利用するブロックをゼロクリアしません。そのため、同じリクエスト内の先行フィールドのうち、PHPが値をコピーしてデコードし、直ちに解放するものは、解放された内容が破損した配列が後から確保するブロックと同じ領域を正確に占めるようにサイズを調整できます。古いエントリは、攻撃者が完全に定義した値になります。これを利用して、デストラクターのポインターがlibcのsystem()を指し、コマンド文字列が同じブロック内に置かれた偽の配列を合成しました。
試行が成功した場合にサーバー側に残る唯一のアーティファクトは、Webサーバーのログに記録される一般的な致命的エラーです:
PHP Fatal error: Allowed memory size of 16777216 bytes exhausted (tried to allocate 3670048 bytes) in Unknown on line 0
このin Unknown on line 0がその証拠です。PHPファイルが一度も実行されていなかったことを示しています。
CVE-2026-109619:無効なグローバル状態が後続のリクエストによって消費される
2つ目のCVEも同じ仕組みですが、影響はプロセス単位の状態に及ぶため、悪用までの猶予期間は後続のリクエストへと移ります。ここで中断されるのは、状態をグローバル変数にキャッシュし、その状態をクリアするには自身のクリーンアップ呼び出しに到達する必要がある操作です。PHPは、アクティブなシリアライザーとデシリアライザーのコンテキストをモジュールグローバル変数にキャッシュし、ネストレベルが0以外の場合は、キャッシュされたポインターを再利用します。対応する破棄呼び出しだけがネストレベルを減らしてポインターをクリアし、拡張機能のリクエスト初期化ルーチンだけがモジュールグローバル変数の状態をリセットします。セッション処理も同様に、セッション識別子、ハンドラーの状態、セッション変数、アップロード進捗の状態を、専用のシャットダウンルーチンがクリアするグローバル変数に保持します。
致命的なエラーによってこれらの操作のいずれかが中断されると、対応する解放処理がスキップされます。グローバル変数はポインターを保持したままで、ネストカウンターもその操作がまだ実行中であることを示したままです。その後、リクエストのシャットダウン処理でリクエストごとのヒープ全体が解放されますが、グローバル変数はリクエストではなくプロセスに属するため、そのまま残ります。PHPには、すでに解放したメモリを指すモジュールレベルのポインターが残ります。
これを実現可能かつ制御可能にする要因は2つあります:
- ワーカーの再利用:Apache
mod_phpのような永続ワーカー展開では、同じプロセスが次のリクエストを処理します。古いグローバル変数はそのまま残っており、操作は実行中であると見なされ、解放されたポインターの読み取り、書き込み、またはそれに基づく分岐が行われます。その時点でそのメモリを占有しているものはすべて攻撃者の影響下にあります。介在するリクエストも攻撃者のものだからです。攻撃者はそのサイズと内容を選択でき、Zend MMの割り当ての振る舞いはそれらを確実に配置できるほど決定論的です。PHP-FPMはリクエストの起動に失敗したワーカーを終了するため、起動フェーズで中断が発生した場合の再利用は排除されます。一方、すでに実行中のスクリプト内で中断が発生した場合はFPMワーカーが存続し、同様に危険に晒されます。 - 起動時の処理順序: php_request_startup()は、拡張機能のリクエスト初期化コールバックを実行する前に、リクエスト変数を解析します。そのため、マルチパートアップロードの進捗処理では、それらのグローバル変数を所有する拡張機能がリセットする機会を得る前に、セッション処理とシリアライズ処理のコードが実行され、リクエストの最初期に古いポインターが使用される可能性があります。
メモリ枯渇だけでなく、実行タイムアウトやクライアント接続の中断によってもこの状態に至る可能性があり、いずれも同じzend_bailout()の経路をたどります。そのため、悪用は2段階で行われます。1つ目のリクエストがワーカーを汚染し、2つ目のリクエストが解放済みメモリの使用を引き起こします。
脆弱性の影響
両方の問題の重大度は、リクエスト処理の早い段階に位置していることに起因します。PHPが内部リクエスト変数を構築している段階、すなわち拡張機能の初期化前かつアプリケーションコードの実行前に破損が発生するため、影響を受けるサーバー上で到達可能なすべてのPHPエンドポイントは、静的フォーム、ログインルート、ヘルスチェックを含め、実際に攻撃に利用できる攻撃対象領域となります。回避すべき認証も、盗み取るセッションも、発見すべき脆弱なエンドポイントもありません。この問題は、使用しているCMS、フレームワーク、プラグインに関係なく発生します。
この攻撃も、実質的にデジタルフットプリントを残しません。ワーカープロセスは安定した状態を維持し、HTTP 200レスポンスが返され、アプリケーションログにも異常は記録されません。アプリケーションコードが一度も実行されないためです。サーバー側に残る痕跡は、Webサーバーのエラーログに記録されたメモリ制限に関する致命的エラー1件だけです。これは通常ノイズとして片付けられてしまうため、防御担当者は、侵害されたエンドポイントの特定に活用できるWebテレメトリを得られません。
Apache preforkのようなマルチプロセスWebサーバーでは、各ワーカーは単一の親プロセスから派生するため、攻撃者が依存するメモリレイアウトはワーカーの再起動後も一貫して保たれます。この安定性により、攻撃者はコストをかけずにペイロードを改良できます。失敗した試行はワーカーの再生成によって吸収され、監視システムはその再生成を通常の動作として扱います。
2つのCVEは、運用上重要となる形で、影響範囲とタイミングが異なります。CVE-2026-109618は単独で完結します。1つのリクエストが自身のデータを破損させ、その結果として生じる処理は、同じリクエストのクリーンアップ中に実行されます。そのため、迅速かつアトミックで、事後に再構成することが困難です。CVE-2026-109619は複数のリクエストにまたがり、同じワーカーを対象とする汚染ステップとトリガーステップを必要とします。一方で、より広範な内部状態に影響し、メモリ枯渇だけでなく、タイムアウトやクライアント接続の切断によっても引き起こされます。この2つ目のクラスで重大なエクスポージャーを抱えるのは、永続ワーカーを使用する展開構成です。
いずれかの問題の悪用に成功すると、Webサーバーのユーザー権限でコマンドを実行されます。これは、Webシェルの展開、設定ファイルからのデータベースおよびAPI認証情報の収集、内部ネットワークへの移動の標準的な足掛かりとなります。パッチ適用が、完全な修復を実現する唯一の方法です。即時のパッチ適用が難しい場合、エンドポイントの振る舞い監視が主要な防御手段となります。悪用自体はHTTP層では見えない一方、その結果はエンドポイント上で観測できるためです。
影響を受ける環境
どちらの脆弱性も、PHPの7.xおよび8.xシリーズのすべてのリリースに存在します。CVE-2026-109618の背景にあるHashTableの特定の処理順序は、2017年10月のパフォーマンス最適化によって導入され、PHP 7.3.0で初めてリリースされました。(2018年12月)。CVE-2026-109619の背景にあるシリアライズパスとセッションパスはさらに古く、それぞれPHP 7.1およびPHP 5.4にまで遡ります。言い換えれば、リリース済みのPHPには、悪用可能な状態が7年以上にわたって存在していたことになります。サポートされているブランチで影響を受けないものはありません。
バージョンだけでエクスポージャーが決まるわけではありません。メモリ枯渇を引き起こす条件は、PHPがリクエストデータを解析している間に到達可能でなければなりません。そのため、スクリプトの実行前にメモリ上限を使い果たすほどリクエストボディが大きくなることを許容する必要があります。実際には、設定はおおよそ次の条件を満たす必要があります:
memory_limit < 4 × post_max_size
PHPの出荷時のデフォルト設定(post_max_size = 8M、memory_limit = 128M)はこの条件を満たさないため、標準インストールでは、今回実証したURLエンコードされた経路を通じてこの脆弱性に到達することはできません。しかし、本番環境における多くの展開では大容量のアップロードを受け入れるように設定が調整されており、post_max_size = 64Mとmemory_limit = 128M、あるいはpost_max_size = 100Mとmemory_limit = 256Mといった一般的な組み合わせはこの条件を満たします。なお、post_max_sizeは.htaccessまたはバーチャルホストでディレクトリ単位に設定できます。そのため、共有ホスティング環境では、サーバー全体に変更を加えなくても、単一のサイトだけを脆弱な構成にすることが可能です。クラウドストライクは、テストペイロードのサイズを小さく抑えることのみを目的に、memory_limitを16Mに引き下げ、post_max_sizeを64Mに引き上げてこの問題を再現しました。前述のより大きな制限値でも、より大きなペイロードを使用すれば同じバグに到達します。
サーバーで実行されているPHPのバージョンと設定を確認するには、php --versionを実行します。上記に挙げたオプションについてサーバーで設定されている値を確認するには、次のコマンドを実行します:
php -i | grep -E "^(memory_limit|post_max_size|upload_max_filesize|Loaded Configuration File)”
CrowdStrike Falconが顧客を保護する方法
クラウドストライクは多層防御アプローチを採用し、機械学習や、攻撃の痕跡 (IOA) を含む振る舞い検知を活用して、脆弱性の悪用および悪用後の活動を検知します。Falconプラットフォームは、PHP脆弱性の悪用後の活動に対する可視性と検知カバレッジを標準で提供します。既存のIOAは、Webシェルの展開、PHPやWebサーバーのプロセスによって生成された不審な子プロセス、悪意のあるコマンド実行など、一般的な悪用後の活動を検知・阻止します。
クラウドストライクは、この脆弱性に関連する悪用手法の研究を続けています。

図1:PHP脆弱性の悪用チェーン
Falcon Exposure Managementによる脆弱なPHPバージョンの特定
CrowdStrike Falcon® Exposure Managementは、影響を受けた環境全体で包括的な可視性を提供します。Network Scan(Netscan)とEASM(外部攻撃対象領域管理)の機能により、インターネットに公開されているアセットをバナー情報に基づいて速やかに検知できます。センサーによる可視性により、サポート対象のLinuxディストリビューション、Windows、macOSにわたるベンダー提供の下流パッチを自動評価できます。
Falcon Exposure Managementは、既存のパッケージマネージャーと特定のアプリケーションチェックを通じて、サポート対象のすべてのセンサーバージョンでこれらのCVEを検知します。これらのチェックには、センサーの最低バージョン要件はありません。
クラウドストライクは、センサーによる以下の検知も追加しました。これらの検知は、既存のチェックでは見逃される可能性のあるPHPのインストールを検出します。たとえば、カスタムインストールやパッケージマネージャーを介さずに行われたインストールです。これらは、以下に記載されたセンサーバージョンでのみ利用できます:
- Windows(Falconセンサー7.40以降): デフォルトドライブ全体を対象に、PHP実行ファイルをチェックします。これにより、既存のチェックでは特定できない可能性があるPHPのインストール(カスタマイズされたインストールを含む)も検出できます。
- Linux(Falconセンサー7.31以降): 実行中のプロセスをチェックし、想定されるパッケージマネージャーを介さずにインストールされたPHPを特定します。
以前のセンサーバージョンをご利用のお客様も、パッケージマネージャーによるチェックと対象アプリケーションのチェックの対象となります。アップグレードするまで、追加のシステム全体チェックや実行中プロセスチェックは行われません。

図2:Falcon Exposure Managementにおける脆弱なPHPバージョンの特定
Next-Gen SIEMによる脅威の特定
Falcon Next-Gen SIEM:Emerging Threat Response Dashboard(新たな脅威対応ダッシュボード)
この脅威に対するCrowdStrike Falcon Next-Gen SIEMの検知カバレッジは、NG-SIEM Content Packagesリリースの一環として本番環境に展開されました。新たに「PHPのリモートコード実行脆弱性(CVE-2026-109618/109619)」セクションが追加されたCrowdStrike Falcon - Emerging Threat Response Dashboardで確認できます。
Emerging Threat Response Dashboardは、クラウドストライクのお客様が、自社環境に影響を及ぼす可能性のある進行中のキャンペーンや新たに発見された脆弱性を即座に可視化できるよう設計された、対応に特化したツールです。Falconプラットフォームのモジュールから得られる豊富なテレメトリと、Falcon Next-Gen SIEMに取り込まれたサードパーティのデータを活用し、このダッシュボードはすぐに使える検知クエリとハンティングクエリを提供します。これにより、セキュリティチームは脅威への曝露状況を迅速に把握し、侵害された可能性のあるアセットを特定できます。
- 「Next-Gen SIEM(次世代SIEM)」>「Log management(ログ管理)」>「Dashboards(ダッシュボード)」を参照してください

図3:Falcon Next-Gen SIEM内のEmerging Threat Response Dashboard(新たな脅威への対応ダッシュボード)の特定
Falcon Next-Gen SIEM相関ルールテンプレート
CVE-2026-109618およびCVE-2026-109619を検知して対応するための、新しいFalcon Next-Gen SIEM相関ルールテンプレート (CRT) を追加しました。これらのルールは、これらの脆弱性に関連する悪用の試みをカバーします。
汎用 - Web - リクエスト開始時のPHPメモリ枯渇(CVE-2026-109618とCVE-2026-109619)
この検知は、PHPスクリプトの実行開始前に発生するPHPの致命的なメモリ枯渇エラーを特定します。具体的には、Webサーバーのエラーログに「Allowed memory size of N bytes exhausted」と「in Unknown on line 0」の組み合わせとして現れる特徴的なパターンを検知します。「Unknown on line 0」というマーカーにより、起動フェーズでの障害を、ノイズがはるかに多いスクリプト実行中のメモリ枯渇エラーと区別できます。これにより防御担当者は、アプリケーションコードの実行前にPHPインタプリターがメモリを枯渇させる要因が生じていることを示す、より高精度なシグナルを得られます。このようなパターンは、PHPエンジンに対する悪用の試みを示している可能性があります。お客様はこちらからテンプレートをご確認いただけます([US-1] [US-2] [US-3] [EU-1] [US-GOV-1] [US-GOV-2])。
まとめと推奨事項
クラウドストライクは、変化し続けるこの状況を継続的に監視しており、新たな情報が得られ次第、最新情報をお知らせします。並行して、公開されたPHP脆弱性CVE-2026-109618およびCVE-2026-109619に関連する悪意のある活動を検知・阻止するため、Falconプラットフォームで新たな振る舞い検知ロジックの開発と展開を続けています。
お客様には、脆弱性のあるバージョンのPHPを実行しているシステムを特定し、PHP公式またはベンダーのガイダンスに従って最新のセキュリティ更新プログラムを適用することをお勧めします。