Sploitus

Exploit for Out-of-bounds Write in Fortinet Fortiproxy

githubexploit Β· 2026-09-01

Exploit Code

README239 lines
## 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)