0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

wp2shell(CVE-2026-63030)を「入口で止められない」理由と、ランタイムで捕まえる設計

0
Posted at

3行サマリ

  • 2026年7月17日、WordPress Core 本体に未認証(事前認証)RCE「wp2shell」(CVE-2026-63030)が公表された。プラグイン不要、stock install がそのまま対象で、影響範囲は Log4Shell 級。
  • この攻撃の悪性リクエストは「正規エンドポイントへの整形式 JSON POST 1発」で、シェルのメタ文字も何もない。入口(WAF シグネチャ/バージョン検知)で悪性だけを選り分けるのが極めて難しい。
  • だからこそ価値の中心は「侵入を100%防ぐ」ではなく「侵入後の挙動を実行時に即座に捕まえて封じ込める」側に移る。本記事は根本原因をパッチ差分から解説し、Falco / Sysdig でどう検知・封じ込めるか、そもそもの環境をどう設計すべきかまでを扱う。

免責: 本記事は公開済みの一次情報とパッチ差分に基づく技術解説です。攻撃を再現・実行するための手順ではありません。掲載する点検プローブは非破壊で、自組織の資産確認を目的とした範囲に留めてください。

1. 何が起きたか

wp2shell は Searchlight Cyber(旧 Assetnote)の Adam Kues 氏が発見し、2026年7月17日に WordPress セキュリティチームと協調して開示された脆弱性チェーンです。GitHub Security Advisory(GHSA-ff9f-jf42-662q)と WordPress 7.0.2 / 6.9.5 の緊急リリースが同時に出ました。

正体は独立した2つの脆弱性のチェーンです。ここを分けて理解するのが大事です。

CVE 種別 深刻度 概要
CVE-2026-63030 REST API バッチルート混同 → RCE Critical WP_REST_Server::serve_batch_request_v1() の認可迂回
CVE-2026-60137 SQL インジェクション High WP_Queryauthor__not_in パラメータ経由

CVE-2026-63030 の route confusion で本来通らない権限チェックを迂回し、CVE-2026-60137 の SQLi に繋いでコード実行に至ります。全体像は次の通りです。

影響バージョンと修正

SQLi は 6.8 以降に存在し、RCE は 6.9 以降でのみ成立します。この差が「どのブランチに何を当てるか」を分けます。

ブランチ 影響 修正版
6.8.5 以前 RCE 非該当(SQLi は 6.8 以降に存在) 6.8.6(SQLi のみ修正)
6.9.0 – 6.9.4 wp2shell RCE チェーン該当 6.9.5
7.0.0 – 7.0.1 wp2shell RCE チェーン該当 7.0.2
7.1 beta 一部 beta が該当 7.1 Beta 2

「6.8 系は RCE には非該当」だが、SQLi(CVE-2026-60137)の修正として 6.8.6 が出ています。6.8 系だから安全と早合点せず、当てられるなら当てるのが安全です。

なぜ深刻度が「異常」なのか

「未認証(誰でも撃てる)× 結果は RCE(乗っ取り)× プラグイン不要で stock install が対象」の3条件が同時に揃うのは極めて稀です。WordPress は W3Techs 集計で全 Web サイトの約43%(推定5億サイト以上)を動かしており、Core 本体でこのクラスは10年に一度あるかどうか。WordPress.org は影響バージョンに自動更新を強制プッシュするという異例の対応を取りました。それだけ危険という証左です。

CVSS が割れている件(正直に書く)

