2026年7月17日、WordPress に深刻度「緊急」の脆弱性(CVE-2026-63030 / CVE-2026-60137)を修正した WordPress 7.0.2、6.9.5、6.8.6 が公開されました。この2つを組み合わせた攻撃チェーンは「wp2shell」と呼ばれており、脆弱なバージョンでは、ログインなど一切の認証なしに、外部からサーバー上で任意のコードを実行されるおそれがあります。
つまり、条件が揃えば「サイトを閲覧できる人なら誰でも」攻撃を仕掛けられる、という非常に厳しい内容です。データベース内の情報の窃取、ページの改ざん、マルウェアの設置など、幅広い被害につながりかねません。
脆弱性の技術的な詳細は、GMO Flatt Security さんの解説記事が詳しいので、そちらをご覧ください。
本記事では、Webの相談所のお客様にも利用者の多いさくらインターネットのレンタルサーバを対象に、「自分のサイトに wp2shell を狙ったアクセスがなかったか」をアクセスログから確認するためのシェルスクリプトを公開しましたので、その使い方をご紹介します。
まず最優先はアップデートです
ログの確認より先に、何よりもまず WordPress 本体のアップデートを行ってください。影響を受けるバージョンと更新先は以下のとおりです。
| 利用中のバージョン | 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 | 対象外(ただし SQLi の修正あり) | 6.8.6 以上 |
| 6.8 未満 | 対象外 | 最新の安定版を推奨 |
WAF やプラグインでのアクセス制限は「更新までのつなぎ」にはなりますが、脆弱なコード自体は残るため、アップデートの代わりにはなりません。自動更新を有効にしている場合も、実際に修正版へ更新されたことを必ず確認してください。
なぜアクセスログを確認するのか
wp2shell の攻撃は、WordPress REST API のバッチエンドポイント /batch/v1 へのリクエストを起点として成立します。標準的な構成では、次のような URL へのアクセスがその入口になります。
POST /wp-json/batch/v1POST /?rest_route=/batch/v1POST /<サブディレクトリ>/wp-json/batch/v1(サブディレクトリ設置の場合)
普段の運用でこのエンドポイントが外部から叩かれることはあまりありません。そのため、アクセスログに /batch/v1 へのアクセスが残っていれば、攻撃が試みられた可能性を疑うサインになります。逆に、脆弱性の公開後にこうしたアクセスが1件もなければ、ひとまず初期侵入の痕跡はなかったと考える材料になります。
公開したスクリプト:check-log-and-file-sakura.sh
コードはgithubで公開しておりますのでご確認ください
https://github.com/web-soudan/wp-security-audit-on-jp-hosting/blob/main/check-log-and-file-sakura.sh
コードを詳しく見る
#!/usr/bin/env bash
# さくらインターネット レンタルサーバー向けログの確認スクリプト
# ホームフォルダで実行し ~/log/access_*.gz から「過去N日分」を対象に検索する
# N は第2引数で指定するか、未指定なら端末(/dev/tty)から対話入力する。
# gz ファイルをストリーム解凍して cat | grep "/batch/v1" を探す
#
# 実行例:
# ./check-log-and-file-sakura.sh (対話入力)
# ./check-log-and-file-sakura.sh /batch/v1 7 (引数で指定)
# curl -s <URL> | bash (対話入力: /dev/tty から読む)
# curl -s <URL> | bash -s -- /batch/v1 7 (引数で指定・非対話)
# set -u : 未定義の変数を参照した時点でエラー終了させる安全装置。
# タイプミスした変数が「空文字」として黙って処理されるのを防ぐ。
# 例えば $log_dir を打ち間違えると、"$HOME/log" のつもりが
# ルート近くを検索してしまう…といった事故を未然に止められる。
set -u
# 検索パターン(第1引数で上書き可。デフォルト /batch/v1)
# "${1:-/batch/v1}" は「第1引数があればそれを、無ければ /batch/v1 を使う」
# という bash のデフォルト値展開。wp2shell の攻撃起点である
# REST API のバッチエンドポイント /batch/v1 を既定値にしている。
# パーマリンク有効時の /wp-json/batch/v1 も、?rest_route=/batch/v1 も、
# どちらも「/batch/v1」という文字列を含むため、この1パターンで両方拾える。
pattern="${1:-/batch/v1}"
# 対象日数(第2引数で指定可。未指定なら端末から対話入力)
# ここではまだ空のまま受け取り、後段で「対話入力するか・エラーにするか」を分岐する。
days="${2:-}"
# ログディレクトリ
# さくらのレンタルサーバーでは、コントロールパネルでアクセスログの保存を
# 有効にすると、ホームディレクトリ直下の ~/log/ に gzip 圧縮された
# アクセスログ(access_*.gz)が保存される。
log_dir="$HOME/log"
# 日数が引数で渡されなければ端末(/dev/tty)から対話入力する。
#
# ここがこのスクリプトの一番のポイント。
# 「curl -s <URL> | bash」で実行した場合、bash の標準入力(stdin)には
# curl が取ってきた“スクリプト本体”が流れ込んでいる。
# そのため普通に read で stdin から読もうとすると、ユーザーの入力ではなく
# スクリプトの続きの行を「入力」として食べてしまい、対話できない。
#
# そこで stdin ではなく /dev/tty(= いま操作している端末そのもの)から
# 読むことで、パイプ実行でもプロンプトを表示して入力を受け取れる。
#
# /dev/tty から読むことで curl | bash でも stdin(=スクリプト本体)を消費せずに済む。
# [ -r /dev/tty ] は端末が無くても真になるため、実際に開けるかで判定する
# (2>/dev/null を先に置き、制御端末が無い場合の open エラーも抑止する)。
#
# 「: 2>/dev/null < /dev/tty」の「:」は何もしない組み込みコマンド。
# つまり「/dev/tty を実際に開いてみて、開けたら真」というテストとして
# 使っている。cron など端末の無い環境では開けないので偽になり、
# その場合は対話をあきらめてエラー終了する。
if [ -z "$days" ]; then
if : 2>/dev/null < /dev/tty; then
# read も同じく < /dev/tty で端末から直接読む。
# -r はバックスラッシュをエスケープ扱いしない指定、
# -p はプロンプト文字列の表示。未入力(Enterのみ)なら 7 日にする。
read -r -p "過去何日分のログを対象にしますか? (例: 7 / Enterで7) : " days < /dev/tty
days="${days:-7}"
else
echo "エラー: 非対話実行では日数を第2引数で指定してください (例: ... | bash -s -- \"$pattern\" 7)" >&2
exit 1
fi
fi
# 入力チェック(正の整数のみ)
# case のパターン ''|*[!0-9]* は「空文字」または
# 「数字(0-9)以外の文字を1つでも含む」場合にマッチする。
# つまり "7" のような正の整数だけを通し、"abc" や "-1"、"7; rm -rf ~"
# のような値はここで弾く。$days は後で find のオプションに渡すため、
# 数字以外を混入させないことがそのまま安全性につながる。
case "$days" in
''|*[!0-9]*)
echo "エラー: 日数は正の整数で入力してください: $days" >&2
exit 1
;;
esac
# --- 対象ファイルの選択 -------------------------------------------------
# mtime が過去 N 日以内の access_*.gz を集める(新しい順は sort で整える)。
# find -mtime -N は FreeBSD / GNU 双方で使え、ファイル名の書式に依存しない。
#
# さくらのレンタルサーバーは FreeBSD ベースのため、GNU 拡張オプションに
# 頼らない書き方にしている。またログのファイル名(日付部分の書式)を
# パースするのではなく、ファイルの更新時刻(mtime)で絞ることで、
# 命名規則が変わっても動く作りにしている。
# ----------------------------------------------------------------------
files=()
# find の結果を1行ずつ配列 files に詰めていく。
# 「while read ... done < <(find ...)」はプロセス置換と呼ばれる書き方で、
# 「find ... | while read」とせずこの形にしているのは、パイプだと while が
# サブシェルで動いてしまい、ループを抜けた後に配列が空に戻ってしまうため。
# IFS= と read -r の組み合わせは、パスに空白等が含まれても崩れないための定石。
while IFS= read -r f; do
files+=("$f")
done < <(find "$log_dir" -type f -name 'access_*.gz' -mtime -"$days" 2>/dev/null | sort)
# 対象が1件も無ければ、その旨を表示して終了する。
# ログ保存が無効・保存期間切れ・日数指定が短すぎる、などが考えられる。
if [ "${#files[@]}" -eq 0 ]; then
echo "エラー: 過去 ${days} 日以内に $log_dir/access_*.gz が見つかりません" >&2
exit 1
fi
# 何を・どの範囲で検索するのかを実行前に明示する。
# 「どのログファイルが対象になったか」が見えると、
# ログ保存期間が足りているかの確認にもなる。
echo "対象期間 : 過去 ${days} 日"
echo "対象ファイル : ${#files[@]} 件"
printf ' %s\n' "${files[@]}"
echo "検索パターン : $pattern"
echo --------------------------------------------------
# gz をストリーム解凍(ディスクに展開しない)して grep(解凍は1回のみ)
#
# gzip -dc は「-d: 解凍」「-c: 標準出力へ」の組み合わせで、
# 展開結果をファイルに書き出さずそのまま grep へパイプで流す。
# レンタルサーバーのディスク容量を消費せず、一時ファイルの
# 消し忘れも起きない。複数ファイルをまとめて渡せるので解凍は1回で済む。
#
# grep の「--」は「これ以降はオプションではなく検索パターン」という区切り。
# 検索パターンを自由に指定できる仕様のため、仮に「-e」などハイフン始まりの
# 文字列が渡されても、grep のオプションとして誤解釈されないようにしている。
#
# 末尾の「|| true」は、マッチ0件のとき grep が終了コード1を返す仕様への対処。
# ここでスクリプトが「失敗」扱いにならないよう握りつぶし、
# 0件かどうかの判定は次の if で行う。
matches=$(gzip -dc "${files[@]}" | grep -- "$pattern" || true)
# マッチした行があればそのまま表示し、行数を数える。
# echo ではなく printf '%s\n' を使うのは、ログの中身に
# 「-n」などechoがオプションと解釈しうる文字列や、環境差のある
# エスケープ処理が混ざっても、内容をそのまま安全に出力するため。
# wc -l の出力に付く余白は tr -d ' ' で取り除いている。
count=0
if [ -n "$matches" ]; then
printf '%s\n' "$matches"
count=$(printf '%s\n' "$matches" | wc -l | tr -d ' ')
fi
echo --------------------------------------------------
# マッチ件数 0 → 対象期間のログに /batch/v1 へのアクセスは記録されていない。
# 1件以上 → アクセスがあったということ。ただし「攻撃が成功した」ことを
# 意味するわけではない点に注意(スキャナ等の探索アクセスも多い)。
echo "マッチ件数 : $count"さくらインターネットのレンタルサーバーでは、コントロールパネルでアクセスログの保存を有効にしていると、ホームディレクトリの ~/log/ に gzip 圧縮されたアクセスログ(access_*.gz)が保存されます。
今回公開したスクリプトは、この ~/log/access_*.gz を対象に、過去 N 日分のログをストリーム解凍しながら指定パターン(既定は /batch/v1)を検索します。ディスク上にログを展開しないので、容量を圧迫せず手軽に実行できます。
WP-CLI は不要で、さくらのレンタルサーバーに標準で入っている gzip / find / grep だけで動作します。読み取り専用のスクリプトで、サイトのファイルやデータベース、ログには一切変更を加えません。
使い方
さくらのレンタルサーバーに SSH で接続し、ホームディレクトリで実行します。
curl で直接実行する場合(おすすめ)
# 対話入力(何日分を対象にするかプロンプトで聞かれます。Enter で7日)
curl -s https://raw.githubusercontent.com/web-soudan/wp-security-audit-on-jp-hosting/refs/heads/main/check-log-and-file-sakura.sh | bash
# 引数で指定する場合(/batch/v1 を過去7日分から検索)
curl -s https://raw.githubusercontent.com/web-soudan/wp-security-audit-on-jp-hosting/refs/heads/main/check-log-and-file-sakura.sh | bash -s -- /batch/v1 7パイプ実行でも /dev/tty から日数を読み取るため、対話プロンプトがそのまま使えます。cron など端末のない環境で実行する場合は、第2引数で日数を必ず指定してください。
ダウンロードして実行する場合
# 対話入力
bash check-log-and-file-sakura.sh
# 引数で指定(第1引数: 検索パターン、第2引数: 日数)
bash check-log-and-file-sakura.sh /batch/v1 7実行結果の見方
実行すると、対象期間・対象ファイル・検索パターンが表示されたあと、マッチしたログの行と件数が出力されます。
- マッチ件数が 0 件 … 対象期間のログには
/batch/v1へのアクセスは記録されていません。 - マッチした行がある …
/batch/v1へのアクセスがあったということです。後述の注意点を踏まえて、内容を確認してください。
結果を見るときの注意点
このスクリプトはあくまで「初期侵入の入口へのアクセスがあったか」を確認する一次スクリーニングです。次の点にご注意ください。
ログにアクセスがあった=侵害された、ではありません。 脆弱性公開後は攻撃者だけでなくスキャナやリサーチャーによる探索的なアクセスも増えます。また、バッチ API はレスポンス全体に 207 を返し、個々のリクエストの成否は JSON 本文に格納されるため、アクセスログのステータスコードだけでは攻撃の成否を判断できません。
ログにアクセスがなかった=100%安全、でもありません。 さくらのレンタルサーバーでは、アクセスログの保存設定や保存期間によっては、確認したい期間のログ自体が残っていない場合があります。まずコントロールパネルでログの保存設定と保存期間を確認してください。また、_method クエリパラメータや X-HTTP-Method-Override ヘッダーで HTTP メソッドを上書きする手法もあるため、単純な条件の検索では見落としの可能性が残ります(本スクリプトはメソッドを限定せず、パス文字列でマッチさせる方針にしています)。
アクセスが確認でき、かつ脆弱なバージョンを使っていた期間と重なる場合は、侵害されている前提で対応を検討することをおすすめします。同リポジトリの check-wp.sh を使うと、WP-CLI の verify-checksums によるコア・プラグインのファイル改ざん検知も実行できます。判断が難しい場合や被害が疑われる場合は、専門のセキュリティ会社への相談をご検討ください。
まとめ
- まずアップデート(7.0.2 / 6.9.5 / 6.8.6 以上へ)
- アクセスログを確認(本スクリプトで
/batch/v1へのアクセスを検索) - 怪しい場合はファイル改ざん検知や専門家への相談へ
インストールされているWordPressの情報をまとめて取得する場合はこちらの記事も併せてご覧ください
Webの相談所では、さくらインターネットをはじめとする国内ホスティング上の WordPress サイトの保守・セキュリティ対応を行っています。「自分のサイトが該当バージョンか分からない」「ログの見方に自信がない」という場合も、お気軽にご相談ください。

