Research/sslvpn-pot-of-gold/two-public-doors
SeriesN-dayCVE-2024-21762CriticalPublic

The SSL-VPN pot of gold · Phase 1

Two public doors

The kit is two n-days with private offsets. Family A is chunked HTTP on sslvpnd. Family B is enc= on hostcheck. Files are on GitHub; this page is the shape.

Name
Two public doors
Series
The SSL-VPN pot of gold
Phase
1 · Two public doors
Type
N-day analysis
CVE
CVE-2024-21762
CVE Risk
critical
Disclosure Status
public
Vendor
Fortinet
Affected
FortiGate SSL-VPN portal (sslvpnd)
Published
31 Aug 2026
Updated
01 Sept 2026
Tags
n-day, fortinet, ssl-vpn, cve-2024-21762, cve-2023-27997

Naming is a tell

Almost every file is exp_<SKU>-v<MAJOR.MINOR.PATCH>.py. They live in exp/ on GitHub. The operator is expected to pick the matching file first - or let poc.py pick it from a portal hash. There is no SKU flag on the clients themselves. Arguments are targeting and callback: remote host/port, listener host/port, sometimes a separate host that serves second-stage bytes, sometimes an nCpu hint that several scripts ignore and hard-code anyway.

lHost / lPort are the operator. They are not the FortiGate. Certificate verification is off. Retries are a loop, because heap layout is a mood.

SHA-256 hashes are all unique. Near-duplicate source is common: two adjacent patch levels often differ only in a handful of addresses. That is what retargeting looks like when you extract sslvpnd from fifty images and you are not writing a thinkpiece.

One outlier filename has an extra .F. Treat it as 4201F / 7.2.5 and move on.

Shared prefix

Every class in this kit:

  1. Open TLS to the SSL-VPN listener.
  2. Shape heap or parser state with crafted HTTPS.
  3. Trigger one of two memory-corruption primitives.
  4. Redirect control into either a small ARM stager or FortiOS's built-in command helper / Node path.
  5. Retry, because the first try is allowed to be ugly.

The clients are in the repo. This page still does not walk a recipe. Public papers already taught the bugs. I am describing the shape so you can instrument a portal.

Family A - CVE-2024-21762

Fortinet: FG-IR-24-015. Assetnote: Two Bytes is Plenty. Bishop Fox's scanner notes are the defender-shaped version of the same parser change.

Conceptually: SSL-VPN HTTP parsing accepts Transfer-Encoding: chunked on paths that still exercise a vulnerable reader. A malformed chunk-size / trailer sequence writes past a buffer near saved parser state. Fortinet's fix added length checks. Public write-ups describe an overwrite that includes a \n\r pattern from trailer handling. Two bytes. Plenty, if you are standing in the right living room.

What this kit does with it, in English:

  • Open many TLS connections.
  • Spray gadgets or fake SSL objects through large POST /remote/error bodies.
  • Send POST /aaaa/bbbb with Transfer-Encoding: chunked. That path is not a real portal resource.
  • Land the overflow on a saved index, function pointer, or object header.
  • Continue into ROP or a faked object whose function slot is the command helper.

/aaaa/bbbb is an indicator. Legitimate FortiOS clients do not use it. If you see it on a portal VIP, you are not looking at a password manager.

Family A variants I actually saw (collapsed on purpose)

A1 - ARM32 ROP + optional shellcode stager. Small E-series, context.arch = arm, 32-bit Thumb. About 87 samples carry a stager whose intent is: fork, connect to a shellcode host, mmap RWX, pull bytes, connect back to the operator, jump. Some older 6.0 files skip the extra host and park a Node one-liner next to ROP that calls the command helper.

A2 - amd64, command helper + Node. Common on D-series 6.0 and larger E/F. No Thumb stager. A subset also define a TFTP fallback that pulls s.js into /tmp and runs /bin/node on it. Outbound TFTP from a firewall is not a firmware update.

A3 - AArch64 fake SSL / sslcon object. F-series and some 7.x. Same Family A trigger (chunked). The payload is a forged heap object. Nine of these also POST to /remote/hostcheck_validate as an allocator, without Family B's enc= encoder. They remain 21762.

Family B - CVE-2023-27997

Fortinet: FG-IR-23-097. Lexfo: XORtigate. Bishop Fox: the internet was still unpatched, and later how they built a working client.

Conceptually: /remote/hostcheck_validate takes enc= - a small encrypted blob. FortiOS derives a keystream from a salt (available from /remote/info), a seed, and a fixed ASCII constant that also shows up in the public papers. A length field is checked incorrectly. Decoded bytes walk off the heap buffer.

What this kit does with it, still in English:

  • GET /remote/info for the salt.
  • Brute seeds offline so each desired heap patch becomes an enc= blob the daemon will accept. Some samples cache seeds next to the script.
  • Spray neighboring allocations with padding and a Node command.
  • POST /remote/hostcheck_validate with enc=.
  • Poke leftover sockets so a later handshake or request calls the command helper.

Family B files do not use /aaaa/bbbb. Distinctive pair: GET /remote/info immediately followed by POST /remote/hostcheck_validate with enc=.

CVE labels here are inferred from trigger, endpoint, magic constant, and affected-version overlap. A lab could still argue an individual Family B sample shares DNA with an older SSL-VPN heap issue. Nothing in the corpus requires a third CVE to explain the code.

After the corruption

FortiOS /bin/sh is a museum piece. So the kit does not ask it for a shell.

Node fetch-and-eval (92 samples if you count TFTP variants). Intent: /bin/node -e does an HTTP GET to http://lhost:lport/s.js and evals the body. The firewall makes an outbound GET for /s.js. Content of s.js is a config stealer, not a general shell. It was missing from the first drop. It is in the repo now. Node is a legitimate FortiOS binary. Node spawned from sslvpnd right after odd portal requests is not.

ARM32 stager (87). Two outbound TCP destinations from the firewall: the byte server (shellcode_server.py on 8888 in this copy) and the callback (send_cmd.py on 7777). Neither is FortiGuard.

TFTP adjunct (10, 6.0-era D-series). Same Node goal, extra hop through /bin/tftp into /tmp. High-signal if you still have 6.0 D-series on the internet, which you should not, but here we are.

Attribution of capability, not of a flag

Observation Conclusion
Public CVE, public papers, public crash PoCs Any competent operator could learn the bugs
179 per-SKU/per-build clients with private naming This pack was assembled, not cloned from Exploit-DB as-is
No comments, no framework CLI, hard-coded offsets Operational, not a teaching repo
Second stage now in-tree (s.js, shellcode/) Same kit, not a second exploit family. Config theft, not a generic shell

Do not equate "found in the wild" with "nation-state gift set" or with "script-kiddie zip." It is a professional targeting library for two known edge RCEs. That is already enough.