Skip to content

assetnote/cpanel2shell-scanner

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cpanel2shell-scanner

A high-fidelity scanner for the cPanel/WHM authentication bypass tracked as CVE-2026-41940. It identifies vulnerable hosts without producing the false-negatives common to public proofs-of-concept and detections, and without triggering the account lockout and root-IP-allowlist mechanisms that interfere with naive scanning.

The tool also bundles a separate, opt-in exploit chain for a same-family CalDAV path-traversal bug on cpdavd (ports 2079 plain / 2080 TLS) — CVE-2026-29205 — that lets a remote attacker read arbitrary files as root once a small SMTP-driven setup step succeeds. cPanel 11.134.0.26 fixes the underlying RAII lifetime bug so the read now runs as the unprivileged account owner; the traversal itself still reaches cpdavd but cannot escalate beyond what that account can already read. The CalDAV chain is gated behind --exploit and is off by default because it sends real emails and reads files from confirmed targets — see the Exploit mode (active) section.

Why this scanner

Most public detections for CVE-2026-41940 share three problems. This scanner addresses each of them.

You can read our blog post on this detection technique here: https://slcyber.io/research-center/high-fidelity-check-for-the-cpanel-authentication-bypass-cve-2026-41940/

It checks the proxy paths, not just the management ports

cPanel's per-vhost Apache configuration installs a ProxyPass that forwards /___proxy_subdomain_whm to 127.0.0.1:2086 and /___proxy_subdomain_cpanel to 127.0.0.1:2080 regardless of the request's Host header. The RewriteCond only constrains the rewrite that maps the management subdomain onto the proxy path; the ProxyPass itself is unconditional. Hitting these paths on any vhost served by a cPanel-managed Apache reaches the same vulnerable backend as the management ports.

Scanners that only probe ports 2082/2083/2086/2087 will report a host as not vulnerable when those ports are firewalled, even though the bug is fully reachable through 443. This scanner probes 2087, 2083, and the two proxy paths on 443 by default.

It does not get blocked by cphulkd or the root-IP allowlist

cPanel ships cphulkd, which locks accounts out after a small number of failed password attempts, and authorized_whm_root_ips, which restricts root logins to a configured list of source addresses. A scanner that exploits the bypass by trying to inject a session for root will:

  • be silently ignored when the scanner's IP is not in the root allowlist, producing a false negative; and
  • contribute failed-password events for whichever account it targets, eventually locking that account out and preventing both detection and legitimate logins.

This scanner avoids both issues on the WHM side by injecting expired=1 into the session payload under a randomly generated username. The session injection is verified by visiting the resulting cpsessXXXX URL and matching msg_code:[expired_session] in the response body, which is only present when the injection succeeded. No real account is targeted, so no real account can be locked out, and the root allowlist is irrelevant because no root login is attempted.

It uses a username wordlist where it has to

The cPanel daemon (cpaneld, ports 2083 and the /___proxy_subdomain_cpanel path) requires the supplied username to correspond to an existing cPanel account on disk (-f /var/cpanel/users/$user). A username of root will never satisfy this check because root is a system user, not a cPanel user. Detections that try only root produce false negatives on this surface. This scanner uses a configurable wordlist of common cPanel usernames against the cPanel surface and falls back to the random-username path on the WHM surface, which has no such restriction.

How the detection works

For each target the scanner performs the following steps per surface:

  1. Issue GET /login and read the Set-Cookie header for either whostmgrsession (WHM) or cpsession (cPanel). The cookie contains a comma-separated session-name component.
  2. Issue GET / with an Authorization: Basic header whose decoded value is <user>:\xff\nexpired=1. The trailing \nexpired=1 is the session-injection payload. The session cookie from step 1 is replayed unmodified.
  3. Read the Location header from the response and extract the cpsessXXXX token.
  4. Issue GET /<cpsessXXXX>/ with the original cookie and look for msg_code:[expired_session] in the body. Its presence proves the session injection succeeded and the host is vulnerable.

On WHM (port 2087 and the /___proxy_subdomain_whm path on 443) the username is a random u followed by ten hex characters. On cPanel (port 2083 and the /___proxy_subdomain_cpanel path on 443) the scanner walks its username wordlist and stops at the first match.

By default the scanner probes 2087, 2083, and 443 in that order and stops as soon as any surface confirms vulnerability.

CalDAV path-traversal exploit (--exploit)

CVE-2026-29205 — cPanel/WHM WP2 Security Update, May 13 2026. Fixed in cPanel 11.134.0.26. The advisory tracks the same cpdavd privilege-drop regression this exploit chain abuses.

Full write-up of the bug and the exploitation chain: https://slcyber.io/research-center/new-age-of-collisions-reading-arbitrary-files-pre-auth-as-root-in-cpanel-cve-2026-29205

cpdavd on ports 2079 (plain HTTP) and 2080 (TLS) trusts the <principal>/<collection>/... path it builds when serving CalDAV/CardDAV resources. By crafting a request whose path component encodes .. segments and pointing it at a maildir folder whose on-disk name also encodes traversal (x-attachment-1-y), cpdavd can be coerced into reading any file on disk as root, regardless of ownership or permissions — including /etc/shadow, /etc/passwd, and the per-user mail spools.

The defense-in-depth that was supposed to drop privileges to the account owner before the read silently failed: the Cpanel::AccessIds::ReducedPrivileges object was constructed in void context, so its destructor restored root privileges before the read ran. cPanel 11.134.0.26 binds the object to a my $privs lexical so it lives through the -f / stat / open / read chain; on patched hosts the read therefore runs as the unprivileged account owner instead of root.

The vulnerable folder must exist on disk before the read works. cPanel auto-creates a folder named .x-attachment-1-y for the recipient <user>+x-attachment-1-y@<domain> the first time an email lands at that sub-address. The chain is therefore:

  1. Enumerate plausible recipient domains from the host's TLS certificate SANs.
  2. For each domain, derive candidate local-parts (the domain's first label, plus a small wordlist of common mailbox prefixes such as info, admin, webmaster).
  3. Open one SMTP session against a configured outbound relay (e.g. SendGrid) and send <prefix>+x-attachment-1-y@<domain> to each candidate. Accepted RCPT TO responses are tracked.
  4. Wait through a retry ladder (5 s, 10 s, 20 s, 30 s) for the cPanel inbox delivery to materialise the folder.
  5. For each accepted recipient, send the path-traversal GET against cpdavd on ports 2080 (TLS) and 2079 (plain), under both the /calendar/ and /addressbook/ collection prefixes.

A success returns the file's bytes; the finding records the email used, the collection, the byte count, and the first 200 bytes as a preview.

This check is disabled unless --exploit is passed. When enabled it requires a working outbound SMTP relay (see Configuration file below) because the folder-creation step cannot be skipped.

Targeted vs sprayed exploitation

The exploit only works against real virtual email accounts configured under cPanel's Email Accounts feature. Catch-all addresses do not work — a catch-all routes via Exim's system_aliases router, never reaches the dovecot_virtual_delivery transport, and therefore never triggers the lda_mailbox_autocreate path that produces the .x-attachment-1-y/ folder.

When you already know a valid virtual email on the target, pass it with --email:

python scanner.py --config scanner.ini --email admin@target.com target.com

--email skips the cert-SAN enumeration and the prefix wordlist entirely and sends exactly one message to the address you supplied. It may be repeated to target multiple known accounts. The targeted path is much more reliable than the spray path.

Without --email, the scanner falls back to spraying ~15 common prefixes (info, admin, webmaster, etc.) per domain extracted from the host's TLS certificate. Assetnote's measurement on a sample of 200 cpdavd-exposed hosts was a ~10% spray hit rate: a clean result from spray-mode is weak evidence that the host is patched, and re-running with --email against a real account is the only reliable way to confirm.

Configuration file

Exploit mode reads an INI file via --config:

python scanner.py --config scanner.ini --exploit example.com

Copy scanner.ini.example to scanner.ini, fill in the SMTP credentials, and optionally tune the CalDAV defaults. scanner.ini is in .gitignore so the populated copy stays local. The SMTP password can also be supplied via the SCANNER_SMTP_PASSWORD environment variable, which takes precedence only when the password field in the file is empty.

Installation

pip install -r requirements.txt

Python 3.8 or later is required.

Usage

Single target:

python scanner.py example.com

Multiple targets via positional arguments:

python scanner.py host-a.example.com host-b.example.com:2083

A file of targets, one per line. Lines starting with # are ignored:

python scanner.py -f targets.txt

Reading targets from stdin:

cat targets.txt | python scanner.py

A target may be either a hostname or host:port. When a port is specified the scanner only probes that port; otherwise it probes 2087, 2083, and 443.

Common options

  • -u, --users — comma-separated cPanel usernames to try on the cPanel surface. Defaults to a small built-in list.
  • -U, --users-file — file with one cPanel username per line.
  • -p, --ports — comma-separated ports to probe when no port is specified on the target. Defaults to 2087,2083,443.
  • -t, --threads — per-target threads used to walk the username list against the cPanel surface. Defaults to 10.
  • -c, --concurrency — number of targets scanned in parallel. Defaults to 20.
  • -T, --timeout — per-request timeout in seconds. Defaults to 15.
  • -o, --output — append vulnerable targets, one per line, to this file as they are discovered.
  • --json — write a JSON Lines record per target to this file.
  • -q, --quiet — only print vulnerable targets on stdout. Connection failures and clean targets are still recorded in --json and counted in the summary.
  • --no-progress — disable the progress bar.
  • --exploit — enable the CalDAV path-traversal chain. Disabled by default; see Exploit mode (active) for the side effects this flag unlocks.
  • --config — INI file with the SMTP relay credentials and CalDAV tunables. See scanner.ini.example.
  • --read-file — file to exfiltrate when the CalDAV chain succeeds. Overrides the value in the config file. Defaults to /etc/shadow — a root-only file, so a successful read distinguishes pre-patch (returns shadow contents) from post-patch (open denied, body empty → reported NOT VULNERABLE). Use --read-file /etc/passwd to test traversal reachability without distinguishing patched/unpatched.
  • --caldav-only — skip the 41940 check and only run the CalDAV chain. Implies --exploit. Useful for re-running the chain against a target list already known to be CalDAV-reachable.
  • --email ADDR — known virtual email account on the target. Skips cert SAN enumeration and the spray wordlist; sends exactly one message to ADDR and reads against that principal. May be repeated. Implies --exploit. See Targeted vs sprayed exploitation.
  • -v, --verbose — emit per-domain progress for the CalDAV chain (cert SAN list, spray count, retry ladder).

Output

One line is written to stdout per finding, so a target running both checks will print twice:

[!] host  cve-2026-41940    VULNERABLE (port 443)
[!] host  caldav-traversal  VULNERABLE via admin@host (read 1842b from /etc/passwd)
[+] host  cve-2026-41940    NOT VULNERABLE
[?] host  cve-2026-41940    CONNECTION FAILED

The --json output is one record per target with a findings array:

{"target": "host", "status": "VULNERABLE", "findings": [
  {"check": "cve-2026-41940", "status": "VULNERABLE", "detail": {"port": 443}},
  {"check": "caldav-traversal", "status": "VULNERABLE",
   "detail": {"email": "admin@host", "domain": "host", "collection": "calendar",
              "file": "/etc/shadow", "bytes": 1218, "preview": "root:$6$..."}}
]}

status at the top level is the worst case across all findings.

A summary line with totals is written to stderr at the end. The progress bar is rendered on stderr and is automatically suppressed when stderr is not a terminal.

The exit code is 0 if any target is vulnerable, 1 if every reachable target was clean, and 2 if no target could be reached.

Examples

Scan a list of targets, write hits to a file, and stay quiet on stdout:

python scanner.py -f targets.txt -o vulnerable.txt -q

Scan with a custom username list against the cPanel surface, increased parallelism, and JSON output for downstream processing:

python scanner.py -f targets.txt -U cpanel-users.txt -c 100 --json results.jsonl

Probe a non-default port set:

python scanner.py -p 2083,2087,8443 -f targets.txt

Run the CalDAV chain end-to-end against a single target with the email-spray fallback (requires a populated scanner.ini):

python scanner.py --config scanner.ini --exploit example.com

Targeted exploitation against a known virtual email — much higher hit rate than spray:

python scanner.py --config scanner.ini --email admin@example.com example.com

Run only the CalDAV chain against a list of confirmed cpdavd hosts, dumping /etc/passwd previews to JSON:

python scanner.py --config scanner.ini --caldav-only \
    -f cpdavd-hosts.txt --json caldav-results.jsonl

Notes on safety

Default mode (safe)

The default scanner.py <target> invocation only runs the CVE-2026-41940 detector. It sends the requests required to confirm the session injection and nothing else. It does not log in as any real user, does not target the root account, does not escalate to a shell, and does not accumulate failed-password events against any valid account on a target system. The marker it matches (msg_code:[expired_session]) is generated by the application itself in response to the injected expired=1 session field and is the same indicator the upstream cPanel login page uses when a legitimately expired session is replayed.

Exploit mode (active)

Passing --exploit (or --caldav-only) unlocks the CalDAV path-traversal chain. This is no longer a detector — it is a working exploit. When enabled the scanner will, for every target where domain enumeration succeeds:

  • open a real SMTP session against the relay configured in scanner.ini and send one short message per candidate recipient (typically 10–15 per domain, 3 domains per target);
  • wait up to ~65 seconds per domain for delivery to materialise the malicious maildir folder;
  • attempt to read the configured --read-file from every confirmed target.

The default file is /etc/shadow. A successful read returns the file's bytes; an empty body indicates either an unreachable target or a host where the privilege-drop fix (cPanel 11.134.0.26, my $privs = …) is in place. Detection compares received body length to zero, not to the response's advertised Content-Length (which is derived from stat() on the attacker-chosen path and will be populated even when the subsequent open() is denied).

To test traversal reachability independently of the privilege-drop fix — e.g. when you want to know whether cpdavd is reachable and the maildir prerequisite was satisfied on a patched host — re-run with --read-file /etc/passwd. /etc/passwd is world-readable so it returns bytes on both pre-patch and post-patch hosts; combining the two signals (/etc/shadow body present = pre-patch root read; /etc/shadow empty + /etc/passwd present = traversal reachable but priv-drop fix applied) classifies a host unambiguously.

The contents and a 200-byte preview are written to the JSONL output and printed on stdout. Per-domain CalDAV exploitation can take a minute or more — this is the retry ladder waiting for mail delivery, not a hang.

Only run --exploit against assets you own or have explicit written authorisation to test. The SMTP traffic is logged by the configured relay and by every recipient mail system; the file reads are logged by cpdavd.

About

High fidelity scanner for CVE-2026-41940 (cPanel & WHM authentication bypass)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages