Field Notes: CVE-2026-41940 Exploitation in the Wild
This is a live, ongoing incident. New patterns, attacker IPs, and IOCs are still surfacing as we publish; we update this page as they do, and section anchors stay stable so cross-links keep resolving. Primitive analysis is in the companion write-up; corroboration and new evidence welcome via GitHub issues on the source repo.
This is a field journal of what we observed during the opening week of CVE-2026-41940 exploitation in the wild, and what we continue to surface in the days after. 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 opening 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 and the three-script operator toolkit at the end are what we are running detection and posture against today; both grow as new evidence comes in.
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): field notes
- First probe
- 2026-04-11T05:32Z
- Vendor disclosure
- 2026-04-28T17:05Z
- Public CVE
- 2026-04-29T19:46Z
- Most recent event
- 2026-05-02T11:33Z
- The chain
- Layered, not parallel. Every confirmed compromise we examined ran the same upstream chain (Pattern X → D → E → F). Only the destructive terminal stage varied (A, B, C, H). That is one toolkit shared, not independent attacker workflows.
- Multi-tool
- Multi-tool activity, suggestive tells. Three websocket-Shell dimension fingerprints (24×80, 24×120, 24×134) plus distinct UA families (Go-http-client automation, browser-driven Firefox, browser-driven Chrome/Opera) point to at least two or three concurrent toolchains. Both signals are spoofable, so treat them as tells, not forensic-grade attribution. The strongest multi-actor evidence is independent: at least one host took two end-to-end kill chains in a 51-minute window with no session-token reuse, and Pattern H's competitor-kill
pkill -9 nuclear.x86 kswapd01 xmrigconfirms cross-actor competition for the same vulnerable cohort. - First probe
- 2026-04-11 05:32 UTC. First Pattern X event observed in the wild: single IP, single host, canonical badpass shape (multi-line
pass=field throughsaveSession(), injectedcp_security_token=/cpsess[N],tfa_verified=1with no real login). The 17-day quiet window starts here and runs through vendor disclosure on 04-28. The same source IP returns on subsequent days against different host sets, paced and surgical: an actor with a working primitive walking a target list, not a scanner.
Defender quick-links
Tier-1 list (KB-known + access-log 2xx success). csf/apf/iptables one-shot dispatcher snippet.
Audit posturesessionscribe-mitigate.sh in --check (read-only) or --apply (active). Idempotent, snapshot-first.
sessionscribe-ioc-scan.sh on-host triage; verdict pair + section matrix. Read-only.
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.First-Probe Fingerprint
The earliest Pattern X event we have on disk lands at 2026-04-11 05:32 UTC. Single source IP, single host, canonical badpass shape: a session file written through saveSession() with a multi-line pass= field, an injected cp_security_token=/cpsess[N], and tfa_verified=1 with no real login origin. That is the bug working, against a real target, seventeen days before the vendor disclosed anything publicly.
The shape of this event is the canonical fingerprint we track against everything that follows. Two attributes carry over into every later operator profile:
- Same session-file shape, different IPs. Across the 17-day quiet window, the same IOC ladder (
token_denied=1,cp_security_token=/cpsess[N],tfa_verified=1, multi-linepass=) appears from a small set of recurring source IPs. The shape is deterministic, the source IPs are not yet attribution. - Surgical pacing. Same IP, different host set, paced inside a six-hour window and then quiet for hours or days. Targets share no obvious identifier beyond a vulnerable cPanel/WHM build. This is an actor with a working primitive walking a target list, not a scanner spraying the internet.
Disclosure & Public CVE
Day 17 reframes everything that came before. cPanel published KB 40073787579671 at 2026-04-28 17:05 UTC, filed only as an unauthenticated authentication bypass with no CVE assigned. The URL is publicly reachable but distributed through the providers KB feed, so in practice it landed as a vendor disclosure to providers, not a wide announcement.
The patched build shipped about four hours later, at 21:36 UTC (04:36 PM CT). That evening we reverse-engineered the build (see our reverse-engineering write-up) to recover the underlying primitive, then swapped the broad first-wave rules for primitive-aware WAF signatures and host posture changes.
CVE-2026-41940 and a working PoC from watchTowr Labs landed 27 hours after the KB, on 04-29 at 19:46 UTC. That 27-hour KB-to-PoC gap was the entire defensive window any provider had 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
Wide shot first, then per-stage detail. The diagram below maps the campaign window across the 22 days of activity: phase ribbon (zero-day → vendor disclosure → public CVE tail), the upstream chain every confirmed compromise ran, the destructive variants we saw at the terminal stage, and the three operator-tell buckets. Click to zoom.
The most useful thing we can hand to another defender is a walked-through kill chain on a single compromised host, with the artifacts the on-host scanner surfaces at each stage. The sequence below is what sessionscribe-ioc-scan.sh recovered on a host that took the full chain; hostnames and account names are redacted, everything else is on-disk evidence. The collapsible block at the end has the raw sectioned report for the same host.
Kill Chain· Pattern X to D to G to E to F to 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
Operators pivot from JSON-API into the WHM in-browser shell at /cpsess[N]/websocket/Shell. Three observed terminal-dimension fingerprints (24×80, 24×120, 24×134) plus distinct UA families separate the toolchains in use; both signals are spoofable, so treat them as tells, not forensic attribution.
- 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 toolchain fingerprints share the upstream chain and the JSON-API recon toolkit. They diverge at the interactive stage (websocket Shell dimensions) and at the UA string. The strongest case for treating these as separate operators is behavioral, not signature-based: at least one host took two end-to-end kill chains in a 51-minute window with no shared session token between them. Dimensions and UA strings are spoofable, so the cards below are best read as tooling buckets we observed in the wild, not named-actor attribution. Behavioral co-occurrence (timing, sequencing, post-shell command patterns) is what promotes a tooling bucket to a defensible operator claim.
- 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 toolchain that produces the 24×134 dimension. Browser-driven, similar TTPs to Operator B, but with measurably different terminal width and a different UA family. Recurs across the post-disclosure window from the same source IP.
- ips
183.82.160.147- user-agents
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
The 24×134 dimension is the most operator-specific tell in the dataset because it is non-default for both terminal emulators and automation libraries; it strongly suggests a person sizing a terminal pane to a custom width. We see this fingerprint return on multiple hosts across the post-disclosure window, which makes it the most reliable of the three for cross-host correlation.
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) separate the toolchains we observed, with the caveat that dimensions are passed in the request and are trivially spoofable.
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: Validate, Defend, Block
Three scripts cover the operational picture, each with a distinct role. Pick by what you need to answer:
Is this host compromised? Read-only on-host scan of session store, access logs, accounting log, cpsrvd binary, and Pattern A through I destruction artifacts. Emits a code_verdict + host_verdict pair with section matrix.
Is this host defended? Audits patch state, ModSecurity rule pack, CSF/APF cpsrvd-port scrub, proxysub enforcement; with --apply, brings the host into the patched + posture-correct state in one phased idempotent pass.
Which hosts in my fleet are vulnerable? Non-destructive remote verdict by HTTP code; fires a canary-tagged session that ioc-scan recognizes and routes to PROBE_ARTIFACT, so self-tests do not escalate. Designed for parallel fleet sweeps before you have shell access everywhere.
sessionscribe-ioc-scan.sh: validate host state
Read-only by design. Sectioned report on stderr by default; structured JSON, JSONL stream, or single-row CSV available. Exit code is the highest-priority verdict observed (0 clean, 1 vulnerable, 2 inconclusive, 3 suspicious, 4 compromised).
# 1) on-host triage (sectioned report on stderr)
curl -s https://sh.rfxn.com/sessionscribe-ioc-scan.sh | bash
# 2) JSON envelope to file + JSONL stream for aggregation
bash sessionscribe-ioc-scan.sh -o /root/scan.json --jsonl --quiet > host.jsonl
# 3) tighten log/heuristic window for fast retriage (90 days)
bash sessionscribe-ioc-scan.sh --since 90 --verbose
# 4) single-row CSV summary (fleet-rollup friendly)
bash sessionscribe-ioc-scan.sh --csv > host.csvsessionscribe-mitigate.sh: validate & enforce defensive posture
Idempotent. Writes timestamped backups of every mutated file under /var/cpanel/sessionscribe-mitigation/<TS>/. Default mode is --check (posture audit, no mutations); --apply mutates. Phases: 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>sessionscribe-remote-probe.sh: fleet-level scan
Non-destructive remote verdict over the network. Sends a canary-tagged probe request and reads HTTP response codes plus a single redirect to determine SAFE vs VULNERABLE without actually exploiting. Pair with xargs -P or your fleet runner of choice for parallel sweeps. The canary attribute on the resulting session file is recognized by ioc-scan's PROBE_ARTIFACT bucket, so a probe sweep does not self-trigger compromise alerts on the targets.
# 1) probe a single host (non-destructive verdict by HTTP code)
curl -s https://sh.rfxn.com/sessionscribe-remote-probe.sh | bash -s -- --target host.example.com
# 2) parallel fleet sweep, JSONL out (16 concurrent probes)
xargs -a fleet.txt -P 16 -I {} \
bash sessionscribe-remote-probe.sh --target {} --jsonl --quiet \
>> fleet-probe.jsonl
# 3) verdict summary (after sweep)
jq -r '[.host, .verdict] | @tsv' fleet-probe.jsonl | sort -u | column -t
# 4) targets that came back VULNERABLE; pipe into per-host triage
jq -r 'select(.verdict=="VULNERABLE") | .host' fleet-probe.jsonl \
| while read h; do
ssh "$h" 'bash -s' < sessionscribe-ioc-scan.sh \
--jsonl --quiet > "${h}.jsonl"
doneTier-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); recurs across the window | 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 |
# Tier-1 IC-5790 attacker IPs. Run as root; auto-routes to whichever
# host firewall is present (csf preferred, apf next, iptables fallback).
IPS=(
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
)
COMMENT="IC-5790-T1"
if command -v csf >/dev/null 2>&1; then
for ip in "${IPS[@]}"; do csf -d "$ip" "$COMMENT"; done
elif command -v apf >/dev/null 2>&1; then
for ip in "${IPS[@]}"; do apf -d "$ip" "$COMMENT"; done
else
# No csf/apf; raw iptables fallback. Drops + persists via your distro's
# iptables-save tooling (iptables-services, netfilter-persistent, etc.)
for ip in "${IPS[@]}"; do
iptables -I INPUT -s "$ip" -j DROP -m comment --comment "$COMMENT"
ip6tables -I INPUT -s "$ip" -j DROP -m comment --comment "$COMMENT" 2>/dev/null || true
done
fiOpen 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.
sessionscribe-ioc-scan.sh v2.5.0 shipped this week and is the v4-class release referenced in the worked example above. Headline changes already in production:
- Verdict precision. Post-attack activity required for a COMPROMISED verdict; attempt-shaped IOCs tier into a separate SUSPICIOUS bucket, and sessions tagged with the remote-probe canary land in a dedicated PROBE_ARTIFACT bucket so self-tests do not escalate. Stops attempt counts from inflating compromise counts.
- 2xx-on-cpsess split. Real exploit success gates separately from generic recon 2xx; access-log traffic against
/cpsess[N]/endpoints is the operator-success signal, traffic against everything else is the recon signal. - Real Pattern X timestamps. Per-event wall-clock timestamps replace forged-session iso for timeline analysis, which is what makes the operator-tells correlation in the operator-profiles section hold up.
- Pre-mitigation snapshot. Companion
sessionscribe-mitigate.sh v0.5.1captures evidence (users/, accounting.log, sessions/, cpanel.config, tweaksettings) before any active defense mutates host state, so the kill-chain remains reconstructable post-cleanup. - Section-matrix rendering + bracketed status tags. Output is now portable across non-UTF8 terminals (CL6/EL6 hosts, console SSH on minimal images), with a per-section verdict matrix at the top of the summary block. Visible in the sectioned report sample above.
Next iteration in flight: kill-chain timestamp reconstruction from .bash_history #<epoch> markers (the current Pattern F timestamp source is file mtime, which any subsequent shell session bumps), plus an access-log fingerprint for the CRLF primitive itself so detection still fires on hosts where mitigate.sh has already quarantined the forged session files. Next field-notes entry follows once those land.