技術約9分で読めます

WordPress Coreのwp2shellはREST APIバッチ経由で認証前RCEになる

いけさん目次

TL;DR

影響 WordPress 6.9.0〜6.9.4と7.0.0〜7.0.1は、REST APIバッチルート混乱とSQLインジェクションの組み合わせによる認証前RCE(リモートコード実行)の対象。
6.8.0〜6.8.5はRCEチェーンの対象外だが、プラグイン、テーマ、mu-plugin、独自コードなどが信頼できない値を WP_Query へ渡す構成ではSQLインジェクションの対象

対応 WordPress 7.0.2、6.9.5、6.8.6へ更新。強制自動更新は有効化済みだが、実際の稼働版は管理画面やWP-CLIで要確認

一時策 更新までの間だけ、未認証ユーザーからのREST APIバッチアクセスを遮断。WAFで認証状態を判定できなければ /wp-json/batch/v1?rest_route=/batch/v1 自体の遮断になるが、正規のREST API連携も止まるため短時間限定


WordPress 7.0.2が2026年7月17日に出た。
通常の保守リリースではなく、1件のCriticalと1件のHighを含むセキュリティリリースだ。
WordPress.orgは深刻度を理由に、影響を受けるサイトへ自動更新システム経由の強制更新も有効化した。

修正された片方は、REST APIのバッチルート混乱とSQLインジェクションを組み合わせてRCEへ進む問題。
Searchlight CyberのAdam Kues氏が報告し、wp2shellと呼ばれている。
報告者側の説明では、匿名ユーザーがプラグインなしの標準WordPressに対して悪用できる。 ただし、Adam Kues氏へのクレジットはRCEチェーン側で、SQLインジェクション側はWordPress公式がTF1T、dtro、haongoの3氏の報告としている。

数日前に書いた WordPress REST APIはApplication Passwords、CORS、レート制限を別々に締める は、REST APIを外部連携の入口としてどう扱うかの話だった。
今回のwp2shellでは、CoreのREST APIバッチ処理とSQLインジェクションが重なり、RCEになる。
プラグインの permission_callback ミスではなく、CoreのREST APIバッチ処理が起点だ。

6.8系と6.9以降で被害の形が違う

WordPress公式リリースは、7.0.2で「facilitated SQL injection」と「REST API batch-route confusion and SQL injection issue leading to Remote Code Execution」を修正したと書いている。
GHSAも2件に分かれる。 深刻度は評価元で異なり、WordPress公式は2件をCriticalとHigh、GitHub AdvisoryはSQLインジェクション側をModerateとしている。

識別子内容影響版修正版
CVE-2026-60137 / GHSA-fpp7-x2x2-2mjfWP_Queryauthor__not_in の無害化不備。単体では、プラグイン、テーマ、mu-plugin、独自コードなどが信頼できない値を渡すとSQLインジェクションになる6.8.0〜6.8.5、6.9.0〜6.9.4、7.0.0〜7.0.16.8.6、6.9.5、7.0.2
CVE-2026-63030 / GHSA-ff9f-jf42-662qREST APIバッチルート混乱。上のSQLインジェクションと組み合わせるとRCE6.9.0〜6.9.4、7.0.0〜7.0.16.9.5、7.0.2

6.8系は今回のRCEチェーンには入らない。
ただしSQLインジェクション側は6.8.0から6.8.5まで対象なので、6.8系に残すなら6.8.6まで上げる。

6.9.0〜6.9.4と7.0.0〜7.0.1は、2件がつながる。
WordPress 7.1 Beta 1も対象で、7.1 Beta 2に修正が入った。

バッチルートが別リクエストの処理へずれる

入口として案内されているのは /wp-json/batch/v1 だ。
これは複数のREST APIサブリクエストを1回のHTTPリクエストにまとめるためのルートで、WordPress 5.6から存在する。 バッチ機能そのものが最初から脆弱だったのではない。ルート混同の原因となったエラー処理はWordPress 6.9で入っており、そのためCVE-2026-63030の影響範囲も6.9以降に限られる。

公開された修正差分では、バッチ処理がサブリクエスト、対応するルート・ハンドラ、検証結果を別の配列で管理していた。
不正なサブリクエストが WP_Error になった時だけハンドラ側の配列に要素が追加されず、それ以降のリクエストとハンドラの対応が1つずれる。
その結果、本来と異なるハンドラと検証結果の組み合わせでサブリクエストが処理され、バッチからは本来呼べないハンドラや、検証をバイパスしたハンドラへ到達できる。ここから WP_Queryauthor__not_in へスカラー値を渡すと、値が整数化されないままSQLの NOT IN 句へ連結される。

flowchart TD
    A["匿名HTTPリクエスト"] --> B["/wp-json/batch/v1"]
    B --> C["サブリクエスト処理がずれる"]
    C --> D["想定外のRESTハンドラへ到達"]
    D --> E["WP_Query author__not_inへ入力が入る"]
    E --> F["SQLインジェクション"]
    F --> G["6.9以降ではRCEチェーンへ進む"]

Searchlight Cyberは、管理者の更新時間を確保するため、自社の詳細な技術情報をまだ出していない。
ただしWordPress 7.0.2の公開差分では、配列の対応関係を保つ修正に加え、REST APIディスパッチ中に別の最上位REST処理を開始させない再入防止と、author__not_in を必ず整数IDのリストに変換する修正が確認できる。

