找回密碼
 註冊
搜索
查看: 902|回復: 0

[AlamLinux] cpanel 偵測腳本尋找入侵指標,並檢查檔案系統中的會話。

[複製鏈接]
發表於 2026-5-4 19:09:46 | 顯示全部樓層 |閱讀模式
Push to Facebook
ioc_checksessions_files.sh


  1. #!/bin/bash
  2. # Scan for compromised cPanel/WHM session files.
  3. #
  4. # Each check function inspects a single session file and, if the IOC
  5. # matches, calls report_finding with a severity. report_finding records
  6. # the finding, prints a one-line header, and dumps the session for triage.
  7. # A summary of all findings (grouped by severity) is printed at the end.


  8. # Default paths
  9. SESSIONS_DIR="/var/cpanel/sessions"
  10. ACCESS_LOG="/usr/local/cpanel/logs/access_log"

  11. # Flags
  12. VERBOSE=0
  13. PURGE=0
  14. ASSUME_YES=0

  15. # Parse flags
  16. while [ $# -gt 0 ]; do
  17.     case "$1" in
  18.         --verbose)
  19.             VERBOSE=1
  20.             ;;
  21.         --purge)
  22.             PURGE=1
  23.             ;;
  24.         --yes|-y)
  25.             ASSUME_YES=1
  26.             ;;
  27.         --sessions-dir)
  28.             SESSIONS_DIR="$2"; shift
  29.             ;;
  30.         --access-log)
  31.             ACCESS_LOG="$2"; shift
  32.             ;;
  33.         --help|-h)
  34.             echo "Usage: $0 [--verbose] [--purge [--yes]] [--sessions-dir DIR] [--access-log FILE]"
  35.             exit 0
  36.             ;;
  37.         *)
  38.             echo "Unknown argument: $1" >&2
  39.             exit 1
  40.             ;;
  41.     esac
  42.     shift
  43. done

  44. # Findings accumulator. Each entry: "SEVERITY|session_file|short_message"
  45. FINDINGS=()
  46. # Ordered list of unique session files that produced findings.
  47. FINDING_SESSIONS=()
  48. # Parallel array: token value associated with each entry in FINDING_SESSIONS
  49. # (first non-empty token seen for that session).
  50. FINDING_TOKENS=()
  51. # Parallel array: highest severity reported for each session (by index)
  52. FINDING_SEVERITIES=()
  53. COUNT_CRITICAL=0
  54. COUNT_WARNING=0
  55. COUNT_INFO=0
  56. COUNT_ATTEMPT=0

  57. # ---------------------------------------------------------------------------
  58. # Helpers
  59. # ---------------------------------------------------------------------------

  60. # Extract the value of a key=value line from a session file (first match).
  61. # Use: get_field <file> <key>
  62. get_field() {
  63.     local file="$1" key="$2"
  64.     grep "^${key}=" "$file" | head -1 | cut -d= -f2-
  65. }

  66. hr() {
  67.     echo "    ----------------------------------------------------------------"
  68. }

  69. # Dump full contents of a session file plus related context (matching
  70. # pre-auth file, access_log hits for the injected token, file metadata).
  71. # Use: dump_session <session_file> [token_value]
  72. dump_session() {
  73.     local session_file="$1"
  74.     local token_val="$2"
  75.     local session_name preauth_file
  76.     session_name=$(basename "$session_file")
  77.     preauth_file="$SESSIONS_DIR/preauth/$session_name"

  78.     hr
  79.     echo "    SESSION DUMP: $session_file"
  80.     hr
  81.     echo "    File metadata:"
  82.     ls -la "$session_file" 2>/dev/null | sed 's/^/      /'
  83.     echo
  84.     echo "    Full session contents:"
  85.     sed 's/^/      /' "$session_file"
  86.     echo

  87.     if [ -f "$preauth_file" ]; then
  88.         echo "    Matching pre-auth file: $preauth_file"
  89.         ls -la "$preauth_file" 2>/dev/null | sed 's/^/      /'
  90.         echo "    Pre-auth contents:"
  91.         sed 's/^/      /' "$preauth_file"
  92.         echo
  93.     fi

  94.     if [ -n "$token_val" ] && [ -r "$ACCESS_LOG" ]; then
  95.         echo "    Access log hits for token '$token_val':"
  96.         grep -aF -- "$token_val" "$ACCESS_LOG" | sed 's/^/      /' || echo "      (none)"
  97.         echo
  98.     fi
  99.     hr
  100. }

  101. # Record a finding and print a brief header line. The full session dump is
  102. # deferred to print_summary so that multiple findings for the same session
  103. # are grouped together and the session is only dumped once. When the same
  104. # session matches multiple IOCs at different severities, only the highest
  105. # (CRITICAL > WARNING > ATTEMPT > INFO) is kept.
  106. # Use: report_finding <SEVERITY> <session_file> <token_value> <message>
  107. # SEVERITY is one of: CRITICAL, WARNING, ATTEMPT, INFO
  108. report_finding() {
  109.     local severity="$1"
  110.     local session_file="$2"
  111.     local token_val="$3"
  112.     local message="$4"

  113.     # Severity ranking: CRITICAL=3, WARNING=2, ATTEMPT=1, INFO=0
  114.     local sev_rank=0
  115.     case "$severity" in
  116.         CRITICAL) sev_rank=3 ;;
  117.         WARNING)  sev_rank=2 ;;
  118.         ATTEMPT)  sev_rank=1 ;;
  119.         INFO)     sev_rank=0 ;;
  120.     esac

  121.     local i found=0 prev_sev prev_rank
  122.     for i in "${!FINDING_SESSIONS[@]}"; do
  123.         if [ "${FINDING_SESSIONS[$i]}" = "$session_file" ]; then
  124.             found=1
  125.             prev_sev="${FINDING_SEVERITIES[$i]}"
  126.             case "$prev_sev" in
  127.                 CRITICAL) prev_rank=3 ;;
  128.                 WARNING)  prev_rank=2 ;;
  129.                 ATTEMPT)  prev_rank=1 ;;
  130.                 INFO)     prev_rank=0 ;;
  131.             esac
  132.             if [ "$sev_rank" -le "$prev_rank" ]; then
  133.                 # Existing finding is at least as severe; ignore.
  134.                 return
  135.             fi
  136.             # Upgrade in place: replace severity, token, FINDINGS entry,
  137.             # and roll back the previous severity counter so the new one
  138.             # can be incremented below without double-counting.
  139.             FINDING_SEVERITIES[$i]="$severity"
  140.             [ -n "$token_val" ] && FINDING_TOKENS[$i]="$token_val"
  141.             local j
  142.             for j in "${!FINDINGS[@]}"; do
  143.                 local entry="${FINDINGS[$j]}"
  144.                 local entry_sev="${entry%%|*}"
  145.                 local entry_file="${entry#*|}"; entry_file="${entry_file%%|*}"
  146.                 if [ "$entry_file" = "$session_file" ] && [ "$entry_sev" = "$prev_sev" ]; then
  147.                     FINDINGS[$j]="${severity}|${session_file}|${message}"
  148.                     break
  149.                 fi
  150.             done
  151.             case "$prev_sev" in
  152.                 CRITICAL) COUNT_CRITICAL=$((COUNT_CRITICAL - 1)) ;;
  153.                 WARNING)  COUNT_WARNING=$((COUNT_WARNING - 1))   ;;
  154.                 ATTEMPT)  COUNT_ATTEMPT=$((COUNT_ATTEMPT - 1))   ;;
  155.                 INFO)     COUNT_INFO=$((COUNT_INFO - 1))         ;;
  156.             esac
  157.             break
  158.         fi
  159.     done

  160.     if [ "$found" -eq 0 ]; then
  161.         FINDING_SESSIONS+=("$session_file")
  162.         FINDING_TOKENS+=("$token_val")
  163.         FINDING_SEVERITIES+=("$severity")
  164.         FINDINGS+=("${severity}|${session_file}|${message}")
  165.     fi

  166.     case "$severity" in
  167.         CRITICAL) COUNT_CRITICAL=$((COUNT_CRITICAL + 1)) ;;
  168.         WARNING)  COUNT_WARNING=$((COUNT_WARNING + 1))   ;;
  169.         ATTEMPT)  COUNT_ATTEMPT=$((COUNT_ATTEMPT + 1))   ;;
  170.         INFO)     COUNT_INFO=$((COUNT_INFO + 1))         ;;
  171.     esac

  172.     echo "[${severity}] ${message}: ${session_file}"
  173. }

  174. # ---------------------------------------------------------------------------
  175. # IOC checks
  176. # ---------------------------------------------------------------------------

  177. # IOC 0: token_denied counter alongside cp_security_token, in a session
  178. # whose origin is badpass or otherwise non-benign.
  179. #
  180. # - token_denied is incremented by do_token_denied() (cpsrvd.pl:3821)
  181. #   every time a request supplies the wrong cp_security_token. The
  182. #   session is killed on the third failure.
  183. # - cp_security_token itself is set by newsession() unconditionally
  184. #   while security tokens are enabled (Cpanel/Server.pm:2290), so its
  185. #   presence is NOT by itself an IOC. The pair (token_denied,
  186. #   cp_security_token) tells us only that someone is actively trying
  187. #   tokens against this session.
  188. #
  189. # Auth markers (successful_*_auth_with_timestamp, hasroot=1,
  190. # tfa_verified=1, or an access_log hit on the security token) cannot
  191. # legitimately appear in a badpass session: the badpass call site
  192. # (Cpanel/Server.pm:1244-1252) doesn't pass them, hasroot is not even
  193. # in _SESSION_PARTS (Cpanel/Server.pm:2216-2247), and tfa_verified is
  194. # forced to 0 unless the caller passes a truthy value (line 2295).
  195. #
  196. # Severity tiers:
  197. #   CRITICAL - badpass origin AND auth markers present (post-exploit)
  198. #   INFO     - badpass origin, no auth markers, pass looks like a real
  199. #              encoded password (likely an unrelated failed login that
  200. #              happened to receive bad-token traffic)
  201. #   WARNING  - origin is neither badpass nor a known-benign method
  202. #              (handle_form_login, create_user_session,
  203. #              handle_auth_transfer); the suspicious origin itself is
  204. #              the IOC
  205. #
  206. # Legitimate badpass sessions never carry a pass= line (the badpass
  207. # call site at Cpanel/Server.pm:1244-1252 does not pass `pass` to
  208. # newsession, and saveSession only writes pass= when length is
  209. # non-zero - Cpanel/Session.pm:181). When we see one anyway we defer
  210. # classification to IOC 5 (check_failed_exploit_attempt), which flags
  211. # it as ATTEMPT.
  212. check_token_denied_with_injected_token() {
  213.     local session_file="$1"

  214.     grep -q '^token_denied='      "$session_file" || return
  215.     grep -q '^cp_security_token=' "$session_file" || return

  216.     local token_val external_auth internal_auth hasroot tfa used
  217.     token_val=$(get_field      "$session_file" cp_security_token)
  218.     external_auth=$(get_field  "$session_file" successful_external_auth_with_timestamp)
  219.     internal_auth=$(get_field  "$session_file" successful_internal_auth_with_timestamp)
  220.     hasroot=$(get_field        "$session_file" hasroot)
  221.     tfa=$(get_field            "$session_file" tfa_verified)
  222.     used=""
  223.     if [ -r "$ACCESS_LOG" ]; then
  224.         used=$(grep -aF -- "$token_val" "$ACCESS_LOG" | grep -m1 " 200 ")
  225.     fi

  226.     local has_auth_markers=0
  227.     if [ -n "$external_auth" ] || [ -n "$internal_auth" ] \
  228.        || [ "$hasroot" = "1" ] || [ "$tfa" = "1" ] || [ -n "$used" ]; then
  229.         has_auth_markers=1
  230.     fi

  231.     if grep -q '^origin_as_string=.*method=badpass' "$session_file"; then
  232.         if [ "$has_auth_markers" -eq 1 ]; then
  233.             report_finding CRITICAL "$session_file" "$token_val" \
  234.                 "Exploitation artifact - token_denied with injected cp_security_token (badpass origin, token used)"
  235.         else
  236.             # A pass= line on a badpass session is itself anomalous;
  237.             # defer to IOC 5 (ATTEMPT).
  238.             if grep -q '^pass=' "$session_file"; then
  239.                 return
  240.             fi
  241.             report_finding INFO "$session_file" "$token_val" \
  242.                 "Possible injected session (badpass origin, no usage observed)"
  243.         fi
  244.     elif grep -q '^origin_as_string=.*method=handle_form_login' "$session_file" || \
  245.          grep -q '^origin_as_string=.*method=create_user_session' "$session_file" || \
  246.          grep -q '^origin_as_string=.*method=handle_auth_transfer' "$session_file"; then
  247.         # Known-benign origins where token_denied + cp_security_token
  248.         # genuinely happens during normal use.
  249.         return
  250.     else
  251.         report_finding WARNING "$session_file" "$token_val" \
  252.             "Suspicious session with token_denied + cp_security_token (non-badpass origin)"
  253.     fi
  254. }

  255. # IOC 1: A session that still has its pre-auth marker file but already
  256. # contains an auth-success timestamp (external or internal).
  257. #
  258. # write_session creates $SESSIONS_DIR/preauth/<session_name> when the
  259. # session is written with needs_auth=1, and removes that marker once
  260. # needs_auth is cleared on promotion (Cpanel/Session.pm:225-235). A
  261. # legitimately authenticated session therefore never has both the
  262. # preauth marker and an auth-success timestamp at the same time.
  263. #
  264. # Both successful_external_auth_with_timestamp and
  265. # successful_internal_auth_with_timestamp are checked: the original
  266. # poc.py payload injects the external variant; the watchtowr payload
  267. # (poc/poc_watchtowr.py:35) injects the internal variant.
  268. check_preauth_with_auth_attrs() {
  269.     local session_file="$1"
  270.     local session_name preauth_file
  271.     session_name=$(basename "$session_file")
  272.     preauth_file="$SESSIONS_DIR/preauth/$session_name"

  273.     [ -f "$preauth_file" ] || return

  274.     local marker
  275.     if grep -qE '^successful_external_auth_with_timestamp=' "$session_file"; then
  276.         marker="successful_external_auth_with_timestamp"
  277.     elif grep -qE '^successful_internal_auth_with_timestamp=' "$session_file"; then
  278.         marker="successful_internal_auth_with_timestamp"
  279.     else
  280.         return
  281.     fi

  282.     report_finding CRITICAL "$session_file" \
  283.         "$(get_field "$session_file" cp_security_token)" \
  284.         "Injected session - ${marker} present in pre-auth session"
  285. }

  286. # IOC 2: tfa_verified=1 outside of a legitimate origin method.
  287. #
  288. # tfa_verified=1 is set in only two places:
  289. #   - Cpanel/Security/Authn/TwoFactorAuth/Verify.pm:122, after a real
  290. #     TFA token validation succeeds.
  291. #   - Cpanel/Server.pm:2295, when a caller passes tfa_verified=1 to
  292. #     newsession().
  293. # In both cases the legitimate origin method is one of handle_form_login,
  294. # create_user_session, or handle_auth_transfer. tfa_verified=1 with any
  295. # other origin (notably badpass) cannot occur in a benign flow.
  296. check_tfa_with_bad_origin() {
  297.     local session_file="$1"

  298.     grep -qE '^tfa_verified=1$' "$session_file" || return
  299.     grep -q '^origin_as_string=.*method=handle_form_login'    "$session_file" && return
  300.     grep -q '^origin_as_string=.*method=create_user_session'  "$session_file" && return
  301.     grep -q '^origin_as_string=.*method=handle_auth_transfer' "$session_file" && return

  302.     report_finding WARNING "$session_file" \
  303.         "$(get_field "$session_file" cp_security_token)" \
  304.         "Session with tfa_verified=1 but suspicious origin"
  305. }

  306. # IOC 3: Session file contains a line that is not in `key=value` form.
  307. #
  308. # Three structural invariants together guarantee that every legitimate
  309. # line matches ^[A-Za-z_][A-Za-z0-9_]*=:
  310. #
  311. #   1. write_session serializes via Cpanel::Config::FlushConfig::flushConfig
  312. #      with '=' as the separator (Cpanel/Session.pm:221), so the on-disk
  313. #      format is one key=value pair per line.
  314. #   2. Keys come from a fixed whitelist (_SESSION_PARTS at
  315. #      Cpanel/Server.pm:2216-2247, applied at lines 2268-2270), so they
  316. #      always match the identifier shape above.
  317. #   3. Cpanel::Session::filter_sessiondata strips \r\n from every value
  318. #      (Cpanel/Session.pm:315) and additionally strips \r\n=, from origin
  319. #      sub-values (line 312), so values can never re-introduce line
  320. #      breaks. The `pass` value is additionally encoded by saveSession
  321. #      (Cpanel/Session.pm:181-189) into either lowercase hex (with-secret
  322. #      via Cpanel::Session::Encoder->encode_data) or the literal prefix
  323. #      `no-ob:` followed by lowercase hex (no-secret via
  324. #      Cpanel::Session::Encoder->hex_encode_only), so it cannot
  325. #      reintroduce structural characters either.
  326. #
  327. # Any non-blank line that fails the regex is the footprint of an
  328. # injection that bypassed these invariants - typically raw payload bytes
  329. # that didn't form valid key=value pairs. Note: an injection whose
  330. # smuggled lines DO match key=value (e.g. the watchtowr payload at
  331. # poc/poc_watchtowr.py:35, which fabricates successful_internal_auth_
  332. # with_timestamp/user/tfa_verified/hasroot lines) will not trip this
  333. # check; it is caught by IOC-0 and IOC-4 instead.
  334. check_malformed_session_line() {
  335.     local session_file="$1"

  336.     # Look for any non-blank line that doesn't start with key=...
  337.     grep -nE -v '^[A-Za-z_][A-Za-z0-9_]*=|^[[:space:]]*$' "$session_file" >/dev/null 2>&1 || return

  338.     report_finding CRITICAL "$session_file" \
  339.         "$(get_field "$session_file" cp_security_token)" \
  340.         "Malformed session line(s) detected (not key=value - newline injection footprint)"
  341. }

  342. # IOC 4: badpass origin combined with markers that no legitimate cpsrvd
  343. # code path writes into a badpass session.
  344. #
  345. # The badpass call site (Cpanel/Server.pm:1244-1252) is:
  346. #
  347. #   $randsession = $self->newsession(
  348. #       'needs_auth' => 1,
  349. #       %security_token_options,            # adds cp_security_token
  350. #       'origin' => { 'method' => 'badpass' },
  351. #   );
  352. #
  353. # %security_token_options is why badpass sessions legitimately carry
  354. # cp_security_token, but no auth-related options are ever supplied.
  355. # newsession() filters %OPTS through the _SESSION_PARTS whitelist
  356. # (Cpanel/Server.pm:2216-2247, applied at lines 2268-2270), so any key
  357. # not in that whitelist cannot land in the session via newsession at
  358. # all. Per marker:
  359. #
  360. #   successful_external_auth_with_timestamp - whitelisted, but the
  361. #       badpass caller doesn't pass it
  362. #   successful_internal_auth_with_timestamp - same
  363. #   tfa_verified=1 - newsession unconditionally writes 0 unless the
  364. #       caller passed a truthy value (Cpanel/Server.pm:2295), and the
  365. #       badpass caller doesn't
  366. #   hasroot=1 - NOT in _SESSION_PARTS, so newsession cannot write it
  367. #       for ANY session. A repo-wide grep finds no caller of
  368. #       Cpanel::Session::Modify->set('hasroot', ...) either: hasroot is
  369. #       never written to a session by legitimate code. Its presence in
  370. #       any session file is conclusive evidence of newline injection
  371. #       (the watchtowr payload at poc/poc_watchtowr.py:35 smuggles
  372. #       hasroot=1 via \r\n in a user-controlled field).
  373. check_badpass_with_auth_markers() {
  374.     local session_file="$1"

  375.     grep -q '^origin_as_string=.*method=badpass' "$session_file" || return

  376.     local markers=()
  377.     grep -q '^successful_external_auth_with_timestamp=' "$session_file" \
  378.         && markers+=("successful_external_auth_with_timestamp")
  379.     grep -q '^successful_internal_auth_with_timestamp=' "$session_file" \
  380.         && markers+=("successful_internal_auth_with_timestamp")
  381.     grep -qE '^hasroot=1$'      "$session_file" && markers+=("hasroot=1")
  382.     grep -qE '^tfa_verified=1$' "$session_file" && markers+=("tfa_verified=1")

  383.     [ "${#markers[@]}" -gt 0 ] || return

  384.     local joined
  385.     joined=$(IFS=,; echo "${markers[*]}")
  386.     report_finding CRITICAL "$session_file" \
  387.         "$(get_field "$session_file" cp_security_token)" \
  388.         "badpass origin combined with authenticated markers ($joined) - impossible in benign flow"
  389. }

  390. # IOC 5: Failed exploit attempt - a badpass session that carries a
  391. # pass= line, a token_denied counter, and no auth markers.
  392. #
  393. # A legitimate badpass session is created at Cpanel/Server.pm:1244-1252:
  394. #
  395. #   $randsession = $self->newsession(
  396. #       'needs_auth' => 1,
  397. #       %security_token_options,
  398. #       'origin' => { 'method' => 'badpass' },
  399. #   );
  400. #
  401. # %security_token_options carries only cp_security_token,
  402. # requested_token_at_next_login, and previous_session_user
  403. # (Cpanel/Server.pm:1205-1226) - never `pass`. saveSession only
  404. # writes a pass= line when length($session_ref->{pass}) is non-zero
  405. # (Cpanel/Session.pm:181), so legitimate badpass sessions have no
  406. # pass= line at all.
  407. #
  408. # An exploit that tampers with a user-controlled field on a
  409. # badpass-bound request leaves a pass= line behind (saveSession
  410. # encodes it as `<hex>` or `no-ob:<hex>` per Cpanel/Session.pm:181-189,
  411. # but the format is irrelevant - its presence is the indicator). Combined
  412. # with token_denied (someone was poking at cp_security_token) and the
  413. # absence of auth markers (the injection didn't promote - otherwise
  414. # IOC-0 or IOC-4 fires CRITICAL), this is the signature of a failed
  415. # exploit attempt.
  416. check_failed_exploit_attempt() {
  417.     local session_file="$1"

  418.     grep -q '^origin_as_string=.*method=badpass' "$session_file" || return
  419.     grep -q '^token_denied=' "$session_file" || return

  420.     # If auth markers are present, IOC-4 (CRITICAL) handles it.
  421.     grep -q '^successful_internal_auth_with_timestamp=' "$session_file" && return
  422.     grep -q '^successful_external_auth_with_timestamp=' "$session_file" && return

  423.     # Legitimate badpass sessions never carry pass=.
  424.     grep -q '^pass=' "$session_file" || return

  425.     report_finding ATTEMPT "$session_file" "$(get_field "$session_file" cp_security_token)" \
  426.         "Failed exploit attempt (badpass origin, token_denied, no auth markers, anomalous pass= line)"
  427. }

  428. # ---------------------------------------------------------------------------
  429. # Main
  430. # ---------------------------------------------------------------------------

  431. scan_sessions() {
  432.     local session_file
  433.     while IFS= read -r -d '' session_file; do
  434.         check_token_denied_with_injected_token "$session_file"
  435.         check_preauth_with_auth_attrs          "$session_file"
  436.         check_tfa_with_bad_origin              "$session_file"
  437.         check_malformed_session_line           "$session_file"
  438.         check_badpass_with_auth_markers        "$session_file"
  439.         check_failed_exploit_attempt           "$session_file"
  440.     done < <(find "$SESSIONS_DIR/raw" -type f -print0 2>/dev/null)
  441. }


  442. print_summary() {
  443.     local total=$((COUNT_CRITICAL + COUNT_WARNING + COUNT_INFO + COUNT_ATTEMPT))

  444.     echo
  445.     echo "================================================================="
  446.     echo "                       SCAN SUMMARY"
  447.     echo "================================================================="
  448.     echo "  CRITICAL findings: $COUNT_CRITICAL"
  449.     echo "  WARNING  findings: $COUNT_WARNING"
  450.     echo "  ATTEMPT  findings: $COUNT_ATTEMPT"
  451.     echo "  INFO     findings: $COUNT_INFO"
  452.     echo "  Total            : $total"
  453.     echo "-----------------------------------------------------------------"

  454.     if [ "$total" -eq 0 ]; then
  455.         echo "[+] No indicators of compromise found."
  456.         return
  457.     fi

  458.     # --purge has destructive blast radius (live session files for every
  459.     # logged-in user). Require either --yes for non-interactive use, or
  460.     # an explicit "yes" at an attached TTY.
  461.     if [ "$PURGE" -eq 1 ] && [ "$ASSUME_YES" -ne 1 ]; then
  462.         if [ ! -t 0 ]; then
  463.             echo "[ERROR] --purge requires --yes when stdin is not a TTY (cron, pipes, etc)" >&2
  464.             echo "        Re-run with --yes to confirm deletion." >&2
  465.             exit 64
  466.         fi
  467.         echo
  468.         echo "About to delete ${#FINDING_SESSIONS[@]} session file(s) plus matching preauth markers."
  469.         local confirm=""
  470.         read -r -p "Type 'yes' to confirm: " confirm
  471.         if [ "$confirm" != "yes" ]; then
  472.             echo "[+] Aborted; no files deleted."
  473.             PURGE=0
  474.         fi
  475.     fi


  476.     # For each unique session, print only the highest-severity finding, then dump/purge as needed.
  477.     local i session token severity message found=0
  478.     for i in "${!FINDING_SESSIONS[@]}"; do
  479.         session="${FINDING_SESSIONS[$i]}"
  480.         token="${FINDING_TOKENS[$i]}"
  481.         severity="${FINDING_SEVERITIES[$i]}"
  482.         found=0
  483.         # Find the first matching finding for this session and severity.
  484.         # Use `read` with three names so the last variable (entry_msg)
  485.         # absorbs any remaining `|` characters - the previous `${var##*|}`
  486.         # form took only the suffix after the LAST `|`, which would
  487.         # silently truncate any future message that contained one.
  488.         for entry in "${FINDINGS[@]}"; do
  489.             local entry_sev entry_file entry_msg
  490.             IFS='|' read -r entry_sev entry_file entry_msg <<< "$entry"
  491.             if [ "$entry_file" = "$session" ] && [ "$entry_sev" = "$severity" ]; then
  492.                 message="$entry_msg"
  493.                 found=1
  494.                 break
  495.             fi
  496.         done
  497.         echo
  498.         echo "================================================================="
  499.         echo "  SESSION: $session"
  500.         echo "================================================================="
  501.         echo "  Findings:"
  502.         if [ "$found" -eq 1 ]; then
  503.             printf "    [%-8s] %s\n" "$severity" "$message"
  504.         else
  505.             printf "    [%-8s] %s\n" "$severity" "(no message found)"
  506.         fi
  507.         echo
  508.         if [ "$VERBOSE" -eq 1 ]; then
  509.             dump_session "$session" "$token"
  510.         fi
  511.         if [ "$PURGE" -eq 1 ]; then
  512.             echo "    [ACTION] Deleting session file: $session"
  513.             rm -f -- "$session"
  514.             local preauth_marker="$SESSIONS_DIR/preauth/$(basename "$session")"
  515.             if [ -e "$preauth_marker" ]; then
  516.                 echo "    [ACTION] Deleting preauth marker: $preauth_marker"
  517.                 rm -f -- "$preauth_marker"
  518.             fi
  519.         fi
  520.     done

  521.     if [ "$COUNT_CRITICAL" -gt 0 ] || [ "$COUNT_WARNING" -gt 0 ]; then
  522.         echo
  523.         echo "[!] INDICATORS OF COMPROMISE DETECTED - IMMEDIATE ACTION REQUIRED"
  524.         echo "    1. Purge all affected sessions"
  525.         echo "    2. Force password reset for root and all WHM users"
  526.         echo "    3. Audit /var/log/wtmp and WHM access logs for unauthorized access"
  527.         echo "    4. Check for persistence mechanisms (cron, SSH keys, backdoors)"
  528.     fi
  529. }

  530. if [ ! -d "$SESSIONS_DIR/raw" ]; then
  531.     echo "[ERROR] Sessions directory not found: $SESSIONS_DIR/raw" >&2
  532.     echo "        Pass --sessions-dir DIR to point at a different location" >&2
  533.     echo "        (the default is /var/cpanel/sessions)." >&2
  534.     exit 64
  535. fi

  536. echo "[*] Scanning session files for injection indicators..."
  537. scan_sessions
  538. print_summary

  539. # Exit codes (for cron / monitoring):
  540. #   2 - at least one CRITICAL or WARNING finding (compromise indicators)
  541. #   1 - only ATTEMPT or INFO findings (probing, no confirmed compromise)
  542. #   0 - clean scan
  543. if [ "$COUNT_CRITICAL" -gt 0 ] || [ "$COUNT_WARNING" -gt 0 ]; then
  544.     exit 2
  545. elif [ "$COUNT_ATTEMPT" -gt 0 ] || [ "$COUNT_INFO" -gt 0 ]; then
  546.     exit 1
  547. fi
  548. 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
您需要登錄後才可以回帖 登錄 | 註冊

本版積分規則

Archiver|手機版|小黑屋|TShopping

GMT+8, 2026-5-13 10:37 , Processed in 0.022911 second(s), 18 queries .

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回復 返回頂部 返回列表