Field Notes: A Week of CVE-2026-41940 Exploitation in the Wild
This is a field journal of what we observed during the first week of CVE-2026-41940 exploitation in the wild. The companion to this piece is our reverse-engineering write-up of the underlying primitive and the patch that closes it. That article describes what the bug is; this one describes what we saw it do. We are deliberately not naming providers, victims, or quantifying scope. The IOCs, attacker IPs, operator tells, and command patterns are the parts a defender actually needs.
The journal is structured chronologically through the 04-11 → 05-02 window: a 17-day quiet probing arc, then vendor disclosure, then a 72-hour exploitation surge once a public PoC dropped. A worked-example kill-chain analysis from the on-host scanner output sits in the middle. The pattern catalog at the end is the version we are running detection against today, and the actionable section after it has the mitigation tooling and the high-confidence block list ready to push.
github.com/rfxn/cpanel-sessionscribe
Canonical source for sessionscribe-ioc-scan.sh, sessionscribe-mitigate.sh, sessionscribe-remote-probe.sh, sessionscribe-revsnap.sh, and the ModSecurity rule pack referenced throughout this journal. Mirror of the sh.rfxn.com one-liners with history, issues, and tags.
CVE-2026-41940 (cPanel/WHM SessionScribe) — week-1 field notes
- First probe
- 2026-04-11T05:32Z
- Private disclosure
- 2026-04-28T17:05Z
- Public CVE
- 2026-04-29T19:46Z
- Most recent event
- 2026-05-02T11:33Z
TL;DR
Three things stood out in the data once the dust settled:
- The campaign is layered, not parallel. Every confirmed compromise we examined ran the same upstream chain (Pattern X → D → E → F). Only the destructive terminal stage diverged. That is one toolkit shared across multiple operators.
- Multiple operators, deterministic tells. Three distinct websocket-Shell terminal dimensions (24×80, 24×120, 24×134) plus four UA strings give us reliable per-operator attribution from the same per-host evidence.
- Pre-disclosure evidence merits investigation. At least one host shows a websocket-Shell hit dated four months before the public CVE. Whether this is genuine pre-disclosure exploitation, a forged session timestamp, or an artifact of our scanner's clamping logic is the most important open question of the week.
The First Probe
The earliest Pattern X event we have on disk lands on 2026-04-11 at 05:32 UTC. One IP, one host, the canonical badpass session shape: a session file written through saveSession() with a multi-line pass= field, an injected cp_security_token=/cpsess[N], and tfa_verified=1 without a real login. That is the bug working.
The probing was paced. Same IP would return on a different day, walk a small set of unrelated targets inside a six-hour window, then disappear. The targets had nothing in common except running a vulnerable cPanel/WHM version. This is not a scanner spraying the internet; this is an actor with a working primitive walking a target list.
saveSession(); no other stage activity for the rest of the day.The Quiet Window
2026-04-12 → 2026-04-27For the next sixteen days the same shape repeats. Pattern X events arrive in small daily batches with a peak around 11/day across the population we monitor. None of these probes translate into Pattern D recon or any post-attack stage during the window. Either the actor was characterizing the bug without using it, or the activity-stage code was being held back deliberately.
Two things distinguish this window from generic scanner noise:
- Surgical target selection. An operator walking a list rather than a scanner sweeping a /16: small host sets, no obvious shared identifier, paced in blocks.
- Single-IP recurrence. The same source IPs return across multiple days, against partially overlapping target sets. A scanner moves on.
04-11 revisits a different host set inside a roughly six-hour block. No interactive stage follows. Pacing rules out an automated scanner.Pre-Disclosure Evidence
One host's session corpus contains a Pattern E websocket-Shell hit dated 2025-12-22 from an IP that returns in the post-disclosure data with the same 24×134 fingerprint, attributing the early hit to the same operator (Operator C) we observe later in the window. The dimensions are rows=24&cols=134 , the third operator fingerprint in our taxonomy below. If the timestamp is real, it places at least one operator inside the primitive roughly four months before the public CVE.
Disclosure & Public CVE
Day 17 reframes everything that came before it. The vendor notified providers privately on 2026-04-28 at 17:05 UTC, and a public CVE plus a working PoC from watchTowr Labs landed about 27 hours later, on 04-29 at 19:46 UTC. The 27-hour gap is the entire defensive window any provider had to rollout a mitigation before commodity exploitation began.
The Floodgates
2026-04-30 → 2026-05-02The post-CVE window is what most defenders will see in their own logs. Pattern X events, which had been single-digit per day during the quiet window, jump by more than an order of magnitude. Pattern D recon traffic, which we had not seen at all pre-disclosure, appears immediately and consistently from the same Go-http-client UA against any host that does not have the WAF rules in place.
The destructive stages (A, B, C, H) cluster on day 04-30, which is the day after the public CVE and the first day of generalized operator activity. Hosts that had defenses landed before 04-29 19:46 UTCtook Pattern X attempts but no post-attack stages. Hosts that had not yet been reached by the rollout are where the destructive payloads landed.
Worked Kill Chain — One Host, One Day
The most useful thing we can hand to another defender is a walked-through kill chain on a single compromised host, with the artifacts an on-host scanner would surface at each stage. Below is sanitized output from sessionscribe-ioc-scan.sh on a host that took the full chain. Hostnames and account names are redacted; everything else is real.
# sessionscribe-ioc-scan.sh on Host A — 2026-04-30 06:18 UTC
[+] CVE-2026-41940 IOC scanner v2.5.0
[stage 1/7] PATTERN X — initial CRLF Authorization
/var/cpanel/sessions/raw/******.scribe
token_denied=1
cp_security_token=/cpsess1234567890
origin_as_string=address=192.81.219.190,app=whostmgrd,method=badpass
tfa_verified=1
pass= (multi-line value, CRLF injection confirmed)
VERDICT: COMPROMISED (1 session file matches)
[stage 2/7] PATTERN D — JSON-API enumeration
access_log: /json-api/version,gethostname,listaccts,getdiskusage,getips
user-agent: Go-http-client/1.1
fileman API: /etc/shadow,/etc/passwd,/root/.ssh/id_rsa{,.pub},
/root/.aws/credentials,/root/.bash_history
VERDICT: RECON CONFIRMED
[stage 3/7] PATTERN D — Reseller persistence
/var/cpanel/accounting.log:
CREATEAPITOKEN:root:root:not-applicable:WHM_FullRoot
CREATE:root:root:4ef72197.cpx.local:96.30.39.236:sptadm
ADDRESELLER:root:root:4ef72197.cpx.local:sptadm
IOC: sptadm reseller account, WHM_FullRoot API token (revoke separately)
[stage 4/7] PATTERN G — SSH key persistence
3 non-standard ssh-rsa keys planted under /root/.ssh, mtime forged to
2019-12-13 12:59:16, ctime 2026-04-30 06:14:22 (real)
IOC: IP-labeled keys 209.59.141.49, 50.28.104.57
[stage 5/7] PATTERN E — websocket Shell
GET /cpsess1234567890/websocket/Shell?rows=24&cols=80 HTTP/1.1
source: 192.81.219.190 (Operator A, Go-http-client UA family)
[stage 6/7] PATTERN F — automated harvester envelope
/root/.bash_history:
printf '__S_MARK__'; cat /etc/shadow; printf '__E_MARK__'
printf '__S_MARK__'; for f in /root/.ssh/id_*; do cat "$f"; done; printf '__E_MARK__'
printf '__S_MARK__'; find /root /home -maxdepth 3 -name '.bash_history' \
-exec cat {} \;; printf '__E_MARK__'
VERDICT: AUTOMATED AGENT (~5h after stage 1)
[stage 7/7] DESTRUCTIVE PAYLOAD — Pattern A (.sorry encryptor)
/root/sshd sha256: 2fc0a056fd4eff5d31d06c103af3298d711f33dbcd5d122cae30b571ac511e5a
C2: 68.183.190.253
README.md (in user homes): qTox ID 3D7889AEC00F2325E1A3FBC0ACA4E521670497F11E47FDE13EADE8FED3144B5EB56D6B198724
encrypted extension: .sorry (system files included — restore-in-place will not recover)
[7/7 stages confirmed] Confidence: HIGH
[recommendation] reimage required; rotate every credential touched in stage 2/3/4Rendered as a chain:
Kill Chain· Pattern X → D → G → E → F → destructive
- 01Access
Pattern X — Initial CRLF Authorization access
An Authorization: Basic header lands on an existing session with a multi-line value. saveSession() writes it verbatim, and the resulting session file gets a token_denied=1 with an injected cp_security_token=/cpsess[N], plus origin_as_string carrying the attacker IP, and tfa_verified=1 despite no real login.
- regex
^pass=.*\n. - id
token_denied=1 + cp_security_token=/cpsess[N] - id
origin_as_string=address=<IP>,app=whostmgrd,method=badpass
- regex
- 02Recon
Pattern D — JSON-API enumeration
Once the forged cpsess token is in hand, an automated Go-http-client agent walks /json-api/* in a deterministic order: version, gethostname, listaccts, getdiskusage, systemloadavg, getips. It then reads /etc/shadow, /etc/passwd, the full ~/.ssh key set, and /root/.aws/credentials via the Fileman API.
- ua
Go-http-client/1.1 - cmd
/json-api/listaccts - file
/etc/shadow, /etc/passwd, /root/.ssh/id_*
- ua
- 03Persistence
Pattern D — Reseller-as-persistence
Same recon agent then issues /json-api/createacct + setupreseller + setacls + setresellerlimits, leaving a sptadm reseller with all-ACLs and a WHM_FullRoot API token. The token survives the cPanel patch and is the operator's way back in after remediation.
- id
username=sptadm - id
domain=4ef72197.cpx.local - id
contactemail=a@exploit.local - cmd
CREATEAPITOKEN ... WHM_FullRoot
- id
- 04Persistence
Pattern G — SSH key persistence (parallel layer)
Non-standard ssh-rsa keys planted across /root/.ssh, /etc, and cron paths, with mtimes forged to 2019-12-13 to blend with provisioning artifacts. Comments include IP-labeled keys mimicking provider internal-key style. ctime gives them away; touch can backdate mtime and atime, not ctime.
- cmd
find /root /etc /var/spool/cron -type f -exec grep -l 'ssh-rsa' - regex
^[0-9.]{7,15} ssh-rsa
- cmd
- 05Interactive
Pattern E — Interactive websocket Shell
Operator pivots from JSON-API into the WHM in-browser shell at /cpsess[N]/websocket/Shell. Different operators run with different terminal dimensions (24×80, 24×120, 24×134) which act as a deterministic actor fingerprint when the shell traffic is otherwise indistinguishable.
- regex
GET /cpsess[0-9]+/websocket/Shell\?rows= - id
rows=24&cols=80 (operator A) - id
rows=24&cols=120 (operator B) - id
rows=24&cols=134 (operator C)
- regex
- 06Harvester
Pattern F — Automated agent harvester
Inside the websocket shell a follow-up tool wraps every command with __S_MARK__/__E_MARK__ delimiters and harvests SSH keys, /etc/shadow (twice; likely retried), and every shell history file under /root and /home. The wrapper is the strong actor tell: a human does not type printf '__S_MARK__'; cmd; printf '__E_MARK__'.
- regex
printf '__S_MARK__'.*printf '__E_MARK__' - cmd
find /root /home -maxdepth 3 -name '.bash_history'
- regex
- 07Destructive
Destructive payload — multiple variants
Terminal stage. Different operators on the same host have chosen different destructive payloads in the same window. The upstream chain (X → D → E → F) is identical across them; only the final stage diverges.
- 7.1Destructive
Pattern A — .sorry encryptor + qTox ransom
Encryptor binary masquerading as /root/sshd; encrypts user files plus system files (so an attempted in-place restore does not recover); drops README.md with a TOX ID; C2 over a single IP.
- file
/root/sshd (masquerades as ssh daemon) - hash
2fc0a056fd4eff5d31d06c103af3298d711f33dbcd5d122cae30b571ac511e5a - ip
68.183.190.253 (C2) - id
qTox ID 3D7889AEC00F2325E1A3FBC0ACA4E521670497F11E47FDE13EADE8FED3144B5EB56D6B198724
- file
- 7.2Destructive
Pattern B — DB wipe + index.html ransom note
Drops a BTC-ransom note in every /home/*/public_html/index.html (and nested directories), removes /var/lib/mysql/mysql, breaking MariaDB. Files are NOT encrypted; restore-from-backup recovers cleanly. Simpler stage than Pattern A.
- id
BTC bc1q9nh4revv6yqhj2gc5usncrpsfnh7ypwr9h0sp2 - cmd
rm -rf /var/lib/mysql/mysql - regex
to recover your files, kindly send 0\.1 BTC
- id
- 7.3Destructive
Pattern C — Mirai / nuclear.x86 cryptominer
Mirai-family dropper fetched from a hosting-redirector domain; binary lands at well-known paths and persistence is set via cron and systemd. Largest commodity-malware bucket of the campaign by host count.
- file
nuclear.x86 (Mirai variant) - ip
87.121.84.78 (binary host) - id
raw.flameblox.com (C2)
- file
- 7.4Destructive
Pattern H — seobot.php SEO defacement
Per-site PHP webshell drop into every public_html, plus a competitor-kill bash_history (pkill -9 nuclear.x86 kswapd01 xmrig) and an ALLDONE marker. Confirms live cross-actor competition for the same vulnerable cohort.
- file
*/public_html/seobot.php - regex
pkill -9 (nuclear\.x86|kswapd01|xmrig) - regex
echo ALLDONE
- file
ExpandRaw sessionscribe-ioc-scan.sh v2.5.0 sectioned report (anonymized, ANSI stripped)
Real stderr output as rendered by the scanner's sectioned-report mode (default). Hostname, account names, session-file basename, and one source IP have been redacted; everything else is on-disk evidence. Section IDs (version, cpsrvd, iocscan, sessions, destruct) match the SECTION_ORDER array in the script.
== version == cpanel -V vs published patched-build cutoffs
[OK] cpanel -V parsed: 11.130.0.18 (tier 130, build 18)
[FAIL] vendor cutoff for tier 130 is .19; this host is one build behind
code_verdict: VULNERABLE
== patterns == static config-file patterns (ancillary; not CVE-driver)
[OK] /var/cpanel/cpanel.config: ProxyPass.cpanel = on
[OK] /etc/apache2/conf.d/modsec2.user.conf: 1500030 present
[OK] /etc/apache2/conf.d/modsec2.user.conf: 1500031 present
== cpsrvd == cpsrvd binary patch markers
[WARN] cpsrvd binary mtime predates vendor patch window
[WARN] filter_sessiondata symbol present; saveSession() path NOT routed
through filter (matches pre-patch primitive)
== iocscan == access_log scan over 30d window
[IOC] 192.81.219.190 badpass exploit hits=14 2xx=11 ua=Go-http-client/1.1
[IOC] 38.146.25.154 /json-api/createacct hits=3 2xx=3 ua=Go-http-client/1.1
[IOC] 192.81.219.190 /cpsess[REDACTED]/websocket/Shell?rows=24&cols=80
hits=2 ua=(none)
[WARN] attacker-IP traffic during recon-window (30d) — escalates SUSPICIOUS
== sessions == session-store IOC ladder (vendor + CVE-2026-41940 ladder)
scanned 47 session files under /var/cpanel/sessions/raw/
[IOC] [REDACTED].scribe : multi-line pass= field (CRLF injection)
[IOC] [REDACTED].scribe : token_denied=1 + injected cp_security_token=/cpsess[REDACTED]
[IOC] [REDACTED].scribe : origin_as_string=address=192.81.219.190,app=whostmgrd,method=badpass
[IOC] [REDACTED].scribe : tfa_verified=1 with no real login origin
[ALERT] 4-of-4 CVE-2026-41940 co-occurrence on 1 session file
[OK] no PROBE_ARTIFACT canary — these are not from sessionscribe-remote-probe.sh
host_verdict: COMPROMISED (1 session matches; 4-of-4 ladder)
== destruct == destruction IOC scan (Patterns A-I)
[IOC] Pattern D : /var/cpanel/accounting.log lines for sptadm reseller +
WHM_FullRoot CREATEAPITOKEN (timestamps 2026-04-30 06:18-06:21Z)
[IOC] Pattern G : 3 non-standard ssh-rsa keys in /root/.ssh/authorized_keys
mtime 2019-12-13 (forged), ctime 2026-04-30 06:14:22 (real)
[IOC] Pattern E : websocket Shell hit on 04-30 06:22Z, dimensions 24x80
[IOC] Pattern F : __S_MARK__/__E_MARK__ envelopes in /root/.bash_history
(15 wrapped commands, ~5h after stage 1)
[IOC] Pattern A : /root/sshd present
sha256 2fc0a056fd4eff5d31d06c103af3298d711f33dbcd5d122cae30b571ac511e5a
C2 reachability test (-Z dryrun): 68.183.190.253 reachable
[OK] Pattern B : no /var/lib/mysql/mysql removal
[OK] Pattern C : no nuclear.x86, no Mirai-family persistence
[OK] Pattern H : no seobot.php in any docroot
== probe == localhost marker probe
[SKIP] --probe not requested
==============================================================================
Summary matrix
version : VULNERABLE (tier 130 .18 vs cutoff .19)
cpsrvd : VULNERABLE (saveSession path not filtered)
iocscan : COMPROMISED (Pattern X attacker IP, Pattern E pivot)
sessions : COMPROMISED (4-of-4 ladder match on 1 session file)
destruct : COMPROMISED (Patterns D+E+F+G+A confirmed; Pattern B/C/H clean)
probe : SKIP
code_verdict : VULNERABLE
host_verdict : COMPROMISED (compromised host can also be vulnerable;
patch + mitigate after IR cleanup)
exit code : 4Operator Profiles
Three distinct operator fingerprints share the upstream chain and the JSON-API recon toolkit. Their tells diverge at the interactive stage (websocket Shell dimensions) and at the UA string. Treat these as defensible attribution buckets, not named-actor attribution.
- ttp
- Most disciplined of the three. Issues the canonical Pattern D recon sequence in deterministic order, opens a 24×80 websocket Shell, runs the __S_MARK__/__E_MARK__ harvester. Reseller persistence (sptadm + WHM_FullRoot API token) is its strongest persistence tell.
- ips
192.81.219.19038.146.25.154- user-agents
Go-http-client/1.1
This operator is responsible for the cleanest, most repeatable kill chain we observed. Where Pattern D appears, this operator is almost always one of the actors involved. The Go-http-client UA and the consistent 24×80 dimensions suggest a single piece of automation rather than a person.
- ttp
- Operates the websocket Shell directly from a browser, dimensions 24×120: that is a person resizing a terminal pane to non-default width, not a tool. UA strings are real-browser plausible. Less recon discipline than Operator A; arrives after the cpsess token is already minted.
- ips
149.102.229.144- user-agents
Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Firefox/142.0
We see this operator on hosts that already have an active forged session from Operator A. The pattern looks like token reuse or token-handoff between members of the same crew, not independent compromise.
- ttp
- The operator that produces the 24×134 dimension. The same fingerprint appears on the pre-disclosure host above. Browser-driven, similar TTPs to Operator B, but with measurably different terminal width and a different UA family.
- ips
183.82.160.147- user-agents
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
The pre-disclosure timestamp question hinges on this operator. If 2025-12-22 is genuine, this is the operator who had the primitive months before anyone else. If it's a forged session timestamp, the operator's first verifiable activity is post-public-CVE, like the others.
Pattern Catalog
The ten patterns we are running detection against today. Pattern X is initial access; D/G/I are persistence; E/F are interactive; A/B/C/H are destructive terminal stages. The catalog is sequenced as the chain runs in the wild, not alphabetically.
Initial CRLF Authorization access
stableThe bug working: a multi-line pass= field written through saveSession() produces a session file with a forged cpsess[N] token and tfa_verified=1. Universal across every confirmed compromise.
^pass=.*\n.token_denied=1 + cp_security_token=/cpsess[N]origin_as_string=address=<IP>,app=whostmgrd,method=badpassJSON-API enumeration + reseller persistence
newTwo-phase. The recon side is a Go-http-client agent walking/json-api/*and the Fileman API. The persistence side creates a sptadm reseller and a WHM_FullRoot API token, both of which survive the cPanel patch and need separate revocation.
Go-http-client/1.1username=sptadmdomain=4ef72197.cpx.localcontactemail=a@exploit.localCREATEAPITOKEN:root:root:not-applicable:WHM_FullRoot96.30.39.236— claimed source-IP in createacct accounting.log entry; IOC for join, not necessarily real originwebsocket/Shell interactive RCE
newOnce a forged session is in hand, operators pivot to /cpsess[N]/websocket/Shell for an interactive root shell. Three observed dimensions (24×80, 24×120, 24×134) function as a deterministic per-operator fingerprint.
GET /cpsess[0-9]+/websocket/Shell\?rows=rows=24&cols=80: Operator Arows=24&cols=120: Operator Brows=24&cols=134: Operator CAutomated agent recon shell wrapper
newInside the websocket shell, a follow-up tool wraps every command with __S_MARK__ and __E_MARK__ delimiters and harvests credentials, SSH keys, and shell history files. The wrapper is the strong actor tell: a human does not type these delimiters, so any host with the envelope in .bash_history has been touched by this specific tool.
printf '__S_MARK__'.*printf '__E_MARK__'find /root /home -maxdepth 3 -name '.bash_history' -exec catSSH key persistence with mtime forging
newNon-standard ssh-rsa keys planted across /root/.ssh, /etc, and cron paths. Comments include IP-labeled keys mimicking provider internal-key style. Detection should rely on ctime, not mtime; touch backdates the latter two but cannot backdate the former.
find /root /etc /var/spool/cron -type f -exec grep -l 'ssh-rsa' +^[0-9.]{7,15} ssh-rsa .sorry encryptor + qTox ransom
stableEncryptor binary masquerading as /root/sshd. Encrypts user files and system files (in-place restore fails; re-image required), drops a README with a TOX ID, C2 over a single IP. Files get the .sorry extension.
/root/sshd2fc0a056fd4eff5d31d06c103af3298d711f33dbcd5d122cae30b571ac511e5a— sha25668.183.190.253— C23D7889AEC00F2325E1A3FBC0ACA4E521670497F11E47FDE13EADE8FED3144B5EB56D6B198724— qTox IDDB wipe + index.html ransom note
stableCheaper variant. Drops a BTC-ransom note in every /home/*/public_html/index.html and removes /var/lib/mysql/mysql. Files are not encrypted; restore-from-backup recovers cleanly.
bc1q9nh4revv6yqhj2gc5usncrpsfnh7ypwr9h0sp2— BTC addressto recover your files, kindly send 0\.1 BTCMirai / nuclear.x86 cryptominer
stableCommodity Mirai-family dropper. Largest single bucket of the campaign by host count among the destructive variants. Persistence via cron and systemd; binary lands at well-known paths.
nuclear.x8687.121.84.78— binary hostraw.flameblox.com— C2 / dropperseobot.php SEO defacement + competitor-kill
newPer-site PHP webshell drop into every public_html, preceded by a pkill against known competitor processes (nuclear.x86, kswapd01, xmrig) and a final ALLDONE marker. Confirms cross-actor competition for the same vulnerable cohort.
*/public_html/seobot.phppkill -9 (nuclear\.x86|kswapd01|xmrig)echo ALLDONEprofile.d service-stub backdoor
evolvingA parallel persistence layer that surfaced in adjacent triage. A /etc/profile.d shim launches a non-standard binary at /root/.local/bin/system-service on every interactive shell login. Triggering on profile.d instead of cron means it fires more often and looks less like a scheduled task. Detection signal is a chmod permission-denied log line from a non-root shell login; that is exactly how it surfaced first.
/etc/profile.d/system_profiled_service.sh/root/.local/bin/system-servicechmod: cannot access '/root/\.local/bin/system-service'Cross-Provider Signal
We've corroborated the upstream chain (Pattern X → D → E → F) against several peer providers running their own detection. The granular tells (operator dimensions, the harvester envelope, the reseller-as-persistence pattern) show up consistently, which is what gives us confidence the toolkit is shared.
What Worked, What Didn't
A short, opinionated list. We're keeping this section generalizable: what would also be true for a similar incident tomorrow, not specifics about one provider's rollout.
Worked
- ModSecurity rule pack landed inside the disclosure window, before the public PoC. The WAF layer is the right place to close Pattern X; it intercepts at the request stage, before
saveSession()ever sees the malformedpass=field. - Pre-positioning the on-host IOC scanner allowed retrospective triage of hosts that had been touched before the WAF rules were live. Detect-then-mitigate is realistic; detect-only is not.
- Treating the reseller persistence and the API token as a separate cleanup step (not part of the cPanel patch) caught operators who tried to come back through the token after the patch landed.
Didn't
- CSF, APF, and standard host firewalls are not enough. Pattern X traffic looks like legitimate authenticated WHM; it goes to
:2087, it carries anAuthorizationheader, and it's structurally valid HTTPS. A perimeter firewall will not block it. - WHM-port-open hosts without an upstream WAF were the population that fell. The right posture is WAF inside the auth boundary, not just at the edge.
- Vendor IOC scripts shipped with at least one
grep -Pregex bug that produced silent false negatives. Don't trust an IOC script you haven't read.
Actionable: Mitigate & Block
Two concrete things you can push today.sessionscribe-mitigate.sh applies the active-defense posture in one phased pass; the tier-1 IP block list is the high-confidence operator infrastructure observed in the wild this week.
Mitigation tooling
sessionscribe-mitigate.sh is idempotent and writes timestamped backups of every mutated file under /var/cpanel/sessionscribe-mitigation/<TS>/. Default mode is --check (read-only posture audit); --apply mutates. Phases run in order: snapshot, patch posture, preflight, upcp (if unpatched), proxysub enforcement, CSF scrub, APF scrub, runfw inspection, Apache check, modsec rules, session quarantine, optional remote-probe self-test.
# 1) read-only posture audit (no mutations)
curl -s https://sh.rfxn.com/sessionscribe-mitigate.sh | bash
# 2) full apply (run as root; ModSec rules + CSF/APF cpsrvd-port scrub +
# proxysub enforcement + session quarantine into backup dir)
curl -s https://sh.rfxn.com/sessionscribe-mitigate.sh | bash -s -- --apply
# 3) selective phases (e.g. modsec only, no firewall mutations)
bash sessionscribe-mitigate.sh --apply --only modsec
# 4) JSONL output for fleet aggregation (Ansible/Salt/SSH-wrap)
bash sessionscribe-mitigate.sh --apply --jsonl --quiet > host.jsonl
# 5) revoke any sptadm reseller and the WHM_FullRoot API token
# (mitigate.sh does not touch reseller state; do this manually)
whmapi1 listacct | grep -i sptadm
whmapi1 delacct user=sptadm
whmapi1 list_tokens | grep -i 'WHM_FullRoot\|not-applicable'
whmapi1 revoke_api_token token_name=<token>Tier-1 attacker IPs (block today)
KB-known plus access-log 2xx success against the fleet. Both signals are observed at the IP level and are independent of host-verdict logic, so they survive any verdict-precision change in the scanner. Roles below are observed behavior in this corpus; treat as defensible attribution buckets, not named-actor attribution.
| IP | Observed role | UA |
|---|---|---|
| 80.75.212.14 | broad-scope exploitation; highest 2xx success volume in corpus | |
| 94.231.206.39 | TLS handshake to :2095, badpass exploit (KB-known) | |
| 142.93.43.26 | badpass exploit at scale | |
| 45.82.78.104 | TLS handshake to :2082, websocket Shell pivot (KB-known) | Chrome 135 / Opera 120 / Firefox 142 |
| 206.189.2.13 | leakix scanner badpass (KB-known) | leakix/2.0 |
| 157.245.204.205 | leakix scanner badpass (KB-known) | leakix/2.0 |
| 136.244.66.225 | session-origin pool, 2xx success | |
| 68.233.238.100 | badpass exploit (KB-known) | python-requests/2.33.1 |
| 159.223.155.255 | post-CVE 2xx wave (DigitalOcean cluster) | |
| 137.184.77.0 | badpass exploit (KB-known) | |
| 38.146.25.154 | Pattern D createacct source; Operator A | Go-http-client/1.1 |
| 67.205.134.215 | post-CVE 2xx wave (DigitalOcean cluster) | |
| 206.189.227.202 | post-CVE 2xx wave (DigitalOcean cluster) | |
| 192.81.219.190 | Pattern D enum + websocket Shell; Operator A (24x80) | |
| 146.19.24.235 | badpass exploit, recurring origin | |
| 149.102.229.144 | websocket Shell pivot; Operator B (24x120) | Mozilla/5.0 Firefox/142.0 |
| 183.82.160.147 | websocket Shell pivot; Operator C (24x134); pre-disclosure timestamp | Mozilla/5.0 |
| 87.121.84.78 | Pattern C nuclear.x86 binary host | |
| 68.183.190.253 | Pattern A .sorry encryptor C2 | |
| 96.30.39.236 | claimed source-IP in Pattern D createacct API call body (attacker-controlled field; useful for log join, not necessarily real origin); KB-known |
# csf.deny
csf -d 80.75.212.14 IC-5790-T1
csf -d 94.231.206.39 IC-5790-T1
csf -d 142.93.43.26 IC-5790-T1
csf -d 45.82.78.104 IC-5790-T1
csf -d 206.189.2.13 IC-5790-T1
csf -d 157.245.204.205 IC-5790-T1
csf -d 136.244.66.225 IC-5790-T1
csf -d 68.233.238.100 IC-5790-T1
csf -d 159.223.155.255 IC-5790-T1
csf -d 137.184.77.0 IC-5790-T1
csf -d 38.146.25.154 IC-5790-T1
csf -d 67.205.134.215 IC-5790-T1
csf -d 206.189.227.202 IC-5790-T1
csf -d 192.81.219.190 IC-5790-T1
csf -d 146.19.24.235 IC-5790-T1
csf -d 149.102.229.144 IC-5790-T1
csf -d 183.82.160.147 IC-5790-T1
csf -d 87.121.84.78 IC-5790-T1
csf -d 68.183.190.253 IC-5790-T1
csf -d 96.30.39.236 IC-5790-T1
# iptables (drop, log to /var/log/messages)
for ip in 80.75.212.14 94.231.206.39 142.93.43.26 45.82.78.104 \
206.189.2.13 157.245.204.205 136.244.66.225 68.233.238.100 \
159.223.155.255 137.184.77.0 38.146.25.154 67.205.134.215 \
206.189.227.202 192.81.219.190 146.19.24.235 149.102.229.144 \
183.82.160.147 87.121.84.78 68.183.190.253 96.30.39.236; do
iptables -I INPUT -s "$ip" -j DROP
doneOpen Questions
What's Next
The detection toolkit ships as four scripts: sessionscribe-ioc-scan.sh (on-host read-only triage; current version v2.5.0), sessionscribe-mitigate.sh (active defense: ModSecurity rules 1500030/1500031, cpsrvd-port scrub via CSF/APF, proxysub enforcement, patch posture), sessionscribe-remote-probe.sh (non-destructive remote verdict by HTTP code), and sessionscribe-revsnap.sh (pre/post-upgrade snapshot collector for binary-diff analysis). Canonical source at github.com/rfxn/cpanel-sessionscribe; curl-friendly mirror at sh.rfxn.com.
v4 of the scanner is in flight. The headline changes are:
- Verdict precision. Post-attack activity required for a COMPROMISED verdict; attempt-shaped IOCs tier into a separate ATTEMPT bucket. Stops attempt counts from inflating compromise counts.
- 2xx-on-cpsess split. Real exploit success gets its own gate, separate from generic recon 2xx.
- Real Pattern X timestamps. Per-event wall-clock timestamps replace forged-session iso for timeline analysis, addressing the open-question above.
- Pre-mitigation snapshot. Captures evidence before any active defense mutates host state, so the kill-chain remains reconstructable post-cleanup.
Next field-notes entry will be written after v4 ships and we've re-run detection across the existing corpus. We expect the cohort sizes to come down as attempts and compromises separate cleanly. The catalog above will not.