WordPress Coreに、認証なしの第三者がコード実行に至る脆弱性「wp2shell」(CVE-2026-63030/CVE-2026-60137)が公表されました。プラグインもテーマも入れていない素のWordPressで、前提条件なしに成立します。影響バージョンと更新手順はすでに各所でまとまっています。この記事が扱うのは、その先で「対応したつもり」になりやすい2つの落とし穴です。ひとつは、影響範囲内のバージョンでも /batch/v1 へのGETが404を返すため404を安全の証明と誤読してしまうこと。もうひとつは、Docker公式イメージに修正版のタグがまだ無く、イメージを引き直しても直らないことです。以下はすべて実機のWordPress 6.9.4で実行し、出力を確認したものです。

影響範囲内のWordPress 6.9.4に対し、GETでbatch/v1を叩くと404が返って安全に見えるが、OPTIONSで叩くとHTTP 200とmethods POSTが返り到達可能と分かるターミナル出力
影響範囲内の WordPress 6.9.4 実機での実測。GET の 404 を「安全」と読むと判定を誤る(本記事の検証環境で取得)

30秒でわかるポイント

「バージョンを上げれば終わり」ではない人向けの記事です。

GETの404は安全の証明にならない/batch/v1 はPOST専用のため、影響範囲内の6.9.4でもGETは「存在しない経路」と同じ404を返す。curl -X OPTIONS でないと到達性を判定できない
Docker公式イメージは修正版タグ自体が未提供。2026年7月20日 18:45 JST の再確認時点でも wordpress:7.0.2 6.9.5 6.8.6 は404、latest は 7.0.1 と同一イメージ(影響範囲内)のまま。docker pull では直らない
影響範囲は 6.9.0〜6.9.4 と 7.0.0〜7.0.1。修正版は 6.9.5 / 7.0.2。6.8系は連鎖こそ成立しないが、SQLi側(CVE-2026-60137)の影響を受けるため 6.8.6 が必要
深刻度スコアは評価機関で逆方向に割れている(WPScan と CISA-ADP で高低が入れ替わる)。スコアだけで優先度を決めない
更新だけでは終わらない。公開エクスプロイトが出回り悪用の兆候も報告されている。更新前に侵入されていないかの確認が要る

wp2shellの攻撃チェーン概念図。REST APIのPOST /batch/v1を入口に、経路の取り違え(CVE-2026-63030)からWP_QueryのSQLインジェクション(CVE-2026-60137)へ連鎖し、DB操作を経て管理者作成、最終的に認証不要のリモートコード実行に至る。影響範囲は6.9.0〜6.9.4と7.0.0〜7.0.1、修正版は6.9.5/7.0.2(6.8系は6.8.6)。
まず全体像から。wp2shellの攻撃チェーン(概念図)。REST API → 経路の取り違え(CVE-2026-63030)→ SQLインジェクション(CVE-2026-60137)→ 管理者作成 → RCE の流れ。攻撃の再現手順・PoCは研究者が非公開としているため、本図は概念的な流れの図解にとどめる。この後、各ステップの「対応したつもり」になりやすい落とし穴を順に見ていく。

なお、今回のようにソフトウェアそのものの取り込み経路が攻撃面になる問題を体系的に押さえたい場合は、サプライチェーンセキュリティ2026|攻撃手法・防御ツール・実践チェックリストを合わせて読んでください。本記事はそのうち「WordPressコアの脆弱性に対して、自分の環境をどう判定するか」に絞った実務編にあたります。

GETで叩いた404は「安全」の証明にならない

本記事をここから始めるのは、この誤読がいちばん多いと考えられるためです。 影響バージョンを知った人がまず試すのは、問題のエンドポイントに直接アクセスしてみることでしょう。ところが、その結果だけでは何も判定できません。

実機の影響範囲内であるWordPress 6.9.4に対してGETで叩いた結果:

curl -s -o /dev/null -w "%{http_code}\n" "https://example.com/?rest_route=/batch/v1"
404

そして、そもそも存在しない架空の経路に対して同じことをした結果:

curl -s "https://example.com/?rest_route=/nope/v1"
{"code":"rest_no_route","message":"No route was found matching the URL and request method.","data":{"status":404}}

区別がつきません。 バッチエンドポイントはPOSTのみを受け付けるため、GETでは「経路が無い」のと同じ応答を返します。ここで404を見て「うちにはbatch/v1が無い=安全」と結論づけるのが、最も危険な誤読です。

確実なのは OPTIONS です。

curl -s -X OPTIONS "https://example.com/?rest_route=/batch/v1"

影響範囲内の6.9.4での実測出力:

{"methods":["POST"],"endpoints":[...]}

