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?

ログインページからRCEまで——CVE-2026-64638(XSS2Shell)の仕組みと公開WordPressの実態

0
Posted at

はじめに

2026年8月6日、WordPressのコアに存在する重大な脆弱性チェーンが公開されました。

CVE-2026-64638、通称「XSS2Shell」

認証なしの攻撃者が、ログインページへの1回のリクエストを起点に、管理者のブラウザを経由してサーバー上でのリモートコード実行(RCE)まで到達できるという脆弱性チェーンです。

WordPressはインターネット上のWebサイトの43%以上で使われており、推定5億以上のサイトが影響を受けました。
WordPress 7.0.3で修正済みですが、アップデートされていないサイトは現在も脆弱な状態のままです。

今回は脆弱性の仕組みを整理したうえで、IT資産検索を使って公開WordPressの実態を調べ、実際に脆弱かどうかをcurlで確認してみました。

XSS2Shellの仕組み

全体像

攻撃チェーンは5つのステップで構成されています。

Step 1: ログインページでXSSを発火させる
         ↓
Step 2: opener windowでApplication Password承認ページを開く
         ↓
Step 3: Application Passwordを取得する
         ↓
Step 4: REST APIで管理者権限のJavaScriptを公開ページに埋め込む
         ↓
Step 5: プラグインをアップロードしてPHP実行(RCE)

Step 1:パーサーの不一致によるXSS

今回の脆弱性の核心は、WordPressが使う2つのサニタイザーの動作の違いにあります。

/wp-login.phpでログインに失敗すると、入力したユーザー名がエラーメッセージにそのまま含まれます。このユーザー名はwp_strip_all_tags()でサニタイズされた後、wp_kses_post()を経由してHTMLに出力されます。

wp_strip_all_tags()の動作:

PHPのstrip_tags()<の直後に文字が続く場合だけHTMLタグとして認識します。<と文字の間にスペースがあると、タグとして認識しません。

strip_tags('<area id=test>');    // '' ← 削除される
strip_tags('< area id=test>');   // '< area id=test>' ← 生き残る

wp_kses_post()の動作:

WordPressのKESSは独自のパーサーを使っており、< areaのように<とタグ名の間にスペースがあっても有効な<area>タグとして解釈します。そして<area>はKESSの許可リストに含まれています。

結果:

  • wp_strip_all_tags()は「テキストだ」として通過させる
  • wp_kses_post()は「有効なHTMLタグだ」として解釈する

この2つのパーサーの不一致により、攻撃者が入力した文字列がDOMにLive elementとして挿入されます。

Payload:

< area id=ajaxurl href=/?rest_route=/&_method=GET&_jsonp=alert>
< div id=color-picker class=reset-pass-submit>
< button class="wp-generate-pw color-option">X

<の後のスペース1つが、この攻撃全体の出発点です。

なぜJavaScriptが実行されるのか

WordPressのログインページにはuser-profile.jsというスクリプトが読み込まれています。本来はプロフィール編集ページ用ですが、パスワードリセット機能のためにログインページ全体でも読み込まれています。

このスクリプトは$(document).readyでDOMを走査し、.reset-pass-submit内の.wp-generate-pwボタンを自動クリックします。攻撃者が挿入したDOMにはこれらのクラスが含まれているため、スクリプトが自動的に反応します。

クリックイベントは#color-pickerへの委譲ハンドラに伝播し、$.post(ajaxurl, ...)が実行されます。ajaxurlはDOM clobbering(id=ajaxurl<area>要素がwindow.ajaxurlとして参照される)によってPayloadのhrefが使われます。

REST APIのJSONPレスポンスをjQueryがscriptとして評価することで、JavaScriptがWordPressオリジンで実行されます。

Step 2〜5:XSSからRCEへ

Step 2:opener windowの利用

攻撃者は管理者に細工したページを開かせます。そのページは管理者のセッションでApplication Password承認ページを開きつつ、子ウィンドウからXSSを発火させます。

Step 3:Application Passwordの取得

子ウィンドウのXSSがopener windowの承認ボタンをクリックし、管理者のApplication Passwordが攻撃者のサーバーに送られます。

Step 4:JavaScriptの公開

取得したApplication PasswordでREST APIを使い、unfiltered_html権限でscriptタグを含むページを公開します。

Step 5:プラグインアップロードとRCE

管理者のcookieセッションでプラグインをアップロードします。プラグインの有効化は不要で、wp-content/plugins/以下のPHPファイルは直接URLでアクセスできます。

HTTP/1.1 200 OK
Hacked: true

{"rce":true,"user":"www-data"}

影響を受けるバージョンと対応

