|
|
ioc_checksessions_files.sh
- #!/bin/bash
- # Scan for compromised cPanel/WHM session files.
- #
- # Each check function inspects a single session file and, if the IOC
- # matches, calls report_finding with a severity. report_finding records
- # the finding, prints a one-line header, and dumps the session for triage.
- # A summary of all findings (grouped by severity) is printed at the end.
- # Default paths
- SESSIONS_DIR="/var/cpanel/sessions"
- ACCESS_LOG="/usr/local/cpanel/logs/access_log"
- # Flags
- VERBOSE=0
- PURGE=0
- ASSUME_YES=0
- # Parse flags
- while [ $# -gt 0 ]; do
- case "$1" in
- --verbose)
- VERBOSE=1
- ;;
- --purge)
- PURGE=1
- ;;
- --yes|-y)
- ASSUME_YES=1
- ;;
- --sessions-dir)
- SESSIONS_DIR="$2"; shift
- ;;
- --access-log)
- ACCESS_LOG="$2"; shift
- ;;
- --help|-h)
- echo "Usage: $0 [--verbose] [--purge [--yes]] [--sessions-dir DIR] [--access-log FILE]"
- exit 0
- ;;
- *)
- echo "Unknown argument: $1" >&2
- exit 1
- ;;
- esac
- shift
- done
- # Findings accumulator. Each entry: "SEVERITY|session_file|short_message"
- FINDINGS=()
- # Ordered list of unique session files that produced findings.
- FINDING_SESSIONS=()
- # Parallel array: token value associated with each entry in FINDING_SESSIONS
- # (first non-empty token seen for that session).
- FINDING_TOKENS=()
- # Parallel array: highest severity reported for each session (by index)
- FINDING_SEVERITIES=()
- COUNT_CRITICAL=0
- COUNT_WARNING=0
- COUNT_INFO=0
- COUNT_ATTEMPT=0
- # ---------------------------------------------------------------------------
- # Helpers
- # ---------------------------------------------------------------------------
- # Extract the value of a key=value line from a session file (first match).
- # Use: get_field <file> <key>
- get_field() {
- local file="$1" key="$2"
- grep "^${key}=" "$file" | head -1 | cut -d= -f2-
- }
- hr() {
- echo " ----------------------------------------------------------------"
- }
- # Dump full contents of a session file plus related context (matching
- # pre-auth file, access_log hits for the injected token, file metadata).
- # Use: dump_session <session_file> [token_value]
- dump_session() {
- local session_file="$1"
- local token_val="$2"
- local session_name preauth_file
- session_name=$(basename "$session_file")
- preauth_file="$SESSIONS_DIR/preauth/$session_name"
- hr
- echo " SESSION DUMP: $session_file"
- hr
- echo " File metadata:"
- ls -la "$session_file" 2>/dev/null | sed 's/^/ /'
- echo
- echo " Full session contents:"
- sed 's/^/ /' "$session_file"
- echo
- if [ -f "$preauth_file" ]; then
- echo " Matching pre-auth file: $preauth_file"
- ls -la "$preauth_file" 2>/dev/null | sed 's/^/ /'
- echo " Pre-auth contents:"
- sed 's/^/ /' "$preauth_file"
- echo
- fi
- if [ -n "$token_val" ] && [ -r "$ACCESS_LOG" ]; then
- echo " Access log hits for token '$token_val':"
- grep -aF -- "$token_val" "$ACCESS_LOG" | sed 's/^/ /' || echo " (none)"
- echo
- fi
- hr
- }
- # Record a finding and print a brief header line. The full session dump is
- # deferred to print_summary so that multiple findings for the same session
- # are grouped together and the session is only dumped once. When the same
- # session matches multiple IOCs at different severities, only the highest
- # (CRITICAL > WARNING > ATTEMPT > INFO) is kept.
- # Use: report_finding <SEVERITY> <session_file> <token_value> <message>
- # SEVERITY is one of: CRITICAL, WARNING, ATTEMPT, INFO
- report_finding() {
- local severity="$1"
- local session_file="$2"
- local token_val="$3"
- local message="$4"
- # Severity ranking: CRITICAL=3, WARNING=2, ATTEMPT=1, INFO=0
- local sev_rank=0
- case "$severity" in
- CRITICAL) sev_rank=3 ;;
- WARNING) sev_rank=2 ;;
- ATTEMPT) sev_rank=1 ;;
- INFO) sev_rank=0 ;;
- esac
- local i found=0 prev_sev prev_rank
- for i in "${!FINDING_SESSIONS[@]}"; do
- if [ "${FINDING_SESSIONS[$i]}" = "$session_file" ]; then
- found=1
- prev_sev="${FINDING_SEVERITIES[$i]}"
- case "$prev_sev" in
- CRITICAL) prev_rank=3 ;;
- WARNING) prev_rank=2 ;;
- ATTEMPT) prev_rank=1 ;;
- INFO) prev_rank=0 ;;
- esac
- if [ "$sev_rank" -le "$prev_rank" ]; then
- # Existing finding is at least as severe; ignore.
- return
- fi
- # Upgrade in place: replace severity, token, FINDINGS entry,
- # and roll back the previous severity counter so the new one
- # can be incremented below without double-counting.
- FINDING_SEVERITIES[$i]="$severity"
- [ -n "$token_val" ] && FINDING_TOKENS[$i]="$token_val"
- local j
- for j in "${!FINDINGS[@]}"; do
- local entry="${FINDINGS[$j]}"
- local entry_sev="${entry%%|*}"
- local entry_file="${entry#*|}"; entry_file="${entry_file%%|*}"
- if [ "$entry_file" = "$session_file" ] && [ "$entry_sev" = "$prev_sev" ]; then
- FINDINGS[$j]="${severity}|${session_file}|${message}"
- break
- fi
- done
- case "$prev_sev" in
- CRITICAL) COUNT_CRITICAL=$((COUNT_CRITICAL - 1)) ;;
- WARNING) COUNT_WARNING=$((COUNT_WARNING - 1)) ;;
- ATTEMPT) COUNT_ATTEMPT=$((COUNT_ATTEMPT - 1)) ;;
- INFO) COUNT_INFO=$((COUNT_INFO - 1)) ;;
- esac
- break
- fi
- done
- if [ "$found" -eq 0 ]; then
- FINDING_SESSIONS+=("$session_file")
- FINDING_TOKENS+=("$token_val")
- FINDING_SEVERITIES+=("$severity")
- FINDINGS+=("${severity}|${session_file}|${message}")
- fi
- case "$severity" in
- CRITICAL) COUNT_CRITICAL=$((COUNT_CRITICAL + 1)) ;;
- WARNING) COUNT_WARNING=$((COUNT_WARNING + 1)) ;;
- ATTEMPT) COUNT_ATTEMPT=$((COUNT_ATTEMPT + 1)) ;;
- INFO) COUNT_INFO=$((COUNT_INFO + 1)) ;;
- esac
- echo "[${severity}] ${message}: ${session_file}"
- }
- # ---------------------------------------------------------------------------
- # IOC checks
- # ---------------------------------------------------------------------------
- # IOC 0: token_denied counter alongside cp_security_token, in a session
- # whose origin is badpass or otherwise non-benign.
- #
- # - token_denied is incremented by do_token_denied() (cpsrvd.pl:3821)
- # every time a request supplies the wrong cp_security_token. The
- # session is killed on the third failure.
- # - cp_security_token itself is set by newsession() unconditionally
- # while security tokens are enabled (Cpanel/Server.pm:2290), so its
- # presence is NOT by itself an IOC. The pair (token_denied,
- # cp_security_token) tells us only that someone is actively trying
- # tokens against this session.
- #
- # Auth markers (successful_*_auth_with_timestamp, hasroot=1,
- # tfa_verified=1, or an access_log hit on the security token) cannot
- # legitimately appear in a badpass session: the badpass call site
- # (Cpanel/Server.pm:1244-1252) doesn't pass them, hasroot is not even
- # in _SESSION_PARTS (Cpanel/Server.pm:2216-2247), and tfa_verified is
- # forced to 0 unless the caller passes a truthy value (line 2295).
- #
- # Severity tiers:
- # CRITICAL - badpass origin AND auth markers present (post-exploit)
- # INFO - badpass origin, no auth markers, pass looks like a real
- # encoded password (likely an unrelated failed login that
- # happened to receive bad-token traffic)
- # WARNING - origin is neither badpass nor a known-benign method
- # (handle_form_login, create_user_session,
- # handle_auth_transfer); the suspicious origin itself is
- # the IOC
- #
- # Legitimate badpass sessions never carry a pass= line (the badpass
- # call site at Cpanel/Server.pm:1244-1252 does not pass `pass` to
- # newsession, and saveSession only writes pass= when length is
- # non-zero - Cpanel/Session.pm:181). When we see one anyway we defer
- # classification to IOC 5 (check_failed_exploit_attempt), which flags
- # it as ATTEMPT.
- check_token_denied_with_injected_token() {
- local session_file="$1"
- grep -q '^token_denied=' "$session_file" || return
- grep -q '^cp_security_token=' "$session_file" || return
- local token_val external_auth internal_auth hasroot tfa used
- token_val=$(get_field "$session_file" cp_security_token)
- external_auth=$(get_field "$session_file" successful_external_auth_with_timestamp)
- internal_auth=$(get_field "$session_file" successful_internal_auth_with_timestamp)
- hasroot=$(get_field "$session_file" hasroot)
- tfa=$(get_field "$session_file" tfa_verified)
- used=""
- if [ -r "$ACCESS_LOG" ]; then
- used=$(grep -aF -- "$token_val" "$ACCESS_LOG" | grep -m1 " 200 ")
- fi
- local has_auth_markers=0
- if [ -n "$external_auth" ] || [ -n "$internal_auth" ] \
- || [ "$hasroot" = "1" ] || [ "$tfa" = "1" ] || [ -n "$used" ]; then
- has_auth_markers=1
- fi
- if grep -q '^origin_as_string=.*method=badpass' "$session_file"; then
- if [ "$has_auth_markers" -eq 1 ]; then
- report_finding CRITICAL "$session_file" "$token_val" \
- "Exploitation artifact - token_denied with injected cp_security_token (badpass origin, token used)"
- else
- # A pass= line on a badpass session is itself anomalous;
- # defer to IOC 5 (ATTEMPT).
- if grep -q '^pass=' "$session_file"; then
- return
- fi
- report_finding INFO "$session_file" "$token_val" \
- "Possible injected session (badpass origin, no usage observed)"
- fi
- elif grep -q '^origin_as_string=.*method=handle_form_login' "$session_file" || \
- grep -q '^origin_as_string=.*method=create_user_session' "$session_file" || \
- grep -q '^origin_as_string=.*method=handle_auth_transfer' "$session_file"; then
- # Known-benign origins where token_denied + cp_security_token
- # genuinely happens during normal use.
- return
- else
- report_finding WARNING "$session_file" "$token_val" \
- "Suspicious session with token_denied + cp_security_token (non-badpass origin)"
- fi
- }
- # IOC 1: A session that still has its pre-auth marker file but already
- # contains an auth-success timestamp (external or internal).
- #
- # write_session creates $SESSIONS_DIR/preauth/<session_name> when the
- # session is written with needs_auth=1, and removes that marker once
- # needs_auth is cleared on promotion (Cpanel/Session.pm:225-235). A
- # legitimately authenticated session therefore never has both the
- # preauth marker and an auth-success timestamp at the same time.
- #
- # Both successful_external_auth_with_timestamp and
- # successful_internal_auth_with_timestamp are checked: the original
- # poc.py payload injects the external variant; the watchtowr payload
- # (poc/poc_watchtowr.py:35) injects the internal variant.
- check_preauth_with_auth_attrs() {
- local session_file="$1"
- local session_name preauth_file
- session_name=$(basename "$session_file")
- preauth_file="$SESSIONS_DIR/preauth/$session_name"
- [ -f "$preauth_file" ] || return
- local marker
- if grep -qE '^successful_external_auth_with_timestamp=' "$session_file"; then
- marker="successful_external_auth_with_timestamp"
- elif grep -qE '^successful_internal_auth_with_timestamp=' "$session_file"; then
- marker="successful_internal_auth_with_timestamp"
- else
- return
- fi
- report_finding CRITICAL "$session_file" \
- "$(get_field "$session_file" cp_security_token)" \
- "Injected session - ${marker} present in pre-auth session"
- }
- # IOC 2: tfa_verified=1 outside of a legitimate origin method.
- #
- # tfa_verified=1 is set in only two places:
- # - Cpanel/Security/Authn/TwoFactorAuth/Verify.pm:122, after a real
- # TFA token validation succeeds.
- # - Cpanel/Server.pm:2295, when a caller passes tfa_verified=1 to
- # newsession().
- # In both cases the legitimate origin method is one of handle_form_login,
- # create_user_session, or handle_auth_transfer. tfa_verified=1 with any
- # other origin (notably badpass) cannot occur in a benign flow.
- check_tfa_with_bad_origin() {
- local session_file="$1"
- grep -qE '^tfa_verified=1$' "$session_file" || return
- grep -q '^origin_as_string=.*method=handle_form_login' "$session_file" && return
- grep -q '^origin_as_string=.*method=create_user_session' "$session_file" && return
- grep -q '^origin_as_string=.*method=handle_auth_transfer' "$session_file" && return
- report_finding WARNING "$session_file" \
- "$(get_field "$session_file" cp_security_token)" \
- "Session with tfa_verified=1 but suspicious origin"
- }
- # IOC 3: Session file contains a line that is not in `key=value` form.
- #
- # Three structural invariants together guarantee that every legitimate
- # line matches ^[A-Za-z_][A-Za-z0-9_]*=:
- #
- # 1. write_session serializes via Cpanel::Config::FlushConfig::flushConfig
- # with '=' as the separator (Cpanel/Session.pm:221), so the on-disk
- # format is one key=value pair per line.
- # 2. Keys come from a fixed whitelist (_SESSION_PARTS at
- # Cpanel/Server.pm:2216-2247, applied at lines 2268-2270), so they
- # always match the identifier shape above.
- # 3. Cpanel::Session::filter_sessiondata strips \r\n from every value
- # (Cpanel/Session.pm:315) and additionally strips \r\n=, from origin
- # sub-values (line 312), so values can never re-introduce line
- # breaks. The `pass` value is additionally encoded by saveSession
- # (Cpanel/Session.pm:181-189) into either lowercase hex (with-secret
- # via Cpanel::Session::Encoder->encode_data) or the literal prefix
- # `no-ob:` followed by lowercase hex (no-secret via
- # Cpanel::Session::Encoder->hex_encode_only), so it cannot
- # reintroduce structural characters either.
- #
- # Any non-blank line that fails the regex is the footprint of an
- # injection that bypassed these invariants - typically raw payload bytes
- # that didn't form valid key=value pairs. Note: an injection whose
- # smuggled lines DO match key=value (e.g. the watchtowr payload at
- # poc/poc_watchtowr.py:35, which fabricates successful_internal_auth_
- # with_timestamp/user/tfa_verified/hasroot lines) will not trip this
- # check; it is caught by IOC-0 and IOC-4 instead.
- check_malformed_session_line() {
- local session_file="$1"
- # Look for any non-blank line that doesn't start with key=...
- grep -nE -v '^[A-Za-z_][A-Za-z0-9_]*=|^[[:space:]]*$' "$session_file" >/dev/null 2>&1 || return
- report_finding CRITICAL "$session_file" \
- "$(get_field "$session_file" cp_security_token)" \
- "Malformed session line(s) detected (not key=value - newline injection footprint)"
- }
- # IOC 4: badpass origin combined with markers that no legitimate cpsrvd
- # code path writes into a badpass session.
- #
- # The badpass call site (Cpanel/Server.pm:1244-1252) is:
- #
- # $randsession = $self->newsession(
- # 'needs_auth' => 1,
- # %security_token_options, # adds cp_security_token
- # 'origin' => { 'method' => 'badpass' },
- # );
- #
- # %security_token_options is why badpass sessions legitimately carry
- # cp_security_token, but no auth-related options are ever supplied.
- # newsession() filters %OPTS through the _SESSION_PARTS whitelist
- # (Cpanel/Server.pm:2216-2247, applied at lines 2268-2270), so any key
- # not in that whitelist cannot land in the session via newsession at
- # all. Per marker:
- #
- # successful_external_auth_with_timestamp - whitelisted, but the
- # badpass caller doesn't pass it
- # successful_internal_auth_with_timestamp - same
- # tfa_verified=1 - newsession unconditionally writes 0 unless the
- # caller passed a truthy value (Cpanel/Server.pm:2295), and the
- # badpass caller doesn't
- # hasroot=1 - NOT in _SESSION_PARTS, so newsession cannot write it
- # for ANY session. A repo-wide grep finds no caller of
- # Cpanel::Session::Modify->set('hasroot', ...) either: hasroot is
- # never written to a session by legitimate code. Its presence in
- # any session file is conclusive evidence of newline injection
- # (the watchtowr payload at poc/poc_watchtowr.py:35 smuggles
- # hasroot=1 via \r\n in a user-controlled field).
- check_badpass_with_auth_markers() {
- local session_file="$1"
- grep -q '^origin_as_string=.*method=badpass' "$session_file" || return
- local markers=()
- grep -q '^successful_external_auth_with_timestamp=' "$session_file" \
- && markers+=("successful_external_auth_with_timestamp")
- grep -q '^successful_internal_auth_with_timestamp=' "$session_file" \
- && markers+=("successful_internal_auth_with_timestamp")
- grep -qE '^hasroot=1$' "$session_file" && markers+=("hasroot=1")
- grep -qE '^tfa_verified=1$' "$session_file" && markers+=("tfa_verified=1")
- [ "${#markers[@]}" -gt 0 ] || return
- local joined
- joined=$(IFS=,; echo "${markers[*]}")
- report_finding CRITICAL "$session_file" \
- "$(get_field "$session_file" cp_security_token)" \
- "badpass origin combined with authenticated markers ($joined) - impossible in benign flow"
- }
- # IOC 5: Failed exploit attempt - a badpass session that carries a
- # pass= line, a token_denied counter, and no auth markers.
- #
- # A legitimate badpass session is created at Cpanel/Server.pm:1244-1252:
- #
- # $randsession = $self->newsession(
- # 'needs_auth' => 1,
- # %security_token_options,
- # 'origin' => { 'method' => 'badpass' },
- # );
- #
- # %security_token_options carries only cp_security_token,
- # requested_token_at_next_login, and previous_session_user
- # (Cpanel/Server.pm:1205-1226) - never `pass`. saveSession only
- # writes a pass= line when length($session_ref->{pass}) is non-zero
- # (Cpanel/Session.pm:181), so legitimate badpass sessions have no
- # pass= line at all.
- #
- # An exploit that tampers with a user-controlled field on a
- # badpass-bound request leaves a pass= line behind (saveSession
- # encodes it as `<hex>` or `no-ob:<hex>` per Cpanel/Session.pm:181-189,
- # but the format is irrelevant - its presence is the indicator). Combined
- # with token_denied (someone was poking at cp_security_token) and the
- # absence of auth markers (the injection didn't promote - otherwise
- # IOC-0 or IOC-4 fires CRITICAL), this is the signature of a failed
- # exploit attempt.
- check_failed_exploit_attempt() {
- local session_file="$1"
- grep -q '^origin_as_string=.*method=badpass' "$session_file" || return
- grep -q '^token_denied=' "$session_file" || return
- # If auth markers are present, IOC-4 (CRITICAL) handles it.
- grep -q '^successful_internal_auth_with_timestamp=' "$session_file" && return
- grep -q '^successful_external_auth_with_timestamp=' "$session_file" && return
- # Legitimate badpass sessions never carry pass=.
- grep -q '^pass=' "$session_file" || return
- report_finding ATTEMPT "$session_file" "$(get_field "$session_file" cp_security_token)" \
- "Failed exploit attempt (badpass origin, token_denied, no auth markers, anomalous pass= line)"
- }
- # ---------------------------------------------------------------------------
- # Main
- # ---------------------------------------------------------------------------
- scan_sessions() {
- local session_file
- while IFS= read -r -d '' session_file; do
- check_token_denied_with_injected_token "$session_file"
- check_preauth_with_auth_attrs "$session_file"
- check_tfa_with_bad_origin "$session_file"
- check_malformed_session_line "$session_file"
- check_badpass_with_auth_markers "$session_file"
- check_failed_exploit_attempt "$session_file"
- done < <(find "$SESSIONS_DIR/raw" -type f -print0 2>/dev/null)
- }
- print_summary() {
- local total=$((COUNT_CRITICAL + COUNT_WARNING + COUNT_INFO + COUNT_ATTEMPT))
- echo
- echo "================================================================="
- echo " SCAN SUMMARY"
- echo "================================================================="
- echo " CRITICAL findings: $COUNT_CRITICAL"
- echo " WARNING findings: $COUNT_WARNING"
- echo " ATTEMPT findings: $COUNT_ATTEMPT"
- echo " INFO findings: $COUNT_INFO"
- echo " Total : $total"
- echo "-----------------------------------------------------------------"
- if [ "$total" -eq 0 ]; then
- echo "[+] No indicators of compromise found."
- return
- fi
- # --purge has destructive blast radius (live session files for every
- # logged-in user). Require either --yes for non-interactive use, or
- # an explicit "yes" at an attached TTY.
- if [ "$PURGE" -eq 1 ] && [ "$ASSUME_YES" -ne 1 ]; then
- if [ ! -t 0 ]; then
- echo "[ERROR] --purge requires --yes when stdin is not a TTY (cron, pipes, etc)" >&2
- echo " Re-run with --yes to confirm deletion." >&2
- exit 64
- fi
- echo
- echo "About to delete ${#FINDING_SESSIONS[@]} session file(s) plus matching preauth markers."
- local confirm=""
- read -r -p "Type 'yes' to confirm: " confirm
- if [ "$confirm" != "yes" ]; then
- echo "[+] Aborted; no files deleted."
- PURGE=0
- fi
- fi
- # For each unique session, print only the highest-severity finding, then dump/purge as needed.
- local i session token severity message found=0
- for i in "${!FINDING_SESSIONS[@]}"; do
- session="${FINDING_SESSIONS[$i]}"
- token="${FINDING_TOKENS[$i]}"
- severity="${FINDING_SEVERITIES[$i]}"
- found=0
- # Find the first matching finding for this session and severity.
- # Use `read` with three names so the last variable (entry_msg)
- # absorbs any remaining `|` characters - the previous `${var##*|}`
- # form took only the suffix after the LAST `|`, which would
- # silently truncate any future message that contained one.
- for entry in "${FINDINGS[@]}"; do
- local entry_sev entry_file entry_msg
- IFS='|' read -r entry_sev entry_file entry_msg <<< "$entry"
- if [ "$entry_file" = "$session" ] && [ "$entry_sev" = "$severity" ]; then
- message="$entry_msg"
- found=1
- break
- fi
- done
- echo
- echo "================================================================="
- echo " SESSION: $session"
- echo "================================================================="
- echo " Findings:"
- if [ "$found" -eq 1 ]; then
- printf " [%-8s] %s\n" "$severity" "$message"
- else
- printf " [%-8s] %s\n" "$severity" "(no message found)"
- fi
- echo
- if [ "$VERBOSE" -eq 1 ]; then
- dump_session "$session" "$token"
- fi
- if [ "$PURGE" -eq 1 ]; then
- echo " [ACTION] Deleting session file: $session"
- rm -f -- "$session"
- local preauth_marker="$SESSIONS_DIR/preauth/$(basename "$session")"
- if [ -e "$preauth_marker" ]; then
- echo " [ACTION] Deleting preauth marker: $preauth_marker"
- rm -f -- "$preauth_marker"
- fi
- fi
- done
- if [ "$COUNT_CRITICAL" -gt 0 ] || [ "$COUNT_WARNING" -gt 0 ]; then
- echo
- echo "[!] INDICATORS OF COMPROMISE DETECTED - IMMEDIATE ACTION REQUIRED"
- echo " 1. Purge all affected sessions"
- echo " 2. Force password reset for root and all WHM users"
- echo " 3. Audit /var/log/wtmp and WHM access logs for unauthorized access"
- echo " 4. Check for persistence mechanisms (cron, SSH keys, backdoors)"
- fi
- }
- if [ ! -d "$SESSIONS_DIR/raw" ]; then
- echo "[ERROR] Sessions directory not found: $SESSIONS_DIR/raw" >&2
- echo " Pass --sessions-dir DIR to point at a different location" >&2
- echo " (the default is /var/cpanel/sessions)." >&2
- exit 64
- fi
- echo "[*] Scanning session files for injection indicators..."
- scan_sessions
- print_summary
- # Exit codes (for cron / monitoring):
- # 2 - at least one CRITICAL or WARNING finding (compromise indicators)
- # 1 - only ATTEMPT or INFO findings (probing, no confirmed compromise)
- # 0 - clean scan
- if [ "$COUNT_CRITICAL" -gt 0 ] || [ "$COUNT_WARNING" -gt 0 ]; then
- exit 2
- elif [ "$COUNT_ATTEMPT" -gt 0 ] || [ "$COUNT_INFO" -gt 0 ]; then
- exit 1
- fi
- exit 0
複製代碼 如果確認伺服器已被root使用者入侵,則需要將cPanel帳號移轉到已知安全的伺服器上:
https://support.cpanel.net/hc/en-us/articles/40073787579671-Security-CVE-2026-41940-cPanel-WHM-WP2-Security-Update-04-28-2026?_hsenc=p2ANqtz-_K3OQGhYmp27sKEqzJwRVauXmecf8h-pyDLetmnfoPRRKTnRl7z3H0PC6qKdbQfISSijwZ8JNVQMqVwyze5GYLKN3mYA&_hsmi=416839519&utm_content=416839519&utm_medium=email&utm_source=hs_email
|
|