
If you have ever cleaned up a hacked WordPress site only to see it reinfected days later, you already know the uncomfortable truth: removing the obvious malware is easy. Finding every backdoor an attacker left behind is the hard part. Backdoors are designed to survive plugin updates, theme reinstalls, and even a fresh WordPress core. This guide walks through the four places they hide most often and gives you concrete commands to hunt them down.
Why a Reinstall Alone Does Not Fix It
Reinstalling WordPress core replaces the files that ship with WordPress itself, but it does nothing to your plugins, your theme, your uploads folder, or your database. Most persistent backdoors live in exactly those untouched places. An attacker who gets code execution once will typically plant several redundant footholds, so that fixing one still leaves two or three others quietly waiting. Treat backdoor removal as an investigation, not a single cleanup step.
Rogue Admin Users
The simplest backdoor is also the easiest to miss: a second administrator account hiding among your real users. Attackers often give it a name close to a legitimate one, or reuse a generic username like support or wp-admin2.
Check every account and its role directly:
wp user list --fields=ID,user_login,user_email,roles
wp user list --role=administratorLook for accounts you do not recognize, accounts created around the time of the compromise, and email addresses that do not match your team. If you find one, do not just delete it. Check wp usermeta for that user ID first, since some backdoors also store secondary credentials or API keys in meta fields tied to a legitimate-looking account.
wp user meta list <user_id>
wp user delete <user_id> --reassign=1Rotate passwords for every remaining administrator afterward, since the original entry point (a weak password, a leaked credential, or a vulnerable plugin) is what let the rogue account get created in the first place.
Injected Files
File-based backdoors range from a single obfuscated PHP function dropped into a real plugin file, to entirely new files disguised with innocuous names like class-cache.php sitting somewhere it should not be, most commonly inside wp-content/uploads, which should never contain executable PHP at all.
Start by comparing your WordPress core files against known-good checksums:
wp core verify-checksumsFor plugins and themes, wp-cli does not verify checksums the same way core does, so compare against a clean copy downloaded fresh from the plugin repository or your license vendor. A quick way to surface suspicious files is to search for common obfuscation patterns and to hunt for PHP files where they should not exist:
grep -rl "eval(base64_decode" wp-content/
grep -rl "gzinflate" wp-content/
find wp-content/uploads -name "*.php"Also check for recently modified files across the whole install, since a fresh timestamp on a file you have not touched is a strong signal:
find . -type f -mtime -14 -name "*.php"Not every match is malicious (some plugins legitimately use compression functions), but any hit inside uploads, in a theme’s root directory, or in a plugin folder you did not expect deserves a manual look.
Database Hooks
File scanners miss backdoors stored entirely in the database, which is one reason database-only injections are so persistent. Common hiding spots include the wp_options table (malicious code stashed in active_plugins, template, stylesheet, or widget and theme mod entries), and the wp_posts table, where malicious code gets embedded inside a post or page as a disguised shortcode that executes on render.
Pull suspicious option values directly:
wp option get active_plugins
wp option get template
wp option get stylesheetIf any of these point to a plugin or theme slug you do not recognize, that is your lead. Search the options table broadly for the same obfuscation signatures you looked for in files:
wp db query "SELECT option_name FROM wp_options WHERE option_value LIKE '%eval(%' OR option_value LIKE '%base64_decode%'"Widgets deserve special attention, since a text or custom HTML widget is a common place to store a payload that only executes on the front end, making it invisible in the admin dashboard unless you go looking for it.
Cron Implants
WordPress’s own cron system is a favorite place to plant a backdoor, because a scheduled event can silently re-download malware, recreate a deleted admin user, or reinject a cleaned file on a recurring basis, long after you think you have cleaned the site.
List every scheduled event and read the hook names carefully:
wp cron event listAnything unfamiliar, especially a hook name that does not match a plugin you have installed, is worth investigating before you delete it. Once you have identified the malicious plugin or code that registered the event, remove the event and the source together:
wp cron event delete <hook_name>Deleting the cron event without removing the code that re-registers it accomplishes nothing, since the next page load can simply schedule it again.
A Practical Hunting Workflow
- Put the site in maintenance mode and take a full backup, even of the infected state, in case you need to reference it later.
- Run
wp core verify-checksumsand compare plugins and themes against clean copies. - Audit users and their meta with
wp user listandwp user meta list. - Search the database for obfuscated code in options and post content.
- Review every scheduled event with
wp cron event list. - Rotate all passwords, API keys, and secret keys (
wp config shuffle-saltsfor the WordPress salts) once every foothold is removed. - Update WordPress core, all plugins, and your theme, since an unpatched vulnerability is almost always how the attacker got in to begin with.
Reducing the Chance of a Repeat
Once the site is clean, a few structural changes make future backdoors harder to plant and easier to spot. Disable PHP execution inside the uploads directory at the web server level. Keep plugins and themes to a minimal, well-maintained set rather than a large collection of rarely-updated ones. Put Cloudflare in front of the origin with the origin IP unpublished, since a WAF and bot mitigation layer stop a meaningful share of the exploit attempts that lead to backdoors in the first place. A managed host that keeps PHP current, applies server-level hardening, and monitors for suspicious file changes (which is part of how ServerBorn approaches this) removes a layer of risk that a lone site owner would otherwise have to manage by hand.
Takeaway
Backdoors persist because they are built with redundancy in mind: a rogue user, an injected file, a database hook, and a cron implant working together, so that removing one still leaves the others intact. Hunt all four systematically, verify with wp-cli rather than assuming a plugin scan caught everything, and treat the underlying vulnerability, not just the payload, as the thing you actually need to fix.