- 対象: WordPress サイトを運用している Web 担当者・制作会社の方(wp-cli が使えるサーバー)
- 解決すること: 「誰が管理者なのか分からない」「退職者や昔の制作会社のアカウントが残っている」「脆弱性の公表後、不審な管理者が作られていないか不安」という状態をなくします
- 方法: 管理者アカウント・アプリケーションパスワード・登録設定を wp-cli と SQL で棚卸しし、不要なものを外します(コマンドはコピペで使えます)
なぜ「管理者の棚卸し」なのか
WordPress のプラグイン脆弱性の公表を見ていると、被害の行き着く先は多くの場合「攻撃者が管理者アカウントを作る・乗っ取る」です。管理者になれば、プラグインのアップロードを通じて任意の PHP を置けるためです。WordPress 公式のハードニング文書も「管理者アカウントにアクセスされると悪意のあるスクリプトをインストールされる」と書いています。
更新で脆弱性をふさいでも、すでに作られた管理者アカウントやアプリケーションパスワードは残ります。更新とは別に「いま誰が鍵を持っているか」を確認する作業が必要です。
| よくある残骸 | 何が起きるか |
|---|---|
| 退職者・前任の制作会社の管理者 | パスワード使い回しから乗っ取られる |
共有の admin アカウント |
誰が何をしたか追えない |
| 使っていないアプリケーションパスワード | 管理画面のログイン対策(2 段階認証など)を通らずに REST API を叩ける |
default_role が administrator |
会員登録しただけで管理者になる |
| 管理画面の一覧に出ない管理者 | マルウェアが一覧から自分を隠している |
以下、手順 0 → 7 の順で進めます。作業前には必ずバックアップを取ってください。
手順 0: 作業前の準備
# WordPress のディレクトリへ移動(パスは環境に合わせて変更)
cd /var/www/example.com/public
# wp-cli とサイトの状態確認
wp --info
wp core version
# DB のバックアップ(ドキュメントルートの外へ)
wp db export ~/backup-$(date +%Y%m%d-%H%M).sql
# テーブル接頭辞を確認(以降の SQL の wp_ を置き換える)
wp db prefix
wp db prefix が wp_ 以外(例: wpx9_)なら、以降の SQL の wp_users / wp_usermeta / wp_capabilities をその接頭辞に置き換えてください。
手順 1: 管理者の一覧を「2 つの経路」で出す
まず wp-cli で管理者を一覧します。プラグインとテーマを読み込まずに実行するのがポイントです。不正なプラグインが一覧結果を書き換えている可能性を減らせます。
wp user list --role=administrator \
--fields=ID,user_login,user_email,user_registered \
--skip-plugins --skip-themes
ただし wp-cli の公式リファレンスにある通り、--skip-plugins を付けても mu-plugins(wp-content/mu-plugins/)は読み込まれます。そこで、WordPress を通さず DB を直接見た結果と突き合わせます。
wp db query "
SELECT u.ID, u.user_login, u.user_email, u.user_registered, m.meta_value
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\"%'
ORDER BY u.user_registered;
" --skip-plugins --skip-themes
確認ポイントは次の 3 つです。
- 2 つの件数が一致するか。DB 側にだけいるアカウントがあれば、何かが一覧から隠している疑いがあります(手順 6 へ)
-
user_registeredが最近のものはないか。特に脆弱性公表の前後の日付は要注意です - メールアドレスが自社・取引先のドメインか。フリーメールや見覚えのないドメインは確認対象です
あわせて、administrator 以外のロールに強い権限が付いていないかも見ておきます。
# ロールの一覧(見覚えのないロールがないか)
wp role list --skip-plugins --skip-themes
# administrator 以外のユーザーについて、ロールと個別に付いた権限を表示
wp user list --fields=ID,user_login,roles,caps --skip-plugins --skip-themes
caps 欄に administrator や manage_options などが個別付与されている非管理者がいれば、理由を確認してください。
マルチサイトの場合は、ネットワーク全体を操作できる特権管理者も別に確認します。
wp super-admin list
手順 2: 1 人ずつ「誰の・何のための」台帳を作る
一覧が出たら、アカウントごとに持ち主と用途を書き出します。CSV で出して表計算ソフトで管理すると楽です。
wp user list --role=administrator \
--fields=ID,user_login,display_name,user_email,user_registered \
--format=csv --skip-plugins --skip-themes > ~/admin-audit-$(date +%Y%m%d).csv
台帳には次の列を足します。
| 列 | 記入例 | 判断 |
|---|---|---|
| 持ち主 | 広報 担当 A / 制作会社 B 社 | 書けない = 削除候補 |
| 用途 | プラグイン更新 / 記事公開のみ | 記事だけなら編集者で足りる |
| 最終利用 | 手順 3 のセッション確認で把握 | 長期未使用は降格候補 |
| 共有か | 複数人で同じログインを使う | 1 人 1 アカウントに分ける |
持ち主が書けないアカウントは残さない、が原則です。
手順 3: 不要な管理者は「降格 → 様子見 → 削除」
いきなり削除すると、そのユーザーの投稿の扱いで困ることがあります。まずは権限を下げて、一定期間問題がないことを確認してから削除します。
# いまのログイン状況(最終利用の目安)
wp user session list old-agency --skip-plugins --skip-themes
# 記事を書くだけなら編集者へ降格
wp user set-role old-agency editor --skip-plugins --skip-themes
# 誰も使わないなら購読者へ降格し、セッションも切る
wp user set-role old-agency subscriber --skip-plugins --skip-themes
wp user session destroy old-agency --all --skip-plugins --skip-themes
問題がなければ、投稿を別のユーザーに引き継いでから削除します。--reassign には引き継ぎ先のユーザー ID を指定します。
# ID 12 のユーザーを削除し、投稿は ID 1 のユーザーへ引き継ぐ
wp user delete 12 --reassign=1 --skip-plugins --skip-themes
マルチサイトでは、このコマンドは既定で「現在のサイトから外す」だけです。DB からも消すには --network が必要です。
手順 4: アプリケーションパスワードを棚卸しする
アプリケーションパスワードは WordPress 5.6 で入った仕組みで、外部アプリが REST API などに認証するための専用パスワードです。管理画面のログインフォームを通らないため、ログイン画面側の対策(2 段階認証プラグインなど)の外側にあることがあります。
全ユーザーについて、発行済みのものを洗い出します。
for u in $(wp user list --field=ID --skip-plugins --skip-themes); do
n=$(wp user application-password list "$u" --format=count --skip-plugins --skip-themes)
if [ "$n" -gt 0 ]; then
echo "== user $u ($n 件)"
wp user application-password list "$u" \
--fields=uuid,name,created,last_used,last_ip --skip-plugins --skip-themes
fi
done
created と last_used は Unix 時刻で出ます。人が読める形にするには date -d @1730000000 のように変換してください。
用途が分からないもの・長く使われていないものは削除します。
# 1 件だけ削除(UUID は一覧の uuid 列)
wp user application-password delete 12 6633824d-c1d7-4f79-9dd5-4586f734d69e
# そのユーザーの分をすべて削除
wp user application-password delete 12 --all
外部連携で使っていないサイトなら、機能ごと無効にできます。テーマを変えても外れないよう、mu-plugin として置きます。
<?php
/**
* Plugin Name: Disable Application Passwords
* 配置先: wp-content/mu-plugins/disable-application-passwords.php
*/
add_filter( 'wp_is_application_passwords_available', '__return_false' );
連携で一部のユーザーだけ使う場合は、ユーザー単位のフィルターで管理者以外を止める、といった絞り方もできます。
<?php
/**
* Plugin Name: Limit Application Passwords
* 配置先: wp-content/mu-plugins/limit-application-passwords.php
* 連携用ユーザー(ログイン名を指定)だけに許可する
*/
add_filter(
'wp_is_application_passwords_available_for_user',
function ( $available, $user ) {
$allowed = array( 'api-integration' );
return $available && in_array( $user->user_login, $allowed, true );
},
10,
2
);
手順 5: 「会員登録で管理者になる」設定を確認する
脆弱性を突かれて、サイトの設定値そのものが書き換えられるケースもあります。登録まわりの 2 つの設定を確認します。
# 誰でも会員登録できるか(1 = できる)
wp option get users_can_register --skip-plugins --skip-themes
# 新規登録ユーザーに付くロール
wp option get default_role --skip-plugins --skip-themes
会員機能を使っていないサイトの正しい状態は 0 と subscriber です。
wp option update users_can_register 0 --skip-plugins --skip-themes
wp option update default_role subscriber --skip-plugins --skip-themes
会員機能を使うサイトでも、default_role が administrator や editor になっていれば、意図した設定か必ず確認してください。
手順 6: 不審なアカウントがあったときの一斉リセット
手順 1〜5 で身に覚えのない管理者やアプリケーションパスワードが見つかった場合は、そのアカウントを消すだけでは足りません。他の経路(バックドアのファイルなど)が残っている可能性があるためです。次の順で対応します。
# 1) 証拠を残す(削除の前に)
wp user list --fields=ID,user_login,user_email,user_registered,roles \
--format=csv --skip-plugins --skip-themes > ~/incident-users-$(date +%Y%m%d-%H%M).csv
# 2) 不審なアカウントを購読者へ降格し、セッションを切る(削除は調査後)
wp user set-role 99 subscriber --skip-plugins --skip-themes
wp user session destroy 99 --all --skip-plugins --skip-themes
wp user application-password delete 99 --all --skip-plugins --skip-themes
# 3) 正規の管理者全員のパスワードを再設定(リセットメールが届く)
wp user reset-password $(wp user list --format=ids --role=administrator --skip-plugins --skip-themes) \
--skip-plugins --skip-themes
# 4) 認証用の鍵(ソルト)を作り直し、全ユーザーを強制ログアウト
wp config shuffle-salts
wp config shuffle-salts は wp-config.php のソルトを新しい値に入れ替えます。ログイン中の Cookie がすべて無効になるので、作業中の人には事前に声をかけてください。
あわせて、ファイル側の改ざん確認(コアのチェックサム照合・mu-plugins や uploads 内の PHP の確認)も行ってください。手順は関連記事にまとめています。
手順 7: 再発防止と定期チェック
管理画面からのファイル編集を止める
wp-config.php に次の 1 行を足します。WordPress 公式文書によると、これは全ユーザーから edit_themes・edit_plugins・edit_files の権限を外すのと同じ効果です(ファイルのアップロード自体は止まらない点に注意)。
// wp-config.php の「編集が必要なのはここまでです」より上に追記
define( 'DISALLOW_FILE_EDIT', true );
管理者には 2 段階認証を
WordPress 公式のハードニング文書も 2 段階認証の導入を勧めています。少なくとも管理者ロールは必須にしてください。
管理者の増減を毎日自動でチェックする
管理者一覧を毎日保存し、前日との差分があれば通知するスクリプトです。
#!/usr/bin/env bash
# /usr/local/bin/wp-admin-diff.sh (サイトの実行ユーザーの crontab で毎日実行)
set -euo pipefail
WP_PATH="/var/www/example.com/public"
STATE_DIR="$HOME/.wp-admin-audit"
MAIL_TO="web-admin@example.com"
mkdir -p "$STATE_DIR"
chmod 700 "$STATE_DIR"
now="$STATE_DIR/admins-$(date +%Y%m%d).txt"
prev="$STATE_DIR/admins-latest.txt"
wp --path="$WP_PATH" db query \
"SELECT 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\"%'
ORDER BY u.user_login;" \
--skip-column-names --skip-plugins --skip-themes > "$now"
if [ -f "$prev" ] && ! diff -q "$prev" "$now" > /dev/null; then
diff "$prev" "$now" | mail -s "[WP] 管理者アカウントに変化あり: $WP_PATH" "$MAIL_TO"
fi
cp "$now" "$prev"
find "$STATE_DIR" -name 'admins-2*.txt' -mtime +90 -delete
# crontab -e(毎朝 7:00)
0 7 * * * /usr/local/bin/wp-admin-diff.sh
DB を直接見ているので、管理画面の一覧から隠れた管理者も差分に出ます。mail コマンドが無い環境では、Slack の Incoming Webhook など社内の通知手段に置き換えてください。
まとめ
| # | 手順 | 防ぐこと |
|---|---|---|
| ① | wp-cli と SQL の 2 経路で管理者を一覧 | 一覧から隠れた管理者の見落とし |
| ② | 持ち主と用途の台帳 | 誰のものか分からないアカウントの放置 |
| ③ | 降格 → 様子見 → --reassign で削除 |
退職者・旧委託先の鍵の残存 |
| ④ | アプリケーションパスワードの棚卸し・無効化 | ログイン対策の外側からの API 操作 |
| ⑤ |
users_can_register / default_role の確認 |
会員登録だけで管理者になる設定 |
| ⑥ | 証拠保全 → 降格 → 全員リセット → ソルト更新 | 不審アカウント削除後の再侵入 |
| ⑦ | ファイル編集停止・2 段階認証・毎日の差分通知 | 次に作られた管理者に気づけないこと |
- 脆弱性の更新は「穴をふさぐ」作業で、管理者の棚卸しは「すでに渡った鍵を回収する」作業です。両方そろって対応完了です
- 手順 1 の件数比較と手順 7 の差分通知は、プラグインを通らない DB 直接の確認なのがポイントです
- 削除は最後に。まず降格して影響を見てから消すと、投稿の引き継ぎ漏れを防げます
関連記事
本記事のような設定の抜けは、Web サイトを 9 つの守りでまるごと守るサイトドックでまとめて対策できます → https://sitedock.jp