in valid administrator cookies or active SSH/SFTP sessions.
# Terminate all active WordPress sessions via WP-CLI
wp user session destroy --all
# Force password rotation for all administrative accounts
wp user list --role=administrator --field=user_login | xargs -I {} wp user update {} --user_pass="$(openssl rand -base64 24)"
Rationale: Automated scanners cannot invalidate active sessions. An attacker with a valid wordpress_logged_in cookie can bypass file-level cleanup by re-injecting payloads through the admin dashboard. Session destruction and credential rotation close the authentication vector before forensic work begins.
Step 2: Filesystem Mutation Tracking and Pattern Hunting
GUI file managers and plugin scanners operate within the compromised application context. True forensics requires direct filesystem access via SSH. Focus on recent mutations and obfuscation signatures rather than known malware hashes.
# Identify PHP files modified in the last 72 hours
find /var/www/html -type f -name "*.php" -newermt "72 hours ago" -print
# Hunt for common obfuscation primitives across plugin directories
grep -rlE "(eval\s*\(|base64_decode|gzinflate|str_rot13|assert\s*\()" /var/www/html/wp-content/plugins/ --include="*.php"
Rationale: Attackers frequently employ timestomping to mask file modifications, but secondary files (logs, cache, or configuration) often retain accurate timestamps. Searching by modification time bypasses timestamp spoofing. Obfuscation functions like eval() or base64_decode() are legitimate in core PHP but highly suspicious when clustered in plugin directories or core includes. This pattern-based approach catches polymorphic payloads that signature scanners miss.
Step 3: Core and Plugin Immutability Enforcement
Manual patching of infected files is unreliable. Attackers often leave secondary loaders or hidden includes that survive partial cleanup. Replace entire codebases with verified, upstream copies.
# Force-reinstall WordPress core without touching wp-content
wp core download --skip-content --force
# Reinstall all active plugins from official repositories
wp plugin list --field=name | xargs -I % wp plugin install % --force --version=latest
Rationale: WP-CLI's --force flag overwrites existing files with cryptographically verified upstream versions. This eliminates hidden includes, modified headers, and injected loaders. Since configuration and user data reside in the database, this operation preserves site functionality while guaranteeing code integrity. Never attempt to manually remove malicious lines from core files; the risk of leaving a secondary trigger is unacceptably high.
Step 4: Database Cron Purge and Serialization Audit
Persistent malware frequently abuses WordPress's scheduled task system. Rogue hooks stored in the wp_options table execute at fixed intervals, downloading fresh payloads or maintaining backdoor access.
-- Extract the serialized cron array
SELECT option_value FROM wp_options WHERE option_name = 'cron' LIMIT 1;
-- Identify and remove rogue scheduled tasks
DELETE FROM wp_options WHERE option_name LIKE '%cron%' AND option_value LIKE '%malicious_hook%';
Rationale: WordPress stores scheduled tasks as serialized PHP arrays. Malware injects custom hooks that point to non-existent plugins or obfuscated functions. Direct database inspection reveals these entries. After purging rogue hooks, regenerate the cron schedule cleanly:
wp cron event list
wp cron event delete --all
wp cron event schedule wp_core_update_check "$(date -d '+1 hour' +%s)" -- recurrence="hourly"
Rationale: Clearing and rebuilding the cron array ensures no hidden serialization artifacts remain. This step is critical because database-level persistence survives file-level cleanup entirely.
Step 5: System-Level Persistence Severance
If the attacker achieved elevated privileges, they may have bypassed WordPress entirely and installed system-level crontabs or systemd timers.
# Check user-level crontabs
crontab -l
# Check system-wide cron directories
ls -la /etc/cron.d/ /etc/cron.daily/ /var/spool/cron/
# Verify active network connections to known C2 patterns
ss -tunap | grep -E "(ESTABLISHED|TIME_WAIT)" | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -nr
Rationale: Server-level persistence operates outside WordPress's execution context. System crontabs can download and execute payloads regardless of file cleanup. Network connection auditing reveals unexpected outbound traffic to command-and-control infrastructure. Remove unauthorized entries and restrict cron execution to known administrative users.
Pitfall Guide
1. GUI Dependency in Compromised Environments
Explanation: Running cleanup plugins through the WordPress admin dashboard assumes the application layer is trustworthy. If the attacker controls the admin session or has injected a persistent loader, the GUI operates within a compromised context.
Fix: Always perform forensic operations via SSH and WP-CLI. Disable the admin interface temporarily during remediation using maintenance mode or IP whitelisting.
2. Manual File Patching Instead of Full Replacement
Explanation: Attempting to delete malicious lines from infected files leaves secondary triggers, hidden includes, or modified headers intact. Attackers design payloads to survive partial cleanup.
Fix: Replace entire core and plugin directories with upstream copies using wp core download --force and plugin reinstallation. Never edit core files manually during incident response.
3. Ignoring System-Level Crontabs
Explanation: Remediation focused solely on WordPress files misses server-level persistence. System crontabs or systemd timers can regenerate backdoors independently of the application.
Fix: Audit /etc/cron.d/, /var/spool/cron/, and user crontabs. Remove unauthorized entries and restrict cron execution to known administrative accounts.
4. Overlooking Active Admin Sessions
Explanation: Valid authentication cookies allow attackers to bypass file-level cleanup and re-inject payloads through the dashboard. Scanners cannot invalidate sessions.
Fix: Terminate all active sessions using wp user session destroy --all and force password rotation for all administrative accounts before beginning cleanup.
Explanation: The wp-content/uploads directory is writable by the web server. Attackers routinely drop executable PHP files here. Without execution restrictions, uploaded malware runs directly.
Fix: Block PHP execution in upload directories using server configuration. Set directory permissions to 755 and file permissions to 644. Never grant write access to core directories.
6. Skipping Database Serialization Audits
Explanation: WordPress stores scheduled tasks and plugin settings as serialized PHP arrays. Malware injects rogue hooks that survive file cleanup. Ignoring the database leaves the persistence trigger intact.
Fix: Query wp_options for cron arrays and autoloaded settings. Purge unrecognized hooks and regenerate the schedule using WP-CLI. Always audit serialized data during incident response.
7. Relying Solely on Plugin-Based WAFs
Explanation: Application-layer WAFs installed as WordPress plugins execute within the same compromised environment. They cannot block attacks that bypass the application or exploit server-level vulnerabilities.
Fix: Deploy edge-level WAFs (Cloudflare, AWS WAF, or Nginx ModSecurity) that filter traffic before it reaches the server. Combine with strict filesystem permissions and FIM for defense-in-depth.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Shared Hosting Environment | CLI Cleanup + Edge WAF | Limited server access requires application-level hardening and external traffic filtering | Low (SaaS WAF subscription) |
| VPS / Dedicated Server | Forensic CLI + FIM + System Cron Audit | Full control enables deep filesystem verification and system-level persistence removal | Medium (Monitoring tool licensing) |
| High-Traffic E-commerce | Immutable Core Replacement + Database Purge + Edge WAF | Downtime intolerance requires surgical, non-destructive cleanup with guaranteed regeneration prevention | High (Enterprise WAF + SLA monitoring) |
| Legacy WordPress Installation | Full Backup + Core Replacement + Permission Lockdown | Outdated codebases lack modern security hooks; clean slate prevents recurring vulnerabilities | Medium (Migration/backup storage) |
Configuration Template
# Nginx: Block PHP execution in upload and cache directories
location ~* ^/(wp-content/uploads|wp-content/cache)/.*\.php$ {
deny all;
access_log off;
log_not_found off;
}
# Nginx: Restrict direct access to sensitive WordPress files
location ~* ^/(wp-config\.php|xmlrpc\.php|readme\.html|license\.txt)$ {
deny all;
access_log off;
}
# Apache: Deny PHP execution in uploads directory
<Directory "/var/www/html/wp-content/uploads">
<FilesMatch "\.php$">
Require all denied
</FilesMatch>
</Directory>
# Apache: Restrict file permissions via .htaccess
<FilesMatch "\.(php|php\d|phtml|phar)$">
<If "%{REQUEST_URI} =~ m#^/wp-content/uploads/#">
Require all denied
</If>
</FilesMatch>
# File Integrity Monitoring baseline (AIDE configuration snippet)
# /etc/aide/aide.conf
/var/www/html/wp-content/plugins p+i+n+u+g+s+b+m+c+acl+selinux+xattrs+sha256
/var/www/html/wp-includes p+i+n+u+g+s+b+m+c+acl+selinux+xattrs+sha256
/var/www/html/wp-config.php p+i+n+u+g+s+b+m+c+acl+selinux+xattrs+sha256
Quick Start Guide
- Isolate & Terminate: SSH into the server, run
wp user session destroy --all, and rotate all administrative passwords.
- Verify & Replace: Execute
wp core verify-checksums, then run wp core download --skip-content --force and reinstall all plugins.
- Purge & Audit: Query
wp_options for the cron array, delete rogue hooks, and regenerate the schedule using wp cron event delete --all.
- Lock & Monitor: Apply Nginx/Apache rules to block PHP execution in uploads, set permissions to
755/644, and initialize a FIM baseline.
- Validate: Monitor server logs and CPU usage for 24 hours. Run
wp core verify-checksums again to confirm immutability.