critical Threat analysis

cPanel & WHM CVE-2026-41940: KEV Authentication Bypass in Hosting Control Planes

CISA added WebPros cPanel & WHM and WP2 CVE-2026-41940 to KEV on 2026-04-30 and marks ransomware use as known. WebPros patched many cPanel branches and WP2 136.1.7, provided session-file IOC checks, and urged immediate update or service exposure reduction.

#cpanel#cisa-kev#zero-day#hosting#ransomware
On this page 0% read

    Executive Summary

    CISA added CVE-2026-41940 to the Known Exploited Vulnerabilities catalog on 2026-04-30, with a due date of 2026-05-03, and marks known ransomware campaign use as Known CISA KEV. The affected products are cPanel & WHM, including DNSOnly, and WP2 (WordPress Squared). CISA describes the issue as an authentication bypass in the login flow that allows unauthenticated remote attackers to gain unauthorized control-panel access CISA KEV.

    WebPros says the issue affects all cPanel software versions after 11.40, pushed patched builds for active branches, and provided a detection script that inspects /var/cpanel/sessions and /usr/local/cpanel/logs/access_log for compromised session indicators cPanel. Treat internet-exposed cPanel or WP2 login surfaces as active intrusion risk until patched-build proof and session-file review are complete.

    Key Facts

    cve: "CVE-2026-41940"
    vendor: "WebPros"
    products:
      - "cPanel & WHM"
      - "cPanel DNSOnly"
      - "WP2 (WordPress Squared)"
    kev_added: "2026-04-30"
    kev_due: "2026-05-03"
    known_ransomware_campaign_use: "Known"
    cwe: "CWE-306"
    vulnerability: "missing authentication for critical function / authentication bypass"
    patched_cpanel_builds:
      - "11.86.0.41 and higher"
      - "11.94.0.28 and higher"
      - "11.102.0.39 and higher"
      - "11.110.0.97 and higher"
      - "11.118.0.63 and higher"
      - "11.124.0.35 and higher"
      - "11.126.0.54 and higher"
      - "11.130.0.19 and higher"
      - "11.132.0.29 and higher"
      - "11.134.0.20 and higher"
      - "11.136.0.5 and higher"
    patched_wp2: "136.1.7 and higher"
    high_value_evidence:
      - "/usr/local/cpanel/cpanel -V"
      - "/var/cpanel/sessions"
      - "/usr/local/cpanel/logs/access_log"

    Source Confidence & Evidence Mapping

    • confirmed: CISA KEV lists CVE-2026-41940 as known exploited and records known ransomware campaign use CISA KEV.
    • confirmed: WebPros lists patched cPanel and WP2 versions, required update commands, mitigations for exposed service ports, and a session-file detection script cPanel.
    • confirmed: NVD maps CVE-2026-41940 to WebPros cPanel & WHM and WP2 and references the vendor advisory NVD.
    • confirmed: WP2 changelog material is one of the vendor references CISA lists for the 136.1.7 fixed line WP2 changelog.
    • unknown: Public sources reviewed here do not provide a complete exploit chain, ransomware family mapping, or victim list.

    Impact Determination

    ClassificationCriteriaRequired evidenceHandling decision
    Confirmed compromiseSession files or access logs match vendor IOC patterns, unexplained control-panel access exists, or post-auth administrative changes follow exposed login traffic.Session files, preauth files, access log rows, account changes, API calls, webshell or backup access evidence.Isolate affected hosts, preserve cPanel evidence, rotate root/reseller/account credentials, and review hosted sites.
    Presumed exposedInternet-exposed cPanel/WHM/WP2 is below a patched build or patch status is unknown.Version output, package inventory, asset scan, firewall exposure, and update tier config.Patch immediately or block ports 2083, 2087, 2095, and 2096; keep session review open.
    Potentially exposedcPanel or WP2 is known in inventory but branch, build, or public exposure is incomplete.CMDB, scanner, DNS, service, update-tier, and firewall evidence.Collect exact version and exposure proof.
    Not exposedNo affected product or exposed control-plane service appears in complete inventory, or patched build and negative session review are proven.Version check, access-log review, session scan, firewall config, and asset inventory.Preserve closure evidence and monitor for reused credentials.
    UnknownVersion, session, access log, or firewall evidence is missing.Named telemetry gap with owner and time window.Keep control planes and hosted-account credentials in scope until evidence is recovered.

    Timeline

    • 2026-04-28: WebPros publishes the initial cPanel/WHM and WP2 security update cPanel.
    • 2026-04-30: CISA adds CVE-2026-41940 to KEV with a 2026-05-03 due date CISA KEV.
    • 2026-05-01 to 2026-05-05: WebPros updates patched branch guidance and detection script false-positive handling cPanel.
    • 2026-06-01: This Halting Problems refresh found no existing local post for CVE-2026-41940.

    Technical Analysis

    This bug sits at a hosting control-plane boundary. A successful unauthenticated bypass against cPanel/WHM can pivot into hosted account access, mailbox control, backup access, DNS changes, webroot modification, and credential theft. CISA’s ransomware-use flag raises the priority: do not wait for webshell evidence before checking session files and patch status CISA KEV.

    WebPros’ required actions are concrete: update with /scripts/upcp --force, verify /usr/local/cpanel/cpanel -V, restart cpsrvd, and manually move pinned or disabled update configurations to a patched branch where needed cPanel. If updating is not possible, WebPros recommends blocking inbound control-panel ports and disabling relevant services until the host can be patched cPanel.

    Detection and Hunting

    Script: local repository and exported telemetry scope

    #!/usr/bin/env python3
    import os
    import sys
    import json
    import subprocess
    from pathlib import Path
    
    ROOT = sys.argv[1] if len(sys.argv) > 1 else "."
    LOG_ROOT = os.environ.get("LOG_ROOT", "")
    OUT = Path(os.environ.get("OUT", "hp-cpanel-whm-cve-2026-41940-kev-scope"))
    SINCE = "2026-06-01T00:00:00Z"
    UNTIL = "2026-06-01T23:59:59Z"
    
    PACKAGES = [
    ]
    VERSIONS = [
    ]
    FILES = [
    ]
    DOMAINS = [
      "www.cisa.gov",
      "support.cpanel.net",
      "nvd.nist.gov",
      "docs.wpsquared.com",
    ]
    URLS = [
      "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json",
      "https://support.cpanel.net/hc/en-us/articles/40073787579671-Security-CVE-2026-41940-cPanel-WHM-WP2-Security-Update-04-28-2026",
      "https://nvd.nist.gov/vuln/detail/CVE-2026-41940",
      "https://docs.wpsquared.com/changelogs/versions/changelog/#13617",
    ]
    IPS = [
      "11.86.0.41",
      "11.94.0.28",
      "11.102.0.39",
      "11.110.0.97",
      "11.118.0.63",
      "11.124.0.35",
      "11.126.0.54",
      "11.130.0.19",
      "11.132.0.29",
      "11.134.0.20",
      "11.136.0.5",
    ]
    HASHES = [
    ]
    PROCESS_PATTERNS = [
    ]
    NETWORK_PATTERNS = [
    ]
    
    # Positive signal: repository, lockfile, artifact, process, or network telemetry contains one of the exact incident selectors above.
    # Escalation: any match tied to a production build, CI run, deployed asset, or secret-bearing host moves the asset to presumed exposed.
    
    OUT.mkdir(parents=True, exist_ok=True)
    indicators_file = OUT / "indicators.txt"
    
    # Collect unique indicators
    indicators = set()
    for group in [PACKAGES, VERSIONS, FILES, DOMAINS, URLS, IPS, HASHES, PROCESS_PATTERNS, NETWORK_PATTERNS]:
        for val in group:
            if val:
                indicators.add(val)
    
    with open(indicators_file, "w") as f:
        for ind in sorted(indicators):
            f.write(ind + "\n")
    
    print(f"[+] Written unique selectors to {indicators_file}")
    
    # Walk local directory
    print(f"[+] Scanning directory: {ROOT} for selectors...")
    matches = []
    exclude_dirs = {"node_modules", "vendor", "dist", ".git"}
    for root, dirs, filenames in os.walk(ROOT):
        dirs[:] = [d for d in dirs if d not in exclude_dirs]
        for filename in filenames:
            filepath = Path(root) / filename
            try:
                content = filepath.read_text(errors="ignore")
                for ind in indicators:
                    if ind in content:
                        matches.append(f"{filepath}: found '{ind}'")
            except Exception:
                pass
    
    if matches:
        (OUT / "repository-indicator-matches.txt").write_text("\n".join(matches) + "\n")
        print(f"[!] Found {len(matches)} matches in codebase!")
    
    # Optional Log Scanning
    if LOG_ROOT and os.path.exists(LOG_ROOT):
        print(f"[+] Scanning telemetry log directory: {LOG_ROOT}...")
        log_matches = []
        for root, _, filenames in os.walk(LOG_ROOT):
            for filename in filenames:
                filepath = Path(root) / filename
                try:
                    content = filepath.read_text(errors="ignore")
                    for ind in indicators:
                        if ind in content:
                            log_matches.append(f"{filepath}: found '{ind}'")
                except Exception:
                    pass
        if log_matches:
            (OUT / "exported-telemetry-indicator-matches.txt").write_text("\n".join(log_matches) + "\n")
            print(f"[!] Found {len(log_matches)} matches in logs!")
    
        if PACKAGES:
            registry_dir = OUT / "registry"
            registry_dir.mkdir(exist_ok=True)
    
    print(f"[+] Wrote scope artifacts under {OUT}")

    Downstream Abuse Audits

    Script: GitHub organization run, release, secret, and workflow audit

    #!/usr/bin/env python3
    import os
    import sys
    import json
    import subprocess
    from pathlib import Path
    
    if "ORG" not in os.environ:
        print("ERROR: Set ORG environment variable to the GitHub organization to audit", file=sys.stderr)
        sys.exit(1)
    
    ORG = os.environ["ORG"]
    SINCE = "2026-06-01T00:00:00Z"
    UNTIL = "2026-06-01T23:59:59Z"
    OUT = Path(os.environ.get("OUT", "hp-cpanel-whm-cve-2026-41940-kev-github-audit"))
    
    SELECTORS = [
      "www.cisa.gov",
      "support.cpanel.net",
      "nvd.nist.gov",
      "docs.wpsquared.com",
      "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json",
      "https://support.cpanel.net/hc/en-us/articles/40073787579671-Security-CVE-2026-41940-cPanel-WHM-WP2-Security-Update-04-28-2026",
      "https://nvd.nist.gov/vuln/detail/CVE-2026-41940",
      "https://docs.wpsquared.com/changelogs/versions/changelog/#13617",
      "11.86.0.41",
      "11.94.0.28",
      "11.102.0.39",
      "11.110.0.97",
      "11.118.0.63",
      "11.124.0.35",
      "11.126.0.54",
      "11.130.0.19",
      "11.132.0.29",
      "11.134.0.20",
      "11.136.0.5",
    ]
    
    # Positive signal: a workflow run, release, secret, key, package, or workflow change overlaps the exposure window and references an incident selector.
    # Remediation trigger: unauthorized post-exposure write activity or a secret-bearing run matching an incident selector requires token revocation and downstream cloud/registry review.
    
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "runs").mkdir(exist_ok=True)
    (OUT / "logs").mkdir(exist_ok=True)
    (OUT / "repos").mkdir(exist_ok=True)
    
    # 1. Write incident-selectors file
    selectors_file = OUT / "incident-selectors.txt"
    with open(selectors_file, "w") as sf:
        for s in SELECTORS:
            if s:
                sf.write(s + "\n")
    
    # 2. Get list of repos
    print(f"[+] Fetching repositories for organization: {ORG}")
    repo_res = subprocess.run(["gh", "repo", "list", ORG, "--limit", "1000", "--json", "nameWithOwner"], capture_output=True, text=True)
    if repo_res.returncode != 0:
        print(f"[-] Failed to fetch repos: {repo_res.stderr}", file=sys.stderr)
        sys.exit(1)
    
    repos = [r["nameWithOwner"] for r in json.loads(repo_res.stdout)]
    
    for repo in repos:
        safe_repo = repo.replace("/", "__")
        print(f"[+] Auditing repository: {repo}")
    
        # Check runs in the window
        runs_res = subprocess.run([
            "gh", "api", f"/repos/{repo}/actions/runs",
            "-f", "per_page=100",
            "-f", f"created=>={SINCE}",
            "--paginate"
        ], capture_output=True, text=True)
    
        if runs_res.returncode == 0:
            try:
                all_runs = json.loads(runs_res.stdout).get("workflow_runs", [])
                filtered_runs = [r for r in all_runs if r["created_at"] <= UNTIL]
    
                if filtered_runs:
                    with open(OUT / "runs" / f"{safe_repo}-runs.jsonl", "w") as rf:
                        for run in filtered_runs:
                            rf.write(json.dumps(run) + "\n")
    
                            # Fetch log dynamically
                            run_id = str(run["id"])
                            log_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--log"], capture_output=True, text=True)
                            if log_res.returncode == 0:
                                (OUT / "logs" / f"{safe_repo}-{run_id}.log").write_text(log_res.stdout)
    
                            # Fetch details
                            view_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--json", "databaseId,workflowName,headSha,event,createdAt,jobs"], capture_output=True, text=True)
                            if view_res.returncode == 0:
                                (OUT / "runs" / f"{safe_repo}-{run_id}.json").write_text(view_res.stdout)
            except Exception as e:
                print(f"[-] Error parsing runs for {repo}: {e}")
    
        # Check releases in window
        subprocess.run(["gh", "api", f"/repos/{repo}/releases", "-f", "per_page=100", "--paginate"], capture_output=True)
        # Check repo secrets updated in window
        subprocess.run(["gh", "api", f"/repos/{repo}/actions/secrets", "-f", "per_page=100", "--paginate"], capture_output=True)
        # Check deploy keys
        subprocess.run(["gh", "api", f"/repos/{repo}/keys", "-f", "per_page=100", "--paginate"], capture_output=True)
    
    # Scan output directory for any indicator selector matches
    print("[+] Scanning gathered telemetry for indicator matches...")
    subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(selectors_file), str(OUT)], capture_output=False)
    
    print(f"[+] Wrote GitHub audit artifacts under {OUT}")

    Script: cloud OIDC and deployment credential follow-on audit

    #!/usr/bin/env python3
    import os
    import json
    import subprocess
    from pathlib import Path
    
    SINCE = "2026-06-01T00:00:00Z"
    UNTIL = "2026-06-01T23:59:59Z"
    OUT = Path(os.environ.get("OUT", "hp-cpanel-whm-cve-2026-41940-kev-cloud-audit"))
    AWS_REGIONS = os.environ.get("AWS_REGIONS", "us-east-1").split(",")
    
    # Positive signal: token exchange or privileged write activity occurs in the exposure window from GitHub, CI/CD, package registry, or deployment automation identity.
    # Remediation trigger: unexpected write, deploy, IAM, secret, or registry activity tied to an exposed CI/CD path requires trust-policy disablement and credential rotation.
    
    OUT.mkdir(parents=True, exist_ok=True)
    
    # 1. AWS CloudTrail Audit
    print("[+] Querying AWS CloudTrail for Web Identity token exchanges...")
    aws_events = []
    for region in AWS_REGIONS:
        res = subprocess.run([
            "aws", "cloudtrail", "lookup-events",
            "--region", region,
            "--start-time", SINCE,
            "--end-time", UNTIL,
            "--lookup-attributes", "AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity",
            "--output", "json"
        ], capture_output=True, text=True)
    
        if res.returncode == 0:
            try:
                events = json.loads(res.stdout).get("Events", [])
                for event in events:
                    ct = json.loads(event.get("CloudTrailEvent", "{}"))
                    ct["region"] = region
                    aws_events.append(ct)
            except Exception as e:
                print(f"[-] Error parsing AWS CloudTrail events: {e}")
    
    if aws_events:
        with open(OUT / "aws-assume-role-with-web-identity.jsonl", "w") as f:
            for ev in aws_events:
                f.write(json.dumps(ev) + "\n")
    
        # Audit follow-on events for returned access keys
        for ev in aws_events:
            access_key = ev.get("responseElements", {}).get("credentials", {}).get("accessKeyId")
            region = ev.get("region", "us-east-1")
            if access_key:
                print(f"[+] Enumerating AWS events for AccessKey: {access_key}")
                f_res = subprocess.run([
                    "aws", "cloudtrail", "lookup-events",
                    "--region", region,
                    "--start-time", SINCE,
                    "--end-time", UNTIL,
                    "--lookup-attributes", f"AttributeKey=AccessKeyId,AttributeValue={access_key}",
                    "--output", "json"
                ], capture_output=True, text=True)
                if f_res.returncode == 0:
                    try:
                        f_events = json.loads(f_res.stdout).get("Events", [])
                        with open(OUT / "aws-follow-on-api-calls.jsonl", "a") as ff:
                            for fe in f_events:
                                ff.write(fe.get("CloudTrailEvent", "{}") + "\n")
                    except Exception as e:
                        print(f"[-] Error writing follow-on events: {e}")
    
    # 2. Azure Activity Log Audit
    print("[+] Querying Azure activity logs...")
    az_res = subprocess.run([
        "az", "monitor", "activity-log", "list",
        "--start-time", SINCE,
        "--end-time", UNTIL,
        "--query", "[?contains(operationName.value, 'write') || contains(operationName.value, 'delete') || contains(operationName.value, 'Microsoft.ManagedIdentity')]",
        "-o", "json"
    ], capture_output=True, text=True)
    
    if az_res.returncode == 0:
        (OUT / "azure-write-delete-activity.json").write_text(az_res.stdout)
    
    # 3. GCP Logging Audit
    print("[+] Querying GCP Cloud Logging...")
    gcp_filter = f'timestamp>="{SINCE}" AND timestamp<="{UNTIL}" AND (protoPayload.methodName="google.sts.v1.SecurityTokenService.ExchangeToken" OR protoPayload.methodName:"GenerateAccessToken" OR protoPayload.methodName:"CreateServiceAccountKey" OR protoPayload.methodName:"SetIamPolicy")'
    gcp_res = subprocess.run([
        "gcloud", "logging", "read", gcp_filter,
        "--format", "json"
    ], capture_output=True, text=True)
    
    if gcp_res.returncode == 0:
        (OUT / "gcp-token-and-iam-activity.json").write_text(gcp_res.stdout)
    
    print(f"[+] Wrote cloud audit artifacts under {OUT}")

    Script: registry metadata and artifact audit

    #!/usr/bin/env python3
    import os
    import json
    import subprocess
    from pathlib import Path
    
    SINCE = "2026-06-01T00:00:00Z"
    OUT = Path(os.environ.get("OUT", "hp-cpanel-whm-cve-2026-41940-kev-registry-audit"))
    PACKAGES = [
    ]
    VERSIONS = [
    ]
    
    # Positive signal: workflow files or extensions reference the affected action/extension names or versions.
    # Remediation trigger: exposed secrets or OIDC federation policies must be immediately rotated.
    
    OUT.mkdir(parents=True, exist_ok=True)
    
    with open(OUT / "affected-versions.txt", "w") as av:
        for version in VERSIONS:
            if version:
                av.write(version + "\n")
    
        # 1. Search local workspace files for the affected actions/extensions
        print("[+] Scanning workspace workflows for selectors...")
        for file in Path(".").glob(".github/workflows/**/*.yml"):
            subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(OUT / "affected-versions.txt"), str(file)])
    
        # 2. HOW TO ROTATE EXPOSED GITHUB ACTIONS SECRETS:
        # Overwrite compromised secrets with newly generated credentials:
        # subprocess.run(["gh", "secret", "set", "COMPROMISED_SECRET_NAME", "--body", "my-new-secret-value", "--repo", "my-org/my-repo"])
        # For organization-level secrets:
        # subprocess.run(["gh", "secret", "set", "COMPROMISED_SECRET_NAME", "--org", "my-org", "--visibility", "private"])
        # Revoke compromised OIDC federated trust credentials in AWS/GCP and redeploy the IAM trust policy:
        # subprocess.run(["aws", "iam", "update-assume-role-policy", "--role-name", "my-role-name", "--policy-document", "file://new-clean-trust-policy.json"])
    
    print(f"[+] Wrote registry audit artifacts under {OUT}")

    Remediation and Closure

    Patch to a vendor-listed fixed build or later, restart cpsrvd, and record version evidence. Closure requires patched-version proof, negative session-file and access-log review, exposed-port review, and downstream hosted-account checks. Where evidence is missing, rotate hosted-account credentials and keep the host in presumed-exposed status.

    Sources

    1. CISA Known Exploited Vulnerabilities catalog JSON
    2. cPanel: CVE-2026-41940 cPanel & WHM / WP2 Security Update
    3. NVD CVE-2026-41940
    4. WP2 changelog 136.1.7

    IOC Clipboard

    8 IOCs
    Defang IOCs
    domain www.cisa.gov www[.]cisa[.]gov
    domain support.cpanel.net support[.]cpanel[.]net
    domain nvd.nist.gov nvd[.]nist[.]gov
    domain docs.wpsquared.com docs[.]wpsquared[.]com
    url https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json hxxps://www[.]cisa[.]gov/sites/default/files/feeds/known_exploited_vulnerabilities[.]json
    url https://support.cpanel.net/hc/en-us/articles/40073787579671-Security-CVE-2026-41940-cPanel-WHM-WP2-Security-Update-04-28-2026 hxxps://support[.]cpanel[.]net/hc/en-us/articles/40073787579671-Security-CVE-2026-41940-cPanel-WHM-WP2-Security-Update-04-28-2026
    url https://nvd.nist.gov/vuln/detail/CVE-2026-41940 hxxps://nvd[.]nist[.]gov/vuln/detail/CVE-2026-41940
    url https://docs.wpsquared.com/changelogs/versions/changelog/#13617 hxxps://docs[.]wpsquared[.]com/changelogs/versions/changelog/#13617