Search
moon
sun

PHP2Shell:CVE-2026-109618/CVE-2026-109619のフルチェーンRCEエクスプロイト

태그
PurpleHound
Property
php2shell-intro.jpg
작성날짜
2026/10/22

1. 概要

2026年10月、エンバーゴ解除前に流出したPHPコアのリモートコード実行(RCE)脆弱性2件が、php2shellという別名で拡散した。この名称は、流出したスクリーンショットに登場したExploitツールの名前に由来するものであり、PHPの公式名称ではない。両脆弱性はいずれもCVSS v4.0で9.2(Critical)と評価され、認証不要のリモートコード実行が可能であるとされた。
流出直後に公開された多くの仮説は、session.upload_progressハンドラーのUse-After-Free(UAF)を原因として指摘した。しかしベンダーは詳細なメカニズムを公表せず、初期に公開されたPoCの多くは再現できなかった。
本報告書は、公式のphp:8.4.26-apache(mod_php、prefork)環境において、OOM時点のlongjmpによって生じるダングリングポインター(UAF)を起点とする攻撃チェーンを分析したものである。このチェーンが、情報漏えいによるASLR回避とヒープの再占有を経て、www-data(Webサーバー)権限での任意コマンド実行に至ることを再現した。
本脆弱性は、アプリケーションコードが実行される前のリクエスト解析段階で成立する。そのため、WordPressなど、使用するフレームワークにかかわらず影響を受ける。

2. 脆弱性の根本原因分析

2.1. 基本情報

項目
内容
CVE
CVE-2026-109618 / CVE-2026-109619
種別
Use-After-Free(CWE-416)。根本原因は例外処理の不備(CWE-755)
深刻度
CVSS v4.0 9.2(Critical)、認証不要
影響を受けるコンポーネント
ext/session(upload progress)、main/rfc1867.c(multipartパーサー)
影響を受ける構成
session.upload_progress.enabled=On、mod_php / lsphp(ワーカー存続型SAPI)
確認済みバージョン
PHP 8.4.26(修正前)

2.2. OOM発生時の異常な制御フロー離脱

PHP 8.4.26では、リクエスト本文がmemory_limitを超えると、割り当て箇所で直ちに致命的エラーが発生し、zend_bailout()によって制御フローが巻き戻される。
*/* Zend/zend_alloc.c : zend_mm_safe_error() */* heap->overflow = 1; zend_try { zend_error_noreturn(E_ERROR, format, ...); */* "Allowed memory size ... exhausted" */* } zend_catch {} heap->overflow = 0; zend_bailout(); */* setjmp地点へlongjmpし、現在の関数群を貫通して脱出する */*
JavaScript
복사
[コード1] OOM発生時、longjmpによって制御フローから即座に離脱する。
問題となるのは、この割り当て失敗がmultipartパーサーの内部(rfc1867_post_handler())で発生する場合である。パーサーはパート処理の途中で放棄され、確保済みリソースの解放と、セッション側の終了イベント(MULTIPART_EVENT_END)が実行されない。さらに、この解析はphp_hash_environment()の中、つまりzend_activate_modules()の前に行われる。そのため、longjmpで制御が戻った時点でもPG(modules_activated)は0のままである。
*/* main/main.c : php_request_shutdown() */* if (PG(modules_activated)) { */* まだ0 */* zend_deactivate_modules(); */* ext/sessionのRSHUTDOWNがすべてスキップされる */* }
JavaScript
복사
[コード2] モジュールが有効化される前に離脱したため、セッションモジュールのリクエスト終了時クリーンアップが省略される。

2.3. ダングリングポインター(UAF)の成立

セッション初期化では、セッションのグローバル構造体にポインターが保存される。代表例は、セッションID文字列であるPS(id)と、$_SESSIONを保持する参照であるPS(http_session_vars)である。上記のコード1およびコード2の流れでリクエストが中断されると、これらのポインターが指していたヒープオブジェクトは解放されるが、セッショングローバル内のポインターは更新されない。 RSHUTDOWNという最後の後始末まで省略されるため、解放済みメモリを指すポインターが次のリクエストまで残る。
mod_php(prefork)では、このOOMリクエストの後もワーカープロセスが存続する。次のリクエストが同じワーカーに到達すると、解放済みメモリが再び参照される。これが本脆弱性の根本的な欠陥であるUse-After-Freeである。
実験では、ダングリングポインターを維持するには、ワーカーがリクエスト間で存続する必要があった。php-fpmでは同じOOMでワーカーが再起動し、ダングリング状態が消えるため、この経路では成立しなかった。mod_phpおよびLiteSpeed lsphpのように、ワーカーが存続するSAPIに限られる。

