API Authentication Bypass in FortiClient EMS 7.4.5-7.4.6–CVE-2026-35616
好的,我现在需要帮用户总结一篇文章的内容,控制在100字以内。首先,我得仔细阅读文章,理解其主要内容和关键点。 文章主要讲的是CVE-2026-35616漏洞,这是一个FortiClient EMS 7.4.5-7.4.6版本中的认证绕过漏洞。攻击者可以通过伪造HTTP请求头来绕过证书认证,进而获得API访问权限。漏洞的根源在于Django应用信任了用户可控的HTTP头,而Apache没有配置移除这些头。此外,证书链验证仅检查了主题和颁发者字符串,没有进行加密签名验证,使得攻击者可以伪造证书。 Fortinet已经发布了热修复补丁,并确认该漏洞在野外被利用。建议用户立即应用补丁或升级到7.4.7版本。 接下来,我需要将这些关键点浓缩到100字以内。要确保涵盖漏洞名称、影响版本、攻击方式、漏洞原因、修复措施以及建议行动。 可能的结构是:首先说明漏洞名称和影响范围,然后描述攻击方式和原因,最后提到修复措施和建议。 例如: FortiClient EMS 7.4.5-7.4.6存在认证绕过漏洞(CVE-2026-35616),允许攻击者通过伪造HTTP头绕过证书认证并获得API访问权限。问题源于Django应用信任用户可控的HTTP头且Apache未移除这些头。此外,证书链验证仅基于字符串匹配而非加密签名验证。Fortinet已发布热修复补丁并确认漏洞被利用。建议立即应用补丁或升级至7.4.7版本。 这样大约在100字左右,并且涵盖了所有关键点。 </think> FortiClient EMS 7.4.5-7.4.6存在认证绕过漏洞(CVE-2026-35616),允许攻击者通过伪造HTTP头绕过证书认证并获得API访问权限。问题源于Django应用信任用户可控的HTTP头且Apache未移除这些头。此外,证书链验证仅基于字符串匹配而非加密签名验证。Fortinet已发布热修复补丁并确认漏洞被利用。建议立即应用补丁或升级至7.4.7版本。 2026-4-7 13:0:0 Author: bishopfox.com(查看原文) 阅读量:25 收藏

TL;DR: Bishop Fox researchers expanded on Fortinet's disclosure of CVE-2026-35616 by identifying the root cause via the released hotfix. The vulnerability allows an unauthenticated attacker to bypass certificate-based authentication on FortiClient EMS 7.4.5-7.4.6 by spoofing HTTP request headers that the Django application trusts as equivalent to Apache mod_ssl WSGI environment variables. Combined with a certificate chain validation that performs only subject/issuer string matching with no cryptographic signature verification, an attacker can forge certificates and gain authenticated API access. Fortinet has released a hotfix and confirmed exploitation in the wild. Organizations should apply the hotfix immediately or upgrade to 7.4.7 when available.

CVE-2026-35616 is an authentication bypass in FortiClient EMS 7.4.5 and 7.4.6. The Django authentication middleware accepts client certificate information from both trusted WSGI environment variables (set by Apache mod_ssl) and user-controllable HTTP request headers, and Apache was never configured to strip the user-controllable variants. An attacker who can reach the EMS web interface over HTTPS needs no credentials to exploit this. Additionally, the certificate chain validation performs only Distinguished Name string matching against embedded Fortinet root CAs with no X.509 cryptographic signature verification, meaning an attacker can generate a passing certificate chain with openssl req using the correct DN strings.

Fortinet has confirmed this vulnerability is being exploited in the wild and has released a hotfix. Organizations running FortiClient EMS 7.4.5 or 7.4.6 should apply the hotfix immediately. A permanent code-level fix is expected in version 7.4.7. Additionally, Bishop Fox researchers developed a non-intrusive scanner that can be used to detect unpatched systems without exercising the auth bypass fully, which you can download here.

FortiClient EMS uses Apache with mod_ssl to terminate TLS connections. When a client presents a TLS certificate during the handshake, Apache's mod_ssl module populates WSGI environment variables such as SSL_CLIENT_VERIFY (whether the cert was verified) and SSL_CLIENT_CERT (the PEM-encoded certificate data). The Django application then reads these variables in its authentication middleware to support certificate-based device authentication, which is how managed FortiGate appliances and FortiClient agents authenticate to EMS.

This certificate authentication was introduced as part of the cert_chain_approved and cert_chain security schemes that gate access to device management endpoints. The middleware evaluates these schemes alongside JWT, session, and basic authentication for each API endpoint based on a per-view security configuration defined through Django view decorators.

