
Detection toolkit for CVE-2026-35616, a pre-authentication API bypass in FortiClient EMS. Includes Python scanner and Nmap NSE script for identifying vulnerable versions and providing remediation guidance.
A critical authentication bypass in Fortinet FortiClient EMS 7.4.5 and 7.4.6 allows a completely unauthenticated, remote attacker to bypass API authentication by spoofing a single HTTP header (X-SSL-CLIENT-VERIFY). The flaw exists because the Django middleware trusts client certificate metadata from user-controlled headers, not just from the trusted reverse proxy. This gives attackers full administrative API access - and from there, arbitrary code execution on managed endpoints across the enterprise.
Actively exploited since March 31, 2026. Added to CISA KEV on April 6, 2026.
| Field | Detail |
|---|---|
| CVE ID | CVE-2026-35616 |
| Vendor | Fortinet |
| Product | FortiClient Enterprise Management Server (EMS) |
| Affected Versions | 7.4.5, 7.4.6 |
| Not Affected | 7.2.x branch, 7.4.4 and earlier |
| CVSS v3.1 | 9.1 (Critical) |
| CWE | CWE-284 - Improper Access Control |
| Attack Vector | Network |
| Authentication | None required |
| User Interaction | None |
| Exploit Maturity | Exploited in the wild |
| CISA KEV | Added April 6, 2026 (deadline: April 9, 2026) |
| Patch | Hotfix available; full fix in 7.4.7 |
| Credited To | Simo Kohonen (Defused Cyber), Nguyen Duc Anh |
FortiClient Enterprise Management Server (EMS) is Fortinet's centralized endpoint management platform. It serves as the command-and-control layer for deploying, configuring, and monitoring FortiClient agents across an organization. Think of it as the brain that governs every endpoint in a Fortinet-managed environment:
When an attacker gains administrative access to EMS, they essentially own the keys to every managed endpoint in the organization.
FortiClient EMS uses a fairly standard web application stack behind the scenes:
+----------------+ +----------------+ +----------------+
| Browser / | HTTPS | Apache | WSGI | Django |
| API Client | -------> | (mod_ssl) | -------> | Backend |
+----------------+ +----------------+ +----------------+
When mutual TLS (mTLS) is configured, Apache's mod_ssl handles the client certificate verification. After validating the certificate, Apache passes the verification result downstream to Django through trusted WSGI environment variables:
SSL_CLIENT_VERIFY - The verification status (SUCCESS, NONE, FAILED)SSL_CLIENT_S_DN - The Subject Distinguished Name from the certificateSSL_CLIENT_SERIAL - The certificate serial numberThis is the standard and secure pattern. The problem is in how the Django middleware reads this data.
In FortiClient EMS 7.4.5 and 7.4.6, the Django authentication middleware was modified to also accept this same information from HTTP request headers:
X-SSL-CLIENT-VERIFYX-SSL-CLIENT-S-DNX-SSL-CLIENT-SERIALThis was likely added to support reverse proxy deployments where Apache isn't the TLS termination point. However, the middleware doesn't distinguish between these two sources. It checks the WSGI variables first, but if they're absent (no mTLS configured, or a direct connection), it falls back to the HTTP headers - which any client can set.
Here's the conceptual breakdown:
SECURE PATH (intended):
Apache mod_ssl validates cert --> sets WSGI env vars --> Django reads env vars [OK]
INSECURE PATH (the vulnerability):
Attacker sets HTTP headers directly --> Django reads headers --> Trusts them [FAIL]
The middleware effectively trusts the client to self-attest their own certificate verification status. That's like a bouncer asking someone "Hey, did the other bouncer already check your ID?" and letting them in when they say "yes."
Step 1: Attacker sends a POST request to an EMS API endpoint
with these headers:
X-SSL-CLIENT-VERIFY: SUCCESS
X-SSL-CLIENT-S-DN: CN=admin
X-SSL-CLIENT-SERIAL: 0000000000000001
Step 2: Django middleware checks for WSGI env vars → not present
Falls back to HTTP headers → finds X-SSL-CLIENT-VERIFY: SUCCESS
Step 3: Middleware treats request as authenticated with admin identity
Step 4: Attacker has full administrative API access
Step 5: From the admin API, attacker can:
- Push malicious policies to all managed endpoints
- Extract stored credentials and certificates
- Deploy payloads via software distribution
- Modify ZTNA configurations
- Pivot into the broader network
The entire attack requires a single HTTP request. No brute-forcing, no credential stuffing, no social engineering. Just one forged header.
The severity here goes beyond the server itself. FortiClient EMS is a force multiplier - compromising it gives an attacker leverage over every managed endpoint:
Immediate Impact:
Downstream Impact (via managed endpoints):
Enterprise Risk:
| Version | Status |
|---|---|
| FortiClient EMS 7.4.6 | Vulnerable |
| FortiClient EMS 7.4.5 | Vulnerable |
| FortiClient EMS 7.4.4 and earlier | Not affected |
| FortiClient EMS 7.2.x | Not affected |
| Date | Event |
|---|---|
| ~Late March 2026 | Vulnerability discovered and reported by Simo Kohonen and Nguyen Duc Anh |
| March 31, 2026 | First exploitation attempts recorded against honeypots (Defused Cyber) |
| April 4, 2026 | Fortinet releases emergency hotfixes for 7.4.5 and 7.4.6 |
| April 6, 2026 | CISA adds CVE-2026-35616 to KEV catalog (deadline: April 9, 2026) |
| April 13, 2026 | This detection toolkit published |
The Python script tests multiple API endpoints using a differential-response technique.
How it works:
HTTP 401 Unauthorized)X-SSL-CLIENT-VERIFY: SUCCESS injected401 to anything else (typically 500 or 200), the authentication bypass is confirmedNo exploitation payload is ever sent. The test is safe for production.
Usage:
# Install dependencies (only stdlib needed for this script)
pip install -r requirements.txt
# Single target
python CVE-2026-35616_FortiClientEMS_detector.py -t 192.168.1.100
# Custom port
python CVE-2026-35616_FortiClientEMS_detector.py -t ems.corp.local -p 8443
# Bulk scan from file
python CVE-2026-35616_FortiClientEMS_detector.py -f targets.txt
# JSON output saved to file
python CVE-2026-35616_FortiClientEMS_detector.py -t 192.168.1.100 --json -o results.json
# With SSL certificate verification enabled
python CVE-2026-35616_FortiClientEMS_detector.py -t 192.168.1.100 --verify-ssl
# Increased timeout for slow networks
python CVE-2026-35616_FortiClientEMS_detector.py -t 192.168.1.100 --timeout 20
Options:
| Flag | Description | Default |
|---|---|---|
-t, --target | Target IP or hostname | - |
-f, --file | File with targets, one per line (lines starting with # are skipped) | - |
-p, --port | Target port | 443 |
--timeout | Connection timeout in seconds | 10 |
--verify-ssl | Enable SSL certificate verification | Disabled |
--json | Output results in JSON format | Off |
-o, --output | Save results to a file | - |
Example output:
╔══════════════════════════════════════════════════════════════╗
║ CVE-2026-35616 - FortiClient EMS Auth Bypass Detector ║
║ Pre-Authentication API Access Bypass → Privilege Escalation║
║ CVSS: 9.1 (Critical) | CISA KEV: Active Exploitation ║
╚══════════════════════════════════════════════════════════════╝
[*] Scanning 192.168.1.100:443...
Target: 192.168.1.100:443
============================================================
[*] FortiClient EMS detected (Version: Unknown)
Vulnerability Test Results:
[VULNERABLE] /api/v1/auth/signin Baseline: 401 → Spoofed: 500
[VULNERABLE] /api/v1/system/status Baseline: 401 → Spoofed: 500
[NOT VULN] /api/v1/endpoints Baseline: 401 → Spoofed: 401
[!] TARGET IS LIKELY VULNERABLE TO CVE-2026-35616
Pre-authentication API bypass confirmed. Apply hotfix immediately!
Remediation: Upgrade to FortiClient EMS 7.4.7 or apply the hotfix
# Install the NSE script
sudo cp CVE-2026-35616_FortiClientEMS.nse /usr/share/nmap/scripts/
sudo nmap --script-updatedb
# Basic scan
nmap -p 443 --script CVE-2026-35616_FortiClientEMS <target>
# Scan a subnet
nmap -p 443 --script CVE-2026-35616_FortiClientEMS 10.0.0.0/24
# Scan multiple targets from a file
nmap -p 443 --script CVE-2026-35616_FortiClientEMS -iL targets.txt
# With service version detection
nmap -sV -p 443 --script CVE-2026-35616_FortiClientEMS <target>
Example Nmap output:
PORT STATE SERVICE
443/tcp open https
| CVE-2026-35616_FortiClientEMS:
| VULNERABLE:
| FortiClient EMS Pre-Authentication API Bypass
| State: VULNERABLE
| IDs: CVE:CVE-2026-35616
| Risk factor: Critical (CVSS: 9.1)
| Disclosure date: 2026-04-04
| Extra information:
| Affected endpoints: 2
| Remediation: Apply hotfix for FortiClient EMS 7.4.5/7.4.6 or upgrade to 7.4.7
| CISA KEV deadline: April 9, 2026
| References:
| https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2026-35616
|_ https://www.cisa.gov/known-exploited-vulnerabilities-catalog
If you want to confirm manually with curl:
# Step 1: Baseline - should return 401
curl -sk -X POST https://<TARGET>:443/api/v1/auth/signin \
-H "Content-Type: application/json" \
-d '{}' \
-o /dev/null -w "%{http_code}\n"
# Step 2: Spoofed - if returns anything other than 401, likely vulnerable
curl -sk -X POST https://<TARGET>:443/api/v1/auth/signin \
-H "Content-Type: application/json" \
-H "X-SSL-CLIENT-VERIFY: SUCCESS" \
-H "X-SSL-CLIENT-S-DN: CN=admin" \
-H "X-SSL-CLIENT-SERIAL: 0000000000000001" \
-d '{}' \
-o /dev/null -w "%{http_code}\n"
If the first returns 401 and the second returns 500 or 200, the instance is vulnerable.
Watch for these signs in your environment:
X-SSL-CLIENT-VERIFY headers from non-proxy sourcesLog sources to review:
Immediate actions (do these now):
Short-term (this week):
Long-term:
X-SSL-CLIENT-VERIFY, X-SSL-CLIENT-S-DN, and X-SSL-CLIENT-SERIAL headers from incoming requests at the network edgeKerem Oruç - Cybersecurity Engineer