2.4. 脆弱性のデモ動画

3. 攻撃の影響

Use-After-Freeを起点として、攻撃は次の3段階で進行する。

3.1. UAFによる情報漏えい → ASLR回避

ダングリング状態のPS(id)が指していた解放済み領域を、攻撃者が制御するデータで再占有する。このとき、**長さフィールドを過大な値に改ざんした偽のzend_string**を配置すると、サーバーがセッションIDを応答ヘッダー(Set-Cookie)へ返す際に、隣接するヒープメモリまで一緒に出力される(ヒープの範囲外読み取り)。漏えいしたバイト列から、次を復元する。
•
Zend MMの2MBチャンク基準アドレス(ヒープベース)
•
zval_ptr_dtorポインターが繰り返し現れる特性を利用したlibphp.soのロードベース
これによりASLRは無効化され、libphp_base + <静的オフセット>からzif_system(PHP内部のsystemハンドラー)の実アドレスを計算できる。

3.2. UAFの再占有 → 型混同 → 任意関数呼び出し

ダングリング状態のPS(http_session_vars)が指していた解放済みページを、攻撃者が細工した偽のzend_reference → 偽の配列(pDestructor = zif_system)+偽のexecute_dataで再占有する。その後、この参照が破棄されるとき、エンジンは配列の破棄処理の中でpDestructorを呼び出す。この値がzif_systemに置き換わっているため、結果として攻撃者のコマンド文字列を引数とするsystem()が実行される。

3.3. 脅威シナリオ

•
認証およびアプリケーションコードより前のRCE: コマンド実行はセッション処理とリクエスト解析の段階で成立するため、ログインや権限検査より前に行われる。フレームワークの種類には依存しない。
•
実測による確認: 公式のphp:8.4.26-apacheイメージに対し、一連のリクエストチェーンでuid=33(www-data)によるコマンド実行を確認した。
•
メモリ安全性対策の無効化: メモリ破損系の脆弱性だが、情報漏えいによってASLRを回避できるため、ASLRやDEPなどは防御にならない。
•
前提と限界: 成立には、①session.upload_progress.enabled=On、②memory_limitの超過を誘発できるアップロードの許可、③OOM後もワーカーが存続するSAPI(mod_php / lsphp。単一ワーカーでヒープグルーミングが決定的になる場合は特に安定する)が必要である。php-fpmでは、OOM時のワーカー再起動によってこの経路は成立しなかった。

3.4. 侵害指標および攻撃の痕跡

