1. ホーム
  2. ブログ
  3. PHP のセキュリティ

PHP2Shell の技術解説|OOM 時の longjmp(bailout)と stale global state による UAF、修正コミットの中身

公開
著者
編集部

CVE-2026-109618 と CVE-2026-109619 を、PHP 公式の勧告(GHSA)と修正コミット「Avoid state corruption via bailout」から読み解く。リクエストの起動と終了、ワーカーの再利用、CVSS v4 のベクター、各社で食い違う説明まで、技術者向けにまとめた。

この記事の流れ約55秒
この記事で分かること

PHP2Shell の技術解説|OOM 時の longjmp(bailout)と stale global state による UAF、修正コミットの中身

  • CVE-2026-109618(GHSA-w8hw-w9fx-j79w)は「RCE via OOM corruption」。OOM を longjmp(bailout)で抜けると、初期化の途中のリクエストのメモリが残り、shutdown の処理がそれを読む
  • CVE-2026-109619(GHSA-6wmf-4rv5-p3rp)は「RCE via stale global state」。リクエストのヒープを指すグローバル変数が初期化されず、同じワーカーの次のリクエストで use-after-free になる
  • 両方を直したのは1つのコミット(01a40e89fb「Avoid state corruption via bailout」)。OOM の bailout を UnwindExit に置き換え、起動中に触るグローバル変数を初期化する
  • 公式の勧告では、mod_php の既定の設定で RCE を確認。PHP-FPM は、109619 のセッションの経路で RCE を確認、109618 はソースの解析で影響ありと判断
1 / 10

前提:PHP のリクエストの一生

PHP は、Web サーバーから受け取ったリクエストごとに、おおむね次の順で処理する

  • request startup:$_GET・$_POST・$_COOKIE・$_FILES を組み立てる。application/x-www-form-urlencoded や multipart/form-data(RFC 1867)の本文は、ここで自動で解析される
  • スクリプトの実行:アプリのコード(WordPress など)が動く
  • request shutdown:出力を送り、デストラクタを呼び、リクエスト用のヒープ(emalloc で確保した領域)を解放する
ここを読む
mod_php と PHP-FPM の違い
2 / 10

CVE-2026-109618:OOM corruption

PHP は、メモリ不足(OOM)のとき、longjmp() で今の処理から一気に抜けて、リクエストを打ち切る

  • 使える Content-Length がない
  • パートごとのヘッダーの数に上限がない
  • 中身が空のファイルの項目と、入れ子の項目名の組み合わせ
ここを読む
3 / 10

CVE-2026-109619:stale global state

PHP は、エラー・セッション・オブジェクト・拡張機能の状態を、グローバル変数に持つ

  • INI の早い段階の診断:リクエストの初期化より前に、警告を出すか・表示するか・記録するかを決める前に、グローバルの状態を読む
  • セッションのアップロードの進み具合:ふつうのセッションの初期化より前に動く。session.upload_progress.enabled は既定で有効で、session_start() を呼ばなくても動く
ここを読む
4 / 10

2つを組み合わせると

109618 で OOM を起こすことは、109619 のリセットを飛ばす引き金にもなる

ここを読む
5 / 10

修正コミットの中身

両方の勧告を直したのは、1つのコミット 01a40e89fb(タイトルは「Avoid state corruption via bailout」)だ

  • OOM の bailout を UnwindExit に置き換える:longjmp() で飛ぶのをやめ、例外のように巻き戻して、後片付けとデストラクタを必ず通す
  • 起動中に触るグローバル変数を、きちんと初期化する
ここを読む
修正前と修正後:メモリ不足のときの抜け方
6 / 10

CVSS v4 のベクターを読む

2つとも CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N、基本値は 9.2 だ

  • AV(攻撃の経路):N
  • AC(攻撃の複雑さ):H
  • AT(攻撃の前提条件):P
ここを読む
7 / 10

SAPI ごとの影響

  • Apache の mod_php(prefork):109618 は既定の設定で HTTP 越しに RCE を確認。109619 も fatal_error_backtraces とセッションの経路で RCE を確認
  • PHP-FPM:109619 のセッションの経路で RCE を確認。109618 はソースの解析で影響ありと判断(完全な攻撃は未実証)
  • php-cgi・FrankenPHP・LiteSpeed:Web のリクエストを受ける PHP として対象(Datadog)