HTTPステータスは200が返ります。methodsPOST が含まれていれば、バッチエンドポイントは存在し、認証なしで到達できる状態です。

この200は「脆弱である」という判定ではありません。 ここで分かるのは「バッチAPIという経路が存在し、外部から到達できる」ことだけです。バッチAPIはWordPressの正規の機能なので、修正版の 7.0.2 や 6.9.5 でも、連鎖が成立しない 6.8系でも、同じように200を返します。 脆弱かどうかを決めるのは稼働バージョン(後述の「手順1:稼働バージョンを確定する」)であって、この応答ではありません。

このOPTIONSの使いどころは2つです。(a) GETの404を「安全」と誤読しないため。(b) WAFで遮断した場合に、その遮断が効いているかを確認するため(後述の「手順2:WAFでの遮断が効いているか確認する」)。「OPTIONSで200が返った=危ない」と早合点しないでください。 まず稼働バージョンを確認し、影響範囲に入っていなければ対応は不要です。

もう1つ、REST APIの経路一覧から確認する方法もあります。

curl -s "https://example.com/?rest_route=/" | \
  python3 -c "import json,sys; print([r for r in json.load(sys.stdin).get('routes',{}) if 'batch' in r])"
['/batch/v1']

補足として、namespaces の一覧を見る方法は使えません。実機で確認したところ、6.9.4の namespaces は以下で、batch は含まれていませんでした。

['oembed/1.0', 'wp/v2', 'wp-site-health/v1', 'wp-block-editor/v1', 'wp-abilities/v1']

/batch/v1 はネームスペースではなくコアが直接登録する経路のため、ここには現れません。ネームスペース一覧を見て「batchが無いから安全」と判断するのも誤りです。

Docker公式イメージには、まだ修正版のタグが無い

Dockerで運用している場合、「イメージを最新にすれば直る」という前提が成り立たないことがあります。

本記事の検証時点(2026年7月20日 11:41 JST、および 同日 18:45 JST の再確認時点)で、Docker Hubの公式 wordpress イメージを確認した結果です。いずれの時点でも状況は同じでした。

for t in 7.0.2 7.0.1 6.9.5 6.9.4 6.8.6; do
  code=$(curl -s -o /dev/null -w "%{http_code}" \
    "https://hub.docker.com/v2/repositories/library/wordpress/tags/$t")
  echo "$t -> HTTP $code"
done

実測出力:

7.0.2 -> HTTP 404
7.0.1 -> HTTP 200
6.9.5 -> HTTP 404
6.9.4 -> HTTP 200
6.8.6 -> HTTP 404

修正版のタグ(7.0.2 / 6.9.5 / 6.8.6)はいずれも存在せず、影響範囲内のタグだけが存在していました。 さらに latest を新規に取得して中身を確認すると:

docker pull wordpress:latest
docker run --rm --entrypoint grep wordpress:latest \
  "wp_version =" /usr/src/wordpress/wp-includes/version.php
$wp_version = '7.0.1';

latest の中身は 7.0.1、すなわち影響範囲内でした。 docker pull wordpress:7.0.2 を試すと manifest unknown で失敗します。

この検証で分かったことが2つあります。1つは、Dockerユーザーは「イメージを引き直す」だけでは対処できないこと。当面はコンテナ内でWordPress自身の更新機能を使うか、バッチ経路の遮断で凌ぐ必要があります。もう1つは、ローカルにキャッシュされたイメージは平気で嘘をつくことです。筆者の環境では、キャッシュ済みの latest は 6.9.1 を指していました。docker rmi してから取得し直して初めて 7.0.1 と分かりました。必ず取得し直して中身を実測してください。

再確認(2026年7月20日 18:45 JST)では、タグの有無に加えてイメージの同一性も確かめました。latest7.0.1 の digest が一致していれば、latest の実体が 7.0.1 であることをpullせずに確認できます。

for t in latest 7.0.1; do
  echo -n "$t -> "
  curl -s "https://hub.docker.com/v2/repositories/library/wordpress/tags/$t" \
    | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['digest'], d['last_updated'])"
done

実測出力(digestが完全に一致しています):

latest -> sha256:d40b86dbdfcfad808a2029acf6543c670c4a61c29f70b9d24605e7d0b31ab83d 2026-07-16T21:19:25.712743Z
7.0.1 -> sha256:d40b86dbdfcfad808a2029acf6543c670c4a61c29f70b9d24605e7d0b31ab83d 2026-07-16T21:14:05.218064Z

last_updated2026年7月16日、つまり修正版がリリースされた7月17日より前である点にも注意してください。latest は修正版リリース後に一度も更新されていません。

