Skip to content

Instantly share code, notes, and snippets.

@sysbitnet
Last active May 14, 2026 19:34
Show Gist options
  • Select an option

  • Save sysbitnet/018ef5466be693a196ce063e820ed2bd to your computer and use it in GitHub Desktop.

Select an option

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
#!/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">&#9888; 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> &nbsp;|&nbsp;
Kernel: <strong>$(uname -r)</strong> &nbsp;|&nbsp;
cPanel: <strong>$cpv</strong> &nbsp;|&nbsp;
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 &nbsp;|&nbsp;
${#KNOWN_IPS[@]} matched known &nbsp;|&nbsp;
<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>&#9888; 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'>&#10003; KNOWN: Confirmed</span>"
elif [ "$sec" = "ATTEMPT" ]; then
status="<span class='bdg p known'>&#10003; KNOWN: Attempt</span>"
else
status="<span class='bdg p known'>&#10003; KNOWN</span>"
fi
act="<span style='color:#888;font-size:12px'>Already listed</span>"
else
status="<span class='bdg o newip'>&#9733; 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 &nearr;</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>&#9888; 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/</\&lt;/g;s/>/\&gt;/g' "$CRIT_TIMELINE" 2>/dev/null)</pre>
</div>
<h2>Session Exploitation Detail</h2>
<div class="card red">
<pre>$(sed 's/</\&lt;/g;s/>/\&gt;/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 &nearr;</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>&#10003;</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>&#10007;</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 &lt;-- This file
SECURITY_REPORT.txt &lt;-- 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"
########################################################################
# 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