影響範囲: WordPress 7.0.3未満のすべてのバージョン
修正バージョン: WordPress 7.0.3(2026年8月6日リリース)
対応: WordPress 7.0.3以上へのアップデートを今すぐ実施

実際に調べてみた

キーワード検索で公開状況を確認する

/wp-json/wp/v2/

/wp-json/wp/v2/ の検索結果.png
(画像:/wp-json/wp/v2/ の検索結果)

キーワード検索で/wp-json/wp/v2/を検索した結果、4,009件が確認されました。日本からも312件の露出が確認されています。

/wp-json/wp/v2/が表示されるということは、WordPressのREST APIが外部から叩ける状態にある資産の候補です。今回の攻撃チェーンではREST APIのJSONP機能を経由してJavaScriptを実行するため、REST APIが外部に公開されているかどうかが重要な確認ポイントになります。

/?rest_route=/ product: "wordpress"

/?rest_route=/ の検索結果.png
(画像:/?rest_route=/ の検索結果)

/?rest_route=/ product: "wordpress"の検索結果、118件が確認されました。

?rest_route=はWordPress REST APIの代替アクセス方法で、今回の攻撃Payloadで直接使われるパラメータです。このパスがバナーに含まれているということは、REST APIが正常に応答している可能性を示しています。

product: wordpressで全体像を確認

product: wordpress

product: wordpress の検索結果.png
(画像:product: wordpress の検索結果)

product: wordpress を検索した結果、26,661件が確認されました。日本だけでも2,634件の露出が確認されています。
検索結果にはWordPressのバージョン情報が表示されている資産もあります。WordPress (3.9.25), (7.0.1)のように修正バージョン(7.0.3)未満の資産が含まれており、パッチ未適用の可能性がある資産を確認できます。

個別資産を実際に確認してみた

Criminal IPの検索結果に含まれていたWordPressサイトの1つを確認してみました。
HTMLソースには?ver=7.0.1というパラメータが複数箇所に含まれており、WordPress 7.0.1が動作していることが確認できました。修正バージョン(7.0.3)未満です。
curlでユーザー名がHTMLエンコードなしで返ってくるかを確認しました。

# ユーザー名がそのまま反映されるか確認
curl -s -X POST "https://staging.*********.com/wp-login.php" \
  -d 'log=TESTUSER_XYZ&pwd=x&wp-submit=Log+In' \
  | grep -o 'TESTUSER_XYZ'
# 特殊文字がエスケープされるか確認
curl -s -X POST "https://staging.**********.com/wp-login.php" \
  -d 'log=TEST"QUOTE&pwd=x&wp-submit=Log+In' \
  | grep -o 'TEST[^<]*QUOTE'

curlの実行結果 - TESTUSER_XYZとTEST"QUOTEが返ってきた.png
(画像:curlの実行結果 - TESTUSER_XYZとTEST"QUOTEが返ってきた)

1つ目のコマンドではTESTUSER_XYZがそのまま返ってきました。2つ目のコマンドではダブルクォートが&quot;にエスケープされず、TEST"QUOTEがそのまま返ってきました。
HTMLエンコーディングが適切に行われていない状態で、CVE-2026-64638の攻撃条件が整っていることが確認できました。
PoCはすでに8月7日に公開されており、認証不要で誰でも試せる状態です。今回確認したのはあくまで一部の資産ですが、product: wordpressで26,661件という数字を考えると、パッチが当たっていない資産はまだ相当数残っていると考えられます。

WordPressを運用している場合は、まず以下を確認することをおすすめします。

  • WordPress 7.0.3以上にアップデートされているか
  • 自動更新が有効になっているか
  • 管理画面(/wp-admin/)や/wp-login.phpが不必要に外部公開されていないか

まとめ

<の後のスペース1つ——これがXSS2Shell全体の出発点です。

2つのサニタイザーが「テキストだ」「有効なHTMLタグだ」と正反対の判断をする。この小さな隙間が、ログインページへのリクエスト1回からRCEまで到達する攻撃チェーンを成立させています。

今回IT資産検索で調べてみると、product: wordpressで26,661件、日本だけで2,634件のWordPress資産が外部に露出していることが確認できました。そのうちREST APIが外部から叩ける状態の資産が4,009件。攻撃チェーンの経路が整っている可能性のある資産が、世界中に相当数存在しています。

実際に確認した資産では、ユーザー名がHTMLエンコードなしでそのまま返ってきました。PoCはすでに公開されており、攻撃の難易度は高くありません。「うちのWordPressは大丈夫だろう」という感覚は、今回の件では通用しないと思っています。

WordPressを運用している場合は、7.0.3へのアップデートをまず確認してください。curlで確認できる内容は少ないですが、それでも「ユーザー名がそのまま返ってくるかどうか」だけでも試してみる価値はあると思います。

参考

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?