1
2

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から12年さかのぼるRCEの家系図

1
Posted at

はじめに

2026 年 10 月、PHP に「php2shell」と呼ばれる脆弱性が出た。

  • 認証なしで RCE(リモートコード実行)に至りうる重大な脆弱性 2 件(CVE-2026-109618 / CVE-2026-109619、どちらも CVSS 9.2)
  • 実行形態のうち mod_php が最も危険(アプリのコードが動く前に攻撃が成立しうる)
  • 完全な緩和策はなく、更新だけが完全な解決策

出典:

この記事の結論はこれ。

PHP の RCE は「新種」より 「同じ構造の再発」 が多い。
CGI に生の文字列を渡す設計は 2012 年(CVE-2012-1823)に塞がれたのに、
Windows の文字変換で 2024 年(CVE-2024-4577)に蘇った。
2026 年の php2shell はメモリ初期化の順序という別系統の穴だった。
だから本命は毎回「更新」で、web サーバ設定は時間稼ぎにしかならない。

この記事で分かること

  • PHP の RCE が どの仕組みのどこで壊れたのか(根本原因ごとの 4 系統)
  • 同じ穴が 12 年越しで再発したという、この記事の背骨になる話
  • 各事件が どれだけ広く影響し、実際に悪用されたのか

👉 攻撃手順や PoC は載せない。「何が起きて、なぜ危険で、どう直ったか」の構造だけを扱う。


家系図:PHP の RCE は 4 系統に分けられる

個別の CVE を年表で並べるより、根本原因でグループ化したほうが見通しがよい。

  • 実線は同じ系統内の流れ、点線は「同じ穴が別の経路で再発した」 を表す
  • 2012 年の CVE-2012-1823 と 2024 年の CVE-2024-4577 は、同じ CGI 引数インジェクションの再発
  • 系統 ④(設計・機能の罠)はメモリ破壊ではないので、この図には載せていない(第 6 章で扱う)

4 系統の一言紹介

  • ① CGI 引数インジェクション:php-cgi に渡す文字列がコマンドライン引数として解釈される(2012 年 → 2024 年)
  • ② FastCGI/FPM メモリ:PHP-FPM のメモリ処理の不備を突く(2019 年・2021 年)
  • ③ メモリ初期化:リクエスト処理の初期化の順序の穴(2026 年 php2shell)
  • ④ 設計・機能の罠:メモリバグではなく、仕様や既定値そのものが罠になる系統

❌ 「RCE は毎回まったく新しい手口」ではない
👉 構造を見ると、境界をまたぐ生の文字列や既定で有効な機能が何度も狙われている

出典(2012 年と 2024 年の関係):


第 3 章:CGI 引数インジェクション — 同じ構造が 12 年越しに再発した

この記事の背骨になる話。一度塞いだ穴が、12 年後に別の経路で開いた。

2012 年:CVE-2012-1823

  • CGI 方式の PHP(php-cgi)で、= を含まないクエリ文字列が コマンドライン引数として php-cgi に渡る構造だった
  • 👉 Web からの入力が、プログラムの起動オプションとして解釈されてしまう
  • CGI 方式の構成で起きる挙動で、少なくとも 8 年気づかれなかった
  • 影響を受けるのは CGI ベースの構成。mod_php や php-fpm の構成は対象外
  • CERT/CC は報告者を De Eindbazen としている
  • 2012 年に修正されたが、最初の修正(5.3.12 / 5.4.2)は不完全だった
  • 5.3.13 / 5.4.3 で修正が完了した(CVE-2012-2311)

出典:

2024 年:CVE-2024-4577

2024 年、Windows の文字変換が 2012 年の修正をすり抜けた。

  • Windows には Best-Fit という変換がある。変換先の文字コードに無い文字を、「近い文字」に置き換える仕組み
  • 日本語(CP932)・簡体字中国語(CP936)・繁体字中国語(CP950)の環境では、ソフトハイフン(0xAD)が通常のハイフン(0x2D、-)に化ける
  • 👉 チェックの時点では - ではなかった文字が、変換後に - になる
  • つまり原因は、2012 年と同じ「入力が引数として解釈される」構造のまま。文字変換によって 2012 年の修正が回避された
2012 年(CVE-2012-1823) 2024 年(CVE-2024-4577)
構造 入力が php-cgi の引数になる 同じ
修正 2012 年に修正 2012 年の修正が回避された
要因 CGI の引数渡し Windows の Best-Fit 変換
  • 公式の助言は「CVE-2012-1823 のバイパス」と明記している
  • 報告者は Orange Tsai (DEVCORE)
  • 修正版は 8.3.8 / 8.2.20 / 8.1.29(これらより前が対象)
  • DEVCORE の解析では、XAMPP は既定の設定で、全ての Windows 版が該当した
  • 繁体字・簡体字・日本語の環境では、直接 RCE に至りうるとされた

❌ 「CGI なんて古い話だから、自分には関係ない」
👉 関係ある。12 年前に塞いだはずの穴が、環境依存の文字変換という別の経路で再発した。

出典:

追いバイパス:CVE-2024-8926 / 8927

  • CVE-2024-8926:CVE-2024-4577 の修正をさらに回避するもの。OEM コードページ(437 など)での文字変換が原因
  • CVE-2024-8927:cgi.force_redirect の設定を回避するもの
  • 👉 文字変換まわりは、一度の修正では閉じきれなかった

出典:

被害規模:CISA の KEV(悪用が確認された脆弱性)に CVE-2012-1823 と CVE-2024-4577 の両方が掲載。CVE-2024-4577 はランサムウェアでの悪用が確認済み(Known)。


第 4 章:FastCGI/FPM のメモリ系 — 設定の隙間とメモリの境界

CGI 引数とは別の系統。PHP-FPM が扱うメモリの境界が壊れた事件が 2 つある。

2019 年:CVE-2019-11043

nginx と PHP-FPM の組み合わせで起きた、認証なしの RCE。

  • nginx の fastcgi_split_path_info は、URL を「スクリプト部分」と「PATH_INFO 部分」に分ける設定
  • 特定の書き方をした正規表現では、PATH_INFO を正しく分割できなくなる場合がある
  • その結果 PATH_INFO が空になる
  • PHP-FPM 側では env_path_info というポインタが、確保したバッファの手前を指してしまう(underflow)
  • 👉 nginx の設定の隙間と、FPM のメモリ処理の不備が 重なったときだけ 成立する

前提になる設定は 2 つ。

  • fastcgi_split_path_info に問題のある正規表現を使っている
  • try_files $uri =404 のような ファイルの存在チェックが無い

❌ 「PHP のバージョンだけ見ていれば安全」ではない
👉 nginx 側の設定が揃って初めて成立する。設定側の 1 行で塞げるのが特徴

発見の経緯と規模:

  • 2019 年 9 月の Real World CTF で、Wallarm の Andrew Danau 氏が異常な挙動に気づいて発見した
  • GitHub 上で 6000 件以上の該当設定が見つかった
  • 修正版は 7.3.11 / 7.2.24 / 7.1.33
  • 修正は、path_info を設定する際に長さを確認するガードの追加
  • 対策として、try_files $uri =404 を入れる方法がある。Wallarm は「easiest way to patch」と書いている

出典:

被害規模:CVE-2019-11043 は CISA の KEV に掲載(2022 年 3 月追加)。ランサムウェアでの悪用は確認済み(Known)。

2021 年:CVE-2021-21703(ローカル権限昇格)

こちらは リモートから認証なしで RCE する話ではない。

  • ❌ リモート RCE ではない
  • 👉 ローカル権限昇格(すでに worker を乗っ取った攻撃者が root になる)

仕組みの構造:

  • PHP-FPM の master プロセスは通常 root、worker は www-data などの非特権ユーザーで動く
  • master と worker は、共有メモリ上の scoreboard を介して状態を共有する
  • scoreboard 内のポインタ(procs[])が worker から書き換えられる
  • 👉 worker が root の master に任意のアドレスを参照させ、worker から root へ昇格できる

つまり、**別の脆弱性で worker を押さえられたあとの「次の一手」**になる。
権限分離という防御の壁が、共有メモリの経路で抜けた事例。

  • ChangeLog 上の記載は「PHP-FPM oob R/W in root process leading to privilege escalation」
  • 修正は 7.4.25(2021 年 10 月 21 日)。8.0.12 / 7.3.32 でも同時期に修正された

出典:


第 5 章:php2shell(2026 年)— リクエスト初期化の穴

2026 年 10 月の事件。原因の異なる 2 つの CVE が同時に出た。混ぜずに分けて見る。

「php2shell」という名前は、修正前の情報が流出した後に出回った PoC ツールの名前に由来する。

CVE-2026-109618:OOM と longjmp