Our analysis builds on the same FortiClient EMS codebase we examined during our CVE-2026-21643 research, where we identified a pre-authentication SQL injection in version 7.4.4. The middleware architecture is familiar territory: the same AuthMiddleware, OpenApi decorator system, and PostgreSQL connection layer we reverse-engineered for that vulnerability are central to this one.

The Vulnerable Code Path

When an HTTP request arrives at a FortiClient EMS endpoint that supports certificate-based authentication, the AuthMiddleware calls CertChainAuth.contains_certificate() to determine whether a client certificate is present. This method, decompiled from fcm/auth/cert_chain_auth.py, performs two checks:

@classmethod 
def contains_certificate(cls, request) -> None: 
    return ( 
        request.META.get('SSL_CLIENT_VERIFY') == 'SUCCESS' 
        or request.META.get('HTTP_X_SSL_CLIENT_VERIFY') == 'SUCCESS' 
    )

The first check, SSL_CLIENT_VERIFY, reads a WSGI environment variable set by Apache's mod_ssl. This is not user-controllable. The second check, HTTP_X_SSL_CLIENT_VERIFY, is Django's automatic mapping of the HTTP request header X-SSL-CLIENT-VERIFY. Under Django's request.META convention, any HTTP header X-Foo-Bar becomes HTTP_X_FOO_BAR. This second value is fully user-controllable: any HTTP client can include X-SSL-CLIENT-VERIFY: SUCCESS in a request.

If contains_certificate() returns True, the middleware proceeds to Certificate.validate_cert_chain(), which reads the certificate PEM data. This method, decompiled from fcm/models/utils/certificate.py, has the same issue:

# BRANCH 1: Checked FIRST - user-controllable HTTP header 
if 'HTTP_X_SSL_CLIENT_CERT' in request.META: 
    client_cert = urllib.parse.unquote(request.META['HTTP_X_SSL_CLIENT_CERT']) 
    certificates = client_cert.split('-----END CERTIFICATE-----') 
    # Parses into client_cert, int_cert, root_cert 

# BRANCH 2: Trusted mod_ssl WSGI vars (only used if header absent) 
else: 
    client_cert = request.META.get('SSL_CLIENT_CERT') 
    int_cert = request.META.get('SSL_CLIENT_CERT_CHAIN_0') 
    # ...

The user-supplied X-SSL-CLIENT-CERT header is checked first and takes priority over the trusted WSGI environment variables. An attacker who sends both X-SSL-CLIENT-VERIFY: SUCCESS and a forged certificate chain in X-SSL-CLIENT-CERT passes the authentication gate and supplies the certificate data that will be validated.

Certificate Chain Validation: String Matching Only

The certificate chain is validated against EmsConsts.ROOT_CA_CERTS, the Fortinet root CAs embedded in the EMS installation. The validation in validate_cert_chain() compares only subject and issuer Distinguished Name strings. For a 2-certificate chain (client + intermediate):

int_cert.subject   == root_ca.subject 
int_cert.issuer    == root_ca.issuer 
client_cert.issuer == int_cert.issuer

For a 3-certificate chain (client + intermediate + root):

root_cert.subject  == root_ca.subject 
root_cert.issuer   == root_ca.subject    (self-signed check) 
int_cert.issuer    == root_cert.issuer 
"subca"            in int_cert.subject   (literal substring check) 
client_cert.issuer == int_cert.subject

No X.509 cryptographic signature verification is performed at any point. An attacker can generate self-signed certificates with openssl req -subj using matching DN strings, and the chain will pass validation. The required DN strings are extractable from any FortiClient EMS installation, FortiClient agent package, or FortiGate firmware image.

Post-Authentication Identity Assignment

On successful chain validation, CertChainAuth._set_user() assigns the client certificate's subject CN as the authenticated user identity with the certificate user role:

cls.set_user( 
    request, 
    name=cert_cn,                              # attacker-controlled 
    full_name=f"Certificate user: {cert_cn}",  # attacker-controlled 
    role=Role.get_certificate_user_role(),     # fixed role assignment 
)

For endpoints using the cert_chain_approved security scheme, FabricDeviceAuth.authorize() performs an additional database lookup using the client CN as a serial number. If the CN matches an authorized device, access is granted. If the CN is unknown and the request is a POST, the device is auto-registered in a pending state.

Why This Happened

The vulnerability has two independent root causes, either of which would be sufficient to block the attack if fixed. The first is a reverse proxy trust boundary violation: the Django application accepts certificate verification status from user-controllable HTTP headers (HTTP_X_SSL_CLIENT_VERIFY) in addition to trusted WSGI environment variables (SSL_CLIENT_VERIFY), and Apache was never configured to strip the user-controllable variants. The second is the absence of cryptographic certificate validation: validate_cert_chain() checks only subject and issuer Distinguished Name strings against the embedded Fortinet root CAs, with no X.509 signature verification at any point in the chain. Even if the header spoofing were fixed, an attacker who could present a TLS client certificate through mod_ssl's SSLVerifyClient optional negotiation could pass chain validation with self-signed certificates bearing the correct DN strings, since no cryptographic proof of issuance is ever checked. The hotfix addresses only the first issue by stripping headers at the Apache layer; the string-only chain validation remains in place.

