2026年11月12日
2026年11月12日
Find and Remove a WordPress Backdoor: WP-CLI Checksums and Hardening
This article is a localized edition of the Japanese original. Read the Japanese original
A backdoor is a file or piece of code an attacker leaves behind after breaking in, so they can come back whenever they want. If you only delete the visible spam pages or redirects and the backdoor stays, the same hack usually returns within days. This article assumes you can use WP-CLI (a VPS or hosting account with SSH access). It covers where to look for backdoors and, once they are removed, how to close the entry point and replace credentials.
Signs that a backdoor may be present
If you see any of the following, look for a backdoor instead of just fixing the visible symptoms.
- Spam pages or suspicious redirects you deleted come back after a while
- There is an administrator account nobody remembers creating
- There are PHP files inside
wp-content/uploads - Only visitors arriving from search engines are redirected to another site
- Unknown plugins, themes, or files in
wp-content/mu-pluginshave appeared - Your hosting company notified you about malicious files or mass email sending
Preserve the current state before you start
Before investigating, copy all files and the database. This copy is still infected, so treat it as material for working out what happened later, not as a restore point, and keep it outside the public web directory.
cd /var/www/html
# Show visitors a maintenance page (limits damage while you work)
wp maintenance-mode activate
# Save the database and files (outside the public directory)
mkdir -p ~/incident
wp db export ~/incident/db-$(date +%Y%m%d).sql
tar czf ~/incident/files-$(date +%Y%m%d).tar.gz -C /var/www html
Copy the web server access logs too, before log rotation deletes them. The location depends on the server: /var/log/apache2/ for Apache on Debian/Ubuntu and /var/log/nginx/ for Nginx are common. Many shared hosts let you download logs from their control panel, so check your host's documentation.
Verify core files against the official checksums
WordPress core files can be checked against the checksums published by WordPress.org.
wp core verify-checksums --include-root
Modified files are reported with the warning File doesn't verify against checksum, and files that should not be there with File should not exist. With --include-root, you also get warnings about files and folders in the WordPress root directory that are not part of WordPress core. Unfamiliar PHP files hidden inside wp-admin or wp-includes are a classic backdoor location.
If you run the Japanese edition of WordPress and get warnings caused by the language difference, verify with --locale=ja.
Verify plugins and themes
Plugins distributed on WordPress.org can be checked against checksums the same way.
wp plugin verify-checksums --all
This only works for plugins whose checksums WordPress.org publishes. Premium and custom plugins can't be verified this way, so download them again from the vendor and replace them. There is no checksum command for themes. Replace themes with files from the original source, or compare them with a known-good copy.
Delete plugins and themes you don't use, which also shrinks what you have to investigate.
wp plugin list --fields=name,status,version
wp theme list --fields=name,status,version
Check the places where backdoors are often hidden
Anything checksums can't cover has to be checked by eye.
PHP files in the uploads directory
wp-content/uploads holds images and other media and normally contains no PHP files. Open and inspect anything you find.
find wp-content/uploads -type f \( -name '*.php' -o -name '*.phtml' -o -name '*.phar' \)
mu-plugins and drop-ins
PHP files in wp-content/mu-plugins load automatically and can't be deactivated from the Plugins screen. The same is true of drop-ins such as advanced-cache.php and object-cache.php directly under wp-content.
wp plugin list --status=must-use
wp plugin list --status=dropin
ls -la wp-content/mu-plugins/
Caching plugins and hosting companies also install files here, so confirm one by one which ones are legitimate.
wp-config.php, .htaccess, and .user.ini
Check whether an unfamiliar include or a long string has been added at the top or bottom of wp-config.php. In .htaccess and .user.ini, look for auto_prepend_file (a setting that loads a given file before every PHP script) and for rewrite rules that send visitors to external sites.
find . -name '.htaccess' -o -name '.user.ini' | xargs grep -n -i -E 'auto_prepend_file|auto_append_file|RewriteRule'
Search for suspicious code
Obfuscated backdoors often rely on functions such as eval and base64_decode and on extremely long single-line strings.
# PHP files containing functions commonly used for obfuscation
grep -rlE --include='*.php' '(eval|assert|base64_decode|gzinflate|str_rot13)[[:space:]]*\(' wp-content
# PHP files containing lines longer than 3000 characters
grep -rlE --include='*.php' '^.{3000,}' wp-content
# PHP files modified in the last 14 days
find . -type f -name '*.php' -mtime -14 -printf '%TY-%Tm-%Td %TH:%TM %p\n' | sort
Legitimate plugins also use base64_decode and similar functions, so not every match is malicious. On the other hand, attackers sometimes fake file modification times, so don't treat a file as safe based on its timestamp alone. Open the files and start with the ones whose style clearly differs from the surrounding code, or that consist mostly of meaningless variable names and long strings.
Check the database and scheduled tasks
Even after you delete the files, an administrator account or a scheduled task left in the database can let the attacker back in.
# List administrators (check the registration dates too)
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
# Scheduled tasks (look for unfamiliar hook names)
wp cron event list --fields=hook,next_run_relative,recurrence
# Scripts embedded in posts or options
wp db search '<script' --all-tables-with-prefix
The wp db search results also include legitimate items such as embed codes. Focus on places that load scripts from external domains and on long code stored in options.
Find the entry point in the access logs
Backdoors are usually called from outside with POST requests, so look for POST requests to files WordPress does not normally accept them on. This example assumes the standard Apache or Nginx log format, where the 7th field is the request path.
grep '"POST ' /var/log/nginx/access.log \
| grep -v -E 'wp-login\.php|admin-ajax\.php|wp-cron\.php|xmlrpc\.php|/wp-json/' \
| awk '{print $7}' | sort | uniq -c | sort -rn | head -30
Cross-check the file names you find here with your earlier results. The time a file was first requested can sometimes tell you which plugin vulnerability or which account was the way in.
Remove the backdoor
Remove what you found and overwrite everything with official packages.
# Overwrite WordPress core with official files of the same version (wp-content is not touched)
wp core download --skip-content --force --version=$(wp core version)
# Reinstall a plugin from WordPress.org (example: contact-form-7)
wp plugin install contact-form-7 --force
# Delete an unknown administrator and reassign their posts to another user (use the IDs from your investigation)
wp user delete 15 --reassign=1
If you use the Japanese edition of WordPress, add --locale=ja. wp core download --force overwrites files but does not delete extra ones. Delete the files reported as File should not exist by verify-checksums, and the PHP files in uploads, by hand. When you reinstall plugins, use the latest versions with the vulnerability fixed.
After the cleanup, verify again.
wp core verify-checksums --include-root
wp plugin verify-checksums --all
If you have a backup from before the intrusion that you know is clean, restoring it may be faster. Even then, the vulnerability the attacker used is still there, so you can't skip the next section.
Replace credentials
Assume the attacker has stolen passwords and the contents of your configuration files, and change all of them.
# Regenerate the authentication keys and salts (logs out every user)
wp config shuffle-salts
# Reset every administrator's password and notify them by email
wp user reset-password $(wp user list --role=administrator --field=ID)
Change the credentials outside WordPress as well.
- The database password (update
DB_PASSWORDinwp-config.phpto match) - SSH, SFTP, and FTP passwords and the list of authorized public keys
- The hosting control panel password
- API keys for external services stored in
wp-config.phpor plugin settings
Settings that prevent reinfection
Don’t let PHP run in uploads
If PHP placed in uploads can't run, backdoors dropped through uploads won't work. On Apache 2.4, put the following in wp-content/uploads/.htaccess (the server's AllowOverride must permit it).
<FilesMatch "\.(php|phtml|phar)$">
Require all denied
</FilesMatch>
On Nginx, place the following block before the location ~ \.php$ block in your server configuration, because regex location blocks are evaluated in the order they appear.
location ~* ^/wp-content/uploads/.*\.(php|phtml|phar)$ {
deny all;
}
Disable file editing in the admin
This keeps anyone who takes over an administrator account from rewriting theme and plugin files in the admin. Add it to wp-config.php above the "That's all, stop editing!" line.
define( 'DISALLOW_FILE_EDIT', true );
Reset permissions to the standard values
The baseline is 755 for directories and 644 for files. Running chmod 755 recursively also makes files executable, so set directories and files separately.
find /var/www/html -type d -exec chmod 755 {} +
find /var/www/html -type f -exec chmod 644 {} +
Verify on a schedule
Checksum verification takes only seconds, so run it daily with cron and get an email when there is a warning. That way you notice the next modification early.
# Example entry for crontab -e (daily at 4:00)
0 4 * * * cd /var/www/html && wp core verify-checksums --include-root 2>&1 | grep -q Warning && echo "checksum warning" | mail -s "WP checksum" admin@example.com
Whether the mail command is available depends on your server setup.
Checks after the cleanup
- Turn off the maintenance page with
wp maintenance-mode deactivateand check the front end and the admin - If the Security issues report in Google Search Console shows a warning, describe what you fixed and request a review
- Keep monitoring access logs and file changes for several days to a few weeks
Summary
- If a backdoor remains, the hack comes back even after you clean the visible damage. Save files, the database, and logs before investigating
- Use
wp core verify-checksums --include-rootandwp plugin verify-checksums --allto find differences from the official packages - Inspect PHP in uploads, mu-plugins, drop-ins, and
auto_prepend_filein.htaccessand.user.iniby eye - Also check administrator accounts and scheduled tasks in the database, and POST requests in the access logs
- Clean up by overwriting with official files and deleting extra files, then change every salt and password
- Prevent reinfection by blocking PHP in uploads, setting
DISALLOW_FILE_EDIT, using 755/644, and verifying regularly
Related articles:
- How to detect and remove malware in WordPress
- Block PHP execution in the WordPress uploads directory
- Disable the WordPress file editor
- Regenerate WordPress security keys
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)