公式の見出しは「RCE via OOM corruption」。

  • PHP はメモリ不足(OOM)のとき、longjmp() で処理を中断して抜ける
  • リクエストの初期化の途中で OOM が起きると、初期化が中途半端なままになる
  • そのあとのリクエスト終了処理(shutdown)が、その不完全な状態を正常なものとして読んでしまう
  • 修正は、OOM での longjmp() を 取り除くもの

👉 原因は「初期化の途中で中断されること」と「終了処理がそれを疑わないこと」

  • mod_php は、リクエスト開始に失敗しても終了処理を無条件で呼ぶ
  • FPM については、完全な攻撃は示されていないと公式は書いている

CVE-2026-109619:残ってしまう状態

公式の見出しは「RCE via stale global state」。

  • リクエストが持っていた状態が、メモリが解放されたあとも残る(use-after-free)
  • 既定で有効な session upload progress が、通常のセッション初期化より前にセッション処理へ到達する
  • アプリが session_start() を呼ぶ必要はない

❌ 「セッションを使っていないアプリなら関係ない」
👉 関係ある。アプリのコードが一度も呼ばなくても、既定の機能が入口になる

2 つの違い

CVE-2026-109618 CVE-2026-109619
公式の見出し RCE via OOM corruption RCE via stale global state
原因 OOM 時の longjmp() で初期化が中断 解放後も残る状態(use-after-free)
壊れる所 shutdown が不正状態を正常扱い セッション処理が古い状態を参照
CVSS 9.2 9.2

なぜ mod_php が最も危険か

  • 公式は「mod_php のリスクが最も高い」としている
  • 理由は、アプリのコードが動く前に攻撃が成立する実証があるから
  • 👉 アプリ側でどれだけ入力を検証しても、その手前で決着がつく
  • 対象は PHP-FPM、Apache の mod_php、php-cgi、FrankenPHP、LiteSpeed

流出と緊急リリース

  • 公開は本来 10 月 22 日の予定(embargo)だった
  • 10 月 20 日に流出した(php.net archive)
  • PHP は 10 月 21 日に緊急で修正版を出した(8.5.12 / 8.4.27 / 8.3.36 / 8.2.35)(php.net archive)
  • PHP 8.1 以前は公式更新なし。7.4 / 8.0 / 8.1 には最小限のバックポートがある

対策は「更新して、全 worker を再起動」

  • 公式は「完全な緩和策は無い。更新だけが完全な解決策」という趣旨の説明をしている
  • CVE-2026-109619 の助言も「設定による完全な緩和策は無い」としている
  • session.upload_progress.enabled=0 は、確認された経路を塞ぐだけで、他の経路は塞げない
  • CVE-2026-109618 の memory_limit=-1 も、完全な緩和ではない
  • 更新後は 全ての worker を再起動して、古いプロセスを残さない

👉 web サーバ側の暫定対応(nginx)は、同リポジトリの別記事で扱う。この記事では設定の中身には踏み込まない

出典:

被害規模(2026 年 10 月下旬時点のベンダ報告をまとめたもの):watchTowr が honeypot で両 CVE の悪用試行を観測したと報告。一方、Huntress は 10 月 21 日時点でパートナー環境での悪用は未観測と報告しており、報道に食い違いがある。CISA の KEV には未掲載。


第 6 章:設計・機能の罠 — メモリバグではないのに被害が大きかった系統

ここまではメモリや引数の不備だった。この章は違う。仕様や既定値そのものが罠になった例を扱う。

register_globals:外部入力が変数に化ける

  • 外部からの入力(クエリ文字列やフォーム)が、そのままスクリプトの変数になる仕組み
  • 初期化していない変数に、外部から値を差し込めてしまう
  • 👉 「ログイン済みか」を示す変数を、外部から先に作られると認可を素通りできる
  • PHP 5.3.0 で非推奨になり、5.4.0(2012 年 3 月)で削除された
  • 5.4.0 の公式 migration guide は「Register globals, magic quotes and safe mode were removed」と書いている

出典:

cgi.fix_pathinfo:存在しない .php パスで手前のファイルが動く

  • cgi.fix_pathinfo の既定値は 1(INI_SYSTEM)
  • nginx の素な location ~ \.php$ と組み合わせると、存在しない .php パスでも、パスの手前にあるファイルが PHP として実行される
  • たとえば image.jpg/x.php のような形の URL で、image.jpg が PHP として扱われる
  • 👉 画像に偽装したファイルをアップロードできる環境では、RCE につながる
  • 緩和は 3 つ(どれも設定の話)
    • try_files $uri =404 で、存在しないファイルを PHP に渡さない
    • cgi.fix_pathinfo=0 にする
    • FPM の security.limit_extensions で実行する拡張子を絞る(既定は .php .phar)