The Apache Configuration

Analysis of the unpatched ems-webserver.conf confirms the gap. Apache is configured with SSLVerifyClient optional and SSLOptions +StdEnvVars +ExportCertData, which correctly populate WSGI environment variables when a real client certificate is presented via TLS. The configuration includes RequestHeader set directives for some SSL variables (such as SSL_CLIENT_S_DN_CN), but no RequestHeader unset directives exist for any of the spoofable headers. The ZTNA worker routing logic uses RewriteCond %{SSL:SSL_CLIENT_VERIFY}, which correctly reads from the WSGI variable (not the HTTP header), so legitimate ZTNA routing is unaffected by header spoofing; spoofed requests fall through to the Django WSGI handler where the vulnerable code resides.

Fortinet's hotfix consists of two components. The primary fix is in apply.sh, a shell script that patches the Apache virtual host configuration to add RequestHeader unset directives:

RequestHeader unset X-SSL-CLIENT-VERIFY 
RequestHeader unset X-SSL-CLIENT-CERT 
RequestHeader unset SSL-CLIENT-VERIFY 
RequestHeader unset SSL-CLIENT-CERT 
RequestHeader unset X-Bypass-SN-Check 
RequestHeader unset X-Forward-To-ZTNAWorker

These directives strip all spoof-able headers before they reach the Django WSGI layer. This is the primary and only fix for the authentication bypass: it prevents the attack regardless of any code changes in the application layer. The additional headers stripped (X-Bypass-SN-Check and X-Forward-To-ZTNAWorker) are internal trust signals used for ZTNA worker communication that were also never stripped from external requests.

The secondary component is an updated auth_middleware.pyc. Our byte-for-byte comparison of the hotfix against the original 7.4.5 auth_middleware.pyc confirms that check_request_authorization(), the core authentication dispatch function, is completely unchanged. The hotfix makes exactly two modifications: a new import (from django.conf import settings) and a session flush in __call__() that destroys the Django session and deletes the sessionid cookie after any certificate-based authentication. This is defense-in-depth against session fixation: without it, an attacker could spoof cert headers to establish a persistent session cookie, and then replay that cookie on session-based endpoints even after the Apache header stripping is deployed.

The vulnerable code in CertChainAuth.contains_certificate() and Certificate.validate_cert_chain() is completely untouched by this hotfix. The code-level fix to the authentication logic is presumably deferred to the 7.4.7 release.

Impact Assessment

A successful exploit grants an unauthenticated attacker authenticated API access with certificate-user role privileges across 16 cert_chain_approved endpoint definitions spanning 15 controllers. Our analysis of 88 controller bytecode files identified the full attack surface, which includes endpoints for quarantining and sending commands to all managed endpoints, reconfiguring EMS server settings and rotating JWT secrets, downloading ZTNA private keys and revoking certificates, creating ZTNA applications and rules, registering and authorizing FortiGate devices, and exporting complete software inventory and endpoint vulnerability data.

FortiClient EMS manages an organization's entire endpoint fleet. Compromise of the management server gives an attacker visibility into and control over every managed endpoint, making this a high-value target for lateral movement and persistence.

We have released an open-source scanner that detects whether the hotfix has been applied without sending any exploit payload:

CVE-2026-35616 Scanner

$ python3 ./cve-2026-35616-check.py 192.168.1.1 
====================================================================== 
FortiClient EMS CVE-2026-35616 Vulnerability Scanner 
Non-Destructive Detection 
====================================================================== 

[*] Target: 192.168.1.1:443 
[*] Testing for CVE-2026-35616 (non-destructive) 

[1] Sending baseline POST (no spoof headers)... HTTP 401 
[2] Sending POST with X-SSL-CLIENT-VERIFY: SUCCESS... HTTP 500 

[*] Analyzing responses... 
[+] Baseline: 401 — Certificate not found in request header. 
[+] Spoofed:  500 — Server encountered an error, please try again later. 
[!] Spoofed header changed server behavior 
[!] X-SSL-CLIENT-VERIFY is reaching Django (hotfix not applied) 

====================================================================== 
RESULT: VULNERABLE to CVE-2026-35616 
Affected versions: FortiClient EMS 7.4.5 - 7.4.6 
Recommendation: Apply Fortinet hotfix (apply.sh) or upgrade to 7.4.7+ 
====================================================================== 