ここを読む
8 / 10

公開されている PoC について

GitHub には、PoC と称するリポジトリがいくつも出ている

ここを読む
9 / 10

用語

  • SAPI:PHP を Web サーバーなどにつなぐ層(mod_php・FPM・CGI など)
  • bailout:致命的なエラーのとき、longjmp で処理を打ち切る PHP の仕組み
  • UnwindExit:例外のように巻き戻して処理を終える仕組み。後片付けが通る
ここを読む
10 / 10

結論

仕組みを見ると、設定の変更で完全にふさぐのが難しい理由が分かる

ここを読む

PHP2Shell は、PHP のアプリケーションのコードではなく、SAPI(PHP を Web サーバーにつなぐ層)と Zend Engine の、リクエストの起動と終了の処理に見つかった脆弱性だ。この記事では、PHP 公式の2つの勧告(GHSA)と、両方を直した修正コミットをもとに、何が壊れていたのかを技術者向けに説明する。

攻撃のコード(PoC)の中身や、実際に動かす手順は載せない。仕組みの理解と、防御・検知の判断に必要な範囲にとどめる。

この記事の要点
  • CVE-2026-109618(GHSA-w8hw-w9fx-j79w)は「RCE via OOM corruption」。OOM を longjmp(bailout)で抜けると、初期化の途中のリクエストのメモリが残り、shutdown の処理がそれを読む
  • CVE-2026-109619(GHSA-6wmf-4rv5-p3rp)は「RCE via stale global state」。リクエストのヒープを指すグローバル変数が初期化されず、同じワーカーの次のリクエストで use-after-free になる
  • 両方を直したのは1つのコミット(01a40e89fb「Avoid state corruption via bailout」)。OOM の bailout を UnwindExit に置き換え、起動中に触るグローバル変数を初期化する
  • 公式の勧告では、mod_php の既定の設定で RCE を確認。PHP-FPM は、109619 のセッションの経路で RCE を確認、109618 はソースの解析で影響ありと判断

前提:PHP のリクエストの一生

PHP は、Web サーバーから受け取ったリクエストごとに、おおむね次の順で処理する。

  1. request startup:$_GET・$_POST・$_COOKIE・$_FILES を組み立てる。application/x-www-form-urlencoded や multipart/form-data(RFC 1867)の本文は、ここで自動で解析される
  2. スクリプトの実行:アプリのコード(WordPress など)が動く
  3. request shutdown:出力を送り、デストラクタを呼び、リクエスト用のヒープ(emalloc で確保した領域)を解放する

大事なのは、1の自動の解析が、アプリのコードより前に動くことだ。認証もルーティングも、まだ何も始まっていない。だから PHP2Shell は「認証前(pre-auth)」に届く。

もう1つの前提は、ワーカーの再利用だ。PHP-FPM のワーカーや、Apache の prefork MPM の子プロセス(mod_php)は、1つのプロセスで多くのリクエストを順に処理する。リクエストのヒープは毎回解放されるが、プロセスのグローバル変数は残る。

mod_php と PHP-FPM の違い
mod_php と PHP-FPM の違い

CVE-2026-109618:OOM corruption

PHP は、メモリ不足(OOM)のとき、longjmp() で今の処理から一気に抜けて、リクエストを打ち切る。これを bailout と呼ぶ。

公式の勧告によると、一部の場面では、リクエストのメモリが完全に初期化される前に、ほかから参照できる状態になっている。その瞬間にメモリの確保が失敗すると、bailout は途中の状態を考えずに抜けてしまう。残された中途半端なデータを、そのあとの request shutdown が読んだり、それをもとに分岐したりする。

勧告が OOM のきっかけとして挙げているのは、multipart の解析の問題だ。

  • 使える Content-Length がない
  • パートごとのヘッダーの数に上限がない
  • 中身が空のファイルの項目と、入れ子の項目名の組み合わせ

Datadog の分析は、application/x-www-form-urlencoded の本文で、とても長い項目名を送って $_POST の組み立て中に OOM を起こす手口を説明している。どちらも、request startup の自動の解析でメモリを使い切らせる、という点は同じだ。

弱点の種類(CWE)は、CWE-416(use after free)・CWE-755(例外的な状態の扱いの誤り)・CWE-824(初期化されていないポインタへのアクセス)・CWE-1325・CWE-1419 が挙げられている。

CVE-2026-109619:stale global state

PHP は、エラー・セッション・オブジェクト・拡張機能の状態を、グローバル変数に持つ。その中には、リクエストのヒープの中を指すポインタもある。こうしたポインタは、リクエストのメモリを解放する前に、必ず初期化(リセット)しなければならない。

公式の勧告によると、致命的なエラー(OOM を含む)でこのリセットが飛ばされることがある。また、shutdown の途中の致命的でない診断のメッセージが、いったん消したエラーの状態を、もう一度埋めてしまうこともある。その結果、リクエストのヒープは解放されたのに、グローバル変数はそこを指したまま残る(ダングリングポインタ)。

同じワーカーの次のリクエストが、このポインタを使うと use-after-free になる。勧告が挙げている「使う側」は次の2つだ。

使う側中身
INI の早い段階の診断リクエストの初期化より前に、警告を出すか・表示するか・記録するかを決める前に、グローバルの状態を読む
セッションのアップロードの進み具合ふつうのセッションの初期化より前に動く。session.upload_progress.enabled は既定で有効で、session_start() を呼ばなくても動く

勧告は、PHP 8.5 の fatal_error_backtraces(致命的なエラーのときにバックトレースを残す機能)の状態を「もっとも強く実証された例」としている。確認された影響は、認証なしのヒープの use-after-free、メモリの漏えい、Web のワーカーの権限での任意のコードの実行だ。

2つを組み合わせると

109618 で OOM を起こすことは、109619 のリセットを飛ばす引き金にもなる。OffSec の解説は、壊れた OOM の POST を送ったあと、同じワーカーに続けて multipart のリクエストを送ると、解放済みのヒープの中身が PHPSESSID の Cookie の値として URL エンコードされて返ってくる、という連鎖を説明している。漏れたメモリの中身は、ASLR(アドレスの無作為化)をくぐるための手がかりになりうる。

JPCERT/CC が侵害の調査で「異常に長い・変な形の PHPSESSID が返っていないか」を確かめるよう求めているのは、この経路があるからだ。

CVE と仕組みの対応は、情報源で食い違っている

PHP 公式の勧告は、109618 を OOM corruption、109619 を stale global state(アップロードの進み具合と fatal_error_backtraces を含む)としている。一方、Wiz の表は、アップロードの進み具合の use-after-free とメモリの漏えいを 109618 に、fatal_error_backtraces と OPcache の型の取り違えを 109619 に割り当てている。この記事は、PHP 公式の勧告に合わせた。

修正コミットの中身

修正前と修正後:メモリ不足のときの抜け方
修正前と修正後:メモリ不足のときの抜け方

両方の勧告を直したのは、1つのコミット 01a40e89fb(タイトルは「Avoid state corruption via bailout」)だ。DEV Community の読み解きによると、変更の要点は2つある。

  • OOM の bailout を UnwindExit に置き換える:longjmp() で飛ぶのをやめ、例外のように巻き戻して、後片付けとデストラクタを必ず通す
  • 起動中に触るグローバル変数を、きちんと初期化する

公式の勧告は、修正で OOM を「遅延したソフト OOM」と「_exit() によるハード OOM」に分けたとしている。変更は 300 ファイル・約 1 万行で、その多くは、配列・unserialize・JSON・PCRE・mbstring・SOAP・GD などで、わざと確保を失敗させても Zend Engine が壊れないかを確かめる oom_* のテストだという。

修正が先に公開されていた

同じ読み解きによると、このコミットは公表の3日前(10月18日・UTC)に、php-src の公開のリポジトリに入っていた。コミットのメッセージには勧告の番号があり、追加されたテストは、どこが入口になるかを示していた。公表前に中身が漏れた原因の1つとみられている。

CVSS v4 のベクターを読む

2つとも CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N、基本値は 9.2 だ。

