WordPressの管理画面に入れなくなったので乗っ取りを疑って切り分けてみた:原因は放置されたreCAPTCHAプラグインだった

IT・セキュリティ検証
スポンサーリンク

はじめに

2026年8月上旬、このブログ(WordPress)の管理画面にログインできなくなりました。正しいはずのパスワードを入れても弾かれます。

以前「WordPressの検索結果を悪用したSEOスパム攻撃」の記事を書いたこともあり、真っ先に疑ったのは Webシェルなどによる乗っ取り でした。

結論から書くと、乗っ取りではありませんでした。原因は、長く更新されていない Invisible reCaptcha プラグイン です。reCAPTCHA の検証が通らなくなり、正しいパスワードでもログインに失敗していました。

この記事では、AI(Claude Code)に手伝ってもらいながら進めた切り分けの手順を、同じ状況になった人が再現できるようにまとめます。

結論

  • 乗っ取りの形跡はなし
  • 原因は Invisible reCaptcha(放置プラグイン)の検証失敗
  • プラグインのフォルダ名を変えて無効化したら、ログインできた
  • 切り分けで一番役に立ったのは、SiteGuard のログイン履歴テーブル(wp_siteguard_history

環境

項目内容
サーバーエックスサーバー
WordPress7.x
テーマCocoon(子テーマ)
セキュリティ系プラグインSiteGuard WP Plugin(ログインURL変更・画像認証・ログインロック)、Invisible reCaptcha、Jetpack

症状

  • 管理画面のログインでパスワードを入れても失敗する
  • /wp-login.php は 404(SiteGuard でログインURLを変更しているため)
  • サイトの表示は普通

切り分けの全体像

flowchart TD
  A[管理画面に入れない] --> B{外から見て乗っ取りの兆候は?}
  B -- なし --> C[DBでパスワードを直接リセット]
  C --> D{ログインできる?}
  D -- できない --> E[SQLでDBの書き換えを検証]
  E -- 正しい --> F[SiteGuardのログイン履歴を確認]
  F -- ロックなし・ただの失敗 --> G[認証を邪魔するプラグインを疑う]
  G --> H[Invisible reCaptchaを無効化]
  H --> I[ログイン成功]

1. 外から乗っ取りの兆候を確認する

ログインできないので、まずサイトの外から見える範囲で乗っ取りの兆候を探しました。

ログインURLまわり

curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" https://example.com/wp-login.php
curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" https://example.com/wp-admin/
URL結果意味
/wp-login.php404SiteGuard がログインURLを隠している
/wp-admin/302 → /login_XXXXXSiteGuard が変更したログインURLへ転送している
/login_XXXXX200正常なログイン画面(画像認証つき)

login_ + 数字5桁は、SiteGuard が自動で作るログインURLの形式です。「変更後のログインURLを忘れただけ」というのもよくある原因ですが、今回はURLは合っていました。

見る人によって中身を変える細工(クローキング)

SEOスパム系の改ざんでは、Googlebot や Google 検索から来た人にだけ別のページを見せる手口がよくあります。User-Agent と Referer を変えて、中身が変わらないか比べました。

curl -sL https://example.com/ -o normal.html
curl -sL -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" https://example.com/ -o googlebot.html
curl -sL -A "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)" -e "https://www.google.com/" https://example.com/ -o google_mobile.html

wc -c normal.html googlebot.html google_mobile.html
grep -oE '(src|href)="https?://[^"/]+' normal.html | sed -E 's#.*//##' | sort -u

結果

確認項目結果
クローキングなし(3パターンでサイズも読み込み先ドメインも完全に一致)
怪しいJS(eval / atob / 強制リダイレクト)なし
読み込み先の外部ドメインAdSense・Jetpack・wordpress.org など、知っているものだけ
よくあるWebシェルのパス(wso.phpalfa.php など)すべて 404
wp-config.php のバックアップ、.env.gitすべて 404
サイトマップ・RSSスパム記事は追加されていない
REST API のユーザー一覧自分だけ

外から見る限り、乗っ取りの兆候はありませんでした。ただし、サーバー内部のファイル改ざんまでは外から分からないので、この時点ではまだ「シロ確定」ではありません。

2. パスワードをリセットする

WordPress のパスワードをリセットする方法は、主に3つあります。

  1. ログイン画面の「パスワードをお忘れですか?」から、メールで再設定する
  2. phpMyAdmin で wp_users テーブルの user_pass を直接書き換える
  3. テーマの functions.phpwp_set_password() を一時的に書く

今回は、確実な2を選びました。

エックスサーバーでの手順

  1. ファイル管理で wp-config.php を開き、DB_NAMEDB_USERDB_PASSWORD$table_prefix をメモする
  2. サーバーパネルから phpMyAdmin に入る
  3. wp_users テーブルの管理者の行で「編集」を押す
  4. user_pass の「関数」で MD5 を選び、「値」に新しいパスワードをそのまま入力して実行する

注意:サーバーパネルの「MySQL設定」でデータベースのパスワードを変えるのとは別の作業です。そちらを変えると、サイト全体が「データベース接続確立エラー」になります。

WordPress 6.8 以降でも MD5 で大丈夫?

WordPress 6.8 からパスワードのハッシュは bcrypt に変わりました。ただ、32文字のMD5ハッシュも互換性のために引き続き受け付けます。ログインに成功すると、自動で bcrypt に変換されます。

3. DBのハッシュが $wpy$… だった。改ざんされた?

user_pass を見ると、$wp$2y$10$… という見慣れない形式の値でした。改ざんを疑いましたが、これは WordPress 6.8 以降の標準の bcrypt 形式 です。

この形式になるのは、次のどちらかのときです。

  • 6.8 以降にパスワードを設定・変更した
  • 古い形式($P$…)のまま、一度でもログインに成功した(そのとき自動で変換される)

以前普通にログインできていたなら、この形式になっていて当然です。なお、ハッシュからは「いつ・誰が」設定したかは分かりません。

改ざんを疑うなら、ハッシュではなく次の点を確認します。

-- 登録メールアドレスが自分のものか(乗っ取られると、まずここを変えられる)
SELECT ID, user_login, user_email, user_registered FROM wp_users;

-- 自分以外に管理者がいないか
SELECT u.ID, u.user_login, u.user_email
FROM wp_users u JOIN wp_usermeta m ON m.user_id = u.ID
WHERE m.meta_key = 'wp_capabilities' AND m.meta_value LIKE '%administrator%';

-- 誰でも管理者として登録できる設定になっていないか
SELECT option_name, option_value FROM wp_options
WHERE option_name IN ('admin_email', 'users_can_register', 'default_role');

今回は、ユーザーは自分1人だけで、メールアドレスも変わっていませんでした。

4. MD5 で書き換えたのにログインできない

パスワードを書き換えても、ログインは失敗しました。

書き換えが正しく入ったかをSQLで確かめる

SELECT DATABASE() AS db, ID, user_login,
       LENGTH(user_pass) AS len,
       user_pass = MD5('新しいパスワード') AS md5_ok
FROM wp_users;
結果意味
md5_ok = 1正しく書き換わっている
len = 63$wp$… のまま)保存されていない、または別のDBを編集した
len がパスワードの文字数と同じ「関数」で MD5 を選び忘れた
len = 32 なのに md5_ok = 0MD5 が2回かかった、または全角文字や空白が入った

このSQLにはパスワードをそのまま書くので、phpMyAdmin の履歴に残ります。ログインできたら、パスワードを変え直しましょう。

結果は len = 32md5_ok = 1DBは正しく書き換わっていました

ログイン画面まわりの確認

  • サイトは普通に表示される → DB接続は壊れていない
  • ログイン画面を2回取得すると、SiteGuard 画像認証の番号(siteguard_captcha_prefix)が毎回変わり、Cache-Control: no-store も付いている → キャッシュのせいで画像認証が必ず失敗する状態ではない
  • reCAPTCHA のバッジは正常に表示され、ブラウザのコンソールにもエラーはない
for i in 1 2; do curl -s https://example.com/login_XXXXX | grep -oE 'siteguard_captcha_prefix" value="[0-9]+'; done

5. SiteGuard のログイン履歴テーブルで切り分ける

ここで役に立ったのが、SiteGuard がログインの試行を記録している wp_siteguard_history テーブル です。管理画面に入れなくても、phpMyAdmin から中身を見られます。

SELECT * FROM wp_siteguard_history ORDER BY id DESC LIMIT 20;
カラム値の意味
operation0=成功、1=失敗、2=フェールワンス(わざと1回失敗させる機能)、3=ロック
type0=ログイン画面から、1=XML-RPC から

分かったこと

  • 直近20件(約1か月分)はすべて operation = 1(失敗)で、成功は1件もない → この期間、攻撃者も含めて誰もログインに成功していない
  • 2(フェールワンス)や 3(ロック)はない → SiteGuard に締め出されているわけではない
  • type = 0 の試行は、自分の回線のIPと一致した
  • type = 1 は、米国やリトアニアなど海外のIPから XML-RPC 経由で来たパスワード総当たりで、すべて失敗していた

つまり、ユーザー名もパスワードも正しく、ロックもされていないのに、認証の途中で「失敗」にされている 状態でした。

最後にログインに成功した日時とIPは、次のSQLで確認できます。

SELECT * FROM wp_siteguard_history WHERE operation = 0 ORDER BY id DESC LIMIT 10;

6. 原因は Invisible reCaptcha

ここまでで、残る候補は3つでした。

  • SiteGuard の画像認証の入力ミス
  • ブラウザの自動入力で古いパスワードが入っている
  • Invisible reCaptcha のサーバー側の検証が失敗している

このうち「何も変えていないのに、ある日急に入れなくなった」に一番合うのは reCAPTCHA です。

  • Invisible reCaptcha は、WordPress.org 上で直近3回のメジャーバージョンでテストされておらず、事実上放置されている
  • Google は古い形式の reCAPTCHA キーを Google Cloud に移している最中で、ログイン画面のバッジにも「reCAPTCHA の利用規約が変更されます」と表示されていた

フォルダ名を変えてプラグインを無効化する

管理画面に入れないときは、FTP やファイル管理でプラグインのフォルダ名を変えると、そのプラグインを無効化できます。

wp-content/plugins/invisible-recaptcha
  ↓
wp-content/plugins/invisible-recaptcha-off

これで、あっさりログインできました

なぜ reCAPTCHA の検証に失敗するようになったのか(キー移行の影響か、プラグイン側の問題か)までは特定していません。どちらにしても、SiteGuard の画像認証があれば reCAPTCHA はなくても困らないので、プラグインは削除することにしました。

ポイント:フォルダ名を元に戻すと、プラグインがまた有効になって同じ状態に戻ることがあります。戻さずに削除しましょう。

7. ログイン後の対応

対応理由
パスワードを変更する作業中にAIに送ったスクショにパスワードが写っていたため。プロフィール画面から変えれば、bcrypt で保存し直される
Invisible reCaptcha を削除する放置プラグインで、SiteGuard の画像認証で代わりが務まる
SiteGuard の「ユーザー名漏えい防御」を有効にするREST API(/wp-json/wp/v2/users)からログインユーザー名が取れていた
Jetpack のブルートフォース保護を確認するXML-RPC 経由の総当たり対策

XML-RPC は無効化しない

XML-RPC 経由の総当たりが来ていたので、SiteGuard の「XMLRPC防御」で完全に無効化しようと考えました。しかし、Jetpack は WordPress.com との通信に XML-RPC を使います。完全に止めると、Jetpack の統計などが動かなくなります。

そこで SiteGuard の設定は「ピングバック無効化」までにとどめ、総当たりはログインロックと Jetpack の保護で防ぐことにしました。

8. 念のため WPScan で事後チェック

最後に、外から見て不備がないか WPScan で確認しました。Docker で実行し、APIトークンなし、プラグインは総当たりせずページ内の情報から探す、リクエスト間隔は0.3秒、という控えめな設定です。

docker run --rm wpscanteam/wpscan:latest \
  --url https://example.com/ \
  --enumerate ap,cb,dbe,u1-10 \
  --plugins-detection passive \
  --plugins-version-detection mixed \
  --throttle 300 \
  --format cli-no-color --no-banner

パスワードの総当たり(--passwords)はやりません。SiteGuard のログインロックで、自分のIPが締め出されるだけです。

項目結果
設定ファイルのバックアップなし
DBダンプの放置なし
WordPress 本体のバージョン外から特定できない
プラグインすべて最新
ユーザー名REST API から取れた → ユーザー名漏えい防御で対応
XML-RPC有効(Jetpack のために残す)
外からの WP-Cron 実行有効(影響は小さい)
readme.html公開されている(バージョンは載っていない)

APIトークンなしでは、既知の脆弱性(CVE)との照合結果は出ません。脆弱性まで確認したい場合は、WPScan で無料のAPIトークン(1日25回まで)を取得してから実行してください。

まとめ

  • 「ログインできない=乗っ取り」と決めつけない。 外から確認 → DB → ログの順に切り分けると、原因を絞り込める
  • $wp$2y$10$… は、WordPress 6.8 以降の正常なハッシュ形式
  • SiteGuard の wp_siteguard_history テーブル は、管理画面に入れないときの切り分けにとても便利
  • 放置プラグインは、攻撃の入口になるだけでなく、自分を締め出す原因にもなる
  • セキュリティ系プラグインを重ねるほど、障害の原因になりうる箇所も増える
  • 管理画面に入れないときは、プラグインのフォルダ名を変えれば無効化できる

参考

コメント

タイトルとURLをコピーしました