#!/usr/bin/env bash # # Remediation helper — fake "Contact Form 7" credential stealer + fileless RCE # --------------------------------------------------------------------------- # Handles the ordering that matters: PRESERVE the credential log as evidence # BEFORE deleting anything, then remove implant artefacts, then sweep for IOCs. # # DRY RUN BY DEFAULT. Nothing is deleted unless you pass --apply. # # ./remediate-fake-cf7.sh /path/to/wordpress # report only # ./remediate-fake-cf7.sh /path/to/wordpress --apply # preserve + remove # # After this script: reinstall Contact Form 7 from wordpress.org, rotate all # credentials, and rotate wp-config.php salts. set -uo pipefail WP_ROOT="${1:-}" APPLY=0 [[ "${2:-}" == "--apply" ]] && APPLY=1 if [[ -z "$WP_ROOT" || ! -d "$WP_ROOT" ]]; then echo "Usage: $0 /path/to/wordpress [--apply]" >&2 exit 1 fi WP_ROOT="$(cd "$WP_ROOT" && pwd)" STAMP="$(date +%Y%m%d-%H%M%S)" EVIDENCE="${WP_ROOT}/../incident-evidence-${STAMP}" CONTENT="${WP_ROOT}/wp-content" CRED_LOG="${CONTENT}/uploads/logs/wpcf7-integration-debug.txt" LOG_DIR="${CONTENT}/uploads/logs" CACHE_DIR="${CONTENT}/uploads/wpcf7-module-cache" CF7_DIR="${CONTENT}/plugins/contact-form-7" say() { printf '\n\033[1m== %s\033[0m\n' "$1"; } act() { if [[ $APPLY -eq 1 ]]; then printf ' [DO] %s\n' "$1"; else printf ' [DRY] %s\n' "$1"; fi; } note() { printf ' %s\n' "$1"; } echo "Target : $WP_ROOT" echo "Mode : $([[ $APPLY -eq 1 ]] && echo 'APPLY (destructive)' || echo 'DRY RUN (no changes)')" # --------------------------------------------------------------------------- # STEP 1 — Preserve evidence FIRST. The credential log is both proof and the # list of people whose passwords must be treated as breached. # --------------------------------------------------------------------------- say "1. Preserve evidence" if [[ -f "$CRED_LOG" ]]; then LINES=$(wc -l < "$CRED_LOG" 2>/dev/null || echo '?') note "FOUND credential log: $CRED_LOG (${LINES} entries)" note "*** Every username/password in this file is compromised. ***" act "copy -> ${EVIDENCE}/wpcf7-integration-debug.txt" if [[ $APPLY -eq 1 ]]; then mkdir -p "$EVIDENCE" cp -p "$CRED_LOG" "${EVIDENCE}/wpcf7-integration-debug.txt" # Distinct accounts affected, for the notification list. grep -o '"user":"[^"]*"' "$CRED_LOG" 2>/dev/null \ | sed 's/"user":"//;s/"//' | sort -u \ > "${EVIDENCE}/affected-accounts.txt" || true note "affected accounts -> ${EVIDENCE}/affected-accounts.txt" fi else note "No credential log at expected path (may have been rotated or fetched already)." fi if [[ -f "${CF7_DIR}/wpcf7.php" ]] && grep -q 'wfc_dp_\|wp_acrions\|WPStaq' "${CF7_DIR}/wpcf7.php" 2>/dev/null; then note "Confirmed implant in ${CF7_DIR}/wpcf7.php" act "copy plugin dir -> ${EVIDENCE}/contact-form-7/" if [[ $APPLY -eq 1 ]]; then mkdir -p "$EVIDENCE" cp -a "$CF7_DIR" "${EVIDENCE}/contact-form-7" fi fi # --------------------------------------------------------------------------- # STEP 2 — Remove implant artefacts. # --------------------------------------------------------------------------- say "2. Remove implant artefacts" if [[ -d "$CF7_DIR" ]]; then act "rm -rf $CF7_DIR (reinstall CF7 from wordpress.org afterwards)" [[ $APPLY -eq 1 ]] && rm -rf "$CF7_DIR" fi if [[ -d "$LOG_DIR" ]]; then act "rm -rf $LOG_DIR (includes the public .htaccess that exposed the log)" [[ $APPLY -eq 1 ]] && rm -rf "$LOG_DIR" fi if [[ -d "$CACHE_DIR" ]]; then note "Staging dir contents (payloads are .jpg but are PHP):" ls -la "$CACHE_DIR" 2>/dev/null | sed 's/^/ /' act "rm -rf $CACHE_DIR" [[ $APPLY -eq 1 ]] && rm -rf "$CACHE_DIR" fi # --------------------------------------------------------------------------- # STEP 3 — IOC sweep. Signatures are drawn from the implant itself; the # runtime-built strings (pack/chr) mean plain 'eval'/'base64_decode' greps miss it. # --------------------------------------------------------------------------- say "3. IOC sweep (other copies / related implants)" sweep() { local label="$1" pattern="$2" local hits hits=$(grep -rlIE "$pattern" "$WP_ROOT" \ --include='*.php' --include='*.txt' --include='*.jpg' 2>/dev/null || true) if [[ -n "$hits" ]]; then printf ' [HIT] %s\n' "$label" printf '%s\n' "$hits" | sed 's/^/ /' else printf ' [ok] %s\n' "$label" fi } sweep "wp_acrions trigger param" 'wp_acrions' sweep "wfc_dp_ / wfc_bin_ functions" 'wfc_dp_|wfc_bin_b64|wfc_log_append' sweep "payload password hash" 'f4cc399f0effd13c888e310ea2cf5399' sweep "log encryption key" 'c74b96f2-e7aa-4e01-ac9c-5c2b8f1939f1' sweep "fake namespace" 'namespace[[:space:]]+WPStaq' sweep "runtime-built base64_decode" '6261736536345f6465636f6465' sweep "runtime-built base64_encode" '6261736536345f656e636f6465' sweep "temp_file_path include vector" 'temp_file_path' sweep "password capture on authenticate" '_wfc_login_pwd' sweep "generic: password in auth filter" "add_filter\([[:space:]]*['\"]authenticate" say "4. Manual follow-up (not automated)" cat <<'EOF' [ ] Reinstall Contact Form 7 from wordpress.org (this file was NOT real CF7). [ ] Force password reset for EVERY account in affected-accounts.txt. [ ] Tell those users to change the password anywhere they reused it. [ ] Rotate wp-config.php salts (invalidates all existing sessions). [ ] Rotate hosting panel, SFTP, and database credentials. [ ] Audit wp_users via SQL for accounts you do not recognise. [ ] Pull access logs for: wp_acrions=, temp_file_path=, and direct requests to /wp-content/uploads/logs/wpcf7-integration-debug.txt (that last one shows who downloaded the harvested passwords). [ ] Assess breach-notification obligations — plaintext passwords were stored in a publicly readable file. EOF echo [[ $APPLY -eq 0 ]] && echo "Dry run complete. Re-run with --apply to preserve evidence and remove." exit 0