## https://sploitus.com/exploit?id=5C790E5A-E7ED-5094-A8AD-F17E24E1851E
# FortiGate SSL-VPN kit (recovered in the wild)
**Research archive.** Recovered as found. I did not write these files.
**Released:** 1 September 2026
This repository is the **orchestrator, second-stage payloads, and operator listeners** that sit on top of a per-SKU FortiGate SSL-VPN exploit catalog. The long-form story, counts, and hunting notes live in the series **[The SSL-VPN pot of gold](https://abraxaslabs.tech/research/sslvpn-pot-of-gold)** on [Abraxas Labs](https://abraxaslabs.tech).
- Site: [abraxaslabs.tech](https://abraxaslabs.tech)
- Series: [The SSL-VPN pot of gold](https://abraxaslabs.tech/research/sslvpn-pot-of-gold)
- GitHub: [github.com/abraxas](https://github.com/abraxas)
- Twitter / X: [@abraxas_null](https://x.com/abraxas_null)
> **Do not run this material** against any host unless you have explicit written permission from both the party hosting this repository and the owner of the target systems. The related CVEs were [public](https://nvd.nist.gov/vuln/detail/CVE-2024-21762) and [patched](https://www.fortiguard.com/psirt/FG-IR-24-015) before this release. This tree is for research, detection, and historical analysis.
---
## What this is
I found a private FortiGate SSL-VPN kit in the wild. The first drop was a directory of working clients: names like [`exp/exp_60E-v6.4.7.py`](exp/exp_60E-v6.4.7.py) and [`exp/exp_1000D-v7.4.0.py`](exp/exp_1000D-v7.4.0.py). Each file is a **per-SKU, per-FortiOS** memory-corruption client. Two public n-days, restamped across hardware and firmware until the folder looked like a parts catalog.
The gold is the **offset matrix**, not a new bug. That argument is in [Phase 0 of the series](https://abraxaslabs.tech/research/sslvpn-pot-of-gold). [Phase 1](https://abraxaslabs.tech/research/sslvpn-pot-of-gold/two-public-doors) covers the two primitives as behavior. [Phase 2](https://abraxaslabs.tech/research/sslvpn-pot-of-gold/the-matrix) is the searchable SKU / train / CPU map. [Phase 3](https://abraxaslabs.tech/research/sslvpn-pot-of-gold/hunting-the-kit) is hunting.
This repository is the **rest of the same kit**, not a second exploit family.
- The [`exp/`](exp/) programs are the per-image clients already described in the series.
- The parent-directory and [`shellcode/`](shellcode/) files are the **launcher, implant fetch, and config-steal listeners** those clients were written to call.
- The `exp_*.py` files never contained the implant. They only knew to (1) pull raw bytes from a βshellcode serverβ or (2) `GET /s.js` and `eval` it. Those missing pieces are here.
The primitives:
| Family | CVE | Fortinet advisory | Public colour |
| --- | --- | --- | --- |
| A | [CVE-2024-21762](https://nvd.nist.gov/vuln/detail/CVE-2024-21762) | [FG-IR-24-015](https://www.fortiguard.com/psirt/FG-IR-24-015) | [Assetnote β Two Bytes is Plenty](https://www.assetnote.io/resources/research/two-bytes-is-plenty-fortigate-rce-with-cve-2024-21762) |
| B | [CVE-2023-27997](https://nvd.nist.gov/vuln/detail/CVE-2023-27997) | [FG-IR-23-097](https://www.fortiguard.com/psirt/FG-IR-23-097) | [Lexfo β XORtigate](https://blog.lexfo.fr/xortigate-cve-2023-27997.html), [Bishop Fox](https://bishopfox.com/blog/cve-2023-27997-exploitable-fortigate-vulnerable) |
Both sit in [CISAβs Known Exploited Vulnerabilities catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog). `poc.py` does **not** implement either CVE. It selects and wraps the two families already classified (chunked-write 21762; `enc=` heap overflow 27997).
---
## Layout
```text
.
βββ poc.py # pocsuite3 launcher / fingerprint / process manager
βββ s.js # Node second stage (config stealer)
βββ send_cmd.py # callback handler for the stager path
βββ init.sh # operator workstation package install
βββ requirements.txt # pwntools
βββ exp/
β βββ exp_-v.py
βββ shellcode/
βββ shellcode_server.py # length-prefixed blob pump
βββ shellcode_arm # ~2.0 MB raw second stage
βββ shellcode_amd64 # ~2.7 MB raw second stage
```
[`exp/index.html`](exp/index.html) and [`shellcode/index.html`](shellcode/index.html) are ordinary Python `http.server` directory listings. They only show that this tree was hosted as a web root at some point.
This copy of `exp/` is the earlier catalog plus **four** extra clients: [`exp_30E-v6.2.4.py`](exp/exp_30E-v6.2.4.py), [`exp_30E-v6.2.12.py`](exp/exp_30E-v6.2.12.py), [`exp_30E-v6.2.14.py`](exp/exp_30E-v6.2.14.py), [`exp_40F-v7.0.5.py`](exp/exp_40F-v7.0.5.py).
---
## How the pieces wire together
```text
poc.py orchestrator (pocsuite3)
β
ββ fingerprint portal βββββββ VERSION_LIBS (hash β SKU + FortiOS)
ββ pick file ββββββββββββββββ exp/exp_-v.py
β
ββ if client is "stager" kind (Family A ARM / some amd64)
β start shellcode/shellcode_server.py
β reads shellcode/shellcode_arm or shellcode_amd64
β start send_cmd.py (wait for callback)
β start config receiver on :9999
β run chosen exp_*.py
β
ββ if client is "Node /s.js" kind (most Family B + many Family A)
serve s.js over HTTP
start config receiver on :9999
run chosen exp_*.py
β firewall GET /s.js β eval β exfil
```
Pipeline in one line: **identify β pick offset file β corrupt `sslvpnd` β pull implant β exfil `/data/config/*.gz` β archive on the operator side.**
That last step is the point. A hit is unauthenticated code execution in [`sslvpnd`](https://abraxaslabs.tech/research/sslvpn-pot-of-gold) on the firewall itself β TLS terminator, routing policy, VPN sessions, often LDAP/RADIUS bindings. FortiOS does not offer a useful `/bin/sh`. Public research and this kit both go through an internal command helper and/or `/bin/node`, which FortiOS actually ships. Treat a confirmed hit as **compromise of the network edge**, not a crashed daemon. See [What a hit means](https://abraxaslabs.tech/research/sslvpn-pot-of-gold).
---
## File-by-file
### [`poc.py`](poc.py) β launcher
Operator front end ([pocsuite3](https://github.com/knownsec/pocsuite3) `POCBase`). It does not implement [CVE-2024-21762](https://nvd.nist.gov/vuln/detail/CVE-2024-21762) or [CVE-2023-27997](https://nvd.nist.gov/vuln/detail/CVE-2023-27997).
1. Fetches `https:///remote/login?lang=en` (fallback `GB2312`).
2. Pulls the cache-buster hash from `/sslvpn/js/login.js?q=`.
3. Looks that hash up in an embedded table `VERSION_LIBS` (**182** `hash,SKU,version` rows). That table is why the kit can choose among 50+ appliances without the operator naming the SKU. Same SKUs as [the matrix](https://abraxaslabs.tech/research/sslvpn-pot-of-gold/the-matrix), driven by a public SSL-VPN resource hash rather than SNMP or a management API.
4. Resolves `exp/exp_-v.py`.
5. Classifies the client by reading its source:
- contains `s.js` β kind 2 (Node loader)
- usage mentions `Shellcode Server` β kind 1 (stager); then looks for `arm` vs `amd64` in the script
6. Starts the matching helpers and `Popen`s the `exp_*.py` with host/port/callback arguments.
Default operator address in this copy: `HOST_IP` from the environment, else `ATTACKER_HOST`. Literal IPv4 addresses in this tree have been replaced with role labels (ports unchanged):
| Label | Role | Where it appeared |
| --- | --- | --- |
| `TARGET_HOST` | FortiGate under attack | Never hardcoded; `rhost` / `target` from argv (see [`poc.py`](poc.py)) |
| `ATTACKER_HOST` | Operator workstation | Default `HOST_IP` in [`poc.py`](poc.py); bind addresses in [`poc.py`](poc.py), [`send_cmd.py`](send_cmd.py), [`shellcode/shellcode_server.py`](shellcode/shellcode_server.py); `exp_*.py` `s.js` / TFTP placeholders that get filled with `lhost` |
| `C2_HOST` | Baked callback in this copy of the implant | [`s.js`](s.js) β not the `lhost` passed into the exploit |
| Port | Role |
| --- | --- |
| 7777 | Callback / [`send_cmd.py`](send_cmd.py) |
| 8888 | Shellcode server |
| 9999 | Config receiver (matches [`s.js`](s.js) and [`send_cmd.py`](send_cmd.py) exfil) |
If the portal hash is missing from `VERSION_LIBS`, it stops with βunknown target hash.β If the hash matches but the `exp/` file is absent, βno exp.β
### [`s.js`](s.js) β the missing Node second stage
This is the file the Family B / Node Family A clients fetch after `execute_cmd`. It is **not** a general reverse shell.
- Hard-coded callback **`C2_HOST:9999`** (C2 host, not `TARGET_HOST` and not the `lhost` / `ATTACKER_HOST` passed into the exploit β this copy was baked for one operator callback).
- Reads `/data/config/` on the firewall.
- Takes non-symlink `*.gz` files (FortiOS compressed config objects).
- Deflates them and ships each over a small length-prefixed TCP protocol, then an end marker.
That is a **configuration stealer**. It explains why [`poc.py`](poc.py) also listens on 9999 and reassembles files into a base64 result. Hunt `C2_HOST` on port 9999 from any FortiGate egress. Hunting notes: [Phase 3](https://abraxaslabs.tech/research/sslvpn-pot-of-gold/hunting-the-kit).
### [`send_cmd.py`](send_cmd.py) β callback handler for the stager path
Used when the chosen `exp_*.py` is the ARM/amd64 shellcode kind (usage includes a shellcode server).
- Listens on **7777**.
- On connect, writes one command: `cd /data/config` and `cat` every `*.gz` to `nc 9999`.
- Same goal as [`s.js`](s.js) (steal config), different transport (shell callback vs Node).
A commented-out variant used `base64` framing instead of raw `cat`.
### [`shellcode/shellcode_server.py`](shellcode/shellcode_server.py) β byte pump for Family A stagers
Matches the protocol already visible in the ARM32 `exp_*.py` stubs:
1. Bind operator-chosen port ([`poc.py`](poc.py) uses 8888).
2. Accept one connection.
3. Send a 4-byte little-endian length.
4. Send the blob `shellcode_` from the same directory.
`exp_*.py` stagers connect here, map writable-executable memory, pull the blob, then connect back to `lhost:lport` (7777 β [`send_cmd.py`](send_cmd.py)).
### [`shellcode/shellcode_arm`](shellcode/shellcode_arm) and [`shellcode/shellcode_amd64`](shellcode/shellcode_amd64)
Raw second-stage blobs (~2.0 MB ARM, ~2.7 MB amd64). They are what the stager downloads. They are **not** the tiny Thumb stub inside the Python files; those stubs only fetch these files. Treat them as the implant for the non-Node path. Size suggests packed or static payloads, not a few dozen instructions.
### [`init.sh`](init.sh) β operator workstation setup
Not used against the firewall. Installs `build-essential`, `binutils-arm-linux-gnueabihf`, and `gcc-aarch64-linux-gnu`. That is for **building** ARM/AArch64 pieces on a Debian-like attack box. Consistent with an author who compiles stagers rather than only shipping Python.
### [`requirements.txt`](requirements.txt)
Single line: [`pwntools`](https://github.com/Gallopsled/pwntools). The `exp_*.py` files and [`shellcode_server.py`](shellcode/shellcode_server.py) import it. [`poc.py`](poc.py) also wants [pocsuite3](https://github.com/knownsec/pocsuite3), `python-dotenv`, [`mmh3`](https://pypi.org/project/mmh3/), and [`requests`](https://pypi.org/project/requests/).
---
## Relation to the `exp/` clients
| Already described in the series | Role in this tree |
| --- | --- |
| Family A ARM `exp_*.py` (stager args) | First stage. Calls [`shellcode_server.py`](shellcode/shellcode_server.py) + [`shellcode_arm`](shellcode/shellcode_arm) (or amd64), then `lhost:7777`. |
| Family A/B Node `exp_*.py` (`/s.js`) | First stage. HTTP GET of parent [`s.js`](s.js), then `eval`. |
| 6.0 D-series TFTP / `/tmp/s` variants | Same `s.js` goal via TFTP; still expects this file on the operator HTTP root. |
Nothing in the parent directory implements a third CVE. Counts, SKU heatmap, and train alignment (Family A through **7.4.0 / 7.4.1**; Family B inside the 27997 window) are in [Phase 0](https://abraxaslabs.tech/research/sslvpn-pot-of-gold) and [Phase 2](https://abraxaslabs.tech/research/sslvpn-pot-of-gold/the-matrix). Heaviest retargeting in the original catalog: FortiGate-**60E**, **100E**, **60F**, **100F** β branch-office hardware.
---
## What this changes
The first drop stopped at βget a callback.β Second-stage bodies were missing. This tree makes the objective explicit:
1. **Steal FortiOS config** from `/data/config/*.gz`, not βgeneric RCE.β That is credential and topology compromise (VPN users, routes, secrets stored in those objects).
2. **IOC set is concrete** (see below).
3. **`VERSION_LIBS` in [`poc.py`](poc.py)** is a ready-made βwhich SKU did they think this hash wasβ list.
4. The kit is a complete steal-config pipeline, not a bag of unconnected PoCs.
---
## Indicators (this copy)
Portal recon and first-stage URIs are documented in [Hunting the kit](https://abraxaslabs.tech/research/sslvpn-pot-of-gold/hunting-the-kit). Distinctive ones already called out in public write-ups and in this tree:
- Portal recon: `GET /remote/login` and `/sslvpn/js/login.js?q=`
- First-stage URIs: `/aaaa/bbbb`, `/remote/error`, `/remote/info`, `/remote/hostcheck_validate` with `enc=`
- Outbound HTTP `GET /s.js` from the firewall
- Outbound TCP from the firewall to **7777 / 8888 / 9999**
- Baked C2 **`C2_HOST:9999`** inside this copy of [`s.js`](s.js) (C2 host)
- Operator listeners on **`ATTACKER_HOST`** ports **7777 / 8888 / 9999** (attacker host)
- Target is never hardcoded: **`TARGET_HOST`** is `rhost` supplied at run time
Hunt egress from the firewall (`TARGET_HOST`) to `C2_HOST:9999` and to any `ATTACKER_HOST` that served `/s.js` during the relevant window.
---
## What this kit does not prove
From [the introduction](https://abraxaslabs.tech/research/sslvpn-pot-of-gold):
- A new 0-day. The primitives are n-days with public write-ups.
- FortiProxy, FortiManager, FortiAnalyzer, FortiWeb. No samples here.
- SKUs not in the recovered matrix (many 70F / 90G / 120G units, VM-only images, 6K / 7K chassis). No compiled offsets.
- Builds past Fortinetβs published cuts. Confirm against the live advisory ([FG-IR-24-015](https://www.fortiguard.com/psirt/FG-IR-24-015), [FG-IR-23-097](https://www.fortiguard.com/psirt/FG-IR-23-097)), not this folder.
- Persistence. That would live in follow-on that is still not implemented as a distinct third stage in these scripts.
A public PoC against a VM is a different artifact. This pack is for whoever answered recon.
---
## Series
1. **[Phase 0 Β· Introduction](https://abraxaslabs.tech/research/sslvpn-pot-of-gold)** β the find, the counts, the stakes.
2. **[Phase 1 Β· Two public doors](https://abraxaslabs.tech/research/sslvpn-pot-of-gold/two-public-doors)** β Family A and Family B as behavior, not a recipe.
3. **[Phase 2 Β· The matrix](https://abraxaslabs.tech/research/sslvpn-pot-of-gold/the-matrix)** β 53 SKUs, trains, CPU. Searchable.
4. **[Phase 3 Β· Hunting the kit](https://abraxaslabs.tech/research/sslvpn-pot-of-gold/hunting-the-kit)** β distinctive URIs, patch cuts, what to do if the portal was reachable.
If you run FortiGates: inventory model, exact FortiOS, SSL-VPN on or off, whether the portal was reachable from untrusted networks. Then read [Phase 2](https://abraxaslabs.tech/research/sslvpn-pot-of-gold/the-matrix). Do not inventory only 60-series branch boxes because those filenames were the loudest.
---
## Notice
These files were recovered in the wild during independent security research. I did not create them. They are released as found, after the related CVEs were publicly disclosed and patched.
**Research / educational use only.** Do not run, deploy, or use this material against any host unless you have explicit written permission from both the party hosting this repository and the owner of the target systems.
β [@abraxas_null](https://x.com/abraxas_null) Β· [abraxaslabs.tech](https://abraxaslabs.tech) Β· [github.com/abraxas](https://github.com/abraxas)