Skip to main content
rfxn
//
modsecuritycpanelvulnerabilitycvesession-forgeryreverse-engineering

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

Ryan MacDonald14 min read

CVE-2026-41940 is the kind of advisory you do not want to wake up to. On 2026-04-28, cPanel disclosed KB 40073787579671, an unauthenticated session forgery in cPanel and WHM. By the end of the same day there was a working public PoC, dropped by Sina Kheirkhah at watchTowr Labs. Severity is the maximum the rubric allows: remote code execution as root, no authentication, no preconditions, every supported tier affected, and the unsupported tiers “likely also affected” per the vendor.

If you operate cPanel hosts at any scale, this one bites. We do, and it did. What follows is the work we did to absorb it: a reverse-engineering pass over the patch, the primitive derived mechanically from the diff, an adjacent identity-injection issue we surfaced along the way, the ModSecurity rule pack we shipped, the detection tooling now running across the fleet, and an architectural conclusion we are standardizing on independent of any single CVE. The public PoC is intentionally not linked here; the vendor advisory and the watchTowr writeup cover the request chain and we are not reproducing it.

TL;DRCritical · ActiveCVE-2026-41940 · cPanel/WHM
The bug
Unauthenticated session forgery. CRLF injection into the password field of a preauth session promotes attacker input into canonical session attributes; outcome is a logged-in root session and root-equivalent cpsess token.
Patched
11.86.0.41 (EL6), 11.110.0.97, 11.118.0.63, 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, 124, 128 have no in-place patch.
Forward
Standardize on proxy-endpoint enforcement. Apache plus mod_security2 in front; cpsrvd ports 2082, 2083, 2086, 2087, 2095, 2096 firewalled to management CIDRs.

Affected Builds#

Patched cPanel/WHM builds per supported tier:

text
11.86.0.41 (EL6)   11.110.0.97        11.118.0.63
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

The 11.86.0.41 build for EL6 was added in the 04/29 advisory revision; 11.130 was bumped from .18 to .19 in the same revision. Tiers excluded from the vendor patch list have no in-place fix: 112, 114, 116, 120, 122, 124, 128. Hosts on those tiers must be upgraded to a patched major series, migrated, or have their cpsrvd listeners firewalled until they are.

Patch Dissection#

The first useful question after a security release is “what changed”. With cPanel that is harder than it sounds. cpsrvd is a launcher/payload pair: two stripped ELF binaries ( cpsrvd and cpsrvd.so), with URL routing, login form parsing, and token validation split across them. The Perl tree under /usr/local/cpanel/Cpanel/ and Whostmgr/ carries the high-level handlers, but for this class of bug the actual fix surface is compiled. There is no source release. To work the diff, you have to capture the binaries pre and post upgrade and reason from strings, dynsym, and disassembly outward.

The collector is sessionscribe-revsnap.sh. Run it before each step of an upgrade, run /scripts/upcp --force, run it again. Each invocation produces one tarball keyed off /usr/local/cpanel/cpanel -V, host, and timestamp. The captured collateral is built for BinDiff, Diaphora, and plain text-diff workflows side by side:

text
cpanel-<ver>-<host>-<ts>/
├── binaries/                cpsrvd, cpsrvd.so, cpanel, whostmgr, …
├── symbols/
│   ├── <bin>.strings        full strings dump
│   ├── <bin>.dynsym         nm -D
│   ├── <bin>.objdump-T      dynamic symbol table
│   ├── <bin>.readelf        full ELF metadata
│   ├── auth-strings/
│   │   ├── *.auth-strings.txt    auth|login|session|token|…
│   │   └── *.regex-candidates.txt PCRE-shaped strings
│   └── disasm/
│       └── *.objdump-d.gz   function-level disassembly
├── modules/
│   ├── Cpanel/{Auth,Session,Server,Cookies,…}
│   ├── Whostmgr/{Auth,Session,ACLS,…}
│   └── _so_files/           cpanel-only .so flattened
├── runtime/
│   ├── preauth-session-schema-sample.txt   anonymized baseline
│   ├── session-dir-layout.txt
│   └── cpsrvd-process-state.txt
└── meta/
    ├── full-tree-hashes.txt      sha256 of every .pm/.so/.pl/exec
    ├── rpms-cpanel-detailed.txt
    └── captured-collateral-rationale.txt

The fast first-pass diff is auth-strings. A patched-vs-vulnerable same-tier comparison surfaces the security delta in seconds:

bash
A=/var/tmp/cpanel-snapshots/cpanel-126.0.18-host-T1
B=/var/tmp/cpanel-snapshots/cpanel-126.0.54-host-T2

diff $A/symbols/auth-strings/cpsrvd.auth-strings.txt \
     $B/symbols/auth-strings/cpsrvd.auth-strings.txt

diff -rq $A/modules/Cpanel $B/modules/Cpanel
diff -ruN $A/modules/Cpanel/Session $B/modules/Cpanel/Session

A note for anyone doing the same diff: capture Cpanel/Session.pm, the parent file that sits one directory level above the Cpanel/Session/ subtree. It is easy to miss; the first cut of our collector did. That parent module holds saveSession(), write_session(), and filter_sessiondata(): the functions that actually explain the bug. Walk only the subtree and you will conclude the primitive is not in the Perl source. It is, one directory up.

The 134-tier carries a second lesson, and it is the harder one. Vulnerable 134.0.17 and patched 134.0.20 produce byte-identical strings outputs from cpsrvd. A naive strings diff returns nothing. The fix is purely in compiled function logic, and the only way to land it is BinDiff or Diaphora over function-level objdump -d. Capture the disassembly at snapshot time. You will need it before you finish the analysis.

The Primitive#

The primitive is two asymmetries that compose. Either one alone is a near miss. Together they yield a session-file write oracle for an unauthenticated attacker, and from there a logged-in root cpsess token follows. Reading them in order:

Asymmetry 1: filter_sessiondata is not on every write path

The session library has two write paths. Cpanel::Session::create() (used by the /login/ form path) calls filter_sessiondata(), which strips CR and LF from every string value before it hits disk. Cpanel::Session::saveSession() (the path used when an Authorization: Basic request lands on an existing session) does not. Anything written via saveSession() lands on disk verbatim.

Asymmetry 2: the encoder short-circuits on a missing ob_part

The whostmgrsession cookie's canonical shape is :NAME,OBHEX. The OBHEX tail seeds the encoder for the password field. Cpanel/Session/Load.pm:get_ob_part() extracts it:

