2026年5月20日
2026年5月20日
WordPressのバックドアを検出・削除する方法|grep・WP-CLIでの探し方と再侵入対策
バックドアは、攻撃者が後からもう一度入り込むために仕掛ける「裏口」です。改ざんされたページやスパムを消しても、バックドアが残っていると、しばらくして同じ被害が繰り返されます。この記事では、WordPress のサイトに残されたバックドアを探し、取り除き、同じ経路で入られないようにする手順を説明します。コマンドは Linux のサーバーに SSH で接続し、WP-CLI が使える環境を前提にしています。内容は2026年10月時点のものです。
始める前に:作業の順番と注意
- いまのファイルとデータベースを丸ごと保存する。削除の前に、原因の調査や、消しすぎたときの復元に使うコピーを取ります。保存したコピーは公開ディレクトリの外に置きます
- 改ざんの前のきれいなバックアップがあれば、それに戻すのが早くて確実性も高い。ただし、侵入された時期より前のバックアップでないと、バックドアごと戻ります。戻した後も、この記事の確認と再侵入対策は行います
- 見つかったファイルは中身を確かめてから消す。以下の検索は「疑わしいもの」を挙げるだけで、正規のプラグインやテーマも引っかかります
- サーバーの root 権限まで取られた疑いがある場合(不審なプロセス、システムのファイルの改ざん、見覚えのない SSH 鍵など)は、WordPress の掃除だけでは足りません。サーバーがハッキングされた時の初動対応手順 に従ってください
# WordPress のディレクトリ(例: /var/www/html)で実行
cd /var/www/html
tar czf ~/evidence-files-$(date +%Y%m%d).tar.gz .
wp db export ~/evidence-db-$(date +%Y%m%d).sql
ステップ1:コアとプラグインのファイルを公式の値と照合する
WP-CLI の verify-checksums は、WordPress.org が公開しているファイルのチェックサムと、手元のファイルを照合します。改ざんされたファイルと、本来ないはずのファイルを見つけられます。
# WordPress 本体(wp-admin と wp-includes)
wp core verify-checksums
# WordPress.org で配布されているプラグイン
wp plugin verify-checksums --all
結果の見方は次のとおりです。
| 表示 | 意味 |
|---|---|
Success: WordPress installation verifies against checksums. |
本体は改ざんされていない |
Warning: File doesn't verify against checksum: wp-includes/… |
公式のファイルと中身が違う。改ざんを疑う |
Warning: File should not exist: wp-includes/… |
公式の配布物にないファイル。バックドアがよく置かれる場所 |
有料のプラグインやテーマ、wp-content/uploads、wp-config.php は照合の対象外です。これらは次のステップで調べます。
ステップ2:不審なPHPファイルを探す
uploadsの中のPHP
wp-content/uploads は画像などを置く場所で、通常は PHP ファイルは不要です。ここにある PHP は最優先で確かめます。
find wp-content/uploads -type f \( -name '*.php' -o -name '*.phtml' -o -name '*.phar' \)
一部のプラグインは、ディレクトリの一覧表示を防ぐための空の index.php などを置きます。中身が <?php // Silence is golden. のような短いものは、たいてい正規のファイルです。
最近変わったPHPファイル
find . -type f -name '*.php' -ctime -14 -printf '%CY-%Cm-%Cd %CH:%CM %p\n' | sort
-ctime -14 は「14日以内に属性が変わった」ファイルです。攻撃者はファイルの更新日時(mtime)を古い日付に書き換えることがありますが、ctime は通常の方法では戻せません。プラグインの更新でも変わるため、更新した日と照らし合わせます。
よく使われるコードの形で探す
# 難読化したコードを実行する形(base64 などで隠したコードを eval で実行)
grep -rlE --include='*.php' 'eval\s*\(\s*(base64_decode|gzinflate|gzuncompress|str_rot13)\s*\(' .
# 外から送られた値をそのまま実行する形(Web シェルの典型)
grep -rlE --include='*.php' '(eval|assert|system|exec|shell_exec|passthru|popen|proc_open)\s*\(\s*\$_(POST|GET|REQUEST|COOKIE|SERVER)' .
# 公開ディレクトリにある PHP 以外の拡張子の実行ファイル、ドットで始まる PHP
find . -type f \( -name '*.phtml' -o -name '*.phar' -o -name '.*.php' \)
これらは典型的な形に当てはまるものを挙げるだけで、正規のプラグインが当てはまることも、難読化を工夫したバックドアが当てはまらないこともあります。見つかったファイルは中身を開き、次の点を確かめます。
- 読めない長い文字列(base64 など)を、デコードして実行していないか
- どのプラグイン・テーマにも属さない場所にないか、名前が正規のファイルに似せていないか(
wp-config-sample.phpに似た名前など) - 同じ名前のファイルが、公式の配布物にあるか
読み込みの入り口になる場所
バックドア本体を読み込むための入り口も確かめます。
# 有効化しなくても読み込まれるプラグイン(mu-plugins)とドロップイン
wp plugin list --status=must-use
wp plugin list --status=dropin
ls -la wp-content/mu-plugins/
# PHP の設定で、すべてのリクエストの前に別のファイルを読み込ませていないか
find . -name '.user.ini' -o -name 'php.ini'
grep -rn 'auto_prepend_file\|auto_append_file' . --include='.user.ini' --include='php.ini' --include='.htaccess'
wp-config.php と、すべての .htaccess も開いて、見覚えのない include・require や転送の設定がないかを確かめます。
ステップ3:アクセスログからWebシェルを探す
Web シェルは POST リクエストで操作されることが多いため、POST を受けているファイルを数えます。
# Apache の場合(Nginx は /var/log/nginx/access.log)
grep '"POST ' /var/log/apache2/access.log \
| grep -vE 'wp-login\.php|wp-admin/admin-ajax\.php|wp-cron\.php|xmlrpc\.php|wp-json' \
| awk '{print $7}' | sort | uniq -c | sort -rn | head -20
wp-content/uploads/ の下や、プラグインの奥にある見慣れない PHP への POST が並んでいれば、そのファイルを調べます。同じ IP アドレスがほかに何にアクセスしたかも、ログから追えます。
ステップ4:データベースとユーザーを確かめる
# 管理者の一覧(覚えのないユーザー、最近登録されたユーザーがいないか)
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
# 登録されている cron の処理(見覚えのないフック名がないか)
wp cron event list --fields=hook,next_run_relative,recurrence
# options テーブルに実行用のコードが入っていないか
wp db query "SELECT option_name, LEFT(option_value, 120) FROM $(wp db prefix)options WHERE option_value LIKE '%base64_decode%' OR option_value LIKE '%eval(%' LIMIT 20"
テーブル名の先頭(wp_ など)はサイトによって違うため、wp db prefix で取り出しています。正規のプラグインが options に暗号化した値やコードの断片を保存していることもあるため、option_name からどのプラグインのものかを確かめてから判断します。
覚えのない管理者は、その投稿を自分のユーザーに移してから削除します(7 は削除するユーザーの ID、1 は投稿を引き継ぐユーザーの ID の例です)。
wp user delete 7 --reassign=1
ステップ5:取り除いて、入れ直す
バックドアのファイルを削除する
調べて確かめたバックドアのファイルを削除します。入り口(mu-plugins のファイル、.user.ini、.htaccess への追記)も忘れずに消します。
WordPress本体を上書きする
WordPress 本体を公式のファイルで上書きします。--skip-content を付けると wp-content には触れません。日本語版を使っている場合は --locale=ja も付けます。
wp core download --force --skip-content --version=$(wp core version)
wp core verify-checksums
上書きでは余分なファイルは消えないため、File should not exist と表示されたファイルは手で削除します。
プラグインとテーマを入れ直す
使っていないプラグインとテーマは削除し、残すものは公式の配布元から入れ直します。WordPress.org のプラグインは wp plugin install プラグイン名 --force で上書きできます。有料のものは、販売元から最新版をダウンロードして入れ直します。
もう一度調べる
ステップ1〜4をもう一度実行し、何も見つからないことを確かめます。
ステップ6:uploadsでPHPを実行させない
アップロード用のディレクトリに PHP を置かれても実行されないようにします。
Apache の場合は、wp-content/uploads/.htaccess に次を書きます。
<FilesMatch "\.(php|phtml|phar)$">
Require all denied
</FilesMatch>
Order Deny,Allow と Deny from all は Apache 2.2 の書き方です。Apache 2.4 では Require all denied を使います。サーバーによっては .htaccess で Require を使えず 500 エラーになるため、書いた後にサイトの画像が表示されることと、uploads の中の PHP の URL が 403 になることを確かめます。
Nginx の場合は、PHP を処理する location ~ \.php$ より前に書きます。正規表現の location は上から順に評価されるためです。
location ~* ^/wp-content/uploads/.*\.(php|phtml|phar)$ {
deny all;
}
WordPress は初期状態では、PHP ファイルをメディアとしてアップロードできません。この設定は、プラグインの脆弱性などで別の経路から置かれた場合に備えるものです。
ステップ7:鍵とパスワードを変える
攻撃者がパスワードやログイン中のセッションを持っている前提で、すべて変えます。
# wp-config.php の認証用キー(ソルト)を作り直す。全員のログイン状態が無効になる
wp config shuffle-salts
# 管理者を含む全ユーザーのセッションを破棄する
wp user list --field=ID | xargs -n 1 wp user session destroy --all
あわせて、次も変えます。
- 管理者全員の WordPress のパスワード(「ユーザー」→ 各ユーザーの編集画面で変更)
- データベースのパスワード(変えたら
wp-config.phpのDB_PASSWORDも直す) - サーバーの管理画面、FTP・SFTP、SSH のパスワード。
~/.ssh/authorized_keysに見覚えのない鍵がないかも確かめる - サーバーの cron(
crontab -l、/etc/cron.d/)に見覚えのない処理がないか
最後に、WordPress 本体・プラグイン・テーマをすべて最新にします。侵入の入り口は、古いプラグインの脆弱性や漏れたパスワードであることが多いため、入り口をふさがないと同じことが起こります。
まとめ
- 削除の前に、ファイルとデータベースを丸ごと保存する。きれいなバックアップがあれば戻すのが早い
wp core verify-checksumsとwp plugin verify-checksums --allで、公式と違うファイル・余分なファイルを見つけるuploadsの PHP、最近ctimeが変わった PHP、eval(base64_decode(や$_POSTを実行する形を探し、中身を確かめる- mu-plugins、
.user.iniのauto_prepend_file、.htaccess、wp-config.phpの読み込みの入り口も確かめる - 管理者・cron・options を確かめ、不要な管理者を削除する
- 本体は
wp core download --force --skip-contentで上書きし、プラグインは公式から入れ直す uploadsでの PHP の実行を止め、wp config shuffle-salts・セッションの破棄・すべてのパスワードの変更で締め出す
関連記事: