
PHP CVE-2026-109618/109619 — 素のPHP 8.5.11における自己完結型の未認証RCE(インバンドPS(id)→Cookieメモリ開示 + UAF連鎖)、完全な制御ラダー、8.5.12での修正版ネガティブコントロール
PHP 8.5.11 の php2shell クラス(CVE-2026-109618、CVE-2026-109619、2026年10月)に関するラボ再現および脆弱性研究パッケージ。 ステータス: 実証済み — 自己完結型 RCE(4つの平文 HTTP リクエストのみ、デバッガ不要、ローカルアクセス不要)。さらに以下に増幅、パッチギャップ、FPM のステイルステートに関する知見を示す。
目次: 結果 · チェーン · 増幅 · パッチギャップ · FPM ステイルステート · WAF バイパス · 環境 · クイックスタート · 証明の規律 · 構成 · 公開コンテキスト · 声明
| # | 知見 | 分類 |
|---|---|---|
| 1 | PHP 8.5.11 におけるエンドツーエンドの自己完結型 RCE — 4つの平文 HTTP リクエスト、PS(id)→Cookie 開示によるインバンドのアドレス取得、512 バイトの PHPSESSID リーク1回から両方のベースを取得; 12/12 発火、uid=33、8つの ASLR 世代 | 実証済み — 自己完結型 RCE |
| 2 | 重複した 6 バイトのマルチパートヘッダによる 52 倍のメモリ増幅 — PHP 7.4–8.5.11 において post_max_size を突破(2.57 MB のボディが 128M の制限を OOM させる) | 実証済み — ラボ、4バージョンすべて |
| 3 | 3つのエクスプロイト経路が 8.5.12 のセキュリティパッチを回避(n=1015 での var_destroy 再入 UAF、シリアライズハンドラの不一致、アップロード進捗インジェクション) | 実証済み — GPG 検証済み 8.5.12 上の ASAN |
| 4 | FPM のステイル BG(unserialize): ワーカーがセッションデコードの OOM を生き延び、ステイルステートが次リクエストの rfc1867 ウィンドウで消費され、ヒープ形状の消費がワーカーを SIGSEGV させる | 実証済み — メカニズム + DoS; RCE は未実証 |
| 5 | Content-Type セパレータバイパス(multipart/form-data, boundary=X)— 公開されているすべての Content-Type アンカーの WAF シグネチャがバイパス可能 | 実証済み — SAPI.c パーサの挙動 |
| 6 | パッチ適用済み 8.5.12 では両方の CVE チェーンが無効(トークン同一の configure、GPG 検証済みソース) | 実証済み — ネガティブコントロール |
| 7 | 公開されている「キャッシュされた zval session.upload_progress」理論 — 4つの標準 PHP バージョン + 3つの LiteSpeed ベンダービルド、すべてのクリーンアップ/並行性の隅々で反証 | 反証済み — 網羅的 |
2つの独立したメモリ破壊チェーンが、標準の php:8.5.11-apache イメージ上で攻撃者制御の間接呼び出しに到達する。エンドツーエンドのラッパーは、チェーン B のメカニズムと、完全リモート発火のための新規インバンド開示を組み合わせている。
| チェーン A — ステイルバケット | チェーン B — ダングリング参照 | |
|---|---|---|
| メカニズム | OOM ベイルアウトにより、HashTable バケットが公開されたまま未初期化で残る; シャットダウンがそれを偽の zval として消費 → 偽の配列 → pDestructor | RINIT 前の OOM により PS(http_session_vars) がダングリング zend_reference を保持; 生存する 31 バイトの文字列がそれを再利用; 偽造された参照 → zend_hash_destroy の pDestructor |
| アロケータ | tracked/system(USE_ZEND_ALLOC=0) | 標準デフォルトの Zend MM |
| 終端 | libc system()(gdb で較正したアドレス) | zif_system @ libphp+0x5996e0(libc 不使用) |
| 発火時のデバッガ | 較正のみ | なし — FIRE_GDB=0 |
| 再現性 | 2つのランダム化された親世代 | 繰り返し、クロスランを含む |
Leak-A はダングリング状態の PS(id) を仕込みます(1001 番目の変数が max_input_vars を超過させ →
headers_sent=1;空クッキーによる reset_id の失敗が send_cookie=1 を維持;
24 MiB のフィールドが RINIT 前に OOM を引き起こします)。Leak-B は 257 要素の配列を着地させ、その
zend_array が PS(id)−0x10 と重なります:ZSTR_LEN が nTableSize=512 を読み取り、
URL エンコードされたクッキーが pDestructor(→ libphp ベース)に加えて 9 個の arData
ヒープポインタ(多数決 → チャンクベース)を運びます。計測されたチャンク−libphp の差分は
世代をまたいで一定ではなく(−0x1a00000 / +0x3600000 / −0x1c00000)、
したがってこの情報漏えいは真に不可欠です。その後、ファイアは漏えいしたアドレスを用いて実行されます。
前提条件:display_errors=On、post_max_size が OOM フィールドより大きく、
memory_limit がそれより小さく、シングルワーカーの prefork。
A status='HTTP/1.1 200 OK' → OOM fatal, worker survives, PS(id) armed
B Set-Cookie: PHPSESSID=<512-byte encoded disclosure>
FIRE → MARKER=uid=33(www-data) gid=33(www-data) groups=33(www-data)
FIRE_OK (×12 across 8 distinct ASLR layouts)
完全なレシートと制御ラダー: docs/END-TO-END.md。
最適化された重複ヘッダー増幅: 短い X: a ヘッダー(ワイヤ上6バイト)が、保存されるヘッダーごとにPHPの固定256バイトの SMART_STR_START_SIZE を悪用します(ワイヤ上6 Bあたり約314 Bのメモリ)。8.5.11で正確なOOM閾値の二分探索により測定。7.4.33 / 8.1.34 / 8.4.26でも同一(51~52倍):
| memory_limit | 最小OOMボディ | 増幅率 |
|---|---|---|
| 8M | 153 KB | 54.8x |
| 32M | 636 KB | 52.8x |
| 128M | 2.57 MB | 52.3x |
| 256M | 5.14 MB | 52.2x |
すべてのボディは標準の post_max_size=8M 内に収まります — 実用的なアップロード障壁は突破されます、未パッチのすべてのバージョンで。チャンク化フレーミングはさらにContent-Lengthの事前チェックをバイパスします(8.85 MBのチャンク化ボディがパースされOOMとなり、同じボディをContent-Length付きで送ると拒否されました)。8.5.12のパッチは両方のベクターを塞ぎます(未知ヘッダーのフィルタリング + ストリーミングによる post_max_size の強制)。ネスト配列の「爆弾」形状は指数関数的には 再現しません(最大9.6倍、1.16 MiB — 8MでもOOMにできません)。詳細: docs/RELEASE-RECONCILIATION-2026-10-21.md。
GPG検証済みでトークンが同一の8.5.12 ASANビルドで検証済み — 2つのCVEは修正されていますが、影響に至る3つの別経路がパッチ後も残存します:
var_unserializer.c:906:67);パッチの ZEND_HAS_OOM ガードはこの経路では無効;BG(serialize_lock) は依然として __destruct ディスパッチの周囲で取得されていません。php_serialize で書き込まれた | キーのセッションが、アプリフェーズの session_start() 後に php ハンドラでデコードされると、mod_phpの php_value とFPMの .user.ini のいずれでもガジェットをインスタンス化します;パッチの incomplete_classes_only ガードはRINIT前のアップロードウィンドウのみにスコープされています。php:8.5.11-fpm でラボ検証済み: memory_limit=32M における24 MBのセッションが php_var_unserialize 内でOOMとなり、ワーカーは生存します — exit-70の障壁は 起動時 のOOMにのみ適用されます。gdbは BG(unserialize).level=1 と古いデータポインタが 次の リクエストのrfc1867 RINIT前ウィンドウまで持続することを示しています;8191バイトのマルチパートフィールドが8216バイトの _emalloc を生成し、それが古いポインタと重なり、リクエスト起動中に var_tmp_var でワーカーをSIGSEGVさせます。測定された影響: 再現可能なワーカークラッシュ(DoS)に加え、メモリ破壊プリミティブ;クラッシュを超えるRCEは未証明です。前提条件: 低い memory_limit、攻撃者の影響を受ける大きなセッション、session_start() エンドポイント、upload_progress=On、そして同一ワーカーでの確実なペアリングのための pm=static max_children=1。したがってFPMはこのクラスに対して 条件的に 保護されているのであり、無条件に保護されているわけではありません。
Content-Type: multipart/form-data, boundary=X(カンマ区切り)はPHPに受け入れられます — SAPI.c は ;、,、スペースでトリムします。Content-Type: multipart/form-data; にアンカーされた公開済みのphp2shell WAFシグネチャはすべてバイパス可能です。防御側向けのガイダンスは docs/WAF-BYPASS.md にあります。
display_errors=Off、高い memory_limit はいずれも真の障壁として残ります — docs/RELEASE-RECONCILIATION-2026-10-21.md のフリート調査を参照);| プロパティ | 必須値 |
|---|---|
| ホスト | ネイティブ Linux x86_64 |
| ターゲット | PHP 8.5.11、Apache モジュール(Chain B はステール状態の作業用に FPM でも実行) |
| イメージ | php@sha256:974e3a920309308e1690ed607e6cc3061d6906f90cc8d30fe519daa48bccf101 |
| Chain A アロケータ | USE_ZEND_ALLOC=0、USE_TRACKED_ALLOC=1(意図的に非デフォルト) |
| Chain B アロケータ | 標準デフォルトの Zend MM |
| PHP 制限(チェーン) | memory_limit=16M、post_max_size=32M、upload_max_filesize=32M |
| Apache | prefork、ワーカーは正確に 1 つ |
| デバッガ | Python サポート付きホスト GDB(キャリブレーション/レシート専用) |
所有かつ隔離された x86_64 Linux ラボでのみ実行してください:
sudo ./scripts/preflight.sh
sudo ./scripts/reproduce.sh
ランナーは環境を検証し、バイト単位で同一の5,767,505バイトのフィクスチャを生成し、安全なtype-0x7スロットを調整したうえで、独立したマーカーバイト、ワーカー遷移、ヘルス、トレースの各チェックを伴うfresh-workerのnegative → abort → markerを実行し、完全なSHA-256マニフェストを書き出します。ガード付き配置のミスは記録され、fresh workerごとに1回再試行されます — ミスが実行として報告されることは決してありません。保持される判定は、runs/DATE/VERDICT.json内のすべてのゲートがtrueである場合にのみ有効です。発火させずに再検証するには: python3 scripts/verify_run.py runs/DATE。
sudo ./scripts/lab-stop.sh # stop container, keep state
sudo ./scripts/lab-reset.sh # remove PoC container + network only
| ケース | 期待される効果 | RCEオラクル |
|---|---|---|
| キャリブレーション | Type-0x7の古いスロットが観測される。ワーカーは生存 | なし |
| ネガティブ | 偽の構造体が存在する。ディスパッチなし。同一ワーカー | マーカー不在 |
| アボート | Refcountのデクリメント。正確なabort@plt。SIGABRT | マーカー不在 |
| マーカー | 正確なターゲットディスパッチ。コマンド出力は/tmp配下 | 正確な一意のバイト列 |
HTTPエラー、空のレスポンス、PHPの致命的エラー、またはワーカーのクラッシュは、コマンド実行の証拠として受け入れられません。FPMの古い状態に関する発見がクラッシュプリミティブとして報告されるのは、まさにこの基準を満たしていないためです。
/tmp配下にのみ書き込む、id/printfクラスのコマンドに制限されます。