This is the long portage: heavy for a stretch, then it opens up. Take it in order and don’t skip steps — the number one cause of reinfection is a cleanup that removed the malware but left the backdoor. You’ll need SSH access, WP-CLI set up, and a recent backup you trust. If you’re still in the first panicked hour, read the first-steps essay first, then come back here for the full job.
Contain first: passwords and a holding page
Before hunting anything, stop the bleeding. Change the hosting, SFTP, and database passwords, and reset every admin password:
wp user list --role=administrator --fields=ID,user_login,user_email
wp user update ADMIN_ID --user_pass="$(openssl rand -base64 18)"
Look hard at that admin list — an account you don’t recognize is the attacker’s, and it gets deleted, not just demoted. If the site is serving spam or malware to visitors, put up a maintenance page at the server level while you work.
Preserve the evidence
Take a full copy of the site as it stands — files and database — before you delete a single thing:
tar -czf ../evidence-$(date +%Y-%m-%d).tar.gz .
wp db export ../evidence-$(date +%Y-%m-%d).sql
This feels backwards. It isn’t. The infected copy tells you how they got in, and if you delete something the site needed, it’s your undo button.
Verify core and plugin checksums
Let WordPress.org tell you which files were modified:
wp core verify-checksums
wp plugin verify-checksums --all
Every file that fails is either infected or added. Reinstall core cleanly rather than editing infected files by hand:
wp core download --force --version=$(wp core version)
Then reinstall each flagged plugin from the repository the same way, with
wp plugin install PLUGIN --force. Note that premium plugins can’t be
checksummed — re-download those from the vendor.
Hunt the backdoors
Checksums don’t cover uploads, mu-plugins, or extra files the attacker dropped. PHP has no business living in your uploads folder:
find wp-content/uploads -name "*.php"
grep -rl "eval(base64_decode\|gzinflate(base64_decode\|str_rot13" wp-content/ --include="*.php"
Also list files changed in the last month (find . -name "*.php" -mtime -30)
and read wp-config.php, the active theme’s functions.php, and everything
in mu-plugins/ line by line. Anything obfuscated, anything you can’t
explain, goes. Expect to find two or three backdoors — attackers plant
spares.
Close the way in
A clean site with the original hole is a clean site for about a week. The usual doors, in order of likelihood: a vulnerable plugin or theme (update everything, remove anything abandoned), a stolen password, or a neighboring site on the same account. Your server’s access logs — see reading your error logs — often show the first malicious POST request, which names the entry point exactly.
Rotate the salts and every credential
Stolen cookies survive password changes; new salts kill every session on the site, including the attacker’s:
wp config shuffle-salts
Then rotate what’s left: the database password (in both MySQL and
wp-config.php), API keys, SMTP credentials, and any reused passwords
elsewhere. Assume everything in wp-config.php was read.
Request blocklist review and watch the logs
If Google flagged the site, open Search Console’s Security Issues report, confirm each issue is fixed, and request a review — it typically clears in a few days. Then verify your work: checksums pass clean, the uploads find comes back empty, no unexplained admins, and the site behaves logged-out in a private window. Watch the access logs daily for two weeks; a returning attacker probing their dead backdoor is loud and obvious. When it stays quiet, spend the next hour on hardening so you never run this tutorial twice.