自動更新を待たずに稼働版を確認する

WordPress.orgは強制自動更新を有効化した。
公式案内が「自動バックグラウンド更新をサポートするサイトでは自動的に開始」と条件を付けている通り、強制更新が全サイトで完了したとは限らない。自動更新を無効化したサイトへの適用範囲も、WordPressはリリース文で説明していない。

確認は管理画面だけでなく、WP-CLIが使えるなら実機で取る。

wp core version

必要な最低版は、7.0系なら7.0.2、6.9系なら6.9.5、6.8系なら6.8.6。
6.8より前は公式リリース上は今回の2件の対象外だが、更新するなら今回の修正が入った6.8.6以降へ上げる。

ホスティング管理画面で「自動更新済み」と表示されていても、複数環境があるなら本番、ステージング、旧ドメイン、サブディレクトリ設置、顧客別インスタンスを別々に確認する。
WordPressは1台のサーバーに複数置かれていることが多く、管理画面1つでは自分の設置分の稼働版しか表示されない。

WAFで止めるならバッチルートだけ短く止める

すぐ更新できない場合、Searchlight Cyberは未認証ユーザーからのバッチAPIアクセスを止める一時策を挙げている。WAFでWordPressの認証状態を確実に判定できない場合は、更新までバッチルートそのものを止める。
WAF(Web Application Firewall。HTTPリクエストを検査して攻撃パターンを遮断する防御層)では、URLデコードと正規化の後に次の2種類を両方対象にする。

  • /wp-json/batch/v1
  • ?rest_route=/batch/v1

パーマリンク設定やサーバー構成によって、WordPress REST APIはきれいなパス形式と rest_route クエリ形式の両方で到達する。
WordPressは _method クエリと X-HTTP-Method-Override ヘッダーでHTTPメソッドを上書きできるため、WAFの遮断条件を受信時の POST だけに限定しない。サブディレクトリ設置とWAFを迂回するオリジン直接接続も確認する。

REST API全体の未認証アクセスを遮断するプラグインも一時策として挙がっている。
ただし、公開投稿取得、ヘッドレスフロント、フォーム、外部連携、AI連携などが未認証のREST APIを使っているサイトでは正規機能も止まる。
前回のREST API記事で書いた通り、公開読み取りAPIと権限付き操作APIを分けていないサイトほど、緊急遮断時に止まる正規機能が増える。

Cloudflareは、CVE-2026-63030のRCEが成立する条件を「永続オブジェクトキャッシュを使っていない場合」と明記している。
RedisやMemcached等の永続オブジェクトキャッシュを実際にWordPressのオブジェクトキャッシュとして使っているサイトは、Cloudflareが説明する既知のRCE成立条件には当てはまらない。
これは修正ではない。SQLインジェクション側は残り、別経路が今後示される可能性もあるため、Core本体の更新は依然として必要になる。

Cloudflareを使う場合、同社は2026年7月17日17:03 UTCにSQLインジェクション用とRCE用の2ルールを無料・有料プラン向けに配備した。Cloudflare経由でないオリジン通信は対象外で、有料プランではManaged Rulesが有効か、アクションが Log に上書きされていないかも確認する。

ログはRESTバッチとauthor_excludeから確認する

公開後にPoCが出ているため、更新済みでもパッチ前の露出期間のログを確認する。PoCの公開と実被害の確認は別で、2026年7月19日時点でCISAのKEVカタログには両CVEとも載っていない。
まず /wp-json/batch/v1rest_route=/batch/v1 へのアクセスを拾う。
普段REST APIバッチを使っていないサイトなら、そこへの匿名POSTはそれだけで目立つ。

受信時のメソッドが POST 以外でも、_method=POSTX-HTTP-Method-Override: POST の記録があれば対象に含める。バッチAPIは通常、個々のサブリクエストの結果をJSON応答本文に入れ、バッチ全体は 207 Multi-Status を返す。そのためアクセスログのステータスコードだけで攻撃の成否は判定できない。
外部のREST APIで使われるパラメータ名は author_exclude で、WordPress Core内部で WP_Queryauthor__not_in パラメータへ変換される。author_exclude やSQL断片はJSONリクエスト本文側に含まれるため、通常のWebアクセスログがボディを記録していなければ文字列検索では見つからない。WAF、CDN、リバースプロキシ、アプリケーションのうち、リクエスト本文まで保持しているログを確認する。Cloudflare利用時は、同社が配備した2つの検知ルールに対するSecurity Eventsも確認する。

RCEまで疑う場合は、WordPress本体の更新とは別に侵害確認が残る。
直近で作成・変更されたPHP、見覚えのない管理者ユーザー、外向き通信、プラグイン・テーマファイルの改変を確認する。EDR、FIM、OS監査ログなどファイルアクセスを記録する仕組みがある場合は、wp-config.php へのPHPや通常と異なるプロセスからのアクセスも調べる。通常のWebアクセスログだけでは、ローカルファイルの読み取り有無は分からない。
侵害が確認された場合は、管理者パスワードのリセット、全セッションの無効化、Application Passwordsの失効、認証ソルト、データベース資格情報、プラグインや外部APIの秘密情報を、露出範囲に応じてローテーションする。

参考: