はじめに
2026年8月上旬、このブログ(WordPress)の管理画面にログインできなくなりました。正しいはずのパスワードを入れても弾かれます。
以前「WordPressの検索結果を悪用したSEOスパム攻撃」の記事を書いたこともあり、真っ先に疑ったのは Webシェルなどによる乗っ取り でした。
結論から書くと、乗っ取りではありませんでした。原因は、長く更新されていない Invisible reCaptcha プラグイン です。reCAPTCHA の検証が通らなくなり、正しいパスワードでもログインに失敗していました。
この記事では、AI(Claude Code)に手伝ってもらいながら進めた切り分けの手順を、同じ状況になった人が再現できるようにまとめます。
結論
- 乗っ取りの形跡はなし
- 原因は Invisible reCaptcha(放置プラグイン)の検証失敗
- プラグインのフォルダ名を変えて無効化したら、ログインできた
- 切り分けで一番役に立ったのは、SiteGuard のログイン履歴テーブル(
wp_siteguard_history)
環境
| 項目 | 内容 |
|---|---|
| サーバー | エックスサーバー |
| WordPress | 7.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.php | 404 | SiteGuard がログインURLを隠している |
/wp-admin/ | 302 → /login_XXXXX | SiteGuard が変更したログインURLへ転送している |
/login_XXXXX | 200 | 正常なログイン画面(画像認証つき) |
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.php、alfa.php など) | すべて 404 |
wp-config.php のバックアップ、.env、.git | すべて 404 |
| サイトマップ・RSS | スパム記事は追加されていない |
| REST API のユーザー一覧 | 自分だけ |
外から見る限り、乗っ取りの兆候はありませんでした。ただし、サーバー内部のファイル改ざんまでは外から分からないので、この時点ではまだ「シロ確定」ではありません。
2. パスワードをリセットする
WordPress のパスワードをリセットする方法は、主に3つあります。
- ログイン画面の「パスワードをお忘れですか?」から、メールで再設定する
- phpMyAdmin で
wp_usersテーブルのuser_passを直接書き換える - テーマの
functions.phpにwp_set_password()を一時的に書く
今回は、確実な2を選びました。
エックスサーバーでの手順
- ファイル管理で
wp-config.phpを開き、DB_NAME・DB_USER・DB_PASSWORD・$table_prefixをメモする - サーバーパネルから phpMyAdmin に入る
wp_usersテーブルの管理者の行で「編集」を押す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 = 0 | MD5 が2回かかった、または全角文字や空白が入った |
このSQLにはパスワードをそのまま書くので、phpMyAdmin の履歴に残ります。ログインできたら、パスワードを変え直しましょう。
結果は len = 32、md5_ok = 1。DBは正しく書き換わっていました。
ログイン画面まわりの確認
- サイトは普通に表示される → 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;
| カラム | 値の意味 |
|---|---|
operation | 0=成功、1=失敗、2=フェールワンス(わざと1回失敗させる機能)、3=ロック |
type | 0=ログイン画面から、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テーブル は、管理画面に入れないときの切り分けにとても便利 - 放置プラグインは、攻撃の入口になるだけでなく、自分を締め出す原因にもなる
- セキュリティ系プラグインを重ねるほど、障害の原因になりうる箇所も増える
- 管理画面に入れないときは、プラグインのフォルダ名を変えれば無効化できる


コメント