O pote de ouro do FortiGate SSL-VPN. CVE-2024-21762 e CVE-2023-27997. 79 clientes de exploit funcionais. 53 SKUs de hardware. 55 builds do FortiOS.
Arquivo de pesquisa. Recuperado como encontrado. Eu não escrevi estes arquivos.
Lançado: 1 de setembro de 2026
Este repositório é o orquestrador, payloads de segundo estágio e listeners do operador que ficam por cima de um catálogo de exploits FortiGate SSL-VPN por SKU. A história completa, contagens e notas de caça vivem na série The SSL-VPN pot of gold no Abraxas Labs.
Não execute este material contra nenhum host a menos que você tenha permissão escrita explícita tanto da parte que hospeda este repositório quanto do proprietário dos sistemas alvo. Os CVEs relacionados eram públicos e corrigidos antes deste lançamento. Esta árvore é para pesquisa, detecção e análise histórica.
Encontrei um kit privado FortiGate SSL-VPN na natureza. O primeiro lote era um diretório de clientes funcionais: nomes como e . Cada arquivo é um cliente de corrupção de memória . Dois n-days públicos, reestampados através de hardware e firmware até a pasta parecer um catálogo de peças.
exp/exp_60E-v6.4.7.pyO ouro é a matriz de offsets, não um novo bug. Esse argumento está na Fase 0 da série. A Fase 1 cobre as duas primitivas como comportamento. A Fase 2 é o mapa pesquisável de SKU / train / CPU. A Fase 3 é a caça.
Este repositório é o resto do mesmo kit, não uma segunda família de exploits.
exp/ são os clientes por imagem já descritos na série.shellcode/ são o launcher, busca de implante e listeners de roubo de configuração que esses clientes foram escritos para chamar.exp_*.py nunca continham o implante. Eles apenas sabiam (1) puxar bytes brutos de um "servidor de shellcode" ou (2) fazer GET /s.js e eval. Essas peças que faltavam estão aqui.As primitivas:
| Família | CVE | Aviso da Fortinet | Cor pública |
|---|---|---|---|
| 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ão no catálogo de Vulnerabilidades Exploradas Conhecidas da CISA. poc.py não implementa nenhum dos CVEs. Ele seleciona e envolve as duas famílias já classificadas (escrita em blocos 21762; overflow de heap enc= 27997).
.
├── poc.py # launcher pocsuite3 / fingerprint / gerenciador de processos
├── s.js # segundo estágio Node (roubador de configuração)
├── send_cmd.py # manipulador de callback para o caminho do stager
├── init.sh # instalação de pacotes da estação de trabalho do operador
├── requirements.txt # pwntools
├── exp/
│ └── exp_<SKU>-v<FortiOS>.py
└── shellcode/
├── shellcode_server.py # bomba de blob com prefixo de comprimento
├── shellcode_arm # ~2,0 MB de segundo estágio bruto
└── shellcode_amd64 # ~2,7 MB de segundo estágio bruto
exp/index.html e shellcode/index.html são listagens de diretório comuns do http.server do Python. Elas apenas mostram que esta árvore foi hospedada como raiz web em algum momento.
Esta cópia de exp/ é o catálogo anterior mais quatro clientes extras: 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 orquestrador (pocsuite3)
│
├─ fingerprint do portal ──── VERSION_LIBS (hash → SKU + FortiOS)
├─ escolher arquivo ───────── exp/exp_<SKU>-v<ver>.py
│
├─ se o cliente for do tipo "stager" (Família A ARM / alguns amd64)
│ iniciar shellcode/shellcode_server.py
│ lê shellcode/shellcode_arm ou shellcode_amd64
│ iniciar send_cmd.py (aguardar callback)
│ iniciar receptor de configuração em :9999
│ executar exp_*.py escolhido
│
└─ se o cliente for do tipo "Node /s.js" (maioria da Família B + muitas Família A)
servir s.js via HTTP
iniciar receptor de configuração em :9999
executar exp_*.py escolhido
→ firewall GET /s.js → eval → exfiltração
Pipeline em uma linha: identificar → escolher arquivo de offset → corromper sslvpnd → puxar implante → exfiltrar /data/config/*.gz → arquivar no lado do operador.
Esse último passo é o ponto. Um acerto é execução de código não autenticada em sslvpnd no próprio firewall — terminador TLS, política de roteamento, sessões VPN, frequentemente bindings LDAP/RADIUS. O FortiOS não oferece um /bin/sh útil. Pesquisa pública e este kit ambos passam por um auxiliar de comando interno e/ou /bin/node, que o FortiOS realmente inclui. Trate um acerto confirmado como comprometimento da borda da rede, não um daemon travado. Veja O que um acerto significa.
poc.py — launcherFront end do operador (pocsuite3 POCBase). Não implementa CVE-2024-21762 ou CVE-2023-27997.
https://<target>/remote/login?lang=en (fallback GB2312)./sslvpn/js/login.js?q=<md5>.VERSION_LIBS (182 linhas hash,SKU,version). Essa tabela é o motivo pelo qual o kit pode escolher entre 50+ appliances sem o operador nomear o SKU. Mesmos SKUs da matriz, dirigidos por um hash de recurso SSL-VPN público em vez de SNMP ou uma API de gerenciamento.exp/exp_<SKU>-v<version>.py.s.js → tipo 2 (loader Node)Shellcode Server → tipo 1 (stager); então procura por arm vs amd64 no scriptPopen do exp_*.py com argumentos de host/porta/callback.Endereço padrão do operador nesta cópia: HOST_IP do ambiente, senão ATTACKER_HOST. Endereços IPv4 literais nesta árvore foram substituídos por rótulos de função (portas inalteradas):
| Rótulo | Função | Onde apareceu |
|---|---|---|
TARGET_HOST | FortiGate sob ataque | Nunca hardcoded; rhost / target de argv (veja poc.py) |
ATTACKER_HOST | Estação de trabalho do operador | Padrão HOST_IP em poc.py; endereços de bind em poc.py, send_cmd.py, shellcode/shellcode_server.py; placeholders s.js / TFTP em exp_*.py que são preenchidos com lhost |
C2_HOST | Callback embutido nesta cópia do implante | s.js — não o lhost passado para o exploit |
| Porta | Função |
|---|---|
| 7777 | Callback / send_cmd.py |
| 8888 | Servidor de shellcode |
| 9999 | Receptor de configuração (corresponde à exfiltração de s.js e send_cmd.py) |
Se o hash do portal estiver ausente de VERSION_LIBS, ele para com "unknown target hash." Se o hash corresponder mas o arquivo exp/ estiver ausente, "no exp."
s.js — o segundo estágio Node que faltavaEste é o arquivo que os clientes da Família B / Node Família A buscam após execute_cmd. Não é um reverse shell genérico.
C2_HOST:9999 (host C2, não TARGET_HOST e não o lhost / ATTACKER_HOST passado para o exploit — esta cópia foi gravada para um callback de operador)./data/config/ no firewall.*.gz que não são symlinks (objetos de configuração compactados do FortiOS).Isso é um roubador de configuração. Explica por que poc.py também escuta na 9999 e remonta arquivos em um resultado base64. Cace C2_HOST na porta 9999 de qualquer egresso FortiGate. Notas de caça: Fase 3.
send_cmd.py — manipulador de callback para o caminho do stagerUsado quando o exp_*.py escolhido é do tipo shellcode ARM/amd64 (uso inclui um servidor de shellcode).
cd /data/config e cat de cada *.gz para nc <LHOST> 9999.s.js (roubar configuração), transporte diferente (callback de shell vs Node).Uma variante comentada usava enquadramento base64 em vez de cat bruto.
shellcode/shellcode_server.py — bomba de bytes para stagers da Família ACorresponde ao protocolo já visível nos stubs ARM32 exp_*.py:
poc.py usa 8888).shellcode_<arch> do mesmo diretório.Os stagers exp_*.py conectam aqui, mapeiam memória executável-gravável, puxam o blob, então conectam de volta para lhost:lport (7777 → send_cmd.py).
Blobs brutos de segundo estágio (~2,0 MB ARM, ~2,7 MB amd64). São o que o stager baixa. Não são o stub Thumb minúsculo dentro dos arquivos Python; esses stubs apenas buscam estes arquivos. Trate-os como o implante para o caminho não-Node. O tamanho sugere payloads empacotados ou estáticos, não algumas dezenas de instruções.
init.sh — configuração da estação de trabalho do operadorNão usado contra o firewall. Instala build-essential, binutils-arm-linux-gnueabihf e gcc-aarch64-linux-gnu. Isso é para compilar peças ARM/AArch64 em uma máquina de ataque tipo Debian. Consistente com um autor que compila stagers em vez de apenas enviar Python.
Linha única: pwntools. Os arquivos exp_*.py e shellcode_server.py o importam. poc.py também quer pocsuite3, python-dotenv, mmh3 e requests.
exp/| Já descrito na série | Função nesta árvore |
|---|---|
Família A ARM exp_*.py (argumentos de stager) | Primeiro estágio. Chama shellcode_server.py + shellcode_arm (ou amd64), depois lhost:7777. |
Família A/B Node exp_*.py (/s.js) | Primeiro estágio. HTTP GET do s.js pai, depois eval. |
Variantes 6.0 D-series TFTP / /tmp/s | Mesmo objetivo s.js via TFTP; ainda espera este arquivo na raiz HTTP do operador. |
Nada no diretório pai implementa um terceiro CVE. Contagens, heatmap de SKU e alinhamento de train (Família A até 7.4.0 / 7.4.1; Família B dentro da janela 27997) estão na Fase 0 e Fase 2. Retargeting mais pesado no catálogo original: FortiGate-60E, 100E, 60F, 100F — hardware de filial.
O primeiro lote parou em "obter um callback." Corpos de segundo estágio estavam faltando. Esta árvore torna o objetivo explícito:
/data/config/*.gz, não "RCE genérico." Isso é comprometimento de credenciais e topologia (usuários VPN, rotas, segredos armazenados nesses objetos).VERSION_LIBS em poc.py é uma lista pronta de "qual SKU eles achavam que este hash era."Recon do portal e URIs de primeiro estágio estão documentados em Hunting the kit. Os distintivos já apontados em write-ups públicos e nesta árvore:
GET /remote/login e /sslvpn/js/login.js?q=/aaaa/bbbb, /remote/error, /remote/info, /remote/hostcheck_validate com enc=GET /s.js do firewallC2_HOST:9999 dentro desta cópia de s.js (host C2)ATTACKER_HOST 7777 / 8888 / 9999 (host atacante)TARGET_HOST é rhost fornecido em tempo de execuçãoCace egresso do firewall (TARGET_HOST) para C2_HOST:9999 e para qualquer ATTACKER_HOST que serviu /s.js durante a janela relevante.
Da introdução:
Um PoC público contra uma VM é um artefato diferente. Este pacote é para quem respondeu ao recon.
Se você executa FortiGates: inventarie modelo, FortiOS exato, SSL-VPN ligado ou desligado, se o portal estava acessível de redes não confiáveis. Então leia a Fase 2. Não inventarie apenas caixas de filial da série 60 porque esses nomes de arquivo eram os mais barulhentos.
Estes arquivos foram recuperados na natureza durante pesquisa de segurança independente. Eu não os criei. Eles são lançados como encontrados, após os CVEs relacionados terem sido divulgados publicamente e corrigidos.
Uso apenas para pesquisa / educacional. Não execute, implante ou use este material contra nenhum host a menos que você tenha permissão escrita explícita tanto da parte que hospeda este repositório quanto do proprietário dos sistemas alvo.