You’ve run the scanner. It says clean. Yet the site keeps throwing 500 errors, redirecting to spam, or getting flagged by Google Safe Browsing again. This is where the standard "delete and restore" workflow fails. The malware isn't just in a file; it's woven into the execution flow, the database, or the server configuration itself.
If you are reading this, you likely have root or sudo access. That is your advantage. We are skipping the "install a plugin" advice. Instead, we are looking at the filesystem, the process table, and the web server logs to find what the scanners miss. These eight moves cover the deep layers where persistent infections hide.
1. Isolate the Execution Environment
Before touching a single file, you need to stop the bleeding. If the malware is active, it will rewrite files you are trying to clean.
Put the site in maintenance mode immediately. Do not rely on a plugin for this if you suspect the plugin directory is compromised. Create a maintenance.html file in the web root.
# .htaccess snippet for Apache
RewriteEngine On
RewriteBase /
RewriteCond %{REMOTE_ADDR} !^192\.168\. [OR]
RewriteCond %{REMOTE_ADDR} !^10\.
RewriteCond %{REQUEST_URI} !^/wp-login.php [NC]
RewriteCond %{REQUEST_URI} !^/wp-admin [NC]
RewriteCond %{REQUEST_URI} !^/wp-includes [NC]
RewriteRule . - [F]
This blocks all traffic except your IP and critical WordPress paths. If you use Nginx, create a simple location / block that returns a 503 status. This prevents the malware from executing while you investigate. It also stops the 500 errors from propagating to users.
2. Audit the Process Table for Anomalies
Malware often runs as a background process to maintain persistence or mine crypto. A standard top or htop view is not enough. You need to see the full command line and the user context.
Run this command to list all processes with their full arguments:
ps auxf --forest
Look for PHP processes that are not tied to a specific web request. Legitimate PHP-FPM processes usually have short lifespans. A php process running as root or www-data that has been alive for hours is a red flag. Check the working directory of these processes.
ls -l /proc/<PID>/cwd
If a PHP process is running from /tmp or /var/www/html/.hidden_dir, you have found the loader. Kill the process, but do not delete the file yet. You need to analyze it first.
3. Check for Obfuscated PHP in Unlikely Locations
Scanners look for known signatures. They often miss obfuscated code that uses base64_decode, gzinflate, or eval in files that should not contain executable PHP.
Search for these dangerous functions across your web root. Exclude the wp-includes and wp-content/plugins directories if you are confident they are clean, to speed up the search.
grep -ri "base64_decode" /var/www/html --include="*.php" | grep -v "wp-includes" | grep -v "wp-content/plugins"
Also check for short tags. Malware often uses <? instead of <?php to bypass simple filters.
grep -ri "<?" /var/www/html --include="*.php" | grep -v "<?php"
If you find a file in /wp-content/uploads/ that contains PHP code, delete it. The uploads directory should only contain static assets. Any .php file there is malicious by definition.
4. Inspect Cron Jobs and System Timers
This is the most common reason for "clean" sites getting infected again. The malware drops a cron job that re-downloads the payload every hour.
Check the user-level crontab for the web user (usually www-data, apache, or nobody).
crontab -l -u www-data
Also check the system-wide cron directories. Malware often places scripts in /etc/cron.d/ or /etc/cron.daily/.
ls -la /etc/cron.d/
ls -la /etc/cron.daily/
Look for entries that call curl, wget, or php with a URL argument. These are direct downloaders. Remove any suspicious entries. If you find a script referenced in a cron job, inspect it before deleting it to understand the infection vector.
5. Database Integrity Check
Filesystem cleaning is not enough if the database is compromised. Malware often injects code into the wp_options table, specifically in the template or theme options, or into custom post content.
Connect to your MySQL/MariaDB instance.
mysql -u root -p
Search for common malicious strings in the options table.
SELECT option_name, option_value FROM wp_options WHERE option_value LIKE '%base64_decode%' OR option_value LIKE '%eval%';
If you find results, manually edit the option_value to remove the malicious code. Be careful not to break the legitimate theme configuration. If the infection is widespread, consider restoring the database from a known clean backup taken before the infection started.
Also check the wp_users table for unauthorized admin accounts.
SELECT user_login, user_email FROM wp_users WHERE user_level = 10;
Remove any accounts you do not recognize.
6. Web Server Log Analysis
Your access logs are a timeline of the attack. They show you how the malware got in.
Look for POST requests to wp-login.php that resulted in a 302 or 301 redirect. These are often brute-force attempts or credential stuffing.
grep "POST /wp-login.php" /var/log/nginx/access.log | grep -E "30[12]"
Identify the IP addresses. If you see a high volume of requests from a single IP, block it at the firewall level.
Also look for requests to unusual file paths. Malware often probes for wp-config.php, .env, or xmlrpc.php.
grep "wp-config.php" /var/log/nginx/access.log
grep "xmlrpc.php" /var/log/nginx/access.log
If you see repeated requests to xmlrpc.php, disable it in your web server configuration. It is not needed for most modern WordPress sites and is a common vector for brute-force attacks.
7. File Permission Hardening
Malware exploits loose file permissions to write files it shouldn't. Ensure your web server user cannot write to directories it does not need to.
Set the ownership of your web root to your system user, not www-data.
chown -R youruser:yourgroup /var/www/html
Set directory permissions to 755 and file permissions to 644.
find /var/www/html -type d -exec chmod 755 {} \;
find /var/www/html -type f -exec chmod 644 {} \;
The wp-config.php file should be 400 or 440.
chmod 400 /var/www/html/wp-config.php
This prevents the web server from reading the database credentials if a PHP file is compromised. It also prevents the web server from writing to the file.
8. Implement a Web Application Firewall (WAF) at the Server Level
Plugins are part of the application stack. If the application is compromised, the plugin is compromised. A WAF at the Nginx or Apache level operates outside the WordPress codebase.
If you use Nginx, consider adding a basic WAF module or using a reverse proxy like HAProxy with WAF rules. If you use Apache, the ModSecurity plugin is a good starting point.
Configure rules to block common attack patterns. For example, block requests containing ../ in the URI to prevent directory traversal.
SecRule REQUEST_URI "\.\./" "id:'1000',phase:1,deny,status:403,msg:'Directory Traversal Attempt'"
This adds a layer of defense that malware cannot easily bypass because it operates at the HTTP request level, before the request reaches PHP.
How do I know if the malware is back?
Set up file integrity monitoring. Use a tool like aide or wazuh to monitor your web root for changes. If a file is modified outside of your deployment process, you will get an alert.
What if the malware is in the database but not in the files?
This is possible if the malware is injected via a plugin that executes SQL queries. In this case, the database is the primary payload. You must clean the database and disable the compromised plugin.
Should I change my database password?
Yes. If the malware has access to wp-config.php, it has your database password. Change it immediately. Update the password in wp-config.php and in your MySQL user table.
What to Check First
Start with the process table. If you see a suspicious PHP process, kill it. Then check the cron jobs. These two steps stop the active infection. After that, clean the filesystem and database. Finally, harden the permissions and add a WAF.
This order is critical. If you clean the files before stopping the active process, the malware will rewrite the files again. If you clean the cron jobs before killing the process, the process will re-add the cron job.
Take your time. A thorough cleanup takes hours, not minutes. The goal is not just to remove the malware, but to understand how it got in and to prevent it from returning. If you are not comfortable with these steps, consider hiring a professional. But if you have the skills, this is the path to a truly clean server.