ソース
IOC
PHPエラーログ
Allowed memory size ... exhaustedの多発
Apacheエラーログ
child Segmentation faultの繰り返し
プロセス
apache2がsh / id / sleepの子プロセスを生成
セッション保存先
sess_attacksessionid0001、sess_aaaaaaaaaaaaaaaaaaa
upload_tmp_dir
親プロセスによる後始末を失ったphp*一時ファイルの急増
アクセスログ
12MBのmultipart POSTと500の多発、User-Agentなし、GET /out/z
リクエスト本文(WAF)
バウンダリBNDRY12345 / S×17、SPRAY###、バイナリフィールド1999
3.4.1. Webサーバーのアクセスログ(access.log)
•
過大なPOSTの集中: _oom()フィールドが12MBであるため、ほぼすべてのリクエストが/index.phpへの12MB超のPOSTになる。短時間に単一IPから数百件が到達する。
•
応答コードの混在: OOMリクエストは500、leak / rotateは200を返す。fetchによるGET /out/zは、最初は404を繰り返し、成功すると200になる。
•
Connection: close、User-Agentなし()、Content-Type: multipart/form-data。
3.4.2. PHPエラーログ(log_errors / stderr)
PHP Fatal error: Allowed memory size of 8388608 bytes exhausted
(tried to allocate N bytes) in Unknown on line 0
•
rotate、stage1、stage2、stage3、baseline primeはいずれもOOMを引き起こし、攻撃1回あたり数十〜数百行が記録される。
3.4.3. Apacheエラーログとクラッシュアーティファクト
•
ワーカーのセグメンテーションフォルト: [core:notice] AH00052: child pid N exit signal Segmentation fault (11)が多数記録される。
•
コアダンプとクラッシュレポート: core_patternに応じて、/var/crash/apache2*(apport)またはcoreファイルが生成され得る。設定によっては、dmesgにapache2[...] segfaultが記録されることもある。
3.4.4. セッションファイルの生成
session.save_path(例:/var/lib/php/sessions)において、use_strict_mode=0のため、攻撃者が指定したSIDがそのままファイル名になる。
•
sess_attacksessionid0001 ← コード内のSID
•
sess_aaaaaaaaaaaaaaaaaaa ← "a"*19
•
内容にはupload_progress_...|a:5:{...}(ファイル名a.txt、攻撃者のフィールド)の残存データが含まれる。固定SIDそのものが強いシグネチャになる。
3.4.5. アップロード一時ファイル(upload_tmp_dir)
•
各リクエストのファイルパート(f=a.txt、64バイトのAAAA…)は、OOMによる中断で親プロセスによる後始末を失う。 その結果、php******形式の一時ファイルがリクエスト数だけ蓄積され、削除されない。ディスクおよびinodeの痕跡が残る。
3.4.6. コマンド実行(system)の痕跡 → 最も重大
•
ファイル: cmd 'id>/var/www/html/out/z'により/var/www/html/out/zが作成される。内容はuid=33(www-data)...で、所有者はwww-dataである。sleepモードではファイルは作成されない。
•
プロセスの系譜: apache2 → sh -c "<cmd>" → id/sleep/...。www-dataがシェルを生成する親子関係は異常であり、EDRおよび振る舞い検知の中核となるIOCである。
•
auditd / execve: /bin/sh -c、id、sleep 5などのexecve記録が残る。プロセスアカウンティング(sa / lastcomm)にも残る。
3.4.7. ネットワークおよびリクエスト本文のシグネチャ(WAF・IDS)
•
multipartバウンダリ: ---…(S×17)、BBBBBBBBBB(B×10)、BNDRY12345。いずれも固有の値である。
•
フィールド名のパターン: PHP_SESSION_UPLOAD_PROGRESS(値pwn)、f0000…f1000、p0000…、フィラー1000…1892(v0000x…)、配列a0000[0]…、スプレーs000…s399。
•
スプレー本文: thoroughモード(_spray_value)にはASCIIマーカーSPRAY000…SPRAY399が含まれ、本文検査で明確に識別できる。packedモード(既定)にはマーカーがなく、.×4071の後に偽造されたバイナリ構造体が続く(zend_reference / HashTableのバイト。0x307、0xFFFFFFF8などのパターン)。
•
フィールド1999: 非表示バイト、Lマーカー、および過大な8バイト長(0x1000 / 0x20000)。範囲外読み取りのトリガーシグネチャである。
•
タイミング: sleep-oracleを使用した場合、成功したリクエスト1件がsleep 5の分だけ遅延する(約5秒以上)。
3.4.8. メモリ(揮発性)
偽造されたzend_reference、HashTable、execute_data、および漏えいしたアドレスはワーカーのメモリ内にのみ存在し、ワーカー終了時に消失する。ただし、クラッシュ時のコアダンプを取得できた場合は、その中に攻撃ペイロードの残存データを捕捉できる。

4. 緩和策および推奨事項

4.1. パッチ

根本的な対策はPHPランタイムのパッチ適用である。WordPressやLaravelなどのアプリケーションを修正しても防ぐことはできない。
ブランチ
修正前
修正を含むバージョン
PHP 8.4.x
8.4.26
8.4.27
PHP 8.3.x / 8.2.x / 8.5.x
各ブランチの直前のリリース
同時に公開されたセキュリティ修正版
PHP 8.1以前(EOL)
—
公式パッチなし
修正版では、メモリが不足してもPHPは処理を突然中断しない。代わりにmultipartリクエストの処理を安全に終了し、使用中のメモリとセッション情報を正常に整理してから抜ける。
これにより、解放済みメモリを指し続ける不正なポインターは残らない。セッションデータを処理する過程で同じ問題が起き得る経路も、同時に遮断される。

4.2. 対応手順

1.
組織内のすべてのPHP環境(サーバー、コンテナ、共有ホスティング、アプライアンス)を棚卸しする。
2.
該当ブランチの修正版へ直ちに更新し、PHP-FPMまたはApacheを再起動する。
3.
パッチ適用前の暫定策として、session.upload_progress.enabled = Offを設定する。コールバック自体が実行されなくなり、ダングリングポインターが生成される時点がなくなる。
4.
EOLブランチ(PHP 8.1以前)には公式パッチがないため、隔離してアクセスを制限したうえで、アップグレードを計画する。
5.
検知の観点では、PHPワーカーがリクエスト処理の初期段階で子プロセスを生成する、または異常終了(クラッシュ)後に再起動するパターンを監視する。
session.serialize_handlerの変更や、CFG、DEP、ASLRなどのメモリ安全性対策は、本脆弱性に対する有効な防御にはならない。前者はこの影響範囲の外に出ず、後者は情報漏えいによって回避される。

4.3. 検知方法

