Last active
May 14, 2026 19:34
-
-
Save sysbitnet/018ef5466be693a196ce063e820ed2bd to your computer and use it in GitHub Desktop.
cPanel & WHM Critical Abuse IP List and Rescue Helper for CVE-2026-41940
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # cPANEL SERVER RESCUE v3.1 — CVE-2026-41940 | |
| # Real-time CRITICAL logs + Community IP Intelligence + HTML/TXT Report | |
| # | |
| # v3.1 fixes (from real-world test runs): | |
| # * Fixed subshell array loss when scraping IPs from access log | |
| # * Fixed grep '-> ' pattern that was being parsed as option | |
| # * Self IPs filtered out of access log scan (failed admin logins != exploits) | |
| # * Skip vendor/.trash/dev-config files in hidden-script detection | |
| # * Fall back gracefully if /scripts/purge_dead_sessions is missing | |
| # * Cleaner KNOWN display when gist has no section headers | |
| # * Banner colour codes render properly | |
| # | |
| # v3 features: | |
| # * Updated gist URL to ip-list.txt | |
| # * Detects CONFIRMED vs ATTEMPT-only IPs | |
| # * Auto-detects self IPs to prevent self-blocking | |
| # * Interactive editor for BLOCK/ALLOW lists | |
| # | |
| # Usage: | |
| # sudo bash cpanel_rescue_v3.1.sh (interactive) | |
| # sudo bash cpanel_rescue_v3.1.sh --audit (read-only) | |
| # sudo bash cpanel_rescue_v3.1.sh --auto (no prompts) | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| [ "$EUID" -ne 0 ] && echo "[!] Must run as root." && exit 1 | |
| AUTO_MODE=0; AUDIT_ONLY=0 | |
| for arg in "$@"; do | |
| [ "$arg" = "--auto" ] && AUTO_MODE=1 | |
| [ "$arg" = "--audit" ] && AUDIT_ONLY=1 | |
| done | |
| # Colours | |
| R='\033[0;31m'; Y='\033[1;33m'; G='\033[0;32m'; C='\033[0;36m' | |
| B='\033[1;34m'; W='\033[1;37m'; DIM='\033[2m'; BOLD='\033[1m'; NC='\033[0m' | |
| RED_BG='\033[41m'; GRN_BG='\033[42m'; YEL_BG='\033[43m'; MAG_BG='\033[45m' | |
| # Directories | |
| TS=$(date +%Y%m%d_%H%M%S) | |
| REPORT_DIR="/root/rescue_${TS}" | |
| mkdir -p "$REPORT_DIR"/{evidence,critical,quarantine,logs} | |
| # Critical files | |
| CRIT_SESSIONS="$REPORT_DIR/critical/01_compromised_sessions.txt" | |
| CRIT_ATTACKERS="$REPORT_DIR/critical/02_attacker_ips.txt" | |
| CRIT_TIMELINE="$REPORT_DIR/critical/03_attack_timeline.txt" | |
| CRIT_RANSOMWARE="$REPORT_DIR/critical/04_ransomware_files.txt" | |
| CRIT_BACKDOORS="$REPORT_DIR/critical/05_backdoors_webshells.txt" | |
| CRIT_USERS="$REPORT_DIR/critical/06_suspicious_users.txt" | |
| CRIT_NETWORK="$REPORT_DIR/critical/07_network_threats.txt" | |
| CRIT_PERSISTENCE="$REPORT_DIR/critical/08_persistence_mechanisms.txt" | |
| CRIT_INTEGRITY="$REPORT_DIR/critical/09_integrity_violations.txt" | |
| CRIT_SUMMARY="$REPORT_DIR/critical/00_CRITICAL_SUMMARY.txt" | |
| ACTIONS_LOG="$REPORT_DIR/logs/actions_taken.txt" | |
| FULL_LOG="$REPORT_DIR/logs/full_output.txt" | |
| HTML_REPORT="$REPORT_DIR/SECURITY_REPORT.html" | |
| TXT_REPORT="$REPORT_DIR/SECURITY_REPORT.txt" | |
| # Community list — UPDATED URL (v3) | |
| GIST_RAW="https://gist.githubusercontent.com/sysbitnet/018ef5466be693a196ce063e820ed2bd/raw/ip-list.txt" | |
| GIST_URL="https://gist.github.com/sysbitnet/018ef5466be693a196ce063e820ed2bd" | |
| COMMUNITY_FILE="$REPORT_DIR/evidence/community_ip_list.txt" | |
| COMMUNITY_IPS=() # All IPs in community list | |
| COMMUNITY_CONFIRMED=() # IPs marked as confirmed breaches (if list uses sections) | |
| COMMUNITY_ATTEMPTS=() # IPs marked as attempts only | |
| NEW_IPS=() # Detected here, NOT in community list | |
| KNOWN_IPS=() # Detected here, IN community list | |
| CONFIRMED_BREACH_IPS=() # Detected here with HTTP 200 success | |
| ATTEMPT_ONLY_IPS=() # Detected here, only login attempts | |
| SELF_IPS=() # Server's own IPs (DO NOT BLOCK) | |
| # Runtime state | |
| ATTACKER_IPS=() | |
| COMPROMISED_SESSIONS=() | |
| CRITICAL_COUNT=0 | |
| WARNING_COUNT=0 | |
| ACTIONS_TAKEN=() | |
| ACTIONS_SKIPPED=() | |
| TOTAL_STEPS=10 | |
| CURRENT_STEP=0 | |
| ENC_COUNT=0 | |
| REAL_SHELLS=0 | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # HELPERS | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| _log() { echo -e "$1" | tee -a "$FULL_LOG"; } | |
| crit_to() { | |
| local file="$1"; shift | |
| echo -e "${RED_BG}${W} [CRITICAL] $*${NC}" | tee -a "$FULL_LOG" | |
| echo "[CRITICAL] $(date '+%H:%M:%S') $*" | tee -a "$file" >> "$CRIT_SUMMARY" | |
| ((CRITICAL_COUNT++)) | |
| } | |
| warn_to() { | |
| local file="$1"; shift | |
| echo -e "${Y} [WARNING] $*${NC}" | tee -a "$FULL_LOG" | |
| echo "[WARNING] $(date '+%H:%M:%S') $*" >> "$file" | |
| ((WARNING_COUNT++)) | |
| } | |
| ok() { echo -e "${G} [OK] $*${NC}" | tee -a "$FULL_LOG"; } | |
| info() { echo -e "${C} [INFO] $*${NC}" | tee -a "$FULL_LOG"; } | |
| det() { echo -e "${DIM} $*${NC}" | tee -a "$FULL_LOG"; } | |
| divider() { echo -e "\n${B}$(printf '=%.0s' {1..72})${NC}" | tee -a "$FULL_LOG"; } | |
| subdiv() { echo -e "${DIM}$(printf -- '-%.0s' {1..72})${NC}" | tee -a "$FULL_LOG"; } | |
| progress() { | |
| ((CURRENT_STEP++)) | |
| local pct=$(( CURRENT_STEP * 100 / TOTAL_STEPS )) | |
| local filled=$(( pct / 5 )) | |
| local bar="" | |
| for ((i=0;i<20;i++)); do [ $i -lt $filled ] && bar+="#" || bar+="."; done | |
| divider | |
| _log "${B}${BOLD} STEP ${CURRENT_STEP}/${TOTAL_STEPS} -- $1${NC}" | |
| _log "${B} [${bar}] ${pct}%${NC}" | |
| echo "" | |
| } | |
| ask() { | |
| [ "$AUTO_MODE" -eq 1 ] && return 0 | |
| [ "$AUDIT_ONLY" -eq 1 ] && return 1 | |
| echo -e "\n${W}${BOLD} > $1${NC}" | |
| echo -ne " ${C}[y/N]: ${NC}" | |
| read -r ans | |
| [[ "$ans" =~ ^[Yy]$ ]] | |
| } | |
| pause() { | |
| [ "$AUTO_MODE" -eq 1 ] && return | |
| echo -ne "\n${DIM} [Press ENTER to continue]${NC}" | |
| read -r | |
| } | |
| run_cmd() { | |
| local desc="$1" cmd="$2" | |
| echo -e " ${C}$ $cmd${NC}" | tee -a "$FULL_LOG" | |
| if [ "$AUDIT_ONLY" -eq 0 ]; then | |
| eval "$cmd" >> "$FULL_LOG" 2>&1 | |
| local rc=$? | |
| if [ $rc -eq 0 ]; then | |
| echo -e " ${GRN_BG}${W} DONE: $desc ${NC}" | tee -a "$ACTIONS_LOG" | |
| ACTIONS_TAKEN+=("$desc") | |
| else | |
| echo -e " ${YEL_BG} FAILED (rc=$rc): $desc ${NC}" | tee -a "$ACTIONS_LOG" | |
| fi | |
| return $rc | |
| else | |
| echo -e " ${DIM} [AUDIT -- not executed]${NC}" | tee -a "$FULL_LOG" | |
| fi | |
| } | |
| action_skip() { | |
| echo -e " ${YEL_BG} SKIPPED: $* ${NC}" | tee -a "$ACTIONS_LOG" | |
| ACTIONS_SKIPPED+=("$*") | |
| } | |
| init_crit_files() { | |
| local hdr="# cPanel Rescue v3 -- $(date) -- $(hostname)" | |
| local files=( "$CRIT_SESSIONS" "$CRIT_ATTACKERS" "$CRIT_TIMELINE" | |
| "$CRIT_RANSOMWARE" "$CRIT_BACKDOORS" "$CRIT_USERS" | |
| "$CRIT_NETWORK" "$CRIT_PERSISTENCE" "$CRIT_INTEGRITY" "$CRIT_SUMMARY" ) | |
| for f in "${files[@]}"; do | |
| { echo "$hdr"; echo "$(printf '#%.0s' {1..72})"; echo ""; } > "$f" | |
| done | |
| } | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # DETECT SERVER'S OWN IPs (to avoid self-blocking) | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| detect_self_ips() { | |
| _log "\n${C} Detecting this server's own IP addresses...${NC}" | |
| # Local interface IPs | |
| local local_ips | |
| local_ips=$(ip -4 addr show 2>/dev/null | grep -oP 'inet \K[\d.]+' | grep -v '^127\.') | |
| while read -r ip; do | |
| [ -n "$ip" ] && SELF_IPS+=("$ip") | |
| done <<< "$local_ips" | |
| # Public IP (best effort) | |
| local public_ip="" | |
| for svc in 'https://api.ipify.org' 'https://ifconfig.me/ip' 'https://icanhazip.com'; do | |
| public_ip=$(curl -sS --max-time 5 "$svc" 2>/dev/null | grep -oP '^\d+\.\d+\.\d+\.\d+$' | head -1) | |
| [ -n "$public_ip" ] && break | |
| done | |
| if [ -n "$public_ip" ]; then | |
| SELF_IPS+=("$public_ip") | |
| info "Public IP: $public_ip" | |
| fi | |
| # Recent SSH admin IPs (most-frequent legitimate admin login source) | |
| local admin_ip | |
| admin_ip=$(last -F 2>/dev/null | grep -v 'reboot\|wtmp\|^$' | awk '{print $3}' | \ | |
| grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | sort | uniq -c | sort -rn | head -1 | awk '{print $2}') | |
| if [ -n "$admin_ip" ]; then | |
| SELF_IPS+=("$admin_ip") | |
| info "Most frequent admin SSH IP: $admin_ip" | |
| fi | |
| # Deduplicate | |
| mapfile -t SELF_IPS < <(printf '%s\n' "${SELF_IPS[@]}" | sort -u | grep -v '^$') | |
| _log " ${G}Self IPs (will NOT be blocked):${NC}" | |
| for ip in "${SELF_IPS[@]}"; do _log " ${G}+ $ip${NC}"; done | |
| } | |
| is_self_ip() { | |
| local ip="$1" | |
| for s in "${SELF_IPS[@]}"; do [ "$s" = "$ip" ] && echo 1 && return; done | |
| echo 0 | |
| } | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # COMMUNITY IP FUNCTIONS | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| load_community_ips() { | |
| _log "\n${MAG_BG}${W} Loading community attacker IP list...${NC}" | |
| det "Source: $GIST_URL" | |
| local dl=0 | |
| if command -v curl &>/dev/null; then | |
| curl -sL --max-time 15 "$GIST_RAW" -o "$COMMUNITY_FILE" 2>/dev/null && dl=1 | |
| fi | |
| [ $dl -eq 0 ] && command -v wget &>/dev/null && \ | |
| wget -q --timeout=15 "$GIST_RAW" -O "$COMMUNITY_FILE" 2>/dev/null && dl=1 | |
| if [ $dl -eq 1 ] && [ -s "$COMMUNITY_FILE" ]; then | |
| # Parse IPs and detect sections (CONFIRMED / ATTEMPT) | |
| local section="UNCATEGORIZED" | |
| while IFS= read -r line; do | |
| # Section headers | |
| if echo "$line" | grep -qiE '^#.*CONFIRMED|^#.*BREACH'; then | |
| section="CONFIRMED" | |
| elif echo "$line" | grep -qiE '^#.*ATTEMPT|^#.*TRYING|^#.*SCAN'; then | |
| section="ATTEMPT" | |
| fi | |
| # Extract IP from line (skips comments/empty) | |
| local ip | |
| ip=$(echo "$line" | grep -oP '^\s*\K\d{1,3}(\.\d{1,3}){3}') | |
| if [ -n "$ip" ]; then | |
| COMMUNITY_IPS+=("$ip") | |
| [ "$section" = "CONFIRMED" ] && COMMUNITY_CONFIRMED+=("$ip") | |
| [ "$section" = "ATTEMPT" ] && COMMUNITY_ATTEMPTS+=("$ip") | |
| fi | |
| done < "$COMMUNITY_FILE" | |
| ok "Community list loaded: ${#COMMUNITY_IPS[@]} IPs" | |
| [ ${#COMMUNITY_CONFIRMED[@]} -gt 0 ] && \ | |
| det "Confirmed breaches: ${#COMMUNITY_CONFIRMED[@]} | Attempts: ${#COMMUNITY_ATTEMPTS[@]}" | |
| { | |
| echo "Community list downloaded: $(date)" | |
| echo "Source: $GIST_URL" | |
| echo "Total IPs: ${#COMMUNITY_IPS[@]}" | |
| [ ${#COMMUNITY_CONFIRMED[@]} -gt 0 ] && \ | |
| echo "Confirmed breaches: ${#COMMUNITY_CONFIRMED[@]}, Attempts: ${#COMMUNITY_ATTEMPTS[@]}" | |
| } >> "$CRIT_ATTACKERS" | |
| # Warn if any of OUR IPs appear in the community list | |
| for self_ip in "${SELF_IPS[@]}"; do | |
| for comm_ip in "${COMMUNITY_IPS[@]}"; do | |
| if [ "$self_ip" = "$comm_ip" ]; then | |
| _log "" | |
| _log "${YEL_BG}${BOLD} WARNING: YOUR OWN IP IS IN THE COMMUNITY BLOCKLIST! ${NC}" | |
| _log " ${Y}IP: $self_ip${NC}" | |
| _log " ${Y}Other admins blocking this list will reject your connections.${NC}" | |
| _log " ${Y}Edit the gist to remove your IP: $GIST_URL${NC}" | |
| { | |
| echo "" | |
| echo "WARNING: Server's own IP ($self_ip) found in community blocklist." | |
| echo "Recommend removing it from $GIST_URL" | |
| } >> "$CRIT_ATTACKERS" | |
| break | |
| fi | |
| done | |
| done | |
| else | |
| warn_to "$CRIT_ATTACKERS" "Could not download community IP list. Check URL or network." | |
| COMMUNITY_IPS=() | |
| fi | |
| } | |
| in_community() { | |
| local ip="$1" | |
| for k in "${COMMUNITY_IPS[@]}"; do [ "$k" = "$ip" ] && echo 1 && return; done | |
| echo 0 | |
| } | |
| community_section() { | |
| # Returns CONFIRMED / ATTEMPT / UNKNOWN | |
| local ip="$1" | |
| for k in "${COMMUNITY_CONFIRMED[@]}"; do [ "$k" = "$ip" ] && echo "CONFIRMED" && return; done | |
| for k in "${COMMUNITY_ATTEMPTS[@]}"; do [ "$k" = "$ip" ] && echo "ATTEMPT" && return; done | |
| echo "UNKNOWN" | |
| } | |
| show_ip_intel() { | |
| local ip="$1" breach="${2:-0}" | |
| # Check if it's our own IP first | |
| local is_self | |
| is_self=$(is_self_ip "$ip") | |
| if [ "$is_self" = "1" ]; then | |
| _log "" | |
| _log " ${G}+-- IP: $ip ${GRN_BG}${W} [YOUR OWN SERVER/ADMIN IP - SAFE] ${NC}" | |
| _log " ${G}Will NOT be added to attacker block list.${NC}" | |
| return | |
| fi | |
| local known | |
| known=$(in_community "$ip") | |
| _log "" | |
| _log " ${W}+-- IP: $ip $( [ "$breach" = "1" ] && echo "${RED_BG}${W}[CONFIRMED BREACH]${NC}" || echo "${YEL_BG}[Attempt only]${NC}" )${NC}" | |
| if [ "$known" = "1" ]; then | |
| local sec | |
| sec=$(community_section "$ip") | |
| if [ "$sec" = "CONFIRMED" ]; then | |
| _log " ${MAG_BG}${W} KNOWN -- listed as CONFIRMED in community list ${NC}" | |
| elif [ "$sec" = "ATTEMPT" ]; then | |
| _log " ${MAG_BG}${W} KNOWN -- listed as ATTEMPT in community list ${NC}" | |
| else | |
| _log " ${MAG_BG}${W} KNOWN -- in community list ${NC}" | |
| fi | |
| det "Reported by other admins worldwide." | |
| echo " $ip STATUS=KNOWN COMMUNITY_SECTION=$sec THIS_SCAN_BREACH=$breach" >> "$CRIT_ATTACKERS" | |
| KNOWN_IPS+=("$ip") | |
| else | |
| _log " ${YEL_BG}${BOLD} NEW IP -- not yet in community list ${NC}" | |
| _log " ${Y}Help others by reporting it!${NC}" | |
| _log "" | |
| _log " ${G}Add to community gist:${NC}" | |
| det " 1. Open: $GIST_URL" | |
| det " 2. Click Edit (free GitHub account)" | |
| det " 3. Add line: $ip" | |
| if [ "$breach" = "1" ]; then | |
| det " 4. Add under section: # CONFIRMED BREACHES" | |
| else | |
| det " 4. Add under section: # ATTEMPTS" | |
| fi | |
| det " 5. Click 'Update secret gist'" | |
| _log " ${G}Report on AbuseIPDB:${NC}" | |
| det " https://www.abuseipdb.com/report" | |
| det " Categories: 18 (Brute Force) + 21 (Web App Attack)" | |
| echo " $ip STATUS=NEW THIS_SCAN_BREACH=$breach REPORT_IT" >> "$CRIT_ATTACKERS" | |
| NEW_IPS+=("$ip") | |
| fi | |
| # Track confirmed vs attempt | |
| if [ "$breach" = "1" ]; then | |
| CONFIRMED_BREACH_IPS+=("$ip") | |
| else | |
| ATTEMPT_ONLY_IPS+=("$ip") | |
| fi | |
| det "AbuseIPDB check: https://www.abuseipdb.com/check/$ip" | |
| _log " +$(printf -- '-%.0s' {1..60})" | |
| } | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # HTML BUILDER | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| build_html() { | |
| local cpv | |
| cpv=$(cat /usr/local/cpanel/version 2>/dev/null || echo "unknown") | |
| cat > "$HTML_REPORT" << 'HTMLEOF' | |
| <!DOCTYPE html><html lang="en"><head> | |
| <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>cPanel Security Incident Report - CVE-2026-41940</title> | |
| <style> | |
| :root{--red:#c0392b;--ora:#e67e22;--grn:#27ae60;--blu:#2980b9;--pur:#8e44ad; | |
| --dark:#1a1a2e;--darker:#0f0f1a;--card:#16213e;--bdr:#2d4068;--txt:#e0e0e0;--mut:#8899aa} | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| body{background:var(--darker);color:var(--txt);font-family:'Segoe UI',system-ui,sans-serif;font-size:14px;line-height:1.6} | |
| .w{max-width:1100px;margin:0 auto;padding:24px 16px} | |
| h1{font-size:26px;font-weight:700;color:#fff;margin-bottom:4px} | |
| h2{font-size:16px;font-weight:600;color:#fff;margin:28px 0 12px;padding-left:10px;border-left:3px solid var(--blu)} | |
| h3{font-size:12px;font-weight:700;color:var(--mut);text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px} | |
| .bdg{display:inline-block;padding:2px 10px;border-radius:12px;font-size:11px;font-weight:700} | |
| .r{background:#ff000022;color:#ff6b6b;border:1px solid #ff000055} | |
| .o{background:#ff880022;color:#ffb347;border:1px solid #ff880055} | |
| .g{background:#00cc4422;color:#5cb85c;border:1px solid #00cc4455} | |
| .p{background:#8800ff22;color:#cc88ff;border:1px solid #8800ff55} | |
| .b{background:#0088ff22;color:#5bc0de;border:1px solid #0088ff55} | |
| .hero{background:linear-gradient(135deg,#2c0000,#1a0022);border:1px solid #660000;border-radius:12px;padding:24px;margin-bottom:24px} | |
| .thr{font-size:30px;font-weight:800;color:#ff4444} | |
| .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-bottom:24px} | |
| .st{background:var(--card);border:1px solid var(--bdr);border-radius:8px;padding:16px;text-align:center} | |
| .st .n{font-size:34px;font-weight:800;line-height:1}.st .l{font-size:11px;color:var(--mut);margin-top:4px;text-transform:uppercase} | |
| .st.red .n{color:#ff4444}.st.ora .n{color:#ffb347}.st.grn .n{color:#5cb85c}.st.pur .n{color:#cc88ff} | |
| .card{background:var(--card);border:1px solid var(--bdr);border-radius:8px;padding:16px;margin-bottom:16px} | |
| .card.red{border-color:#660000;background:#1a0808}.card.ora{border-color:#664400;background:#1a1008} | |
| .card.grn{border-color:#006600;background:#081a08}.card.pur{border-color:#440066;background:#100820} | |
| .card.blu{border-color:#004488;background:#08101a} | |
| table{width:100%;border-collapse:collapse;font-size:13px} | |
| th{background:#0d1526;color:var(--mut);font-size:11px;font-weight:700;text-transform:uppercase; | |
| letter-spacing:.06em;padding:8px 12px;text-align:left;border-bottom:2px solid var(--bdr)} | |
| td{padding:8px 12px;border-bottom:1px solid #1e2d45;vertical-align:top} | |
| tr:last-child td{border-bottom:none}tr:hover td{background:#1e2d4520} | |
| .ip{font-family:monospace;font-size:13px;background:#0d1526;padding:2px 8px;border-radius:4px} | |
| .known{color:#cc88ff;font-weight:700}.newip{color:#ffb347;font-weight:700}.conf{color:#ff4444;font-weight:700} | |
| .self{color:#5cb85c;font-weight:700} | |
| pre{background:#0d1526;border:1px solid var(--bdr);border-radius:6px;padding:12px;overflow-x:auto; | |
| font-size:12px;white-space:pre-wrap;word-break:break-all;color:#aad4ff;line-height:1.5} | |
| .ai{display:flex;gap:10px;padding:8px 0;border-bottom:1px solid #1e2d45;align-items:center;font-size:13px} | |
| .ai:last-child{border-bottom:none} | |
| .cb{background:#100820;border:1px solid #440066;border-radius:8px;padding:16px;margin-top:12px} | |
| a{color:#aad4ff;text-decoration:none;border-bottom:1px dashed #aad4ff55} | |
| a:hover{color:#fff;border-color:#fff} | |
| .sb{background:#0d1526;border:1px solid var(--bdr);border-radius:6px;padding:10px 14px; | |
| margin:6px 0;font-family:monospace;font-size:12px;color:#aad4ff} | |
| .wb{background:#1a1008;border:1px solid #664400;border-radius:6px;padding:12px;margin:8px 0;font-size:13px} | |
| .alert{background:#2c0000;border:1px solid #660000;border-radius:6px;padding:12px;margin:8px 0; | |
| font-size:13px;color:#ff8888} | |
| footer{margin-top:40px;padding-top:16px;border-top:1px solid var(--bdr);color:var(--mut);font-size:12px;text-align:center} | |
| </style></head><body><div class="w"> | |
| HTMLEOF | |
| cat >> "$HTML_REPORT" << HTMLEOF | |
| <div class="hero"> | |
| <div class="thr">⚠ SECURITY INCIDENT CONFIRMED</div> | |
| <div style="font-size:18px;color:#ff8888;margin-top:6px;font-weight:600">CVE-2026-41940 -- cPanel/WHM Authentication Bypass (CVSS 9.8)</div> | |
| <div style="color:var(--mut);margin-top:10px"> | |
| Server: <strong>$(hostname)</strong> | | |
| Kernel: <strong>$(uname -r)</strong> | | |
| cPanel: <strong>$cpv</strong> | | |
| Report: <strong>$(date)</strong> | |
| </div> | |
| </div> | |
| <div class="stats"> | |
| <div class="st red"><div class="n">$CRITICAL_COUNT</div><div class="l">Critical Findings</div></div> | |
| <div class="st ora"><div class="n">$WARNING_COUNT</div><div class="l">Warnings</div></div> | |
| <div class="st red"><div class="n">${#CONFIRMED_BREACH_IPS[@]}</div><div class="l">Confirmed Breaches</div></div> | |
| <div class="st ora"><div class="n">${#ATTEMPT_ONLY_IPS[@]}</div><div class="l">Attempt-only IPs</div></div> | |
| <div class="st red"><div class="n">${#COMPROMISED_SESSIONS[@]}</div><div class="l">Breached Sessions</div></div> | |
| <div class="st pur"><div class="n">${#NEW_IPS[@]}</div><div class="l">New IPs to Report</div></div> | |
| </div> | |
| <h2>Community IP Intelligence</h2> | |
| <div class="cb"> | |
| <h3>Blocklist source -- <a href="$GIST_URL" target="_blank">$GIST_URL</a></h3> | |
| <p style="margin:8px 0;color:var(--mut);font-size:13px"> | |
| ${#COMMUNITY_IPS[@]} IPs in community list | | |
| ${#KNOWN_IPS[@]} matched known | | |
| <span style="color:#ffb347;font-weight:700">${#NEW_IPS[@]} new to report</span> | |
| </p> | |
| HTMLEOF | |
| # Self-IP warning if any of our IPs are in community list | |
| local self_in_list=0 | |
| for self_ip in "${SELF_IPS[@]}"; do | |
| for comm_ip in "${COMMUNITY_IPS[@]}"; do | |
| if [ "$self_ip" = "$comm_ip" ]; then | |
| if [ $self_in_list -eq 0 ]; then | |
| cat >> "$HTML_REPORT" << HTMLEOF | |
| <div class="alert"> | |
| <strong>⚠ WARNING:</strong> Your own server/admin IP appears in the community blocklist:<br> | |
| HTMLEOF | |
| self_in_list=1 | |
| fi | |
| echo " <span class='ip'>$self_ip</span><br>" >> "$HTML_REPORT" | |
| fi | |
| done | |
| done | |
| [ $self_in_list -eq 1 ] && cat >> "$HTML_REPORT" << HTMLEOF | |
| Other admins using this list will block YOUR connection. Edit the gist to remove these IPs. | |
| </div> | |
| HTMLEOF | |
| cat >> "$HTML_REPORT" << HTMLEOF | |
| </div><br> | |
| <table> | |
| <thead><tr><th>IP Address</th><th>Community</th><th>This Scan</th><th>Reputation</th><th>Action</th></tr></thead> | |
| <tbody> | |
| HTMLEOF | |
| for ip in "${ATTACKER_IPS[@]}"; do | |
| [ -z "$ip" ] && continue | |
| local self_check kn breach_html status act sec sec_html | |
| self_check=$(is_self_ip "$ip") | |
| kn=$(in_community "$ip") | |
| sec=$(community_section "$ip") | |
| # Skip our own IPs in this table | |
| if [ "$self_check" = "1" ]; then continue; fi | |
| # Determine breach severity | |
| local was_breach=0 | |
| for bip in "${CONFIRMED_BREACH_IPS[@]}"; do | |
| [ "$bip" = "$ip" ] && was_breach=1 && break | |
| done | |
| if [ "$was_breach" = "1" ]; then | |
| breach_html="<span class='bdg r conf'>BREACHED</span>" | |
| else | |
| breach_html="<span class='bdg o'>Attempt</span>" | |
| fi | |
| if [ "$kn" = "1" ]; then | |
| if [ "$sec" = "CONFIRMED" ]; then | |
| status="<span class='bdg p known'>✓ KNOWN: Confirmed</span>" | |
| elif [ "$sec" = "ATTEMPT" ]; then | |
| status="<span class='bdg p known'>✓ KNOWN: Attempt</span>" | |
| else | |
| status="<span class='bdg p known'>✓ KNOWN</span>" | |
| fi | |
| act="<span style='color:#888;font-size:12px'>Already listed</span>" | |
| else | |
| status="<span class='bdg o newip'>★ NEW</span>" | |
| act="<a href='$GIST_URL' target='_blank'>Add to gist</a> | <a href='https://www.abuseipdb.com/report' target='_blank'>AbuseIPDB</a>" | |
| fi | |
| cat >> "$HTML_REPORT" << HTMLEOF | |
| <tr> | |
| <td><span class="ip">$ip</span></td> | |
| <td>$status</td> | |
| <td>$breach_html</td> | |
| <td><a href="https://www.abuseipdb.com/check/$ip" target="_blank">Check ↗</a></td> | |
| <td>$act</td> | |
| </tr> | |
| HTMLEOF | |
| done | |
| echo "</tbody></table>" >> "$HTML_REPORT" | |
| # Self IPs detected | |
| if [ ${#SELF_IPS[@]} -gt 0 ]; then | |
| cat >> "$HTML_REPORT" << HTMLEOF | |
| <h2>Server's Own IPs (excluded from blocking)</h2> | |
| <div class="card grn"> | |
| <p style="margin-bottom:8px;font-size:13px;color:var(--mut)">These IPs were auto-detected as belonging to this server or your admin sessions and were NOT added to any block list.</p> | |
| <table> | |
| <thead><tr><th>IP</th><th>Source</th></tr></thead><tbody> | |
| HTMLEOF | |
| for ip in "${SELF_IPS[@]}"; do | |
| echo "<tr><td><span class='ip'>$ip</span></td><td><span class='self'>Self / Admin</span></td></tr>" >> "$HTML_REPORT" | |
| done | |
| echo "</tbody></table></div>" >> "$HTML_REPORT" | |
| fi | |
| # New IPs to report | |
| if [ "${#NEW_IPS[@]}" -gt 0 ]; then | |
| local new_confirmed=() new_attempts=() | |
| for ip in "${NEW_IPS[@]}"; do | |
| local was_b=0 | |
| for b in "${CONFIRMED_BREACH_IPS[@]}"; do [ "$b" = "$ip" ] && was_b=1 && break; done | |
| [ "$was_b" = "1" ] && new_confirmed+=("$ip") || new_attempts+=("$ip") | |
| done | |
| cat >> "$HTML_REPORT" << HTMLEOF | |
| <h2>⚠ New IPs -- Help Other Admins Worldwide!</h2> | |
| <div class="card ora"> | |
| <p style="margin-bottom:12px;color:#ffb347">These IPs are not yet in the community list. Reporting them takes 60 seconds and protects every other cPanel server globally.</p> | |
| <h3>Add to the community gist (GitHub)</h3> | |
| <div class="sb">1. Open: <a href="$GIST_URL" target="_blank">$GIST_URL</a></div> | |
| <div class="sb">2. Click the pencil (Edit) icon -- free GitHub account needed</div> | |
| <div class="sb">3. Add these IPs under the appropriate sections:</div> | |
| HTMLEOF | |
| if [ ${#new_confirmed[@]} -gt 0 ]; then | |
| cat >> "$HTML_REPORT" << HTMLEOF | |
| <p style="margin-top:12px"><strong style="color:#ff6b6b">Under "# CONFIRMED BREACHES" section:</strong></p> | |
| <pre> | |
| HTMLEOF | |
| for ip in "${new_confirmed[@]}"; do echo "$ip" >> "$HTML_REPORT"; done | |
| echo "</pre>" >> "$HTML_REPORT" | |
| fi | |
| if [ ${#new_attempts[@]} -gt 0 ]; then | |
| cat >> "$HTML_REPORT" << HTMLEOF | |
| <p style="margin-top:12px"><strong style="color:#ffb347">Under "# ATTEMPTS" section:</strong></p> | |
| <pre> | |
| HTMLEOF | |
| for ip in "${new_attempts[@]}"; do echo "$ip" >> "$HTML_REPORT"; done | |
| echo "</pre>" >> "$HTML_REPORT" | |
| fi | |
| cat >> "$HTML_REPORT" << HTMLEOF | |
| <div class="sb">4. Scroll down, click "Update secret gist"</div> | |
| <br> | |
| <h3>Also report on AbuseIPDB (high visibility)</h3> | |
| <div class="wb"> | |
| URL: <a href="https://www.abuseipdb.com/report" target="_blank">https://www.abuseipdb.com/report</a><br> | |
| Categories: <strong>18</strong> (Brute Force) + <strong>21</strong> (Web App Attack)<br><br> | |
| Comment template:<br> | |
| <em>Exploited CVE-2026-41940 cPanel/WHM CRLF authentication bypass. Gained unauthenticated root WHM access via injected session cookie. Deployed .sorry ransomware after breach. Server: $(hostname)</em> | |
| </div> | |
| </div> | |
| HTMLEOF | |
| fi | |
| cat >> "$HTML_REPORT" << HTMLEOF | |
| <h2>Attack Timeline</h2> | |
| <div class="card red"> | |
| <pre>$(sed 's/</\</g;s/>/\>/g' "$CRIT_TIMELINE" 2>/dev/null)</pre> | |
| </div> | |
| <h2>Session Exploitation Detail</h2> | |
| <div class="card red"> | |
| <pre>$(sed 's/</\</g;s/>/\>/g' "$CRIT_SESSIONS" 2>/dev/null)</pre> | |
| </div> | |
| <h2>Ransomware Assessment</h2> | |
| <div class="card red"> | |
| <table> | |
| <thead><tr><th>Metric</th><th>Value</th></tr></thead> | |
| <tbody> | |
| <tr><td>Encrypted files</td><td><strong style="color:#ff4444">$ENC_COUNT</strong></td></tr> | |
| <tr><td>Ransomware extension</td><td>.sorry</td></tr> | |
| <tr><td>SSL keys encrypted</td><td>$(grep -c '\.key\.sorry' "$REPORT_DIR/evidence/all_encrypted_files.txt" 2>/dev/null || echo 0)</td></tr> | |
| <tr><td>PHP files encrypted</td><td>$(grep -c '\.php\.sorry' "$REPORT_DIR/evidence/all_encrypted_files.txt" 2>/dev/null || echo 0)</td></tr> | |
| <tr><td>Full encrypted file list</td><td><code>$REPORT_DIR/evidence/all_encrypted_files.txt</code></td></tr> | |
| <tr><td>Free decryptors</td><td><a href="https://www.nomoreransom.org/en/decryption-tools.html" target="_blank">nomoreransom.org ↗</a></td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| <h2>Actions Taken</h2> | |
| <div class="card grn"> | |
| HTMLEOF | |
| for a in "${ACTIONS_TAKEN[@]}"; do | |
| echo "<div class='ai'><span>✓</span> $a</div>" >> "$HTML_REPORT" | |
| done | |
| echo "</div>" >> "$HTML_REPORT" | |
| if [ "${#ACTIONS_SKIPPED[@]}" -gt 0 ]; then | |
| echo "<div class='card ora'><h3>Skipped -- Manual action required</h3>" >> "$HTML_REPORT" | |
| for a in "${ACTIONS_SKIPPED[@]}"; do | |
| echo "<div class='ai'><span>✗</span> $a</div>" >> "$HTML_REPORT" | |
| done | |
| echo "</div>" >> "$HTML_REPORT" | |
| fi | |
| cat >> "$HTML_REPORT" << HTMLEOF | |
| <h2>Remaining Manual Tasks</h2> | |
| <div class="card blu"> | |
| <table> | |
| <thead><tr><th>#</th><th>Task</th><th>Priority</th></tr></thead> | |
| <tbody> | |
| <tr><td>1</td><td>Restore encrypted files from backup or try <a href="https://www.nomoreransom.org" target="_blank">nomoreransom.org</a></td><td><span class="bdg r">Critical</span></td></tr> | |
| <tr><td>2</td><td>Replace ALL SSL certificates (private keys were encrypted)</td><td><span class="bdg r">Critical</span></td></tr> | |
| <tr><td>3</td><td>Audit all MySQL databases for data theft or modification</td><td><span class="bdg r">Critical</span></td></tr> | |
| <tr><td>4</td><td>Notify affected users (data may have been exfiltrated)</td><td><span class="bdg o">High</span></td></tr> | |
| <tr><td>5</td><td>Report NEW attacker IPs to community gist and AbuseIPDB</td><td><span class="bdg o">High</span></td></tr> | |
| <tr><td>6</td><td>Enable Two-Factor Authentication in WHM Security Center</td><td><span class="bdg o">High</span></td></tr> | |
| <tr><td>7</td><td>Install CSF firewall: <code>apt install csf</code></td><td><span class="bdg b">Medium</span></td></tr> | |
| <tr><td>8</td><td>Move cPanel backups off-server (Comet is currently on-server)</td><td><span class="bdg b">Medium</span></td></tr> | |
| <tr><td>9</td><td>Enable cPHulk brute-force protection in WHM Security Center</td><td><span class="bdg b">Medium</span></td></tr> | |
| <tr><td>10</td><td>Whitelist your IP only for WHM port 2087 via iptables or CSF</td><td><span class="bdg b">Medium</span></td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| <h2>Critical Log Files</h2> | |
| <div class="card"> | |
| <pre> | |
| $REPORT_DIR/ | |
| SECURITY_REPORT.html <-- This file | |
| SECURITY_REPORT.txt <-- Plain-text version | |
| critical/ | |
| 00_CRITICAL_SUMMARY.txt | |
| 01_compromised_sessions.txt | |
| 02_attacker_ips.txt | |
| 03_attack_timeline.txt | |
| 04_ransomware_files.txt | |
| 05_backdoors_webshells.txt | |
| 06_suspicious_users.txt | |
| 07_network_threats.txt | |
| 08_persistence_mechanisms.txt | |
| 09_integrity_violations.txt | |
| NEW_IPs_to_report.txt | |
| evidence/ | |
| all_encrypted_files.txt | |
| community_ip_list.txt | |
| session_*.txt | |
| processes.txt / listening_ports.txt | |
| quarantine/ | |
| logs/ | |
| full_output.txt | |
| actions_taken.txt | |
| </pre> | |
| </div> | |
| <footer> | |
| Generated by <strong>cpanel_rescue_v3.1.sh</strong> on <strong>$(hostname)</strong> at <strong>$(date)</strong><br> | |
| Community blocklist: <a href="$GIST_URL" target="_blank">$GIST_URL</a> | |
| </footer> | |
| </div></body></html> | |
| HTMLEOF | |
| } | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| # MAIN | |
| # ═══════════════════════════════════════════════════════════════════════════════ | |
| clear | |
| init_crit_files | |
| echo -e "${B}${BOLD} +======================================================================+" | |
| echo -e " | cPANEL SERVER RESCUE v3.1 -- CVE-2026-41940 |" | |
| echo -e " | Self-IP Detection + Community Intelligence + HTML Report |" | |
| echo -e " +======================================================================+${NC}" | |
| echo -e " ${DIM}Host: $(hostname) | Kernel: $(uname -r) | $(date)${NC}" | |
| echo -e " ${DIM}Report dir: $REPORT_DIR${NC}" | |
| echo -e " ${DIM}Gist: $GIST_URL${NC}" | |
| echo "" | |
| [ "$AUDIT_ONLY" -eq 1 ] && echo -e " ${Y}[AUDIT ONLY -- no changes will be made]${NC}" | |
| [ "$AUTO_MODE" -eq 1 ] && echo -e " ${Y}[AUTO MODE -- all fixes applied automatically]${NC}" | |
| pause | |
| # ── STEP 1 ── Self IPs + Community list | |
| progress "DETECT SELF IPs & LOAD COMMUNITY LIST" | |
| detect_self_ips | |
| load_community_ips | |
| pause | |
| # ── STEP 2 ── Sessions | |
| progress "ANALYSE EXPLOITED cPANEL SESSIONS" | |
| SESSIONS_DIR="/var/cpanel/sessions" | |
| CPANEL_LOG="/usr/local/cpanel/logs/access_log" | |
| { echo "=== COMPROMISED SESSION DETAIL ==="; echo "Date: $(date)"; echo ""; } >> "$CRIT_SESSIONS" | |
| if [ -d "$SESSIONS_DIR/raw" ]; then | |
| for sf in "$SESSIONS_DIR"/raw/*; do | |
| [ -f "$sf" ] || continue | |
| sname=$(basename "$sf") | |
| grep -q '^token_denied=' "$sf" 2>/dev/null || continue | |
| grep -q '^cp_security_token=' "$sf" 2>/dev/null || continue | |
| TOKEN=$(grep '^cp_security_token=' "$sf" | head -1 | cut -d= -f2) | |
| ORIGIN=$(grep '^origin_as_string=' "$sf" | head -1 | cut -d= -f2-) | |
| IP=$(echo "$ORIGIN" | grep -oP 'address=\K[\d.]+') | |
| USED=$(grep -a "$TOKEN" "$CPANEL_LOG" 2>/dev/null | grep " 200 " | head -3) | |
| IS_BAD=0; grep -q 'method=badpass' "$sf" 2>/dev/null && IS_BAD=1 | |
| BREACH=0; [ "$IS_BAD" -eq 1 ] && [ -n "$USED" ] && BREACH=1 | |
| if [ "$BREACH" -eq 1 ]; then | |
| crit_to "$CRIT_SESSIONS" "CONFIRMED BREACH -- Session: $sname -- IP: $IP" | |
| det "Token: $TOKEN" | |
| det "Origin: $ORIGIN" | |
| echo " HTTP 200 hits:" >> "$CRIT_SESSIONS" | |
| echo "$USED" >> "$CRIT_SESSIONS" | |
| COMPROMISED_SESSIONS+=("$sname") | |
| [ -n "$IP" ] && ATTACKER_IPS+=("$IP") | |
| cp "$sf" "$REPORT_DIR/evidence/session_${sname}.txt" 2>/dev/null | |
| show_ip_intel "$IP" "1" | |
| elif [ "$IS_BAD" -eq 1 ]; then | |
| warn_to "$CRIT_SESSIONS" "Exploit attempt (no 200 hit) -- $sname -- IP: $IP" | |
| [ -n "$IP" ] && ATTACKER_IPS+=("$IP") | |
| [ -n "$IP" ] && show_ip_intel "$IP" "0" | |
| fi | |
| grep -qzP '(?m)^pass=.*\n.' "$sf" 2>/dev/null && \ | |
| crit_to "$CRIT_SESSIONS" "CRLF INJECTION ARTIFACT: $sf" | |
| grep -q '^tfa_verified=1' "$sf" 2>/dev/null && \ | |
| ! grep -qE 'method=handle_form_login|method=create_user_session' "$sf" 2>/dev/null && \ | |
| warn_to "$CRIT_SESSIONS" "tfa_verified with no valid origin: $sf" | |
| done | |
| fi | |
| # Scrape attempt IPs (uses process substitution to avoid subshell array loss) | |
| if [ -f "$CPANEL_LOG" ]; then | |
| _log "\n${W}All IPs that hit the exploit endpoint:${NC}" | |
| while read -r cnt ip; do | |
| [ -z "$ip" ] && continue | |
| # Skip self IPs - failed WHM logins look identical to exploit attempts at this layer | |
| if [ "$(is_self_ip "$ip")" = "1" ]; then | |
| info "Skipping self IP $ip ($cnt requests -- likely your own failed logins)" | |
| continue | |
| fi | |
| ATTACKER_IPS+=("$ip") | |
| warn_to "$CRIT_ATTACKERS" "$ip -- $cnt login endpoint requests" | |
| done < <(grep -aE 'POST /login/.*login_only=1' "$CPANEL_LOG" 2>/dev/null | \ | |
| awk '{print $1}' | sort | uniq -c | sort -rn) | |
| fi | |
| mapfile -t ATTACKER_IPS < <(printf '%s\n' "${ATTACKER_IPS[@]}" | sort -u | grep -v '^$') | |
| # Run intel for IPs not yet processed | |
| for ip in "${ATTACKER_IPS[@]}"; do | |
| already=0 | |
| for k in "${KNOWN_IPS[@]}" "${NEW_IPS[@]}"; do [ "$k" = "$ip" ] && already=1 && break; done | |
| [ $already -eq 0 ] && show_ip_intel "$ip" "0" | |
| done | |
| { echo ""; echo "TOTAL BREACHED SESSIONS: ${#COMPROMISED_SESSIONS[@]}"; echo "TOTAL ATTACKER IPs: ${#ATTACKER_IPS[@]}"; } >> "$CRIT_SESSIONS" | |
| ok "Sessions scanned. Confirmed: ${#CONFIRMED_BREACH_IPS[@]} Attempts: ${#ATTEMPT_ONLY_IPS[@]}" | |
| pause | |
| # ── STEP 3 ── Timeline | |
| progress "RECONSTRUCT ATTACK TIMELINE" | |
| { echo "=== ATTACK TIMELINE ==="; echo "Date: $(date)"; echo ""; } >> "$CRIT_TIMELINE" | |
| for ip in "${ATTACKER_IPS[@]}"; do | |
| [ "$(is_self_ip "$ip")" = "1" ] && continue | |
| _log "\n${R}${BOLD} -- Attacker: $ip --${NC}" | |
| { echo ""; echo "== $ip =="; } >> "$CRIT_TIMELINE" | |
| if [ -f "$CPANEL_LOG" ]; then | |
| grep -a "^$ip " "$CPANEL_LOG" 2>/dev/null | while IFS= read -r line; do | |
| TS=$(echo "$line" | awk '{print $4}' | tr -d '[') | |
| REQ=$(echo "$line" | awk '{print $6,$7,$8}') | |
| CODE=$(echo "$line" | awk '{print $9}') | |
| UA=$(echo "$line" | grep -oP '"[^"]*"' | sed -n '2p' | tr -d '"') | |
| if [ "$CODE" = "200" ]; then | |
| _log " ${G}[$TS] $CODE $REQ${NC}" | |
| _log " ${DIM} UA: $UA${NC}" | |
| echo " SUCCESS [$TS] $CODE $REQ UA:$UA" >> "$CRIT_TIMELINE" | |
| else | |
| _log " ${DIM}[$TS] $CODE $REQ${NC}" | |
| echo " [$TS] $CODE $REQ" >> "$CRIT_TIMELINE" | |
| fi | |
| done | |
| grep -a "^$ip " "$CPANEL_LOG" 2>/dev/null | grep "json-api\|xml-api" | grep " 200 " | \ | |
| while IFS= read -r line; do | |
| TS=$(echo "$line" | awk '{print $4}' | tr -d '[') | |
| API=$(echo "$line" | grep -oP '/(json|xml)-api/\K[^? ]+') | |
| crit_to "$CRIT_TIMELINE" "WHM API CALL as root: [$TS] $API" | |
| done | |
| fi | |
| done | |
| # Suspicious SSH logins | |
| { echo ""; echo "== SUSPICIOUS SSH LOGINS =="; } >> "$CRIT_TIMELINE" | |
| last -F 2>/dev/null | grep -v 'reboot\|wtmp\|^$' | while IFS= read -r line; do | |
| ssh_ip=$(echo "$line" | awk '{print $3}') | |
| [ "$(is_self_ip "$ssh_ip")" = "1" ] && continue | |
| [ -z "$ssh_ip" ] && continue | |
| crit_to "$CRIT_TIMELINE" "UNKNOWN SSH IP LOGIN: $line" | |
| done | |
| pause | |
| # ── STEP 4 ── Ransomware | |
| progress "RANSOMWARE DAMAGE ASSESSMENT" | |
| { echo "=== RANSOMWARE ASSESSMENT ==="; echo "Date: $(date)"; echo ""; } >> "$CRIT_RANSOMWARE" | |
| SORRY_LIST="$REPORT_DIR/evidence/all_encrypted_files.txt" | |
| find /home /var/www /usr/local/apache/htdocs -name "*.sorry" 2>/dev/null | tee "$SORRY_LIST" > /dev/null | |
| ENC_COUNT=$(wc -l < "$SORRY_LIST" 2>/dev/null || echo 0) | |
| if [ "$ENC_COUNT" -gt 0 ]; then | |
| crit_to "$CRIT_RANSOMWARE" "$ENC_COUNT FILES ENCRYPTED (.sorry ransomware)" | |
| echo "" >> "$CRIT_RANSOMWARE" | |
| echo "-- By user --" >> "$CRIT_RANSOMWARE" | |
| awk -F/ '/^\/home\// {print $3}' "$SORRY_LIST" | sort | uniq -c | sort -rn | \ | |
| while read -r cnt u; do | |
| echo " $u: $cnt files" >> "$CRIT_RANSOMWARE" | |
| warn_to "$CRIT_RANSOMWARE" "User $u -- $cnt files encrypted" | |
| done | |
| echo "" >> "$CRIT_RANSOMWARE" | |
| echo "-- File types --" >> "$CRIT_RANSOMWARE" | |
| sed 's/\.sorry$//' "$SORRY_LIST" | grep -oP '\.[^./]+$' | sort | uniq -c | sort -rn | head -20 \ | |
| >> "$CRIT_RANSOMWARE" | |
| SSL_ENC=$(grep -cE '\.key\.sorry|\.crt\.sorry' "$SORRY_LIST" 2>/dev/null || echo 0) | |
| [ "$SSL_ENC" -gt 0 ] && crit_to "$CRIT_RANSOMWARE" "SSL PRIVATE KEYS ENCRYPTED: $SSL_ENC files" | |
| PHP_ENC=$(grep -c '\.php\.sorry' "$SORRY_LIST" 2>/dev/null || echo 0) | |
| [ "$PHP_ENC" -gt 0 ] && crit_to "$CRIT_RANSOMWARE" "PHP SOURCE FILES ENCRYPTED: $PHP_ENC files" | |
| echo "" >> "$CRIT_RANSOMWARE" | |
| echo "-- First 30 files --" >> "$CRIT_RANSOMWARE" | |
| head -30 "$SORRY_LIST" >> "$CRIT_RANSOMWARE" | |
| echo "(...full list: $SORRY_LIST)" >> "$CRIT_RANSOMWARE" | |
| else | |
| ok "No .sorry encrypted files found." | |
| echo "No .sorry encrypted files found." >> "$CRIT_RANSOMWARE" | |
| fi | |
| for ext in locked encrypted crypt enc ransom WNCRY; do | |
| c=$(find /home /var/www -name "*.$ext" 2>/dev/null | wc -l) | |
| [ "$c" -gt 0 ] && crit_to "$CRIT_RANSOMWARE" "$c files with .$ext extension" | |
| done | |
| NOTES=$(find / -maxdepth 8 \( -name "HOW_TO*" -o -name "README_DECRYPT*" \ | |
| -o -name "RECOVER_FILE*" -o -name "_HELP*" \) 2>/dev/null | head -5) | |
| [ -n "$NOTES" ] && { | |
| crit_to "$CRIT_RANSOMWARE" "RANSOM NOTES FOUND:" | |
| echo "$NOTES" >> "$CRIT_RANSOMWARE" | |
| echo "$NOTES" | xargs -I{} cp {} "$REPORT_DIR/evidence/" 2>/dev/null | |
| } | |
| pause | |
| # ── STEP 5 ── Backdoors | |
| progress "BACKDOORS, WEBSHELLS & PERSISTENCE" | |
| { echo "=== BACKDOORS & WEBSHELLS ==="; echo "Date: $(date)"; echo ""; } >> "$CRIT_BACKDOORS" | |
| { echo "=== PERSISTENCE ==="; echo "Date: $(date)"; echo ""; } >> "$CRIT_PERSISTENCE" | |
| EXCL='vendor/stripe|vendor/webklex|TeamSpeak3|namecheap|coza_epp|RegistrynetAPI|plesk_key' | |
| SHPAT='(eval\(base64_decode|FilesMan|c99shell|r57shell|WSO\s*[Ss]hell|b374k|weevely|phpspy|assert\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)\b|preg_replace.*\/e.*\$_(GET|POST)\b|system\s*\(\s*\$_(GET|POST)\b|passthru\s*\(\s*\$_(GET|POST)\b)' | |
| while IFS= read -r f; do | |
| echo "$f" | grep -qE "$EXCL" && continue | |
| crit_to "$CRIT_BACKDOORS" "WEBSHELL: $f" | |
| { echo " FILE: $f"; echo " PREVIEW: $(head -3 "$f" 2>/dev/null | tr '\n' '|')"; echo ""; } >> "$CRIT_BACKDOORS" | |
| cp "$f" "$REPORT_DIR/quarantine/shell_$(basename "$f").$(date +%s)" 2>/dev/null | |
| ((REAL_SHELLS++)) | |
| done < <(find /home /var/www /tmp /var/tmp -type f \ | |
| \( -name "*.php" -o -name "*.php5" -o -name "*.phtml" \) 2>/dev/null | \ | |
| xargs grep -lEi "$SHPAT" 2>/dev/null) | |
| [ "$REAL_SHELLS" -eq 0 ] && { ok "No real webshells found."; echo "No webshells found." >> "$CRIT_BACKDOORS"; } | |
| find /tmp /var/tmp /dev/shm -name "*.php" -o -name "*.sh" -o -name "*.py" 2>/dev/null | \ | |
| while read -r f; do crit_to "$CRIT_BACKDOORS" "SCRIPT IN TEMP DIR: $f"; done | |
| find /home /var/www -name ".*" -type f \( -name "*.php" -o -name "*.sh" -o -name "*.py" \) 2>/dev/null | \ | |
| grep -vE '/vendor/|/\.trash/|\.php-cs-fixer|\.phpstorm\.meta|\.phpunit\.cache|\.php_cs|\.scrutinizer' | \ | |
| head -10 | while read -r f; do warn_to "$CRIT_BACKDOORS" "HIDDEN SCRIPT: $f"; done | |
| for bfile in /root/.bashrc /root/.bash_profile /root/.profile; do | |
| [ -f "$bfile" ] || continue | |
| SUSP=$(grep -En '(curl|wget)\s+http|\/tmp\/|\/dev\/shm|base64\s+-d|nc\s+-e|bash\s+-i' "$bfile" 2>/dev/null) | |
| [ -n "$SUSP" ] && crit_to "$CRIT_PERSISTENCE" "BACKDOOR IN $bfile: $SUSP" | |
| done | |
| awk -F: '($3==0 && $1!="root"){print $1}' /etc/passwd | \ | |
| while read -r u; do crit_to "$CRIT_USERS" "NON-ROOT UID 0 USER: $u"; done | |
| find /root /home -name "authorized_keys" -newer /etc/passwd -type f 2>/dev/null | \ | |
| while read -r kf; do | |
| crit_to "$CRIT_USERS" "RECENTLY MODIFIED authorized_keys: $kf" | |
| cp "$kf" "$REPORT_DIR/evidence/" 2>/dev/null | |
| done | |
| ss -tnp 2>/dev/null | grep -E ':4444\b|:1337\b|:31337\b|:9001\b' | \ | |
| while read -r line; do crit_to "$CRIT_NETWORK" "C2 PORT CONNECTION: $line"; done | |
| # Use -- to separate pattern from options (pattern starts with '-') | |
| ls -la /proc/*/exe 2>/dev/null | grep -E -- '-> .*(/tmp|/shm)' | grep -v deleted | \ | |
| while read -r p; do crit_to "$CRIT_NETWORK" "PROCESS IN TEMP DIR: $p"; done | |
| [ -s /etc/ld.so.preload ] && { | |
| crit_to "$CRIT_INTEGRITY" "LD_PRELOAD ROOTKIT INDICATOR:" | |
| cat /etc/ld.so.preload >> "$CRIT_INTEGRITY" | |
| } | |
| grep -vE '^#|^$|localhost|127\.|::1|^255\.' /etc/hosts 2>/dev/null | \ | |
| grep -iE 'google|cloudflare|update|cpanel' | \ | |
| while read -r line; do crit_to "$CRIT_INTEGRITY" "DNS HIJACK IN /etc/hosts: $line"; done | |
| ss -tlnp > "$REPORT_DIR/evidence/listening_ports.txt" 2>/dev/null | |
| ss -tnp > "$REPORT_DIR/evidence/connections.txt" 2>/dev/null | |
| ps auxf > "$REPORT_DIR/evidence/processes.txt" 2>/dev/null | |
| last -F > "$REPORT_DIR/evidence/login_history.txt" 2>/dev/null | |
| pause | |
| # ── STEP 6 ── Fixes | |
| progress "APPLY REMEDIATION FIXES" | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Helper: interactive IP list editor | |
| # Lets the user review, add, or remove IPs before they are used. | |
| # $1 = mode label ("BLOCK" or "ALLOW") | |
| # Reads array name from $2, writes final list back to that array. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| edit_ip_list() { | |
| local label="$1" | |
| local -n list_ref="$2" # nameref to caller's array | |
| [ "$AUTO_MODE" -eq 1 ] && return 0 # auto = no editing | |
| while true; do | |
| echo "" | |
| _log "${W}${BOLD} Current $label list (${#list_ref[@]} IP(s)):${NC}" | |
| if [ "${#list_ref[@]}" -eq 0 ]; then | |
| _log " ${DIM} (empty)${NC}" | |
| else | |
| local idx=1 | |
| for ip in "${list_ref[@]}"; do | |
| if [ "$label" = "BLOCK" ]; then | |
| _log " ${R}$idx) $ip${NC}" | |
| else | |
| _log " ${G}$idx) $ip${NC}" | |
| fi | |
| ((idx++)) | |
| done | |
| fi | |
| echo "" | |
| echo -e " ${C}Options:${NC}" | |
| echo -e " ${W}a${NC}) ${C}Add an IP${NC}" | |
| echo -e " ${W}r${NC}) ${C}Remove an IP (by number)${NC}" | |
| echo -e " ${W}c${NC}) ${C}Clear the entire list${NC}" | |
| echo -e " ${W}d${NC}) ${G}Done -- proceed with this list${NC}" | |
| echo -e " ${W}s${NC}) ${Y}Skip this step entirely${NC}" | |
| echo -ne " ${C}Choice [a/r/c/d/s]: ${NC}" | |
| read -r choice | |
| case "$choice" in | |
| a|A) | |
| echo -ne " ${C}IP to add (or several space-separated): ${NC}" | |
| read -r new_ips | |
| for new_ip in $new_ips; do | |
| if [[ "$new_ip" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; then | |
| # Avoid duplicates | |
| local exists=0 | |
| for existing in "${list_ref[@]}"; do | |
| [ "$existing" = "$new_ip" ] && exists=1 && break | |
| done | |
| if [ "$exists" = "1" ]; then | |
| _log " ${Y} $new_ip already in list -- skipped${NC}" | |
| else | |
| list_ref+=("$new_ip") | |
| _log " ${G} Added: $new_ip${NC}" | |
| fi | |
| else | |
| _log " ${R} Invalid IP format: $new_ip${NC}" | |
| fi | |
| done | |
| ;; | |
| r|R) | |
| if [ "${#list_ref[@]}" -eq 0 ]; then | |
| _log " ${Y} List is empty.${NC}" | |
| continue | |
| fi | |
| echo -ne " ${C}Number to remove (1-${#list_ref[@]}): ${NC}" | |
| read -r num | |
| if [[ "$num" =~ ^[0-9]+$ ]] && [ "$num" -ge 1 ] && [ "$num" -le "${#list_ref[@]}" ]; then | |
| local removed="${list_ref[$((num-1))]}" | |
| unset 'list_ref[num-1]' | |
| list_ref=("${list_ref[@]}") # re-index | |
| _log " ${G} Removed: $removed${NC}" | |
| else | |
| _log " ${R} Invalid number.${NC}" | |
| fi | |
| ;; | |
| c|C) | |
| echo -ne " ${Y}Clear all? Type 'YES' to confirm: ${NC}" | |
| read -r confirm | |
| if [ "$confirm" = "YES" ]; then | |
| list_ref=() | |
| _log " ${G} List cleared.${NC}" | |
| fi | |
| ;; | |
| d|D|"") | |
| return 0 | |
| ;; | |
| s|S) | |
| list_ref=() | |
| return 1 | |
| ;; | |
| *) | |
| _log " ${R} Unknown option.${NC}" | |
| ;; | |
| esac | |
| done | |
| } | |
| # ── Build initial BLOCK list (auto-detected attackers, minus self IPs) ── | |
| BLOCK_IPS=() | |
| for ip in "${ATTACKER_IPS[@]}"; do | |
| [ "$(is_self_ip "$ip")" = "1" ] && continue | |
| BLOCK_IPS+=("$ip") | |
| done | |
| # ── Build initial ALLOW list (self IPs detected) ── | |
| ALLOW_IPS=("${SELF_IPS[@]}") | |
| _log "\n${W}${BOLD} REVIEW IP LISTS BEFORE APPLYING FIREWALL CHANGES${NC}" | |
| _log "${DIM} You can add IPs (e.g. office VPN, second admin's home IP, monitoring server)${NC}" | |
| _log "${DIM} or remove any auto-detected IPs that you don't want included.${NC}" | |
| # ── Block list editor ── | |
| divider | |
| _log "${R}${BOLD} Step 6a -- IPs to BLOCK${NC}" | |
| _log "${DIM} Auto-detected attacker IPs (excluding your own) are pre-loaded.${NC}" | |
| edit_ip_list "BLOCK" BLOCK_IPS | |
| proceed_block=$? | |
| if [ $proceed_block -eq 0 ] && [ "${#BLOCK_IPS[@]}" -gt 0 ]; then | |
| if ask "Apply BLOCK rules for these ${#BLOCK_IPS[@]} IP(s)?"; then | |
| for ip in "${BLOCK_IPS[@]}"; do | |
| [ -z "$ip" ] && continue | |
| # Safety: never block a self-IP even if user added one by mistake | |
| if [ "$(is_self_ip "$ip")" = "1" ]; then | |
| _log " ${Y} SKIPPED $ip -- this is your own IP!${NC}" | |
| continue | |
| fi | |
| run_cmd "Block inbound $ip" "iptables -I INPUT -s '$ip' -j DROP 2>/dev/null" | |
| run_cmd "Block outbound $ip" "iptables -I OUTPUT -d '$ip' -j DROP 2>/dev/null" | |
| done | |
| run_cmd "Save iptables" \ | |
| "iptables-save > /etc/iptables/rules.v4 2>/dev/null || iptables-save > /etc/sysconfig/iptables 2>/dev/null" | |
| else | |
| action_skip "IP blocking cancelled" | |
| fi | |
| else | |
| action_skip "IP blocking skipped (empty list or user skip)" | |
| fi | |
| # ── Allow list editor + WHM port restriction ── | |
| divider | |
| _log "${G}${BOLD} Step 6b -- IPs ALLOWED to access WHM/cPanel ports${NC}" | |
| _log "${DIM} Auto-detected self IPs are pre-loaded. Add other admin IPs here.${NC}" | |
| _log "${Y} WARNING: if this list is empty, NOBODY will be able to reach WHM!${NC}" | |
| edit_ip_list "ALLOW" ALLOW_IPS | |
| proceed_allow=$? | |
| if [ $proceed_allow -eq 0 ]; then | |
| if [ "${#ALLOW_IPS[@]}" -eq 0 ]; then | |
| _log "${R}${BOLD} Allow list is empty -- you would lock yourself out!${NC}" | |
| action_skip "WHM port restriction cancelled (empty allow list)" | |
| elif ask "Block WHM/cPanel ports for everyone EXCEPT these ${#ALLOW_IPS[@]} IP(s)?"; then | |
| # 1. Insert ACCEPT rules first (top of chain) | |
| for allow_ip in "${ALLOW_IPS[@]}"; do | |
| for p in 2083 2087 2082 2086 2077 2078 2095 2096; do | |
| run_cmd "Allow $allow_ip on port $p" \ | |
| "iptables -I INPUT -p tcp --dport $p -s '$allow_ip' -j ACCEPT 2>/dev/null" | |
| done | |
| done | |
| # 2. Append DROP rules (bottom of chain) | |
| for p in 2083 2087 2082 2086 2077 2078 2095 2096; do | |
| run_cmd "Drop port $p (default)" "iptables -A INPUT -p tcp --dport $p -j DROP 2>/dev/null" | |
| done | |
| run_cmd "Save iptables" \ | |
| "iptables-save > /etc/iptables/rules.v4 2>/dev/null || iptables-save > /etc/sysconfig/iptables 2>/dev/null" | |
| _log "${G} WHM/cPanel access now restricted to: ${ALLOW_IPS[*]}${NC}" | |
| else | |
| action_skip "WHM port restriction cancelled" | |
| fi | |
| else | |
| action_skip "WHM port restriction skipped" | |
| fi | |
| if ask "Purge ALL cPanel sessions?"; then | |
| # Try multiple known paths for cPanel session purge | |
| if [ -x /scripts/purge_dead_sessions ]; then PURGE=/scripts/purge_dead_sessions | |
| elif [ -x /usr/local/cpanel/scripts/purge_dead_sessions ]; then PURGE=/usr/local/cpanel/scripts/purge_dead_sessions | |
| elif [ -x /usr/local/cpanel/bin/purge_dead_sessions ]; then PURGE=/usr/local/cpanel/bin/purge_dead_sessions | |
| else PURGE=""; fi | |
| if [ -n "$PURGE" ]; then | |
| run_cmd "Purge sessions ($PURGE)" "$PURGE 2>/dev/null" | |
| else | |
| info "purge_dead_sessions script not found -- clearing files manually." | |
| fi | |
| run_cmd "Clear session raw" "rm -rf /var/cpanel/sessions/raw/* 2>/dev/null" | |
| run_cmd "Clear session preauth" "rm -rf /var/cpanel/sessions/preauth/* 2>/dev/null" | |
| else | |
| action_skip "Session purge skipped" | |
| fi | |
| if ask "Patch cPanel now (/scripts/upcp --force)?"; then | |
| _log "${Y} Patching -- may take 5-15 minutes...${NC}" | |
| run_cmd "cPanel update" "/scripts/upcp --force" | |
| run_cmd "Restart cpsrvd" "service cpanel restart 2>/dev/null" | |
| else | |
| action_skip "cPanel NOT patched -- server still vulnerable!" | |
| fi | |
| if ask "Change root password now?"; then | |
| [ "$AUDIT_ONLY" -eq 0 ] && passwd root | |
| ACTIONS_TAKEN+=("Root password changed") | |
| else | |
| action_skip "Root password NOT changed" | |
| fi | |
| if ask "Disable PermitRootLogin in sshd?"; then | |
| run_cmd "Disable root SSH login" \ | |
| "sed -i 's/^PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config && \ | |
| grep -q '^PermitRootLogin' /etc/ssh/sshd_config || echo 'PermitRootLogin no' >> /etc/ssh/sshd_config && \ | |
| systemctl restart sshd 2>/dev/null" | |
| else | |
| action_skip "Root SSH login still permitted" | |
| fi | |
| pause | |
| # ── STEP 7 ── Community summary | |
| progress "COMMUNITY IP REPORT" | |
| _log "" | |
| divider | |
| _log "${MAG_BG}${W} COMMUNITY BLOCKLIST SUMMARY${NC}" | |
| divider | |
| _log " Gist: $GIST_URL" | |
| _log " IPs in community list : ${#COMMUNITY_IPS[@]}" | |
| [ ${#COMMUNITY_CONFIRMED[@]} -gt 0 ] && \ | |
| _log " Confirmed section: ${#COMMUNITY_CONFIRMED[@]}, Attempts section: ${#COMMUNITY_ATTEMPTS[@]}" | |
| _log " Known from this scan : ${#KNOWN_IPS[@]}" | |
| _log " NEW (unreported) : ${#NEW_IPS[@]}" | |
| _log " Confirmed breaches: ${#CONFIRMED_BREACH_IPS[@]}" | |
| _log " Attempts only: ${#ATTEMPT_ONLY_IPS[@]}" | |
| _log "" | |
| # Self-IP collision warning | |
| for self_ip in "${SELF_IPS[@]}"; do | |
| if [ "$(in_community "$self_ip")" = "1" ]; then | |
| _log " ${YEL_BG}${BOLD} WARNING: Your IP $self_ip is in the community list! ${NC}" | |
| _log " ${Y} Other admins blocking this list will reject your connections.${NC}" | |
| _log " ${Y} Edit gist to remove: $GIST_URL${NC}" | |
| fi | |
| done | |
| _log "" | |
| _log "${G}Known attacker IPs (in community list):${NC}" | |
| for ip in "${KNOWN_IPS[@]}"; do | |
| sec=$(community_section "$ip") | |
| if [ "$sec" = "UNKNOWN" ]; then | |
| _log " ${MAG_BG}${W} KNOWN ${NC} $ip" | |
| else | |
| _log " ${MAG_BG}${W} KNOWN-$sec ${NC} $ip" | |
| fi | |
| done | |
| [ "${#KNOWN_IPS[@]}" -eq 0 ] && _log " (none from this scan)" | |
| _log "" | |
| _log "${Y}${BOLD}NEW attacker IPs -- please report:${NC}" | |
| # Separate confirmed from attempts in NEW IPs | |
| NEW_CONFIRMED=() | |
| NEW_ATTEMPTS=() | |
| for ip in "${NEW_IPS[@]}"; do | |
| is_b=0 | |
| for b in "${CONFIRMED_BREACH_IPS[@]}"; do [ "$b" = "$ip" ] && is_b=1 && break; done | |
| [ "$is_b" = "1" ] && NEW_CONFIRMED+=("$ip") || NEW_ATTEMPTS+=("$ip") | |
| done | |
| if [ ${#NEW_CONFIRMED[@]} -gt 0 ]; then | |
| _log " ${R}${BOLD}Confirmed breaches (high priority):${NC}" | |
| for ip in "${NEW_CONFIRMED[@]}"; do | |
| _log " ${RED_BG}${W} CONFIRMED ${NC} $ip" | |
| done | |
| fi | |
| if [ ${#NEW_ATTEMPTS[@]} -gt 0 ]; then | |
| _log " ${Y}Attempts only:${NC}" | |
| for ip in "${NEW_ATTEMPTS[@]}"; do | |
| _log " ${YEL_BG} ATTEMPT ${NC} $ip" | |
| done | |
| fi | |
| [ "${#NEW_IPS[@]}" -eq 0 ] && _log " ${G}All attacker IPs already in community list!${NC}" | |
| # Ready-to-paste file | |
| NEW_IP_FILE="$REPORT_DIR/critical/NEW_IPs_to_report.txt" | |
| { | |
| echo "# New attacker IPs -- $(hostname) -- $(date)" | |
| echo "# CVE-2026-41940 cPanel/WHM Auth Bypass" | |
| echo "# Add to: $GIST_URL" | |
| echo "" | |
| if [ ${#NEW_CONFIRMED[@]} -gt 0 ]; then | |
| echo "# === CONFIRMED BREACHES (HTTP 200 with injected token) ===" | |
| for ip in "${NEW_CONFIRMED[@]}"; do echo "$ip"; done | |
| echo "" | |
| fi | |
| if [ ${#NEW_ATTEMPTS[@]} -gt 0 ]; then | |
| echo "# === ATTEMPTS (hit login endpoint, no successful auth) ===" | |
| for ip in "${NEW_ATTEMPTS[@]}"; do echo "$ip"; done | |
| echo "" | |
| fi | |
| echo "# AbuseIPDB report template" | |
| echo "# Categories: 18 (Brute Force), 21 (Web App Attack)" | |
| echo "# Comment: Exploited CVE-2026-41940 cPanel WHM CRLF authentication bypass." | |
| echo "# Source: $(hostname)" | |
| } > "$NEW_IP_FILE" | |
| ok "New IP report saved: $NEW_IP_FILE" | |
| pause | |
| # ── STEP 8 ── Evidence | |
| progress "COLLECT EVIDENCE" | |
| for authlog in /var/log/auth.log /var/log/secure; do | |
| [ -f "$authlog" ] && cp "$authlog" "$REPORT_DIR/evidence/" && info "Saved: $authlog" | |
| done | |
| [ -f "$CPANEL_LOG" ] && { | |
| for ip in "${ATTACKER_IPS[@]}"; do | |
| [ "$(is_self_ip "$ip")" = "1" ] && continue | |
| grep -a "^$ip " "$CPANEL_LOG" 2>/dev/null | |
| done > "$REPORT_DIR/evidence/attacker_requests.txt" | |
| RCOUNT=$(wc -l < "$REPORT_DIR/evidence/attacker_requests.txt") | |
| info "Attacker requests saved: $RCOUNT lines" | |
| } | |
| { | |
| echo "=== SYSTEM INFO ==="; hostname -f; uname -a | |
| echo "=== SELF IPs DETECTED ==="; printf '%s\n' "${SELF_IPS[@]}" | |
| echo "=== cPANEL VERSION ==="; cat /usr/local/cpanel/version 2>/dev/null | |
| echo "=== OPEN PORTS ==="; ss -tlnp | |
| echo "=== SUID FILES ==="; find / -perm /4000 -type f ! -path "/proc/*" ! -path "/sys/*" 2>/dev/null | |
| echo "=== /etc/hosts ==="; cat /etc/hosts | |
| echo "=== SUDOERS ==="; cat /etc/sudoers 2>/dev/null | |
| echo "=== ROOT AUTHORIZED_KEYS ==="; cat /root/.ssh/authorized_keys 2>/dev/null || echo "(none)" | |
| } > "$REPORT_DIR/evidence/system_info.txt" 2>&1 | |
| ok "Evidence collected." | |
| pause | |
| # ── STEP 9-10 ── Reports | |
| progress "GENERATE HTML & TEXT REPORTS" | |
| _log "\n${W}Building HTML report...${NC}" | |
| build_html | |
| ok "HTML report: $HTML_REPORT" | |
| _log "${W}Building plain text report...${NC}" | |
| { | |
| printf '=%.0s' {1..72}; echo "" | |
| echo " cPANEL SECURITY INCIDENT REPORT" | |
| echo " CVE-2026-41940 -- $(hostname) -- $(date)" | |
| printf '=%.0s' {1..72}; echo "" | |
| echo "" | |
| echo "CRITICAL FINDINGS : $CRITICAL_COUNT" | |
| echo "WARNINGS : $WARNING_COUNT" | |
| echo "ATTACKER IPs (total) : ${#ATTACKER_IPS[@]}" | |
| echo " Confirmed breaches : ${#CONFIRMED_BREACH_IPS[@]}" | |
| echo " Attempts only : ${#ATTEMPT_ONLY_IPS[@]}" | |
| echo "BREACHED SESSIONS : ${#COMPROMISED_SESSIONS[@]}" | |
| echo "ENCRYPTED FILES : $ENC_COUNT" | |
| echo "NEW IPs TO REPORT : ${#NEW_IPS[@]}" | |
| echo "SELF IPs (excluded) : ${#SELF_IPS[@]}" | |
| echo "" | |
| for f in "$CRIT_SUMMARY" "$CRIT_ATTACKERS" "$CRIT_TIMELINE" \ | |
| "$CRIT_RANSOMWARE" "$CRIT_BACKDOORS" "$CRIT_USERS" \ | |
| "$CRIT_NETWORK" "$CRIT_PERSISTENCE" "$CRIT_INTEGRITY"; do | |
| [ -f "$f" ] && { echo ""; cat "$f"; } | |
| done | |
| echo "" | |
| printf '=%.0s' {1..72}; echo "" | |
| echo " REPORT FILES: $REPORT_DIR/" | |
| echo " HTML: $HTML_REPORT" | |
| printf '=%.0s' {1..72}; echo "" | |
| } > "$TXT_REPORT" | |
| ok "Text report: $TXT_REPORT" | |
| # ── Final terminal summary | |
| divider | |
| echo -e "\n${W}${BOLD} RESCUE COMPLETE${NC}\n" | |
| echo -e " ${RED_BG}${W} CRITICAL : $CRITICAL_COUNT ${NC}" | |
| echo -e " ${YEL_BG} WARNINGS : $WARNING_COUNT ${NC}" | |
| echo -e " ${RED_BG}${W} CONFIRMED BREACHES: ${#CONFIRMED_BREACH_IPS[@]} ${NC}" | |
| echo -e " ${YEL_BG} ATTEMPTS ONLY: ${#ATTEMPT_ONLY_IPS[@]} ${NC}" | |
| echo -e " ${MAG_BG}${W} KNOWN IPs : ${#KNOWN_IPS[@]} ${NC}" | |
| echo -e " ${YEL_BG} NEW IPs : ${#NEW_IPS[@]} ${NC}" | |
| echo -e " ${GRN_BG}${W} SELF IPs (safe): ${#SELF_IPS[@]} ${NC}" | |
| echo "" | |
| echo -e " ${G}Report files:${NC}" | |
| echo -e " ${C} HTML : $HTML_REPORT${NC}" | |
| echo -e " ${C} Text : $TXT_REPORT${NC}" | |
| echo -e " ${C} Critical: $REPORT_DIR/critical/${NC}" | |
| echo -e " ${C} Evidence: $REPORT_DIR/evidence/${NC}" | |
| divider | |
| if [ "${#NEW_IPS[@]}" -gt 0 ]; then | |
| echo -e "\n ${Y}${BOLD}Please help others -- report these NEW IPs:${NC}" | |
| echo -e " ${C} Gist: $GIST_URL${NC}" | |
| [ ${#NEW_CONFIRMED[@]} -gt 0 ] && { | |
| echo -e " ${R} Confirmed breaches:${NC}" | |
| for ip in "${NEW_CONFIRMED[@]}"; do echo -e " ${R}-> $ip${NC}"; done | |
| } | |
| [ ${#NEW_ATTEMPTS[@]} -gt 0 ] && { | |
| echo -e " ${Y} Attempts only:${NC}" | |
| for ip in "${NEW_ATTEMPTS[@]}"; do echo -e " ${Y}-> $ip${NC}"; done | |
| } | |
| echo "" | |
| fi | |
| # Final self-IP warning | |
| for self_ip in "${SELF_IPS[@]}"; do | |
| if [ "$(in_community "$self_ip")" = "1" ]; then | |
| echo -e " ${YEL_BG}${BOLD} WARNING: Your IP $self_ip is in community list! ${NC}" | |
| echo -e " ${Y} Edit gist to remove: $GIST_URL${NC}" | |
| echo "" | |
| fi | |
| done | |
| echo -e " ${DIM}Open $HTML_REPORT in a browser for the full formatted report.${NC}\n" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ######################################################################## | |
| # Security: CVE-2026-41940 - cPanel & WHM / WP2 Security | |
| # URL: https://support.cpanel.net/hc/en-us/articles/40073787579671-Security-CVE-2026-41940-cPanel-WHM-WP2-Security-Update-04-28-2026 | |
| ######################################################################## | |
| 5.231.194.46 | |
| 31.141.32.199 | |
| 38.146.25.154 | |
| 42.96.61.68 | |
| 45.32.214.150 | |
| 45.61.139.117 | |
| 68.233.238.100 | |
| 80.75.212.14 | |
| 91.186.212.211 | |
| 94.26.106.59 | |
| 102.134.35.221 | |
| 108.61.200.241 | |
| 134.122.28.88 | |
| 136.244.66.225 | |
| 137.184.154.227 | |
| 146.19.216.119 | |
| 157.245.204.205 | |
| 161.35.108.207 | |
| 167.71.81.114 | |
| 167.88.180.94 | |
| 178.237.58.228 | |
| 185.177.238.46 | |
| 205.237.106.117 | |
| 206.81.12.187 | |
| 206.189.95.232 | |
| 206.189.226.93 | |
| 209.97.180.8 | |
| 209.99.191.199 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment