The SSL-VPN pot of gold · Phase 4
The kit on GitHub
Appendix. The recovered tree is public. What each file is, where it lives, and how the launcher, clients, and missing second stage fit together.
- Name
- The kit on GitHub
- Series
- The SSL-VPN pot of gold
- Phase
- 4 · Appendix
- Type
- N-day analysis
- CVE
- CVE-2024-21762
- CVE Risk
- critical
- Disclosure Status
- public
- Vendor
- Fortinet
- Affected
- FortiGate SSL-VPN kit released as found
- Published
- 01 Sept 2026
- Updated
- 01 Sept 2026
- Tags
- n-day, fortinet, ssl-vpn, github, appendix
The folder is public
Fortigate-SSL-VPN-Exploit-Kit on github.com/abraxas (catalog). Recovered as found. I did not write these files. The related CVEs were public and patched before this release.
This page is a map of the tree, not a usage guide. Do not run it against anything you do not own. Phase 0 is the find. Phase 1 is the two doors. Phase 3 is how you hunt.
The first drop was the exp_*.py clients. The GitHub tree is the rest of the same kit: orchestrator, listeners, and the second stage those clients were written to fetch. Not a third CVE. The objective is explicit now: steal FortiOS config from /data/config/*.gz.
Where the files are
| Path | What it is | GitHub |
|---|---|---|
poc.py |
pocsuite3 launcher / fingerprint / process manager. Does not implement either CVE. | poc.py |
s.js |
Node second stage. Config stealer. | s.js |
send_cmd.py |
Callback handler for the stager path (port 7777). | send_cmd.py |
init.sh |
Operator workstation package install. Not used against the firewall. | init.sh |
requirements.txt |
One line: pwntools. | requirements.txt |
exp/ |
Per-SKU, per-FortiOS clients. exp_<SKU>-v<FortiOS>.py. |
exp/ |
shellcode/shellcode_server.py |
Length-prefixed blob pump (port 8888 in this copy). | shellcode_server.py |
shellcode/shellcode_arm |
~2.0 MB raw second stage. | shellcode_arm |
shellcode/shellcode_amd64 |
~2.7 MB raw second stage. | shellcode_amd64 |
exp/index.html and shellcode/index.html are ordinary Python http.server listings. They only show this tree was hosted as a web root at some point.
This copy of exp/ is the catalog from Phase 0 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
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 -> exfilPipeline in one line: identify → pick offset file → corrupt sslvpnd → pull implant → exfil /data/config/*.gz → zip on the operator side.
That last step is the point. A hit is unauthenticated code execution in sslvpnd on the firewall itself. Treat it as compromise of the network edge.
Literal IPv4 addresses in this copy were replaced with role labels. Ports were left as recovered.
| Label | Role |
|---|---|
TARGET_HOST |
FortiGate. Never hardcoded; rhost at run time. |
ATTACKER_HOST |
Operator workstation. Default bind / lhost. |
C2_HOST |
Baked callback inside this copy of s.js. Not the lhost passed into the client. |
| Port | Role |
|---|---|
| 7777 | Callback / send_cmd.py |
| 8888 | Shellcode server |
| 9999 | Config receiver |
File-by-file (collapsed)
poc.py. Operator front end (pocsuite3 POCBase). Fetches /remote/login, pulls the cache-buster hash from /sslvpn/js/login.js?q=<md5>, looks it up in VERSION_LIBS (182 hash,SKU,version rows), then resolves exp/exp_<SKU>-v<version>.py. 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. It classifies a client by reading its source (s.js → Node loader; "Shellcode Server" → stager) and starts the matching helpers. Unknown hash → stop. Hash matches, file missing → "no exp."
s.js. The file Family B / Node Family A clients fetch after the command helper. Not a reverse shell. Hard-coded callback C2_HOST:9999. Reads /data/config/, takes non-symlink *.gz FortiOS config objects, deflates them, ships each over a small length-prefixed TCP protocol. That is why poc.py also listens on 9999.
send_cmd.py. Stager-path callback on 7777. Same goal as s.js (steal config), different transport. On connect it asks the implant to cat every *.gz toward port 9999.
shellcode/shellcode_server.py. Byte pump. Bind, accept one connection, send a 4-byte little-endian length, send shellcode_<arch>. The tiny stubs inside the ARM exp_*.py files only fetch these blobs.
shellcode_arm / shellcode_amd64. ~2.0 MB and ~2.7 MB. Size says packed or static payload, not a few dozen instructions. Treat them as the implant for the non-Node path.
init.sh. Installs build-essential, binutils-arm-linux-gnueabihf, gcc-aarch64-linux-gnu on a Debian-like attack box. Consistent with an author who compiles stagers rather than only shipping Python.
requirements.txt. pwntools. poc.py also wants pocsuite3, python-dotenv, mmh3, and requests.
What this changes
The first write-up stopped at "get a callback." This tree makes the objective explicit:
- Steal FortiOS config from
/data/config/*.gz. Credential and topology compromise (VPN users, routes, secrets in those objects). - IOC set is concrete. Portal recon, first-stage URIs, outbound
/s.js, outbound TCP to 7777 / 8888 / 9999. See Phase 3. VERSION_LIBSinpoc.pyis a ready-made "which SKU did they think this hash was" list.- The kit is a complete steal-config pipeline, not a bag of unconnected PoCs.
Nothing in the parent directory implements a third CVE. Counts and train alignment stay in Phase 0 and Phase 2.
Notice
These files were recovered in the wild during independent security research. Released as found, after the related CVEs were disclosed and patched. Research / educational use only.