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.