Skip to content

Back to the catalog

rfxn.comwriteup

CVE-2026-41940 (SessionScribe): cPanel/WHM Session Forgery Reverse-Engineered

rfxn's technical dissection of CVE-2026-41940 (SessionScribe), an unauthenticated session-forgery bug in cPanel/WHM that lets an attacker forge a root cpsess session via CRLF injection into the password field of an Authorization: Basic request, exploiting the fact that Cpanel::Session::saveSession() skips the CR/LF-stripping filter_sessiondata() sanitization used by the normal login path, combined with an encoder short-circuit when the whostmgrsession cookie's OBHEX suffix is malformed. The writeup reverse-engineers the patched cpsrvd binaries (since cPanel ships no source for this surface) using a custom snapshot/BinDiff workflow, documents an adjacent lower-severity access-log identity-injection issue, and ships an open-source operator toolkit (ModSecurity rule pack, remote probe, on-host IOC scanner, snapshot collector) plus an architectural recommendation to firewall cpsrvd's direct ports behind Apache+mod_security2 proxy endpoints. Credits researcher Sina Kheirkhah of watchTowr Labs for the original PoC.

TL;DR Deep binary-diff reverse-engineering of CVE-2026-41940, an unauthenticated cPanel/WHM root-session-forgery bug via CRLF injection, with released ModSecurity rules and IOC/detection tooling.
Document

What it is

rfxn's operator writeup of CVE-2026-41940 ("SessionScribe"), an unauthenticated root session-forgery bug in cPanel/WHM disclosed 2026-04-28 (cPanel KB 40073787579671), with a public PoC from Sina Kheirkhah (watchTowr) the same day. The value the summary can't carry: the exact patch boundaries, the code-level primitive, an IOC file shape, a shipped ModSecurity rule pack, and an ordered remediation procedure. The PoC and request chain are deliberately not reproduced.

How it works

Two composing asymmetries. Cpanel::Session::create() (the /login/ path) strips CR/LF via filter_sessiondata(); Cpanel::Session::saveSession() (the path an Authorization: Basic request on an existing session uses) does not, so input lands on disk verbatim. Separately, get_ob_part() in Cpanel/Session/Load.pm:122 extracts the whostmgrsession cookie's OBHEX tail with s/,([0-9a-f]{1,64})$//; five shapes fail it (no comma, trailing comma, non-hex, uppercase hex, tail >64 chars). On failure $ob is undefined and my $encoder = $ob && ... short-circuits, so the password field is never encoded. Composed: an unauthenticated attacker writes CR/LF inside the Basic-auth password, splitting pass= into extra key=value lines. Setting user=root, hasroot=1, and successful_internal_auth_with_timestamp forges a logged-in root cpsess token.