項目値意味
AV(攻撃の経路)Nネットワーク越しに届く
AC(攻撃の複雑さ)H高い。メモリの配置など、成功させるのに工夫が要る
AT(攻撃の前提条件)Pあり。特定の設定や状態がそろう必要がある
PR(必要な権限)N不要(認証前)
UI(利用者の操作)N不要
VC / VI / VAH / H / H機密性・完全性・可用性への影響が大きい
SC / SI / SAN / N / Nほかのシステムへの直接の影響はない

「AC:H」と「AT:P」が付いているのに 9.2 なのは、認証も操作も要らず、成功すれば全部を失うからだ。

SAPI ごとの影響

SAPI公式の勧告での扱い
Apache の mod_php(prefork)109618 は既定の設定で HTTP 越しに RCE を確認。109619 も fatal_error_backtraces とセッションの経路で RCE を確認
PHP-FPM109619 のセッションの経路で RCE を確認。109618 はソースの解析で影響ありと判断(完全な攻撃は未実証)
php-cgi・FrankenPHP・LiteSpeedWeb のリクエストを受ける PHP として対象(Datadog)
CLIWeb のリクエストを受けないので、勧告の対象の外

公開されている PoC について

GitHub には、PoC と称するリポジトリがいくつも出ている。Sophos は、10月22日にいったん「悪用の成功を1件確認」と発表したが、同じ日のうちに訂正し、成功した侵害の証拠はないとした。観測できたのは、スキャンと少数の失敗した試みだった。同社は、攻撃を完成させるのは簡単ではなく、出回っている PoC は実験の環境でしか動かない可能性があるとしている。

出回っている PoC の中には、いたずらのものもあると報じられている。検証に使うとしても、信用できないコードとして扱い、本番の環境では動かさない。

用語

用語意味
SAPIPHP を Web サーバーなどにつなぐ層(mod_php・FPM・CGI など)
bailout致命的なエラーのとき、longjmp で処理を打ち切る PHP の仕組み
UnwindExit例外のように巻き戻して処理を終える仕組み。後片付けが通る
use-after-free(UAF)解放したメモリを、あとから使ってしまう不具合
ダングリングポインタ解放済みの領域を指したまま残ったポインタ
RFC 1867multipart/form-data によるファイルのアップロードの決まり
ASLRメモリの配置を毎回変えて、攻撃を難しくする仕組み
GHSAGitHub のセキュリティ勧告の番号

結論

仕組みを見ると、設定の変更で完全にふさぐのが難しい理由が分かる。OOM はさまざまな場所で起きうるし、リクエストをまたいで残る状態も1か所ではない。公式の勧告が「修正版への更新だけが確実」とするのはこのためだ。修正版は 8.5.12・8.4.27・8.3.36・8.2.35。8.1 以前には正式な修正版がないので、移行を急ぐ。

よくある質問

なぜアプリのコードより前に攻撃が届くのですか?

$_POST や $_FILES の組み立ては、request startup で自動に行われるからです。
認証やルーティングが動く前の段階なので、認証前(pre-auth)の攻撃になります。

PHP-FPM なら mod_php より安全ですか?

影響は受けます。
公式の勧告では、CVE-2026-109619 のセッションの経路で、PHP-FPM でも RCE が確認されています。
mod_php のほうが条件はそろいやすいですが、どちらも更新が必要です。

CVSS の攻撃の複雑さが「高」なのに、なぜ 9.2 なのですか?

認証も利用者の操作も要らず、成功すれば機密性・完全性・可用性のすべてを失うからです。
成功のしにくさより、成功したときの影響の大きさが重く見られています。

無料ツールPHP2Shell の無料診断PHP のバージョンとサーバー会社を選ぶだけで、危険度と今やることが分かります。

参考にした情報

この記事の情報

2026年10月22日時点の公開情報にもとづく。攻撃のコード(PoC)の中身は、悪用を助けないよう載せていない。最新の情報は、PHP の公式の勧告と JPCERT/CC を確認すること。

自社のサーバーの構成で、どの層から手を付けるべきかを、構成を聞いてから伝える。相談は無料。

無料で相談する

この記事の著者

編集部