Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
Fortigate-SSL-VPN-Exploit-Kit — The FortiGate SSL-VPN pot of gold. CVE-2024-21762 and CVE-2023-27997. 79 working exploit clients. 53 hardware SKUs. 55 FortiOS builds. | Kitploit
Tools/GitHubGitHub/abraxas/fortigate-ssl-vpn-exploit-kit
Exploit FrameworksVulnerability AnalysisExploitationWeb Application ExploitationData ExfiltrationNetwork SecurityPenetration TestingCommand and ControlRed Teaming
Payload Development
GitHubabraxas/fortigate-ssl-vpn-exploit-kit

Fortigate-SSL-VPN-Exploit-Kit

The FortiGate SSL-VPN pot of gold. CVE-2024-21762 and CVE-2023-27997. 79 working exploit clients. 53 hardware SKUs. 55 FortiOS builds.

View Repository
8h 29m agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

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 on Abraxas Labs.

  • Site: abraxaslabs.tech
  • Series: The SSL-VPN pot of gold
  • GitHub: github.com/abraxas
  • Twitter / X: @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 and patched 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 and . Each file is a memory-corruption client. Two public n-days, restamped across hardware and firmware until the folder looked like a parts catalog.

exp/exp_1000D-v7.4.0.py
per-SKU, per-FortiOS

The gold is the offset matrix, not a new bug. That argument is in Phase 0 of the series. Phase 1 covers the two primitives as behavior. Phase 2 is the searchable SKU / train / CPU map. Phase 3 is hunting.

This repository is the rest of the same kit, not a second exploit family.

  • The exp/ programs are the per-image clients already described in the series.
  • The parent-directory and 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:

FamilyCVEFortinet advisoryPublic colour
ACVE-2024-21762FG-IR-24-015Assetnote — Two Bytes is Plenty
BCVE-2023-27997FG-IR-23-097Lexfo — XORtigate, Bishop Fox

Both sit in CISA’s 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

root@kitploit:~
.
├── 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_<SKU>-v<FortiOS>.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 and 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_30E-v6.2.12.py, exp_30E-v6.2.14.py, exp_40F-v7.0.5.py.


How the pieces wire together

root@kitploit:~
poc.py                          orchestrator (pocsuite3)
  │
  ├─ fingerprint portal ─────── VERSION_LIBS (hash → SKU + FortiOS)
  ├─ pick file ──────────────── exp/exp_<SKU>-v<ver>.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 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.


File-by-file

poc.py — launcher

Operator front end (pocsuite3 POCBase). It does not implement CVE-2024-21762 or CVE-2023-27997.

  1. Fetches https://<target>/remote/login?lang=en (fallback GB2312).
  2. Pulls the cache-buster hash from /sslvpn/js/login.js?q=<md5>.
  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, driven by a public SSL-VPN resource hash rather than SNMP or a management API.
  4. Resolves exp/exp_<SKU>-v<version>.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 Popens 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):

LabelRoleWhere it appeared
TARGET_HOSTFortiGate under attackNever hardcoded; rhost / target from argv (see poc.py)
ATTACKER_HOSTOperator workstationDefault HOST_IP in poc.py; bind addresses in poc.py, send_cmd.py, shellcode/shellcode_server.py; exp_*.py s.js / TFTP placeholders that get filled with lhost
C2_HOSTBaked callback in this copy of the implants.js — not the lhost passed into the exploit
PortRole
7777Callback / send_cmd.py
8888Shellcode server
9999Config receiver (matches s.js and 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 — 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 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.

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 <LHOST> 9999.
  • Same goal as 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 — byte pump for Family A stagers

Matches the protocol already visible in the ARM32 exp_*.py stubs:

  1. Bind operator-chosen port (poc.py uses 8888).
  2. Accept one connection.
  3. Send a 4-byte little-endian length.
  4. Send the blob shellcode_<arch> 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).

shellcode/shellcode_arm and 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 — 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

Single line: pwntools. The exp_*.py files and shellcode_server.py import it. poc.py also wants pocsuite3, python-dotenv, mmh3, and requests.


Relation to the exp/ clients

Already described in the seriesRole in this tree
Family A ARM exp_*.py (stager args)First stage. Calls shellcode_server.py + shellcode_arm (or amd64), then lhost:7777.
Family A/B Node exp_*.py (/s.js)First stage. HTTP GET of parent s.js, then eval.
6.0 D-series TFTP / /tmp/s variantsSame 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 and Phase 2. 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 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. 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 (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:

  • 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, 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 — the find, the counts, the stakes.
  2. Phase 1 · Two public doors — Family A and Family B as behavior, not a recipe.
  3. Phase 2 · The matrix — 53 SKUs, trains, CPU. Searchable.
  4. Phase 3 · 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. 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 · abraxaslabs.tech · github.com/abraxas

Download Tool