❌ 「画像しか受け付けていないから安全」
👉 拡張子や中身を見ていても、実行側の解釈が別なら意味がない

出典:

httpoxy(CVE-2016-5385):Proxy ヘッダが環境変数に化ける

  • CGI の仕様(RFC 3875)では、リクエストヘッダが HTTP_ を頭に付けた環境変数になる
  • そのため Proxy ヘッダが、HTTP_PROXY という環境変数と衝突する
  • 👉 外部の入力が、サーバ側の通信経路の設定に化ける
  • 同じ構造は 2001 年の libwww-perl から、各言語で繰り返し見つかっていた
  • PHP は 7.0.9 / 5.6.24(2016 年 7 月 21 日) で対応した
  • 緩和は、web サーバで Proxy ヘッダを取り除くこと

出典:

null byte(CVE-2006-7243):拡張子チェックをすり抜ける

  • PHP 5.3.4 より前は、パス名に \0(NULL 文字)が含まれていても受け付けた
  • 文字列として見ると安全な拡張子が後ろにあり、ファイルシステム側ではそこで文字列が終わる
  • 👉 チェックする側と使う側で、文字列の終端の解釈が違う
  • 5.3.4(2010 年 12 月) で「Paths with NULL in them (foo\0bar.txt) are now considered as invalid」となった

出典:

session.upload_progress:既定で有効な入口

  • session.upload_progress.enabled の既定値は 1(INI_PERDIR)
  • アップロードの進捗を、セッションに書く機能
  • 公式マニュアルが警告している。ファイル名やフィールド名は検証されず、PHP コードを含みうる。そのため、ローカルファイルインクルードの脆弱性があれば、悪用がきわめて容易になる
  • 👉 CVE-2026-109619(php2shell)とは入口が同じ。マニュアルの警告はローカルファイルインクルード(LFI)の話で、CVE-2026-109619 は古い状態が残るバグ。別の脆弱性だが、どちらも既定で有効なこの機能を通る
  • 第 5 章の「既定で有効な session upload progress」は、ここで挙げた機能と同じもの

出典:

この系統の共通点

  • どれもメモリを壊していない。仕様どおりに動いた結果が、罠だった
  • 既定値や仕様が、境界をまたぐ文字列(入力→変数、URL→ファイル、ヘッダ→環境変数)をそのまま通していた

❌ 「PHP を最新にしておけば、この系統も全部塞がる」
👉 半分だけ正しい。register_globals(5.4.0 で削除)、null byte(5.3.4 で無効化)、httpoxy(7.0.9 / 5.6.24 で対応)は、PHP 側の修正で塞がった。更新が効く
👉 更新で塞がらないのは、既定値と、PHP を web サーバにつなぐ部分。ここは設定と設計で殺すしかない

  • nginx と cgi.fix_pathinfo の設定(cgi.fix_pathinfo=0、try_files $uri =404)
  • PHP-FPM の pool の設定(security.limit_extensions、既定は .php .phar)
  • web サーバで Proxy ヘッダを除去する
  • session.upload_progress は、不要なら切る。ただし CVE-2026-109619(php2shell)には時間稼ぎにしかならない(確認された経路を塞ぐだけ)。本命は更新

第 7 章:被害規模の一覧

ここまでの CVE を、1 つの表にまとめる。主な被害規模はこの表にまとめた。

年 CVE 通称 系統 影響する構成 CISA KEV 実悪用
2006 CVE-2006-7243 null byte ④ 設計・機能の罠 5.3.4 より前(パス名に NULL を受け付ける) — —
2012 CVE-2012-1823 php-cgi クエリ文字列 ① CGI 引数 CGI ベースの構成(mod_php・php-fpm は対象外) 掲載 KEV 掲載(ランサムウェアは Unknown)
2016 CVE-2016-5385 httpoxy ④ 設計・機能の罠 CGI 系の構成(Proxy ヘッダが HTTP_PROXY に化ける) — —
2019 CVE-2019-11043 FPM underflow ② FPM メモリ nginx + PHP-FPM の特定設定(GitHub に 6000 件以上) 掲載 KEV 掲載(ランサムウェアは Known)
2021 CVE-2021-21703 FPM local root ② FPM メモリ PHP-FPM(ローカル権限昇格) — —
2024 CVE-2024-4577 Best-Fit ① CGI 引数 Windows の php-cgi(XAMPP は既定で該当) 掲載 KEV 掲載(ランサムウェアは Known)
2026 CVE-2026-109618 / CVE-2026-109619 php2shell ③ メモリ初期化 PHP-FPM・mod_php・php-cgi・FrankenPHP・LiteSpeed(mod_php が最も危険) — 悪用試行の観測あり(ベンダ報告に食い違い)
  • CISA KEV の列の出典:CISA Known Exploited Vulnerabilities Catalog
  • 2026 年の行は 10 月下旬時点の報告。watchTowr は悪用試行を観測、Huntress は 10 月 21 日時点で未観測と報告
  • 空欄は「—」。「—」は「掲載なし」「確認できず」を意味し、「悪用されていない」とは限らない