4.3.1. Snort
alert tcp any any -> any $HTTP_PORTS (msg:"purplehound: multipart POST with PHP_SESSION_UPLOAD_PROGRESS (CVE-2026-109618, CVE-2026-109619)"; flow:established,to_server; content:"POST"; http_method; content:"multipart/form-data"; http_header; nocase; content:"PHP_SESSION_UPLOAD_PROGRESS"; http_client_body; fast_pattern; pcre:"/(?i:content-disposition)[ \t]*:(?:[^\r\n]|\r?\n[ \t])*?\;\s*(?i:name)\s*=\s*(?:\x22PHP_SESSION_UPLOAD_PROGRESS\x22|PHP_SESSION_UPLOAD_PROGRESS(?=[\;\s]))/P"; reference:cve,2026-109618; reference:cve,2026-109619; classtype:web-application-attack; sid:1499040051; rev:1;)
JavaScript
복사
4.3.2. Suricata
alert http any any -> any any (msg:"purplehound: multipart POST with PHP_SESSION_UPLOAD_PROGRESS (CVE-2026-109618, CVE-2026-109619)"; flow:established,to_server; http.method; content:"POST"; http.content_type; content:"multipart/form-data"; nocase; http.request_body; content:"PHP_SESSION_UPLOAD_PROGRESS"; fast_pattern; pcre:"/(?i:content-disposition)[ \t]*:(?:[^\r\n]|\r?\n[ \t])*?\;\s*(?i:name)\s*=\s*(?:\x22PHP_SESSION_UPLOAD_PROGRESS\x22|PHP_SESSION_UPLOAD_PROGRESS(?=[\;\s]))/"; reference:cve,2026-109618; reference:cve,2026-109619; classtype:web-application-attack; sid:1499040052; rev:1;)
JavaScript
복사
4.3.3. 攻撃段階別のSnort検知およびSuricata遮断結果
攻撃段階
リクエスト数
Snort検知
Suricata遮断
攻撃の成否
ワーカー交換
4
2/4
2/4
一部可能
情報漏えい(Infoleak)
6
3/6
6/6
不可 — ASLR回避に必要な基準アドレスの取得に失敗
RCE Stage 3
2
2/2
2/2
不可
合計
12
7/12
10/12
RCE不可
[表2] 攻撃段階別のSnort検知およびSuricata遮断結果
[検知結果]
10/22-17:18:07.297678 [**] [1:1499040051:1] purplehound:PHP セッションアップロード進捗フィールドを含む multipart POST リクエスト(CVE-2026-109618, CVE-2026-109619) [**] [Classification: Web Application Attack] [Priority: 1] {TCP} 127.0.0.1:28286 -> 127.0.0.1:8088
•
[図1] PHP_SESSION_UPLOAD_PROGRESSを含むmultipart POSTリクエストの検知結果
上記のルールは、78ResearchLabのPurpleHoundで検証した検知ルールであり、本脆弱性の前提条件であるPHP_SESSION_UPLOAD_PROGRESSフォームフィールドを含むmultipart POSTリクエストを検知する。ただし、このフィールドはPHPの正規のアップロード進捗機能でも使用される。そのため、ルール一致だけでは攻撃やコード実行の成功を確定できず、誤検知の可能性を考慮する必要がある。したがって、これらのルールはあらゆる攻撃を遮断する完全な対策ではなく、補助的な検知・遮断手段である。根本的な対応としては、セキュリティパッチを含むバージョンへPHPをアップグレードする必要がある。

5. 結論

php2shell(CVE-2026-109618 / CVE-2026-109619)は、特定のアプリケーションの欠陥ではなく、PHPランタイム自体のメモリ破損脆弱性である。根本原因は、OOM時点のlongjmpがセッションポインターを解放済みヒープに残すUse-After-Freeである。これを情報漏えい(ASLR回避)とヒープの再占有(型混同)へつなげることで、認証なしのリモートコード実行に到達する。
本分析では、公式のPHP 8.4.26環境でチェーン全体が成立することを確認した。成立条件は、upload_progressが有効であることと、ワーカー存続型SAPIであることに限られる。危険度はCriticalと評価され、インターネットに公開されているPHPホストには直ちにパッチを適用する必要がある。パッチ適用までは、session.upload_progress.enabled=Offが有効な暫定緩和策である。

6. 参考資料

•
78リサーチラボ公開エクスプロイト: https://github.com/78ResearchLab/PHP2Shell-Exploit
•
PHPセキュリティリリースのお知らせ — https://www.php.net/
•
CERT-EU Security Advisory 2026-018
本エクスプロイトは、脆弱性診断およびインシデント防止の目的に限り使用しなければならない。その他の目的で使用したことにより生じた一切の責任は、使用者本人が負う。