The patch writes pass=no-ob:<hex_encode_only(bytes)> when ob_part is missing (CR/LF become hex, can't split lines), with a matching no-ob: read branch. It also embeds Whostmgr::ACLS::* directly (0 → 18+ ACL strings in the 126.0.18→126.0.54 auth-strings diff) as defense-in-depth.

An adjacent lower-severity bug: Cpanel/Server/Auth/HTTP.pm:_handle() calls set_user() before token validation, so a format-valid username from Authorization: WHM is committed to access_log even on a 4xx — access-log poisoning at unauthenticated rates. The ACL gate itself holds (401/403 across all WHM dispatch families).

Affected

Vulnerable = any build below the patched build on the same major tier. Patched: 11.86.0.41 (EL6/CL7 only), 11.110.0.97, 110.0.103 (EL6/CL6 from .50), 11.118.0.63, 11.124.0.35, 11.126.0.54, 11.130.0.19, 11.132.0.29, 11.134.0.20, 11.136.0.5, WP Squared 136.1.7. Tiers 112, 114, 116, 120, 122, 128 have no in-place patch — upgrade, migrate, or firewall. EL7 hosts are capped at the 11.110 tier. Auto-update is not guaranteed: hosts with updates disabled or pinned won't patch themselves.

grep -E '^(UPDATES|RPMUP|SARULESUP|STAGING_DIR)' /etc/cpupdate.conf

Note 134.0.17 vs 134.0.20 produce byte-identical strings output — the fix is pure compiled logic, requiring BinDiff/Diaphora over objdump -d. Also capture Cpanel/Session.pm (one level above the Session/ subtree); it holds saveSession(), write_session(), filter_sessiondata().

What to do

Ship the GPLv2 toolkit at github.com/rfxn/cpanel-sessionscribe. The WAF rule pack covers both surfaces from one file, all denies in phase 1:

curl -fsSLO https://sh.rfxn.com/modsec-sessionscribe.conf
cp modsec-sessionscribe.conf /etc/apache2/conf.d/modsec/modsec2.user.conf
apachectl -t && /usr/local/cpanel/scripts/restartsrv_httpd

Rule 1500030 base64-decodes any Authorization: Basic header and denies on CR/LF (no bypass — no legitimate value decodes to bytes with newlines); 1500031 rejects whostmgrsession cookies lacking a valid ,OBHEX suffix; 1500010/20/21 cover the WHM-token log injection.

On-host IOC scan (read-only; code_verdict + host_verdict axes; exit 0 clean, 1 vuln, 2 inconclusive, 4 compromised):

curl -fsSLO https://sh.rfxn.com/sessionscribe-ioc-scan.sh
bash sessionscribe-ioc-scan.sh --jsonl --quiet > sessionscribe-host.jsonl

Forged session files under /var/cpanel/sessions/raw/ show hasroot=1, user=root, tfa_verified=1, pass=, and origin_as_string=...method=badpass; a timestamp beyond now+365d (9999999999) is independently diagnostic. Quick check:

for f in /var/cpanel/sessions/raw/*; do
  [ -f "$f" ] || continue
  if grep -q '^token_denied=' "$f" \
     && grep -q '^cp_security_token=' "$f" \
     && grep -q '^origin_as_string=.*method=badpass' "$f"; then
    echo "IOC0 hit: $f"
  fi
done

A non-destructive remote probe (sessionscribe-remote-probe.sh, canary-tagged, --cleanup) verdicts by HTTP code at stage 4: 200 or 5xx-with-license-body = VULNERABLE, 401/403 = SAFE.

The durable point (author's real thesis): cpsrvd terminates six listeners — 2082, 2083, 2086, 2087, 2095, 2096 — directly, bypassing Apache and any WAF; the PoC fires straight at 2087. Standardize on proxy-endpoint enforcement so Apache+mod_security2 is the sole public ingress. Ordering matters:

whmapi1 set_tweaksetting key=proxysubdomains value=1   # 1. proxy subdomains
/scripts/proxydomains add
# 2. deploy rule pack (above), verify: httpd -M | grep security2_module
# 3. firewall the six ports to mgmt CIDRs — only after 1 & 2 verified working

Caveats

The rule pack fires only inside Apache; a direct hit on cpsrvd's own ports never traverses it — the port firewalling is what closes that path, and must be applied last. The adjacent log-injection issue is bounded to access-log integrity, not a second RCE. Patched build numbers alone are insufficient: a patched host can still exit COMPROMISED from IOCs left by prior exploitation (probe canaries bucket as PROBE_ARTIFACT and don't escalate).

Organisations named here

  • cPanel KB 40073787579671
  • Sina Kheirkhah

CVEs named here

  • CVE-2026-41940

Technologies named here

  • cPanel & WHM
  • sessionscribe-revsnap.sh
  • sessionscribe-remote-probe.sh
  • WP Squared
  • cpsrvd
  • sessionscribe-ioc-scan.sh
  • modsec-sessionscribe.conf
  • ModSecurity
  • OWASP Core Rule Set
  • BinDiff
  • Diaphora
  • APF
  • csf
  • cPanel
  • WHM
  • Apache
  • WatchTowr Labs

Techniques named here

  • CRLF injection into session password field
  • Encoder short-circuit via malformed whostmgrsession cookie
  • Session forgery to root
  • Identity injection into access logs
  • Direct attack against cpsrvd TCP listeners