
How to Find WordPress Malware After You've Cleaned It Up
Most WordPress cleanups happen under pressure — Google flagged the site, customers are calling, the host sent a warning. You restore a backup, change passwords, update plugins, run a scanner, and the visible problem goes away. But cleanup is not the same as done. This page is the verification process: how to confirm your cleanup actually worked, what to check that most cleanups skip, and how to know when the site is genuinely safe to declare clean.
Why cleanup isn't always done when you think it is
A successful malware cleanup, in the way most people do it, removes the visible damage: the redirect, the spam pages, the injected ads. The scanner reports green. The site loads correctly. Everything looks fine.
Two weeks later, the same site is hacked "again." Same signature, same redirect destination, same Google warning. Almost always, it wasn't a fresh attack. The original attacker left something behind — a backdoor file, a sleeper cron job, a serialised PHP payload in the database — that quietly re-installed the visible malware on a timer.
This pattern is so common that the security industry has a term for it: persistence. Modern WordPress malware is designed to survive cleanup. The first thing an attacker does after gaining access isn't installing the visible malware — it's installing the redundant backdoors that will let them rebuild after you tear it down.
If you didn't specifically hunt for persistence mechanisms during your cleanup, your cleanup isn't done. Here's how to verify.
The post-cleanup verification checklist
Work through this list in order. Each check takes between 5 and 30 minutes. None of them require buying anything.
1. File integrity check. Run `wp core verify-checksums` and `wp plugin verify-checksums --all` from SSH. Anything flagged is either a customisation you forgot about or a tampered file that survived the cleanup.
2. Recently modified files. From SSH: `find . -type f -mtime -45 -name "*.php" -not -path "./wp-content/cache/*"`. Lists every PHP file modified in the last 45 days. Cross-reference against changes you knowingly made.
3. /wp-content/uploads/ audit. Run `find wp-content/uploads -name "*.php"`. There should be zero results. Any PHP file in uploads is a backdoor.
4. mu-plugins directory. List `wp-content/mu-plugins/` and read every file. Most legitimate sites have either nothing here or one small file added by the managed host.
5. All themes' functions.php — active and inactive. Open every `functions.php` in `wp-content/themes/`. Look for `eval(`, `base64_decode(`, `gzinflate(`, `assert(`, or `preg_replace(...,/e)`. These are the standard backdoor primitives. Inactive themes are checked less often and hide longer.
6. Snippet plugins. If you have Code Snippets, WPCode, or any "add custom PHP" plugin installed, open the admin page and review every active snippet. Backdoors hide here as innocuously-named snippets like "site optimization" or "helper code."
7. wp_options malicious entries. `SELECT option_name FROM wp_options WHERE option_value LIKE '%base64_decode%' OR option_value LIKE '%eval(%';`. Any result is malicious.
8. Administrator audit. `wp user list --role=administrator`. Every account should be one you personally created. Delete anything you don't recognise.
9. wp_cron events. `wp cron event list`. Look for hooks that don't match WordPress core, your plugins, or your theme. The most common reinfection mechanism is a scheduled task that re-downloads malware every 24 hours.
The sleeper check — finding scheduled re-infectors
If your site "gets hacked again" exactly 24 hours after each cleanup, you have a sleeper — a scheduled task that re-installs the malware on a timer. There are three places sleepers hide:
WordPress cron (wp_cron). Run `wp cron event list`. Every hook should belong to a recognisable source. Hooks with random-looking names, or hooks scheduled to run very frequently (every 5 minutes, hourly), are the typical pattern. Remove with `wp cron event delete <hook_name>`.
System cron at the OS level. If you have shell access, run `crontab -l` and `cat /etc/cron.d/*` and check `/var/spool/cron/` for entries you don't recognise. Most shared hosts disable user crontab access — but not all of them. If your host gave you SSH and lets you set crons, attackers can too.
PHP auto-prepend / auto-append. Some hosting setups let you specify a PHP file that runs before every page request via the `auto_prepend_file` directive in `php.ini` or `.htaccess`. Check both for any `auto_prepend_file` or `auto_append_file` directives — they should be empty unless you intentionally set them.
The database integrity check
File-based scanners (Wordfence, Sucuri, MalCare) miss database-resident malware completely. The database is where the most sophisticated persistence hides.
wp_options: `SELECT option_name, LEFT(option_value, 200) FROM wp_options WHERE option_value LIKE '%base64_decode%' OR option_value LIKE '%eval(%' OR option_value LIKE '%gzinflate%' OR option_value LIKE '%phar://%';`
wp_postmeta: `SELECT post_id, meta_key, LEFT(meta_value, 200) FROM wp_postmeta WHERE meta_value LIKE '%<script%' OR meta_value LIKE '%base64_decode%';`
Posts content: `SELECT ID, post_title FROM wp_posts WHERE post_content LIKE '%<iframe src="http%' OR post_content LIKE '%document.write%' OR post_content LIKE '%eval(unescape%';`
Orphan tables: `SHOW TABLES LIKE 'wp_%';`. Anything beyond the standard WordPress tables and your installed plugins' tables is suspicious. Tables with random names like `wp_sk29fk2` were planted.
Take a database backup before deleting anything. Some malicious-looking entries are legitimate encoding (theme settings, serialised arrays). Decode and review before removing.
How long this takes
Realistic time estimate for full post-cleanup verification:
Best case — 1 hour. Your cleanup was actually thorough, every check comes back clean on the first pass, you finish quickly because there's nothing to investigate. Possible but not the norm.
Typical case — 3 to 5 hours. You find one or two leftover artefacts that need investigation (was that file always there? is that database entry malicious or legitimate?), spend time researching each, remove what's confirmed malicious, re-run the checks. This is what a competent verification looks like.
Worst case — 8 to 12 hours, sometimes spread over two days. You find multiple backdoors in different locations, have to investigate a sleeper that's been re-injecting malware nightly, decode several base64 payloads to understand what they do, and re-verify everything twice to confirm the cycle is broken.
Add another hour or two of "watching the site" for the next 48 hours after declaring it clean — refresh the homepage, check Search Console, verify no new files appear in `wp-content/uploads/`, confirm no new admin accounts get created.
The toolkit below runs the same 9 verification checks plus 7 more in 2 to 3 minutes per site, which removes most of the time cost — but doesn't remove the need to investigate what it finds. The investigation time is real either way.
When you can declare it really clean
You can call your site clean when all of the following are true.
Every check above returns expected results, twice. Once immediately after cleanup, again 24 to 48 hours later. The 48-hour gap is to catch sleepers that fire on a timer — if a hidden cron job was set to re-install malware nightly, the second pass catches it.
External scanners agree. Run a free scanner (GuardingWP's external scan, Sucuri SiteCheck, MalCare's free scan) and confirm no critical findings. External scanners can't see backdoors but they can see if the visible symptoms have returned.
Search Console reports no security issues. Check Google Search Console under Security & Manual Actions. If Google flagged your site originally, request a review once you're confident the cleanup is verified — Google rescans within 72 hours and removes the warning if everything checks out.
Your hosting panel reports no malware. If you're on a shared host with built-in malware scanning (most cPanel hosts have this), run their scanner. Catches things the WordPress-level scanners miss — files outside the WordPress directory, system-level changes.
The site has been quiet for 7 days. No unexpected posts, no unfamiliar admin accounts, no "site is slow" complaints, no spike in hosting bandwidth. Seven days is enough time for a sleeper-style re-installer to fire at least once. If nothing happens, the persistence mechanism is gone.
Related fix guides
Vulnerable Plugins Detected
One or more WordPress plugins has known security vulnerabilities. Learn how to find and update them.
Login Page Exposed
Your WordPress login page is publicly accessible. Learn how to protect it from brute-force attacks.
User Enumeration
WordPress is exposing your admin usernames via its REST API. Block it with one code snippet.
Sensitive Files Publicly Accessible
WordPress ships with files that reveal your site version. Learn how to block public access in minutes.
GuardingWP checks your site for the 11 most common WordPress vulnerabilities — plus scans your installed plugins against the known CVE database. Free, no account required.
Scan to confirm no entry points are left exposed →Want this whole checklist automated?
The 16-point post-cleanup checklist on this page runs in 30 seconds via the Forensic Toolkit's bash script — checksums, modified-files report, hidden-admin scan, cron audit, and DB integrity sweep, all in one zip you run on your own server. Includes the SSH walkthrough and (Agency tier) a multi-site batch wrapper.
Get the Forensic Toolkit — from $25 →Prefer to have this handled for you? Get this fixed — Full Hardening ($149) →