この状況は公式イメージが更新されれば解消されます。この記事を読んだ時点では既に修正版タグが出ている可能性がありますので、上のコマンドで現在の状態をその場で確認してください。「記事にこう書いてあった」で判断しないことが、この種の確認の要点です。

wp2shellの時系列(Timeline)

日付は一次ソースの公表日に準拠しています。日本の読者向けに、日本時間(JST)とのズレが出る箇所は併記します。

日付 出来事 出典
2026-07-17 WordPress 6.8.6 / 6.9.5 / 7.0.2 をセキュリティリリースとして公開 WordPress公式(Releases・Security)
2026-07-17 CVE-2026-63030 / CVE-2026-60137 がNVDで公開 NVD(Published 07/17/2026)
2026-07-17 Searchlight Cyber(Assetnote)が「wp2shell」として概要を公表。技術詳細は非公開と明言 slcyber.io リサーチ記事
2026-07-18 NVDのCVE情報が更新(Last Modified 07/18/2026)。海外メディアが一斉に報道 NVD/BleepingComputer 他
2026-07-18以降 GitHub上に複数のPoC(実証コード)が公開される BleepingComputer
2026-07-18以降 watchTowrが「実環境での悪用の初期兆候」を報告 BleepingComputer(watchTowr CEO Benjamin Harris のコメント)
2026-07-19 日本のホスティング事業者(エックスサーバー等)がサーバ側対策と注意喚起を公表 各社の告知

公表当初、Rapid7は「公に確認された実環境での悪用は把握していない」と記載していました。その後PoCが公開され、watchTowrが悪用の兆候を報告する、という順で状況が動いています。「最初は悪用なし」という初期の報道だけを見て判断すると、現状を読み違えます。

日付について1点補足します。リリースは米国時間7月17日ですが、日本時間では7月18日の未明〜午前にあたります。日本語記事によって「7月17日」と「7月18日」の表記が混在するのはこのためで、別の出来事ではありません。

「技術詳細は非公開」が意味していたこと

この時系列で運用上いちばん重要なのは、発見者が技術詳細を伏せていた期間が短かったという点です。

Searchlight Cyberは公表時に「脆弱性の深刻度を踏まえ、防御側が更新する時間を確保するため、現時点では技術的詳細を公開しない」と明記していました。これは責任ある開示として妥当な判断ですが、詳細非公開は「安全な猶予期間」ではありません。修正版のリリースそのものが手がかりになるためです。修正前後のコードの差分を比較すれば、どこが変わったかは誰でも確認できます。実際、パッチ公開の翌日以降にはGitHub上に複数のPoCが現れました。

つまり実務上の猶予は、公表からPoC出現までのごく短い期間しかなかったことになります。ここから引き出せる教訓は明快です。「詳細が出ていないから、悪用されるのはまだ先だろう」という判断は成り立ちません。深刻度の高いコアの脆弱性では、パッチ公開が実質的なカウントダウンの開始点だと考えて動く必要があります。

そしてこの記事を読んでいる時点は、既にPoCが出回った後です。更新の要否だけでなく、更新前に何かされていないかの確認までを一続きの作業として計画してください。

wp2shellで何が起きたのか(技術的要点)

技術的な核心は3行で言えます。

  1. WordPressのREST APIには /batch/v1 というバッチ処理用の経路がある。 複数のAPIリクエストを1回のPOSTでまとめて実行するための仕組みです。
  2. このバッチ経路が、リクエストの解釈を取り違える(CVE-2026-63030)。 NVDはこれをCWE-436「Interpretation Conflict(解釈の不一致)」に分類しています。バッチ経路の内側で組み立て直されたリクエストが、本来なら認証や権限チェックで弾かれるはずの経路に到達してしまう、という性質の欠陥です。
  3. その先に、WP_Queryauthor__not_in パラメータのSQLインジェクション(CVE-2026-60137)がある。 SQLインジェクションが成立すればデータベースの内容を操作でき、WordPressの構造上そこからコード実行につなげられます。

重要なのは「単体では大したことのない脆弱性が、経路の欠陥と繋がった瞬間に致命傷になる」という構図です。SQLインジェクション側は、単体では「プラグインやテーマが author__not_in に信頼できない入力を渡した場合に限り成立する」という条件付きの問題でした。NVDの記述もそうなっています。ところがバッチ経路の取り違えを経由すると、その条件を攻撃者側が自分で満たせてしまう。結果として、素のWordPressに対して認証なしで届くRCEになりました。

Searchlight Cyberは「攻撃には前提条件がなく、プラグインを入れていない素のWordPressに対して匿名ユーザーが実行できる」と記載しています。同時に、防御側が更新する時間を確保するため技術的詳細は公開しないとも明記しています。本記事も同じ方針で、攻撃の再現手順やPoCは扱いません。

