WordPress Malware Forensics: Post-Compromise Investigation and Cleanup

When a WordPress site is compromised, the immediate priority is evidence collection before cleanup — overwriting infected files without capturing artefacts destroys the forensic trail needed to understand the attack vector. A structured post-compromise workflow covers: snapshot preservation, IOC extraction, backdoor enumeration, root-cause identification, and hardened restoration.

Problem: A WordPress site has been compromised — backdoor files have been placed, admin accounts created, and plugin files modified — and there is no systematic process for identifying the full scope of the compromise and restoring a clean state.

Solution: Start with isolation: take the site offline or enable maintenance mode. Identify modified files using find . -newer backup.tar.gz -name "*.php" or diff against a fresh WordPress download. Scan with Maldet or ClamAV. Audit wp_users and wp_usermeta for unknown admins. Restore clean files from a trusted backup, rotate all secrets (salts, DB password, SFTP keys), and patch the entry-point vulnerability before going back online.


The commands below walk through a complete forensic investigation: capturing the current state, finding recently modified PHP files, extracting base64-encoded payloads, checking for injected database content, identifying the entry point, and restoring from a known-good state.


#!/bin/bash
# ── 1. Preserve evidence before touching anything ────────────────────────
SITE_ROOT=/var/www/html
EVIDENCE_DIR=/root/forensics/$(date +%Y%m%d_%H%M%S)
mkdir -p "$EVIDENCE_DIR"

# Snapshot all PHP files with mtimes
find "$SITE_ROOT" -name '*.php' -printf '%T@ %p\n' | sort -n \
    > "$EVIDENCE_DIR/php_mtimes.txt"

# Capture running processes and network connections
ps auxf          > "$EVIDENCE_DIR/processes.txt"
ss -tulnp        > "$EVIDENCE_DIR/network.txt"
last -n 50       > "$EVIDENCE_DIR/logins.txt"
crontab -l       > "$EVIDENCE_DIR/crontabs.txt" 2>/dev/null

# ── 2. Find recently modified PHP files (last 30 days) ───────────────────
find "$SITE_ROOT" -name '*.php' -newer "$SITE_ROOT/wp-config.php" \
    -not -path '*/node_modules/*' \
    | tee "$EVIDENCE_DIR/recently_modified.txt"

# ── 3. Find PHP files in uploads (should never exist) ────────────────────
find "$SITE_ROOT/wp-content/uploads" \
    \( -name '*.php' -o -name '*.phtml' -o -name '*.php5' \) \
    | tee "$EVIDENCE_DIR/php_in_uploads.txt"

# ── 4. Scan for base64-encoded payloads (common obfuscation) ─────────────
grep -rn --include='*.php' \
    -E '(eval|assert|preg_replace)\s*\(\s*base64_decode' \
    "$SITE_ROOT" \
    | tee "$EVIDENCE_DIR/base64_payloads.txt"

# ── 5. Find webshell indicators: exec/system/passthru in $_POST/$_GET ────
grep -rn --include='*.php' \
    -E '(exec|system|passthru|shell_exec)\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)' \
    "$SITE_ROOT" \
    | tee "$EVIDENCE_DIR/webshells.txt"

# ── 6. Check wp_options for injected JavaScript or iframes ───────────────
# (run inside MySQL or via WP-CLI)
wp option get siteurl
wp option get home
wp db query "SELECT option_name, LEFT(option_value,200)
             FROM wp_options
             WHERE option_value REGEXP '


NOTE: Restoring WordPress core files with wp core download --force only fixes core — a backdoor planted in a plugin, theme, or the database (injected option values, malicious cron events) will survive the restore; always audit wp_options, wp_usermeta, and the cron schedule with wp cron event list before declaring the site clean.