perl
# Cpanel/Session/Load.pm  get_ob_part() line 122
if ( $$session_name_ref =~ s/,([0-9a-f]{1,64})$// ) { $ob = $1; }

Five cookie shapes fail this regex: no comma, trailing comma, non-hex tail, uppercase hex, hex tail longer than 64 characters. When the regex fails, $ob stays undefined and the next line short-circuits:

perl
my $encoder = $ob && Cpanel::Session::Encoder->new('secret' => $ob);
local $session_ref->{'pass'} = $encoder->encode_data(...);   # never runs

Where they meet

Compose the two and you get a clean primitive. With the encoder skipped and saveSession() not filtering, the password supplied via Authorization: Basic is written to the on-disk session file character for character. CR/LF inside the password splits the single pass= line into multiple key=value lines. cpsrvd reads them back as canonical session attributes. Set successful_internal_auth_with_timestamp, user=root, and hasroot=1, and you have forged a logged-in root session. The vendor advisory and the watchTowr writeup document the request chain that lands the resulting cpsess token.

It is the kind of bug that is elegant in hindsight and almost invisible in foresight. Either defect by itself is benign: saveSession() not filtering is a maintenance debt, not a vulnerability; encoder short-circuit on a malformed cookie is a graceful- degradation path. Drawing the line between them is the work.

What the Patch Fixes#

The patched cpsrvd binary writes pass=no-ob:<hex_encode_only(bytes)> when ob_part is missing. Every byte (including CR and LF) becomes ASCII hex, so the value can no longer split into standalone key=value lines on disk. A companion no-ob: branch on Cpanel/Session/Load.pm reads the value back through hex_decode_only. Together those two surfaces close the write/read symmetry.

The patched cpsrvd binary also embeds the Whostmgr::ACLS::* and token-reader machinery directly. The auth-strings diff on a same-tier patch (126.0.18 to 126.0.54) shows zero ACL strings before and eighteen-plus after. This is defense-in-depth against dynamic-load failures skipping the ACL gate. It is not the primary fix for SessionScribe, but it ships in the same release and it is the kind of belt-and-suspenders work that suggests the team did a serious post-mortem on the failure mode rather than just the bug.

The shape of the fix is the right one. cPanel chose hex-encode-on-write rather than retrofit filter_sessiondata() onto every alternate write path. Both would have closed SessionScribe; the hex-encode approach is harder to undo accidentally on a future refactor, because the read side has to recognize the no-ob: prefix or the session is unreadable. The invariant is encoded in the data, not in a single function call somebody might forget.

The shape of the fix is the right one… it is the kind of belt-and-suspenders work that suggests the team did a serious post-mortem on the failure mode rather than just the bug.

Adjacent Identity-Injection Issue#

The other thing that came out of this analysis is a separate, lower-severity bug we are calling out because it ships with the same rule pack. It lives at Cpanel/Server/Auth/HTTP.pm:_handle(). The handler calls $server_obj->auth()->set_user($username) before token validation. cpsrvd then validates the token; on failure the request 4xxs, but the access-log writer has already pulled the identity. Any format-valid system username supplied via Authorization: WHM lands in the user= field of the access_log line, with no preceding successful authentication.

bash
# the request 4xx's, but user=daemon is committed to access_log
curl -sk -H 'Authorization: WHM daemon:bogus-token' \
     https://host:2087/json-api/loadavg

The good news is that the ACL gate holds. We worked the WHM dispatch families one at a time (json-api outer, UAPI /execute/*, CpXfer /acctxfer*/, WebSocket dispatch, SSE) and saw the same 401 or 403 in every one. Severity is bounded to access-log integrity. That is not nothing. Forensic and SIEM artifacts can be poisoned with attacker-chosen system usernames at unauthenticated rates, which makes incident reconstruction harder than it needs to be. We close it at 1500010 / 1500020 / 1500021 so it gets handled in the same operator change as the CVE.

ModSecurity Rule Pack#

Now to the operational side. The rule pack covers both surfaces, the CVE primitive and the WHM-token log injection, from one ModSecurity file. ID range reserved is 1500000 to 1500099. Every deny runs in phase 1, so the request never reaches the body inspector or the application. The canonical file lives at sh.rfxn.com/modsec-sessionscribe.conf.

RuleSurfaceAction
1500030CRLF inside Authorization: Basic decoded payloaddeny, all sources, all paths
1500031whostmgrsession cookie missing valid ,OBHEX suffixdeny (defense-in-depth)
1500010Authorization: WHM on /json-api/, /execute/, /acctxfer*/deny when source not in trust list
1500020Authorization: WHM on WebSocket dispatch familydeny when source not in trust list
1500021Authorization: WHM on SSE dispatch pathdeny when source not in trust list

Rule 1500030: the CVE primitive

1500030 decodes the base64 payload of any Authorization: Basic header in phase 1 and rejects on CR or LF in the decoded bytes. No legitimate Basic-auth value decodes to bytes with newlines, so the rule has no trust-list bypass: it applies to every source on every path.

modsecurity
SecRule REQUEST_HEADERS:Authorization "@rx (?i)^Basic[[:space:]]+(\S+)" \
    "id:1500030,phase:1,t:none,capture,chain,deny,status:403,log,auditlog,\
msg:'CVE-2026-41940: CRLF inside Authorization: Basic',\
logdata:'src=%{REMOTE_ADDR} path=%{REQUEST_URI}',\
tag:'nxesec-cpanel-bypass',tag:'nxesec-cpanel-bypass/cve-2026-41940'"
    SecRule TX:1 "@rx [\r\n]" "t:base64Decode"

Rule 1500031: OBHEX suffix invariant

1500031 mirrors the inverse of cpsrvd's get_ob_part() regex: any whostmgrsession cookie whose value does not end in a comma followed by 1 to 64 lowercase hex characters falls outside the encoder's safe shape. 1500030 closes the CVE chain on its own; 1500031 is defense-in-depth and surfaces probing-for-shape behavior.

modsecurity
SecRule REQUEST_COOKIES:whostmgrsession "@rx ." \
    "id:1500031,phase:1,t:none,t:urlDecodeUni,chain,deny,status:403,log,auditlog,\
msg:'CVE-2026-41940 secondary: whostmgrsession missing valid ,OBHEX suffix',\
logdata:'src=%{REMOTE_ADDR} cookie=%{REQUEST_COOKIES.whostmgrsession}',\
tag:'nxesec-cpanel-bypass',tag:'nxesec-cpanel-bypass/no-ob'"
    SecRule REQUEST_COOKIES:whostmgrsession \
        "!@rx ,[0-9a-f]{1,64}$" "t:none,t:urlDecodeUni"

Validation

bash
HOST=cpanel.example.com

# CRLF in Basic, must 403
B64=$(printf 'root:x\r\nKEY=VALUE' | base64 -w0)
curl -sk -o /dev/null -w 'cve-crlf:  %{http_code} (expect 403)\n' \
     -H "Authorization: Basic $B64" "https://$HOST/"

# Negative control: well-formed Basic must NOT 403
B64OK=$(printf 'root:bogus-alphanumeric-token' | base64 -w0)
curl -sk -o /dev/null -w 'basic-ok:  %{http_code} (expect non-403)\n' \
     -H "Authorization: Basic $B64OK" "https://$HOST/"

# Confirm audit-log fired
grep -E '\[id "150003[01]"\]' /usr/local/apache/logs/error_log | tail

Apache must be in the request path

These rules run inside Apache. The cpsrvd daemon listens directly on its own ports and is reachable independent of Apache. An attack against those ports does not traverse Apache and these rules do not fire. The proxy-endpoint enforcement section below makes Apache the sole inspection point.

Pull the rule pack:

bash
curl -fsSLO https://sh.rfxn.com/modsec-sessionscribe.conf

# new install (modsec2.user.conf is empty by default):
cp modsec-sessionscribe.conf \
   /etc/apache2/conf.d/modsec/modsec2.user.conf

# or append to an existing user.conf without overwriting operator rules:
sed -n '/^# === RULES ===/,$p' modsec-sessionscribe.conf \
   >> /etc/apache2/conf.d/modsec/modsec2.user.conf

# edit @ipMatch trust list to include management CIDRs, then:
apachectl -t
/usr/local/cpanel/scripts/restartsrv_httpd
apachectl -t -D DUMP_INCLUDES 2>&1 | grep modsec2.user.conf

Detection Toolkit#

Three artifacts ship alongside the rule pack. Each one answers a different operator question: is host X vulnerable, has host X been hit, and what changed between cPanel build A and build B. They are deliberately small, read-only where possible, and built for fleet runs through Ansible, pdsh, or whatever you already use to push shell scripts at thousands of hosts.

Remote probe: sessionscribe-remote-probe.sh

We did not want to put live exploit code on customer hosts even for verification. The probe approximates the chain (mint preauth, inject CRLF, propagate raw to cache, verify via /json-api/version) and then actively logs out. Verdict-determining signal is the HTTP code at stage 4: 200, or 5xx with a license body, is VULNERABLE; 401 or 403 is SAFE. Every test session is tagged with an nxesec_canary_<nonce> attribute so cleanup is wildcard-safe, and no state-mutating API calls are made.

bash
curl -fsSLO https://sh.rfxn.com/sessionscribe-remote-probe.sh
chmod +x sessionscribe-remote-probe.sh

# single host
bash sessionscribe-remote-probe.sh --target 1.2.3.4

# fleet, quiet, exit 2 if any VULN
bash sessionscribe-remote-probe.sh --target 1.2.3.4 --quiet --no-color
echo "exit=$?"

# CSV aggregation across hosts
bash sessionscribe-remote-probe.sh --csv \
  $(awk '{print "--target "$1}' fleet.txt) > fleet.csv

# JSON for parsing
bash sessionscribe-remote-probe.sh --target 1.2.3.4 --all --json | jq .

# clean canary sessions on a target after a run
bash sessionscribe-remote-probe.sh --cleanup

On-host IOC scan: sessionscribe-ioc-scan.sh

Patched build numbers are not enough. A patched host can carry forensic artifacts of prior exploitation, and on a fleet of any size a percentage of those will be sitting on disk by the time the rule pack is in place. The on-host validator is the read- only counterpart: it walks /var/cpanel/sessions/raw/, the access logs, and the cpsrvd binary fingerprint, then returns two independent verdict axes: code_verdict (PATCHED / VULNERABLE / INCONCLUSIVE) from version, Perl source patterns, and cpsrvd binary fingerprint; and host_verdict (CLEAN / SUSPICIOUS / COMPROMISED) from the session-file IOC ladder and access-log scan. The IOC set is the vendor pattern plus our own four-way co-occurrence detector and a forged-timestamp heuristic.

bash
curl -fsSLO https://sh.rfxn.com/sessionscribe-ioc-scan.sh

# default sectioned report
bash sessionscribe-ioc-scan.sh

# JSONL stream for SIEM ingest
bash sessionscribe-ioc-scan.sh --jsonl --quiet > sessionscribe-host.jsonl

# fleet
ansible -i hosts all -m script \
  -a 'sessionscribe-ioc-scan.sh --jsonl --quiet'
pdsh -w cpanel-fleet 'bash -s' < sessionscribe-ioc-scan.sh

Exit codes (highest priority wins): 0 PATCHED + CLEAN, 1 VULNERABLE, 2 INCONCLUSIVE, 4 COMPROMISED. A patched host can still exit 4 if prior exploitation left IOCs on disk or in the access logs. Sessions tagged with the probe's canary are bucketed as PROBE_ARTIFACT and do not escalate to COMPROMISED.

Snapshot collector: sessionscribe-revsnap.sh

The same collector used for the patch dissection at the top of this article. We are publishing it because it generalizes: every future cpsrvd CVE will land in roughly the same surface, and having a tarball pair for the pre-patch and patched build is the difference between hours and days of analysis. If you have a host you can step-upgrade through tiers, this gives you a defensible diff.

bash
curl -fsSLO https://sh.rfxn.com/sessionscribe-revsnap.sh
chmod +x sessionscribe-revsnap.sh

bash sessionscribe-revsnap.sh        # capture current tier
/scripts/upcp --force                # step upgrade
bash sessionscribe-revsnap.sh        # capture next tier

Indicators of Compromise#

Forged session file shape (contents of /var/cpanel/sessions/raw/<sessname> after exploitation):

text
local_port=2087
hasroot=1
hulk_registered=1
pass=x
origin_as_string=address=127.0.0.1,app=whostmgrd,method=badpass
token_denied=1
local_ip_address=127.0.0.1
external_validation_token=cS9C19OfV0hCA4uD
cp_security_token=/cpsess6844364556
ip_address=127.0.0.1
user=root
tfa_verified=1
successful_internal_auth_with_timestamp=9999999999
port=39040
login_theme=cpanel

A normal preauth session never contains pass=, hasroot=1, user=root, tfa_verified=1, or successful_internal_auth_with_timestamp=. Any of those combined with origin_as_string=…method=badpass is diagnostic. A forged-timestamp value beyond now+365d (9999999999 here) is independently diagnostic.

On-host one-liner

bash
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

Access-log signal

Successful 200 / 302 / 307 responses on /json-api/, /execute/, or /scripts2/ paths from non-baseline source IPs without a preceding /login/ 200 in the same session window are worth review. /usr/local/cpanel/logs/login_log entries with NEW <session> <origin> where origin contains method=badpass followed by successful API access lines for the same session is the same signal viewed from a different angle.

Proxy-Endpoint Enforcement#

This is the part of the writeup that outlives the CVE.

cpsrvd terminates six TCP listeners directly:

ServiceNon-SSLSSL
cPanel (user panel)20822083
WHM (admin panel)20862087
Webmail20952096

Every one of those is a direct attack surface, and the architectural shape has been there since 11.x. The published SessionScribe PoC fires straight at TCP/2087 with no Apache, no WAF, no inspection point in the path. That worked because the path does not exist by default. The patch closes this specific hole, and three months from now there will be another cpsrvd advisory that lands on the same six ports. We have decided to stop running the configuration that lets that happen.

cPanel ships everything required to remove that direct surface. mod_security2 is loaded by default on EA4 stacks. The operator rule slot at /etc/apache2/conf.d/modsec/modsec2.user.conf is preserved across updates. Proxy subdomains (cpanel.<domain>, whm.<domain>, webmail.<domain>) resolve through Apache on TCP/80 and TCP/443, terminating on cpsrvd via loopback. Combined with the cpsrvd ports firewalled to management CIDRs, Apache plus mod_security2 becomes the sole public ingress for cPanel and WHM.

The forward standard for cPanel and WHM hosts under our administration is proxy-endpoint enforcement: cpsrvd reachable from the open internet only through Apache, the direct cpsrvd ports closed at the perimeter except for management CIDRs, and ModSecurity rules inspecting every login, cookie, and Authorization header before they reach the daemon. This is the permanent posture, not a SessionScribe-shaped patch. The targeted rule pack we shipped closes this CVE specifically; the architecture closes the next one.

And this is the part where having a WAF in the path pays for itself the day before you need it. A well-tuned OWASP Core Rule Set deployed in the same Apache instance, especially at paranoia level 3 with t:base64Decode applied to Authorization headers, would have flagged a CRLF-laced Basic auth as a generic protocol-injection violation on day zero of the public PoC. Our 1500030 is the precise rule because we control it and we wanted to deny on this exact shape; OWASP CRS is the redundant cousin that catches the class. The architectural argument is not that any one rule would have caught SessionScribe in advance. The argument is that putting Apache plus ModSecurity in front of cpsrvd gives you a place to add rules at all, and any tuned WAF deployment catches a meaningful fraction of the next class of bug generically.

This is not a new technique. The reason most operators have not been doing it across the board, and we have not until now, is the same one: it is not the cPanel default, it costs a small amount of operator effort to set up, and most cpsrvd CVEs over the past decade have been bounded enough that the direct port surface has not been catastrophic. SessionScribe is not bounded. We are out of excuses for shipping cpsrvd to the open internet, and so is the industry. If you operate cPanel hosts, this is the call I would make for your fleet too.

Step 1: Enable proxy subdomains

bash
whmapi1 set_tweaksetting key=proxysubdomains value=1
/scripts/proxydomains add                          # provision DNS
dig +short cpanel.<a-customer-domain> @127.0.0.1   # spot-check zone

Step 2: Deploy the rule pack

See the ModSecurity Rule Pack section above. Confirm httpd -M | grep security2_module before reloading.

Step 3: Firewall the cpsrvd ports

Apply only after Steps 1 and 2 are live and proxy access is verified working.

bash
# APF
# Edit /etc/apf/conf.apf, remove 2082,2083,2086,2087,2095,2096 from IG_TCP_CPORTS
echo 'tcp:in:d=2082,2083,2086,2087,2095,2096:s=10.0.0.0/8  # mgmt' \
  >> /etc/apf/allow_hosts.rules
apf -r

# csf
# Edit /etc/csf/csf.conf, remove the same six ports from TCP_IN
echo 'tcp|in|d=2082,2083,2086,2087,2095,2096|s=10.0.0.0/8  # mgmt' \
  >> /etc/csf/csf.allow
csf -r

# verify off-host
nc -vz <external-IP> 2087    # should TIMEOUT
nc -vz <mgmt-IP>     2087    # should CONNECT

Priority Order#

Immediate

Patch to the build for your tier (86.0.41 EL6 / 110.0.97 / 118.0.63 / 126.0.54 / 130.0.19 / 132.0.29 / 134.0.20 / 136.0.5; WP Squared 136.1.7).
If the tier has no patch (112/114/116/120/122/124/128), firewall TCP/2082, 2083, 2086, 2087, 2095, 2096 to management CIDRs immediately. Plan an upgrade or migration of the major series.
Run sessionscribe-ioc-scan.sh fleet-wide. A patched host can still be compromised.

Forward

Enable proxy subdomains so cPanel/WHM/Webmail are reachable through Apache on 80/443.
Deploy modsec-sessionscribe.conf into modsec2.user.conf with the @ipMatch trust list set.
Firewall TCP/2082, 2083, 2086, 2087, 2095, 2096 to management CIDRs only. Apache plus ModSecurity becomes the sole public ingress.
Standardize this proxy-endpoint posture as the default. The next cpsrvd advisory will land on the same six ports.

Detect

sessionscribe-remote-probe.sh: non-destructive verdict per host. Read-only.
sessionscribe-ioc-scan.sh: on-host IOC ladder. Vendor signals plus four-way co-occurrence and forged-timestamp heuristics.
Watch for /json-api/, /execute/, /scripts2/ 200s without a preceding /login/ 200 in the same session window.
Watch /var/cpanel/sessions/raw/* for the diagnostic IOC fingerprint above.

SessionScribe is a thoroughly resolved bug at this point. The patch is correct and the rule pack closes the surface; the IOC ladder will catch prior compromise. The harder question is the architectural one: how do we stop shipping cpsrvd to the open internet by default? I would rather have that conversation now, while the advisory is fresh, than after the next one. If you want to compare notes, push back on the architectural call, or share what you saw on your fleet, my contact is below.

Research credit: Sina Kheirkhah of watchTowr Labs. Vendor advisory: cPanel KB 40073787579671. The ModSecurity ruleset and detection scripts referenced here are open source under GPL v2; pull from sh.rfxn.com. Additional IOCs or variant samples welcome via Keybase or email.