flowchart LR A["匿名の攻撃者
(認証なし)"] --> B["POST /batch/v1
バッチ処理の入口"] B -->|"CVE-2026-63030
経路の取り違え
CWE-436"| C["本来は到達しないはずの
内部処理へ"] C -->|"CVE-2026-60137
author__not_in
CWE-89"| D["SQLインジェクション
成立"] D --> E["リモートコード実行
(RCE)"] style A fill:#7f1d1d,color:#fff style E fill:#7f1d1d,color:#fff style B fill:#1e3a5f,color:#fff style D fill:#78350f,color:#fff

深刻度スコアは評価機関で割れている

もう1つ、速報では触れられにくい点を挙げます。CVSSスコアは評価した機関によって食い違っており、しかも逆方向に割れています。

CVE WPScan(CNA)の評価 CISA-ADPの評価
CVE-2026-63030(バッチ経路) 9.8 CRITICAL(AV:N/AC:L/PR:N/UI:N/C:H/I:H/A:H) 7.5 HIGH(C:H/I:N/A:N)
CVE-2026-60137(SQLi) 5.9 MEDIUM(AC:H/C:H/I:N/A:N) 9.1 CRITICAL(AC:L/C:H/I:H/A:N)

WPScanは「連鎖の入口である63030が致命的、SQLi単体は条件付きなので中程度」と評価し、CISA-ADPは逆に「SQLi側を高く、63030単体は情報漏洩どまり」と評価しています。どちらか一方の数字だけを引用すると印象が変わるため、本記事は両方を併記します。運用判断としては「連鎖として見れば緊急」で差し支えありません。

バッチAPIは繰り返し狙われる攻撃面

今回の欠陥をWordPress固有の不注意として片付けると、同じ種類の問題を次も見落とします。「複数のリクエストをまとめて処理する仕組み」は、構造的に取り違えを起こしやすいからです。

理由ははっきりしています。通常のWebリクエストは、Webサーバ → 認証 → 権限チェック → 処理という一本道を通ります。ところがバッチAPIは、1つの外側のリクエストの中身を、アプリケーション自身が複数の内側のリクエストへ組み立て直します。このとき内側のリクエストは、外側が通ったチェックを再現しなければなりません。再現の仕方が少しでもズレると、「外側では許可されない操作が、内側では許可される」という状態が生まれます。

NVDが今回の分類に選んだCWE-436「Interpretation Conflict」は、まさにこの種の欠陥を指す分類です。同じデータを、2つの層が違う意味に解釈することで生じる問題を表します。同種の攻撃面は、GraphQLのクエリバッチング、JSON-RPCのバッチリクエスト、各種APIゲートウェイの一括実行機能など、実装を問わず繰り返し現れています。

運用側の教訓としては、「普段誰も使っていない一括実行系のエンドポイントが、認証なしで外部に開いていないか」を棚卸ししておくことです。今回の /batch/v1 も、大半のサイトでは使われていないのに開いていました。使っていない入口は塞いでおくのが原則で、それは今回のような未知の欠陥に対する事前の緩和にもなります。

影響範囲マトリクス

自分がどの列に当てはまるかを確認してください。6.8系の扱いが、速報記事でいちばん誤解されやすい箇所です。

稼働バージョン CVE-2026-63030
(バッチ経路)
CVE-2026-60137
(SQLi)
wp2shell連鎖
(事前認証RCE)
必要な対応
7.0.0 – 7.0.1 影響あり 影響あり 成立する 7.0.2 へ更新(最優先)
6.9.0 – 6.9.4 影響あり 影響あり 成立する 6.9.5 へ更新(最優先)
6.8.0 – 6.8.5 影響なし 影響あり 成立しない 6.8.6 へ更新
6.8.6 / 6.9.5 / 7.0.2 以降 修正済み 修正済み 成立しない 対応済み(更新の確認のみ)

6.8系について正確に書きます。wp2shellの目玉である「事前認証RCE」は6.8系では成立しません。連鎖の入口であるバッチ経路の欠陥(63030)が6.8系には無いためです。ただしSQLインジェクション側(60137)は6.8.0〜6.8.5に影響し、修正版として6.8.6が出ています。この単体のSQLiは、NVDの記述どおり「プラグインやテーマが author__not_in に信頼できない入力を渡した場合」に成立するもので、素の構成でただちに悪用できるものではありません。

つまり6.8系の利用者にとっての正しい理解は、「話題の事前認証RCEの対象ではないが、放置してよいわけでもない。6.8.6へ更新する」です。「6.9と7.0だけが危ない」という要約だけを読んで6.8系を安全と判断すると、SQLi側を取り逃します。

なお、PHPのバージョンによって成立・不成立が変わるという情報は、確認した一次ソースには見当たりませんでした。PHPのバージョンで安全になるという判断はしないでください。

自システムでの確認方法(基本の手順)

ここまでの2点は「知らないと判定を誤る」落とし穴でした。ここからは、環境を問わず踏むことになる基本の確認手順をまとめます。すべて読み取り専用で、攻撃を含みません。自分が管理するサイトに対してのみ実行してください。

手順1:稼働バージョンを確定する

最も確実なのは、ファイルを直接読む方法です。管理画面の表示やキャッシュに影響されません。

# WordPressのドキュメントルートで実行
grep "wp_version =" wp-includes/version.php

実機での出力:

$wp_version = '6.9.4';

WP-CLIが使えるなら、こちらが簡潔です。

wp core version
6.9.4

複数サイトをまとめて確認する場合は、version.php を探して一覧にします。サーバに何十サイトも同居している環境ではこれが実用的です。

# /var/www 配下の全WordPressのバージョンを一覧化
find /var/www -name version.php -path "*wp-includes*" 2>/dev/null | \
  while read f; do
    v=$(grep -m1 "wp_version =" "$f" | sed "s/.*'\(.*\)'.*/\1/")
    echo "$v  ${f%/wp-includes/version.php}"
  done | sort

出力例(バージョン順に並ぶので、古いものが上に来ます):

6.9.4  /var/www/site-a
7.0.1  /var/www/site-b
7.0.2  /var/www/site-c

「自動更新にしているから最新のはず」も実測で確かめてください。 WordPressはマイナー・セキュリティリリースを既定で自動更新しますが、wp-config.php で自動更新を明示的に無効化していると、その対象から外れます。次の2つが設定されていないかを確認してください。

grep -nE "AUTOMATIC_UPDATER_DISABLED|WP_AUTO_UPDATE_CORE" wp-config.php

AUTOMATIC_UPDATER_DISABLEDtrue、または WP_AUTO_UPDATE_COREfalse / 'minor' 以外に設定されている場合、コアの自動更新は期待どおりに動きません。設定を信用せず、上のバージョン実測を優先してください。

手順2:WAFでの遮断が効いているか確認する

緩和策としてWAFで遮断した場合、それが本当に効いているかは実際に叩いて確かめます。遮断が効いていれば、WAFのブロック応答(403など)が返るはずです。

# 2つの入口を両方確認する(片方だけ塞いでも回避される)
for u in "/wp-json/batch/v1" "/?rest_route=/batch/v1"; do
  printf "%-28s -> " "$u"
  curl -s -o /dev/null -w "%{http_code}\n" -X OPTIONS "https://example.com$u"
done

遮断前(影響範囲内の実機)は両方200が返ります。遮断後に403等へ変われば、ルールが効いています。/wp-json/...?rest_route=... は同じ経路への別の入口なので、必ず両方を確認してください。

手順3:ホスティング環境別の確認

環境ごとに手順が変わる部分をまとめます。

環境 確認方法 注意点
共用レンタルサーバ(エックスサーバー、さくら、ConoHa等) 管理パネルのWordPress一覧、またはSSH/ファイルマネージャで wp-includes/version.php を確認 事業者側でWAF等のサーバ側対策が入っている場合がある。ただしそれは更新の代わりにはならない。各社の告知を確認のうえ、自分でもバージョンを実測する
マネージドWordPress(WP Engine等) 事業者の管理画面のバージョン表示、または提供されているWP-CLI 事業者が一括更新を実施している場合がある。実施済みかを実測で確認する
自前サーバ/VPS 上記の grep / wp core version / find 一括確認 複数サイト同居の取りこぼしに注意。find での一括確認を推奨
Docker コンテナ内で実測(イメージ更新では直らない場合がある 前掲の「Docker公式イメージには、まだ修正版のタグが無い」を必ず確認

侵害を受けていないかの確認

先に前提を明確にします。 公的機関やベンダーから、wp2shell固有のIoC(侵害指標)は公表されていません。日本語で最も詳しくまとまっているpiyolog氏の記事も、2026年7月19日時点で「侵害の兆候を確認するための具体的な指標は公表されていない」と明記しています。

したがって以下はwp2shell専用の検知手順ではなく、WordPressが乗っ取られた場合に一般的に残る痕跡を探す標準的な調査手法です。この区別は重要なので、そのまま書いておきます。

それでもこの章を置く理由は、公開エクスプロイトが出回り悪用の兆候も報告されている以上、「更新したから大丈夫」では不十分だからです。更新はこれ以降の侵入を止めますが、更新前に置かれたものは残ります。

以下のコマンドは、実際に侵害を模した環境(Webシェルを配置し、不正な管理者を作成した状態)に対して実行し、検知できることを確認済みです。

1. 想定外の管理者アカウントがないか

wp user list --role=administrator --fields=user_login,user_email,user_registered

検証環境での出力(eviluser が模擬的に追加した不正アカウント):

user_login	user_email	user_registered
admin	[email protected]	2026-07-20 02:42:45
eviluser	[email protected]	2026-07-20 02:43:50

user_registered の日付を必ず見てください。心当たりのない日付に作られた管理者は最優先で調査対象です。WP-CLIが無い場合はデータベースを直接参照します。

wp db query "SELECT u.user_login, u.user_email, u.user_registered
  FROM wp_users u
  JOIN wp_usermeta m ON u.ID = m.user_id
  WHERE m.meta_key = 'wp_capabilities'
    AND m.meta_value LIKE '%administrator%';"

2. uploads配下にPHPファイルが無いか

wp-content/uploads は本来、画像などのメディアだけが置かれる場所です。ここにPHPファイルがあるのは、正常な状態ではまずありません。

find wp-content/uploads -name "*.php" -mtime -30

検証環境での出力:

wp-content/uploads/.cache.php

先頭がドットの隠しファイルになっている点に注目してください。ls では見えません。find を使う理由がこれです。

3. コマンド実行系の関数を含むファイルを探す

grep -rlE 'system\(|passthru\(|shell_exec\(|eval\(|base64_decode\(' \
  wp-content/uploads wp-content/themes --include="*.php"

検証環境での出力:

wp-content/uploads/.cache.php

正規のテーマやプラグインが base64_decode を使うことはあるため、検出=即座に悪性ではありません。uploads配下で引っかかった場合は、ほぼ確実に異常です。

4. コアファイルの改ざんを確認する

WordPressは公式のチェックサムと照合できます。コアファイルが書き換えられていないかを機械的に判定できるので、必ず実行してください。

wp core verify-checksums

検証環境での出力:

Warning: File should not exist: wp-config-docker.php
Success: WordPress installation verifies against checksums.

Success ならコアファイルは公式配布物と一致しています。この例の Warning はDocker公式イメージが同梱するファイルによるもので、異常ではありません。逆に File doesn't verify against checksum が出たら、そのファイルは改ざんされています。

5. 不審なスケジュール実行がないか

永続化のためにWP-Cronへ仕込まれることがあります。

wp cron event list --fields=hook,next_run_gmt

見慣れないフック名、特にランダムな文字列のようなフックがあれば調査対象です。

6. 最近更新されたPHPファイルを俯瞰する

find . -name "*.php" -mtime -14 -not -path "./wp-content/cache/*" -ls | sort -k8

更新作業をした日付以外に更新されているPHPがあれば、内容を確認してください。

7. アクセスログから侵入の有無と時期を絞り込む

ファイル側の痕跡が見つからなくても、ログには残っている場合があります。逆に、ログに batch/v1 へのPOSTが一切なければ、少なくともその期間は試行すらされていないという判断材料になります。

# batch/v1 へのPOSTを時系列で抽出(nginxの例)
zgrep -hE "batch/v1|rest_route=/batch/v1" /var/log/nginx/access.log* \
  | grep '"POST' | awk '{print $4, $1, $9}' | sort

出力は「日時/送信元IP/ステータスコード」の並びになります。見るべき点は3つあります。

ステータスコードが200番台のPOSTがあるか。 400や403ばかりなら、到達はしても処理は失敗している可能性が高い。200が並んでいる場合は要精査
同一IPからの短時間の連続アクセスがあるか。 自動化されたスキャンや攻撃の典型的な形
最初の記録がいつか。 これが侵害の可能性がある期間の起点になる

侵入の疑いがある時刻が絞れたら、その前後でファイルが作られていないかを突き合わせます。

# 特定日以降に作成・更新されたPHPファイル(例:7月17日以降)
find . -name "*.php" -newermt "2026-07-17" -not -path "./wp-content/cache/*" -ls

ログが残っていない場合:保存期間の短い共用サーバでは、この確認自体ができないことがあります。その場合は「侵入されていないことを証明できない」状態です。ファイル側の確認(管理者アカウント・uploads配下のPHP・チェックサム)をより丁寧に行い、それでも不安が残るなら、公表日より前のバックアップからの復旧を検討してください。確認できないことを「問題なし」と読み替えないことが、この段階でいちばん重要です。

調査で1つでも異常が見つかった場合:ファイルを消して終わりにしないでください。侵入経路と滞在期間が分からないままでは再侵入されます。バックアップからの復旧、全ユーザーのパスワードとソルト(wp-config.php の認証キー)の再発行、設置済みプラグイン・テーマの棚卸しまでを一続きで行うのが原則です。規模によっては専門事業者への相談を検討してください。

よくある誤解と、正しい理解

確認作業でつまずきやすい点を、実機で確かめた結果とあわせてまとめます。

よくある誤解 正しい理解
/batch/v1 にアクセスして404だから安全」 GETはPOST専用エンドポイントに対して常に404を返す。影響範囲内の6.9.4でも404だった。OPTIONS で判定する
「REST APIのnamespacesに batch が無いから安全」 /batch/v1 はネームスペースではなくコアが直接登録する経路。6.9.4の実測でもnamespacesには現れなかった
「Dockerイメージを最新にしたから安全」 検証時点で修正版タグは存在せず、latest の中身は7.0.1(影響範囲内)だった。中身をバージョンで実測する
「6.8系なので無関係」 事前認証RCEの連鎖は成立しないが、SQLi側(CVE-2026-60137)の影響を受ける。6.8.6への更新が必要
「自動更新が有効だから対応済み」 自動更新を無効化した環境、ファイルシステムが書き込み不可の環境では適用されない。実測で確認する
「WAFで塞いだから更新は不要」 WAFは時間を稼ぐ応急処置。恒久対策は修正版への更新のみ
「更新したので対応完了」 更新は以降の侵入を止めるだけ。更新前に置かれたWebシェルや作成された管理者は残る
「プラグインを入れていないから安全」 発見者は「プラグインを入れていない素の構成で、前提条件なしに成立する」と明記している

wp2shellへの対策手順

優先度順に整理します。

緊急(今すぐ)

修正版へ更新する。 これが唯一の恒久対策です。

wp core update --version=7.0.2   # 7.0系の場合
wp core update --version=6.9.5   # 6.9系の場合
wp core update --version=6.8.6   # 6.8系の場合

更新後は必ず実測で確認します。

wp core version

更新先は、7.0系なら WordPress 7.0.2、6.9系なら WordPress 6.9.5、6.8系なら 6.8.6 です。メジャーバージョンを跨いで上げる必要はありません。 7.0系を使っているサイトが慌てて構成を変える必要はなく、同じ系列の修正版へ上げれば連鎖は成立しなくなります。テーマやプラグインの互換性リスクを避ける意味でも、まずは同系列の最新へ上げるのが定石です。

WordPressは自動更新が有効な環境に対して更新を強制配信しているとされていますが、自動更新を無効化している環境、Docker等でファイルシステムが書き込み不可の環境では適用されません。 「自動更新だから大丈夫」と考えず、必ずバージョンを実測してください。

更新が本当に反映されたかは、次の3点をセットで確認すると確実です。

wp core version                      # 1. バージョンが修正版になったか
wp core verify-checksums             # 2. コアが公式配布物と一致するか
curl -s -X OPTIONS "https://example.com/?rest_route=/batch/v1"   # 3. 経路の状態

1と2が通れば更新は成功です。3については、更新後も batch/v1 自体は存在し続けます(バッチAPIは正規の機能であり、削除されたわけではなく、経路の取り違えが修正されたためです)。したがって更新後にOPTIONSが200を返しても異常ではありません。ここを「まだ塞がっていない」と誤読しないでください。3を見るのは、WAFでの遮断を選んだ場合にその効きを確認するときです。

緊急(すぐ更新できない場合の緩和)

Searchlight Cyberが案内している緩和策は、バッチAPIへの匿名アクセスを遮断することです。遮断対象は次の2つで、必ず両方を塞ぎます。

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

nginxでの例:

location ~ ^/wp-json/batch/v1 { return 403; }
if ($arg_rest_route ~* "^/batch/v1") { return 403; }

Apache(.htaccess)での例:

RewriteEngine On
RewriteRule ^wp-json/batch/v1 - [F,L]
RewriteCond %{QUERY_STRING} rest_route=/batch/v1 [NC]
RewriteRule ^ - [F,L]

適用後は、前掲の「手順2:WAFでの遮断が効いているか確認する」で実際に403が返ることを確認してください。設定しただけで確認しないのが、この種の緩和策で最も多い失敗です。

遮断による副作用も確認しておきます。バッチAPIはブロックエディタや一部のプラグイン・ヘッドレス構成が内部的に使う場合があります。遮断後は、管理画面での記事の保存・ブロックエディタの動作・連携アプリの挙動を一通り確認してください。 実際に不具合が出た場合は、遮断を解除するのではなく、更新を優先して実施し遮断を外すのが正しい順序です。

Cloudflareは自社WAFで対応ルールを提供していると告知しています。利用中のWAF・CDN・ホスティング事業者が対応ルールを出しているかを確認してください。ただしWAFは時間を稼ぐためのものであり、更新の代替ではありません。

推奨(更新後に実施)

・前章の「侵害を受けていないかの確認」を一通り実行する
wp-config.php の認証キー・ソルトを再発行し、既存セッションを無効化する
・管理者アカウントの棚卸しとパスワード再設定
・アクセスログで batch/v1 へのPOSTを遡って確認する(下記)

grep -E "batch/v1|rest_route=/batch/v1" /var/log/nginx/access.log* | grep POST | head -50

公開エクスプロイトが出回る前後の日時に、心当たりのないPOSTが記録されていないかを確認します。ログの保存期間が短い環境では、この確認自体ができない点にも留意してください。

長期

・WordPressコアの自動更新を有効にする(マイナー・セキュリティリリースだけでも)
wp-content/uploads でのPHP実行をWebサーバ設定で禁止する。Webシェルを置かれても実行させない多層防御になる
・稼働中の全WordPressのバージョンを定期的に一括取得する仕組みを持つ(前掲の find ワンライナーをcron化する等)
・REST APIの匿名アクセス範囲を把握しておく。今回のように、普段意識していない経路が攻撃面になる

uploads配下のPHP実行禁止(nginx):

location ~* ^/wp-content/uploads/.*\.php$ { deny all; }

他の情報源との使い分け

今回のような大型のWordPress 脆弱性では、日本語でも複数の解説が出ます。役割が違うので、併読を勧めます。

情報源 強み この記事との関係
WordPress公式・NVD 一次情報。影響バージョンとCVEの正確な定義 本記事の事実関係はここで裏取りしている
Searchlight Cyber(発見者) 発見者による説明。緩和策の根拠 攻撃の技術詳細は意図的に非公開
piyolog 出来事の整理と時系列。日本語で最も網羅的 実行コマンドは扱っていない。本記事が補完する
GMO Flatt Security 脆弱性の仕組みと痕跡の技術解説 実行コマンドは扱っていない。本記事が補完する
ホスティング各社の告知 自社サービスでのサーバ側対策 自社環境限定。本記事は環境横断でまとめている

本記事の役割は、「読んで理解する」ではなく「自分の環境で叩いて判定する」部分です。とくに、GETでの確認が当てにならないことと、Docker公式イメージが検証時点で未更新だったことは、実際に実行しなければ分からない情報でした。

今回のように「自分が影響範囲かを判定する」手順を重視した記事としては、nginx 2026年6月のCVE 3件解説|Critical 2件は条件付き、自分が対象か判定する手順も同じ構成で書いています。承認や信頼境界のズレが攻撃面になる事例としては、GhostApproval・SymJack解説|AIコーディング支援6種のsymlink承認ハイジャックが近い性質を扱っています。

まとめ

wp2shellは、単体では条件付きだったSQLインジェクションが、REST APIバッチ経路の解釈の取り違えと連鎖したことで事前認証RCEに化けたという事例です。プラグイン不要・前提条件なしで成立するため、影響範囲のバージョンを使っているなら最優先で更新してください。

実務上の要点を3つに絞ります。

  1. バージョンを実測する。 6.9.0〜6.9.4 と 7.0.0〜7.0.1 が連鎖の影響範囲。6.8系は連鎖こそ成立しないが、SQLi側のために6.8.6が要る
  2. 確認方法を間違えない。 GETでの404は安全の証明にならない。curl -X OPTIONS で判定する。Dockerはイメージ更新で直るとは限らず、中身を実測する
  3. 更新して終わりにしない。 公開エクスプロイトが出回った後である以上、更新前の侵入を前提に、管理者アカウント・uploads配下のPHP・コアの整合性を確認する

本記事の検証はすべて2026年7月20日時点の実機で行いました。とくにDocker公式イメージの状況は時間とともに変わるため、記事の記述をそのまま信じず、掲載したコマンドをその場で実行して現在の状態を確認してください。 それがこの記事のいちばんの使い方です。

参照ソース

wp2shell: Pre Authentication RCE in WordPress Core(Searchlight Cyber/Assetnote・発見者による一次ソース)
NVD — CVE-2026-63030(REST APIバッチ経路の取り違え・CWE-436)
NVD — CVE-2026-60137(WP_Query author__not_in のSQLインジェクション・CWE-89)
WordPress News(公式・2026年7月のセキュリティリリース)
CVE-2026-63030: wp2shell a Critical Remote Code Execution Vulnerability in WordPress Core(Rapid7)
WordPress Core “wp2shell” RCE flaws get public exploits, patch now(BleepingComputer)