What a core reinstall actually replaces
wp-admin and wp-includes directories plus the PHP files in the site root, such as wp-login.php, wp-settings.php, and index.php. Reinstalling from the dashboard or running wp core download --force overwrites those known files with clean copies.
Two details explain why that falls short.
Overwriting is not the same as cleaning. The reinstall writes the files that belong to the package. It does not empty the directories first. A backdoor dropped as an extra file, such as a randomly named PHP file inside wp-includes, survives because no file in the package has that name. It keeps working because the web server will still execute it when someone requests its URL.
Several locations are excluded on purpose so a reinstall doesn't destroy the site: wp-config.php, the whole wp-content directory, .htaccess, and the database. That is the right design for routine updates. It also happens to list exactly where persistent malware tends to sit.
At the support desk, this is the part that trips people up most. The official reinstall ran and the core files look clean, so the owner concludes the server or the host must be reinfecting the site. More often the infection never left their own account.
Where the backdoors survive
wp-content are the obvious place. Attackers either modify files inside a legitimate plugin or add a fake plugin folder with a plausible name. A theme's functions.php is another favorite because it runs on every page load. Deactivating a plugin or switching themes doesn't make its files harmless. A PHP file inside an inactive plugin or theme can usually still be requested directly over HTTP. A webshell (a PHP script that lets the attacker run commands through the browser) doesn't need WordPress to load it.
The uploads directory, wp-content/uploads, is meant for media. A PHP file there deserves a close look. Some plugins do write legitimate PHP files into uploads, so check what owns a file before deleting it. Uploads is also one of the few directories the web server can always write to, which is why attackers like it.
Must-use plugins live in wp-content/mu-plugins. WordPress loads every top-level PHP file in that folder automatically on every request. They appear under a separate Must-Use tab rather than the main plugin list, and they can't be deactivated from the dashboard. That makes the folder an ideal hiding spot, and many site owners have never opened it.
Drop-ins are special files placed directly in wp-content, such as advanced-cache.php, object-cache.php, and db.php. Core loads them very early, before regular plugins, under conditions that depend on the file. A tampered drop-in runs before any security plugin has a chance to react.
Configuration files sit outside core entirely. An extra include line in wp-config.php can pull in a payload stored somewhere unrelated. Rewrite rules in .htaccess can send search engine or mobile visitors to spam. A .user.ini or php.ini with auto_prepend_file makes PHP run a chosen file before every script on the site. If injected redirects are part of the picture, auditing .htaccess files for injected redirects covers that side in more depth.
The database is where many people stop looking, and a file-level reinstall can't touch it at all. Common findings include:
- Administrator accounts nobody created
- Script tags injected into posts or widget content
- A changed
siteurlorhomevalue - Scheduled cron events that call attacker code
- Encoded payloads stored in
wp_options
Why the hack comes back even after a careful cleanup
Checking the places a reinstall skips
BASH
# Compare core files with official checksums
# (newer WP-CLI versions also flag extra files in core directories)
wp core verify-checksums
# Compare wordpress.org plugins with their published checksums
wp plugin verify-checksums --allBASH
# PHP files in uploads are usually suspicious
find wp-content/uploads -type f -name '*.php'
# Must-use plugins and drop-ins load without appearing in the normal plugin list
ls -la wp-content/mu-plugins/
ls -la wp-content/ | grep -E 'advanced-cache|object-cache|db\.php'
# Per-directory PHP config that prepends a file to every request
grep -rn 'auto_prepend_file' --include='.user.ini' --include='php.ini' --include='.htaccess' .wp-config.php still loads either way.
BASH
# Administrator accounts: look for ones nobody recognizes
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered --skip-plugins --skip-themes
# Scheduled events: look for hooks that no installed plugin explains
wp cron event list --skip-plugins --skip-themes
# Largest autoloaded options: unexpected large blobs deserve a look
wp option list --autoload=on --fields=option_name,size_bytes --orderby=size_bytes --order=desc --skip-plugins --skip-themes | head -n 20A cleanup pattern that holds up in production
- Keep the isolated copy of the infected site from the previous step for comparison and log review.
- Find and close the entry point. Update or remove vulnerable plugins and themes, and check access logs for requests to unfamiliar PHP files.
- Replace core, every plugin, and every theme with fresh copies from their original sources instead of editing infected files in place. Delete plugins and themes you don't use.
- Inspect what can't simply be replaced: uploads,
mu-plugins, drop-ins,wp-config.php,.htaccess, and any.user.ini. - Clean the database: remove unknown admin accounts, fix injected content, and delete cron events and options that no legitimate plugin owns.
- Rotate every credential: WordPress passwords, the database password, SFTP and control panel logins, and API keys. Also regenerate the salts in
wp-config.php, which logs out every existing session. - Clear OPcache or restart PHP, depending on your stack, so no stale compiled code keeps running.
- Check the same locations again over the following days. A file that reappears points to persistence you haven't found yet.
wp-admin, wp-includes, and the root core files are clean. It says nothing about wp-content, the configuration files, or the database. Check those locations, replace what you can from clean sources, close the entry point, and rotate credentials. Doing all of that is what keeps the site clean.