深刻度スコアは提供元で割れています。

  • Searchlight Cyber / Hadrian: 9.8(AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
  • GitHub Security Advisory: Critical
  • NVD(公開当初): 7.5

なぜ割れるのか。これはチェーン脆弱性で、単体 CVE をどう評価するか、到達条件(後述の object cache)をどう見積もるかで数値が動くからです。外形的なスコアだけでは深刻度の合意すら取れない — この事実自体が、後半で述べる「静的な指標では測りにくい脆弱性である」という本記事の主張への伏線になります。スコアの数字を鵜呑みにせず、「未認証で stock install に届く RCE」という性質で判断すべきです。

2. 根本原因 — パッチ差分から確定した事実

開示当初、Searchlight Cyber は防御側に時間を与えるため技術詳細を伏せました。しかしオープンソースゆえパッチ差分は公開されており、Hadrian が差分から根本原因を再構成・公表しています。以下はその確定情報です。

脆弱性の本体は WP_REST_Server::serve_batch_request_v1() の**配列インデックスのズレ(off-by-one desync)**です。バッチ API は複数の REST サブリクエストを1つにまとめる仕組みで、内部で3つの配列 $requests / $validation / $matches をインデックスが揃っている前提で処理します。

問題のマッチングループ:

foreach ( $requests as $single_request ) {
    if ( is_wp_error( $single_request ) ) {
        $has_error    = true;
        $validation[] = $single_request;   // ← ここには積む
        continue;                          // ← が $matches には積まない
    }

    $match     = $this->match_request_to_handler( $single_request );
    $matches[] = $match;
    // ...permission / validation checks...
}

パースに失敗したサブリクエスト(例: パスに http://: を渡す)は WP_Error になり、$validation には記録されるが $matches には積まれません。この瞬間から $matches だけが1要素短くなる。

ディスパッチ側は素直にインデックスで引きます:

foreach ( $requests as $i => $single_request ) {
    // ...
    $match = $matches[ $i ];   // ← エラー以降、全リクエストが1つズレる
    list( $route, $handler ) = $match;
    $result = $this->respond_to_request( $single_request, $route, $handler, $error );
}

結果、壊れたエントリ以降の全サブリクエストが「隣のサブリクエスト用のルート+権限コールバック」で評価・実行される。攻撃者はサブリクエストの並び順を制御することで、低権限で通る permission check に高権限ハンドラを通すことができます。**認可を通すリクエストと実際に実行されるハンドラが食い違う、典型的な route/handler confusion(CWE-284 Improper Access Control)**です。

図にすると、ズレはこう起きます。先頭の壊れたリクエストが $matches に積まれないため、以降は「送ったリクエスト」と「実際に使われるハンドラ」が1つずつ食い違います。

 dispatch  │ 送ったサブリクエスト  │ $matches[i] が指すハンドラ    │ 実際の評価
 index i   │ (攻撃者が順番を制御)   │ (エラー以降 1つズレる)         │
 ──────────┼───────────────────────┼───────────────────────────────┼──────────────────────
    0      │ 壊れたリクエスト (壊) │ ── 積まれない ──               │ エラー処理
    1      │ リクエスト A          │ B 用のルート+権限コールバック │ A を B の権限で評価 ←ズレ
    2      │ リクエスト B          │ C 用のルート+権限コールバック │ B を C の権限で評価 ←ズレ

A の permission check(低権限で通る)を突破口に、実際には B のハンドラ(高権限)が動く——この一段ズレを積み重ねて reachable sink まで誘導するのが攻撃の骨子です。

修正はわずか2行

  public function serve_request( $path = null ) {
+     if ( $this->is_dispatching() ) {
+         return false;
+     }

  foreach ( $requests as $single_request ) {
      if ( is_wp_error( $single_request ) ) {
          $has_error    = true;
+         $matches[]    = $single_request;   // ← エラーも $matches に積んで整合させる
          $validation[] = $single_request;
          continue;
      }

2つの修正が入っています。(1) エラーも $matches に積んで配列整合を保つ、(2) is_dispatching() の再入ガードを足し、サブリクエストがディスパッチ途中に新しいトップレベル REST サイクルを開始できないようにする。この再入が SQLi と繋がって RCE に化ける経路でした。差分は 6.9 系(6.9.4→6.9.5)と 7.0 系(7.0.1→7.0.2)で同一です。

観測された攻撃チェーン

Hadrian が再構成した流れ:

  1. 未認証で POST /?rest_route=/batch/v1、ボディに "validation":"normal" とサブリクエスト配列を送る。
  2. 先頭サブリクエストを故意にパース失敗させる({"method":"POST","path":"http://:"})。これが $matches に載らず desync が発火する。
  3. 以降の全サブリクエストが「次のハンドラのルート+権限コールバック」で評価される。
  4. 本来落ちるはずの permission check に高権限ハンドラが通される順にサブリクエストを並べ、reachable sink へ誘導する。
  5. 未認可の操作が実行され、コード実行に至る。

時系列で並べるとこうなります。

3. なぜ「入口」で止まらないのか(本記事の核心)

技術者にとって一番重要なのはここです。この攻撃は入口では見えません。

Hadrian が明言している通り、悪性リクエストは「正規のコアエンドポイントへの、整形式 JSON の POST 1発」。シェルのメタ文字も、パストラバーサルも、長さで引っかかるほど大きなペイロードも無い。ブロックエディタが編集中に投げる通常のバッチリクエストと見分けがつきません。

どれくらい見分けがつかないか、正規リクエストと悪性リクエストを並べてみます。

観点 正規(ブロックエディタ) 悪性(wp2shell)
メソッド / 宛先 POST /batch/v1 POST /batch/v1(同一)
Content-Type application/json application/json(同一)
ボディ 整形式 JSON 整形式 JSON(同一)
シェルメタ文字・特殊文字 なし なし
ペイロードサイズ 通常 通常(小さい)
唯一の差 サブリクエストの並び順と、先頭の「わざと壊した」1件

WAF が見るシグネチャの土俵(文字列・パターン・サイズ)では、この2つは実質同一です。ここから2つの帰結が出ます。

(1) WAF は「無力」ではないが、シグネチャでの選別は困難

正確に書き分けます。WAF が役に立たないわけではありません。緊急緩和策として推奨されているのは、バッチエンドポイントを丸ごとブロックすることです。

# 両方の到達形式をブロックする(片方だけでは不十分)
/wp-json/batch/v1
/?rest_route=/batch/v1

/wp-json/... はパーマリンク設定に依存しますが、?rest_route=... の形式は rewrite を無効化していても常に到達可能です。両方を塞がないと迂回されます。これは有効な緩和です。

ただしこれは「エンドポイントごと落とす」措置であって、正規のバッチ API 利用(ブロックエディタ等)も巻き込みます。あくまでパッチ適用までの一時措置です。そして重要なのは、「悪性リクエストだけをシグネチャで選り分けて弾く」ことが極めて難しいという点。悪性・正規が構文上ほぼ同一だからです。だから入口の選別に頼りきれず、ランタイム側の観測が要る、という話に繋がります。

(2) バージョン文字列ベースの検知も外す

受動的にバージョンを推定する検知(トップページの generator 版数など)も当てになりません。generator 版数を隠すのは一般的なハードニングですが、そうやって真面目にハードニングしたサイトほど、受動シグナルが消えて検知網から漏れるという皮肉が起きます。バージョン推定型のスキャナは「対策済みに見えるが実は脆弱」なサイトを取りこぼします。

結論

「入口で止める」前提が崩れている以上、価値の中心は「侵入後の挙動を実行時に捕まえる」側に移ります。これがまさに Runtime Security(Sysdig / Falco)の土俵です。wp2shell は「なぜランタイム検知が必要か」を説明する教材として、ほぼ理想的なケースだと言えます。

4. 自組織の点検 — 非破壊プローブ

Hadrian が公開した非破壊の判定プローブは、自組織の資産が脆弱かを安全に確認できる実用情報です。攻撃コードそのものではなく、desync の有無を確定させる「点検用の一手」です。

3つのサブリクエストを送ります。

{"validation":"normal","requests":[
  {"method":"POST","path":"http://:"},
  {"method":"DELETE","path":"/wp/v2/categories/0"},
  {"method":"POST","path":"/wp/v2/block-renderer/core/paragraph"}
]}

判定は決定論的です。

  • 脆弱なサーバ: 2番目(categories)のサブリクエストが、block-renderer の権限エラー block_cannot_read を返す。→ categories のリクエストが block-renderer ハンドラで評価された = desync している証拠。
  • パッチ済みサーバ: 同じサブリクエストが正しく処理され rest_term_invalid を返す。

なぜ非破壊か: カテゴリ id 0 は存在せず、リクエストは未認証なので書き込み権限は一切付与されません。脆弱経路でも読み取り権限チェックに落ちるだけで削除ロジックには到達しない。Hadrian のラボでは繰り返し実行してもカテゴリ集合は変化しなかったと報告されています。

点検は自組織の管理下にある資産に対してのみ実施してください。第三者のサイトへ送ればそれは不正アクセスです。最も確実な確認方法は結局のところ「バージョンが 6.9.5 / 7.0.2 以降になっているか」を直接見ることです。自動更新は権限やディスクの問題で静かに失敗することがあるため、適用済みだと仮定せず検証しましょう。

5. ランタイムで捕まえる(Sysdig / Falco)

大前提として、Sysdig は脆弱性そのものは塞げません。パッチ適用が最優先です。 しかし wp2shell の本質は「未認証でシェルを取られること」であり、シェルが立った後の挙動は実行時に必ず観測できます。パッチ前の窓(=まさに今)でも、被害の早期検知と封じ込めができる。ここに価値が集約されます。

RCE 後に Web サーバプロセス(php-fpm / apache2 / nginx)は普段しない動作を始めます。以下は説明用の Falco ルール骨子です。Sysdig ではこれらに相当するマネージド Falco ルール(Sysdig Runtime Threat Detection ポリシー)が標準提供されており、自前運用の Falco でも同等の検知が可能です。

「RCE 後の挙動」を「どの層で捕まえるか」をマップにするとこうなります。入口は1つでも、出口(=検知点)は複数あるのがランタイム防御の強みです。

Web サーバプロセスからのシェル起動(最も本丸):

- rule: Shell Spawned by Web Server (wp2shell post-exploitation)
  desc: php-fpm/apache/nginx の子として shell が起動された
  condition: >
    spawned_process and shell_procs
    and proc.pname in (php-fpm, httpd, apache2, nginx, php)
  output: >
    Web server が shell を起動 (parent=%proc.pname cmd=%proc.cmdline
    user=%user.name container=%container.name image=%container.image.repository)
  priority: CRITICAL
  tags: [wp2shell, mitre_execution, T1059]

外部ペイロード取得(追加のマルウェアダウンローダ):

- rule: Payload Fetch Tool Spawned by Web Server
  desc: Web サーバから curl/wget 等のダウンローダが起動された
  condition: >
    spawned_process
    and proc.pname in (php-fpm, httpd, apache2, nginx, php)
    and proc.name in (curl, wget, fetch, python, python3, perl)
  output: >
    Web server が外部取得ツールを起動 (tool=%proc.name cmd=%proc.cmdline
    container=%container.name)
  priority: CRITICAL
  tags: [wp2shell, mitre_command_and_control, T1105]

/tmp/var/tmp からの実行(Web Shell / コインマイナーの定番):

- rule: Execution from Temp Directory
  desc: 一時ディレクトリからバイナリが実行された
  condition: >
    spawned_process
    and (proc.exepath startswith /tmp/ or proc.exepath startswith /var/tmp/)
  output: >
    一時ディレクトリから実行 (exe=%proc.exepath cmd=%proc.cmdline
    container=%container.name)
  priority: CRITICAL
  tags: [wp2shell, mitre_execution]

これらの単純ルールに加え、以下の層が補完します。

  • Drift Control — WordPress コンテナは本来 php / apache しか動きません。イメージに存在しないバイナリの実行そのものをブロックできます。今回の「未知バイナリ投下 → 実行」に正面から効き、単なる検知より一段強い防御になります。
  • Malware Detection — Web Shell / CoinMiner / バックドアをシグネチャ・ハッシュで検知。
  • Network / DNS 検知 — C2・Tor ノード・不審 IP 向け通信を、それを起こしたプロセスに紐付けて検知。「この php-fpm がこの IP へ通信した」まで辿れます。
  • フォレンジック(Activity Audit) — プロセスツリー・実行コマンド・生成ファイル・送信データを時系列で再構成し、侵害の初手から追跡できます。
  • Response Actions — Kubernetes 環境なら侵害 Pod だけを隔離(通信遮断・プロセス停止)して被害拡大を止められます。

整理のため、侵害後の挙動を MITRE ATT&CK にマップしておきます。SOC 運用やアラート設計時の共通言語になります。

侵害後の挙動 MITRE ATT&CK 対応する検知
Web サーバがシェルを起動 T1059 Command and Scripting Interpreter Shell Spawned by Web Server
外部ペイロードを取得 T1105 Ingress Tool Transfer Payload Fetch Tool
/tmp からバイナリ実行 T1059 / TA0002 Execution Execution from Temp Directory
C2 / 不審 IP への通信 TA0011 Command and Control Network / DNS 検知
Web Shell の設置(本体書き込み) T1505.003 Web Shell Drift Control / イミュータブル検知

Falco は CNCF の卒業プロジェクトで、Linux ランタイム検知の事実上の標準です。ここで挙げた検知は「Sysdig 独自の主張」ではなく、業界標準の枠組みで実現できるものです。VM やホスト直載せの WordPress(日本の企業サイト・自治体に多い構成)でも Falco はホストで動くため、コンテナでなくても検知は効きます。

6. そもそもの環境をどう設計すべきか

wp2shell を教材に、「侵入されても被害が広がらない設計」を整理します。単発の対応で終わらせず、恒久設計に落とすのが狙いです。

(1) 多層防御 — 役割分担を明確に

パッチが本丸、WAF は一時緩和、ランタイムは「防げなかった時」の最後の砦。この順序と役割を混同しないことが重要です。

(2) 最小権限・最小構成

WordPress コンテナ/サーバに curl / wget / コンパイラ / 不要なシェルを置かない。§2 の攻撃チェーンで見た通り、後続ステップの多く(ペイロード取得・ツール実行)はここで詰まります。Drift Control と組み合わせると効果が跳ね上がります。

(3) エグレス(送信)制御をデフォルト拒否に

アウトバウンドをデフォルト拒否にしておけば、追加ペイロードの取得も C2 通信も成立しません。Sysdig のネットワーク可視化で「本来必要な通信」を洗い出し、ホワイトリスト化する運用に落とします。

(4) イミュータブル(不変)インフラ

WordPress 本体を読み取り専用でデプロイする。Web Shell の設置は「本体への書き込み」なので、即座に異常として観測できます。

(5) object cache の論点(Cloudflare の指摘)

Cloudflare は「CVE-2026-63030 の脆弱コードパスは永続オブジェクトキャッシュ(persistent object cache)が使われていないときに到達する」と明言しています。RCE は SQLi 経由で成立しますが、そのクエリ結果が Redis / Memcached 等のキャッシュにヒットすると DB クエリまで到達せず、脆弱パスが踏まれにくくなるためです。

つまり object cache 無しのサイトほど直接刺さる。object cache は普通「性能のため」に推奨される構成なのに、その不在が攻撃到達性を広げていた、という皮肉が効いた事実です。

図にすると、キャッシュの有無で脆弱パスへの到達性が変わります。

ただし誤解しないでください。これは到達性が下がるだけで、object cache は防御策ではありません。「キャッシュを入れたから安全」ではなく、パッチが本筋です。設計上の示唆として捉えてください。

(6) エージェント配置

コンテナ環境なら DaemonSet、VM/ホスト直載せならホストエージェントとして Falco / Sysdig エージェントを配置します。「どこに何を置けば今回のような未認証 RCE を実行時に捕捉できるか」を、資産インベントリと合わせて設計しておきます。忘れられがちなステージング環境やキャンペーン用サイトこそ更新漏れの温床なので、必ず棚卸し対象に含めます。

7. まとめ

wp2shell(CVE-2026-63030)は、「未認証 × RCE × stock install が対象」という稀な3条件が揃った、10年に一度級の WordPress Core 脆弱性です。しかも悪性リクエストは正規通信と構文上ほぼ同一で、入口でのシグネチャ選別が効きにくいという厄介な性質を持ちます。

現実的な守りは3層です。

  1. パッチ適用(最優先) — 6.9.5 / 7.0.2 へ。適用済みだと仮定せず検証する。
  2. WAF で一時緩和/wp-json/batch/v1?rest_route=/batch/v1 の両方をブロック(一時措置)。
  3. ランタイム検知(Sysdig / Falco) — 「防げない前提で、侵入後の異常を即座に検知し、被害拡大を止める」。

「侵入を完全に防ぐ」のではなく「侵入されても、すぐ気付き、被害を最小化する」。それが Runtime Security の役割であり、wp2shell はその必要性を最も分かりやすく示すケースです。

参考文献

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?