👉 KEV に載ったのは、CGI 引数の 2 件と、FPM の 2019 年の 1 件。ランサムウェアでの悪用が確認されたのは、このうち 2019 年と 2024 年の 2 件。


第 8 章:教訓 — 再発する 3 つのパターン

事件は違っても、壊れ方は 3 つに収まる。

  • 設計・既定値の罠(try_files や cgi.fix_pathinfo=0 など)には、設定が本物の対策になる
  • メモリや引数の不備には、更新が本命。設定は時間稼ぎ

パターン (a):境界をまたぐ生の文字列は、悪用の入口になる

  • CGI の引数、PATH_INFO、ヘッダから環境変数への変換
  • 入力→変数、URL→ファイル、ヘッダ→環境変数。境界を渡るたびに、解釈が変わる
  • 👉 防御:nginx では try_files $uri =404 で、存在しないファイルを PHP に渡さない
  • 👉 例外は CVE-2019-11043。try_files $uri =404 で nginx 側が発動条件そのものを消すので、設定が本当に塞げる(時間稼ぎではない)

パターン (b):「既定で有効」は攻撃面

  • session upload progress、cgi.fix_pathinfo。register_globals も、5.4.0 で削除された仕様の罠だった
  • アプリが使っていなくても、既定で開いている入口は攻撃面になる
  • 👉 防御:cgi.fix_pathinfo=0 にする。不要な session upload progress は切る
  • ただし php2shell(CVE-2026-109619)には、upload progress を切っても時間稼ぎにしかならない。他の経路は塞げない。本命は更新

パターン (c):実行形態(SAPI)で攻撃面が変わる

  • mod_php:web サーバのプロセスの中で動く。アプリのコードが動く前に攻撃が成立しうる。php2shell で公式が mod_php のリスクを最も高いとしたのはこのため。FPM については、完全な攻撃は示されていない
  • php-cgi:インタプリタが直接さらされやすい。2012 年と 2024 年の CGI 引数インジェクションは、この形態で起きた
  • PHP-FPM:PHP の worker を web サーバから分けられる。ただし万能ではない。2019 年の RCE と 2021 年のローカル権限昇格は、どちらも FPM だった
  • 👉 防御:形態ごとの危険を知ったうえで選ぶ。FPM では security.limit_extensions=.php で実行する拡張子を絞る(PHP-FPM の pool の設定で、nginx の設定ではない)
  • 👉 httpoxy 対策として、web サーバで Proxy ヘッダを取り除く

❌ 「この防御を入れたから、更新しなくていい」
👉 メモリや引数の不備には効かない。更新までの時間稼ぎにはなるが、代わりにはならない。


第 9 章:まとめ

この記事には、一見ぶつかる 2 つの主張がある。原因の種類を分ければ、両方とも正しい。

  • メモリ破壊系(php2shell、FPM の 2019 年):本命は更新
  • 引数インジェクション系(CGI の 2012 年・2024 年):これも本命は更新。メモリ破壊ではないが、PHP 側の修正で塞ぐ
  • 設計・既定値の罠:更新では塞がらない部分は、設定と設計が本命

冒頭の「更新だけが完全な解決策」は php2shell の話で、前の 2 グループにあたる。第 6 章の「設定と設計で殺す」は 3 つ目のグループの話。

  • PHP の RCE は、新種より再発が多い。2012 年の穴は 2024 年に別の経路で蘇った
  • 設定は php2shell を完全には塞げない。upload progress を切っても時間稼ぎにとどまる
  • 実行形態(mod_php・php-cgi・FPM)ごとに攻撃面が違う。どれも万能ではない

👉 まず今の PHP のバージョンを確認する。次に、既定で開いている入口と、web サーバとの接続部分を見直す。ただし php2shell への答えは、見直しではなく更新。

1
2
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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?