$ python3 ./cve-2026-35616-check.py 192.168.1.1   # After hotfix 
====================================================================== 
FortiClient EMS CVE-2026-35616 Vulnerability Scanner 
Non-Destructive Detection 
====================================================================== 

[*] Target: 192.168.1.1:443 
[*] Testing for CVE-2026-35616 (non-destructive) 

[1] Sending baseline POST (no spoof headers)... HTTP 401 
[2] Sending POST with X-SSL-CLIENT-VERIFY: SUCCESS... HTTP 401 

[*] Analyzing responses... 
[+] Baseline: 401 — Certificate not found in request header. 
[+] Spoofed:  401 — Certificate not found in request header. 
[-] Responses identical (header stripped by Apache) 

====================================================================== 
RESULT: NOT VULNERABLE (hotfix applied) 
Apache is stripping X-SSL-CLIENT-VERIFY before it reaches Django 
======================================================================

The scanner works by sending two POST requests to a certificate-authenticated API endpoint: one without any spoofed headers (baseline) and one with X-SSL-CLIENT-VERIFY: SUCCESS but no certificate data. On unpatched targets, the spoofed header changes the response from HTTP 401 ("Certificate not found in request header") to HTTP 500 (server error), because the header passes the contains_certificate() gate but the downstream validate_cert_chain() crashes on missing PEM data. On hotfixed targets, Apache strips the header before it reaches Django, so both requests return an identical HTTP 401. No certificate data is sent and no authentication is attempted.

Patching

Apply the Fortinet-provided hotfix immediately. The hotfix adds Apache RequestHeader unset directives that strip spoof-able headers before they reach the Django application. Upgrade to FortiClient EMS 7.4.7 when available for a permanent code-level fix.

Workarounds and Mitigations

If the hotfix cannot be applied immediately, restrict HTTPS access to the EMS web interface to authorized management networks only. The vulnerability requires direct HTTPS access to the EMS web interface on port 443.

Our analysis followed a four-phase approach after Fortinet published the advisory and hotfix:

Phase 1: Hotfix analysis.

Obtained the hotfix package containing apply.sh and an updated auth_middleware.pyc. The apply.sh script revealed the attack surface immediately: it strips X-SSL-CLIENT-VERIFY, X-SSL-CLIENT-CERT, and related headers at the Apache layer, indicating a trusted-proxy header spoofing vulnerability.

Phase 2: Bytecode decompilation.

Decompiled seven Python 3.10 .pyc modules from the FortiClient EMS 7.4.5 appliance image using xdis and manual reconstruction: auth_middleware, cert_chain_auth, certificate, fabric_device_auth, open_api, ztna_worker_api, and the hotfix auth_middleware. This confirmed the exact spoofing requirements: two HTTP headers bypass the authentication gate, and the certificate chain validation is purely structural with no cryptographic verification.

Phase 3: Attack surface enumeration.

Decompiled the URL routing configuration (urls.pyc, approximately 600 URL patterns) and scanned 88 controller bytecode files for security scheme definitions to identify all endpoints accepting cert_chain and cert_chain_approved authentication. This identified 16 cert_chain_approved endpoints across 15 controllers, plus the Apache virtual host configuration analysis that confirmed no RequestHeader unset directives existed in the unpatched configuration.

Phase 4: Detection tooling.

Developed a hotfix detection scanner that identifies unpatched targets through behavioral analysis of a certificate-authenticated endpoint, without sending any exploit payload.

CVE-2026-35616 is a textbook trusted-proxy header spoofing vulnerability. The Django application trusts HTTP headers that should only come from the reverse proxy layer, and Apache was never configured to strip them from external requests. The additional finding that certificate chain validation performs no cryptographic verification compounds the issue: even if the header spoofing were fixed, an attacker with knowledge of the Fortinet root CA DN strings could forge a passing certificate chain with standard openssl commands.

Organizations running FortiClient EMS 7.4.5 or 7.4.6 should apply the hotfix immediately and verify remediation using the Bishop Fox scanner. The hotfix is a necessary and effective mitigation, but the underlying code-level vulnerabilities in CertChainAuth.contains_certificate() and Certificate.validate_cert_chain() remain until version 7.4.7 ships.

Our affected Cosmos customers were notified of this vulnerability shortly after the vendor disclosure, and we continue to monitor for new threats to exposed services. If you're interested in learning more about managed services delivered through our Cosmos platform, visit bishopfox.com/services/cosmos.

For more vulnerability intelligence insights, visit the Bishop Fox Blog.


文章来源: https://bishopfox.com/blog/api-authentication-bypass-in-forticlient-ems-7-4-5-7-4-6-cve-2026-35616
如有侵权请联系:admin#unsafe.sh