Todas as pesquisas

Quais bibliotecas HTTP realmente buscam URLs gopher://? Uma matriz SSRF testada empiricamente

Matriz SSRF gopher: 6 clientes baseados em libcurl abrem socket gopher, 0 stacks HTTP nativas alcançam gopher, redirect para gopher desabilitado por padrão desde a libcurl 7.65.2 em 2019.

Apenas clientes HTTP construídos sobre a libcurl realmente abrirão um gopher:// socket, e esse único fato decide se um server-side request forgery pode ser escalado de "fazer o servidor buscar uma URL" para "enviar bytes arbitrários para qualquer serviço TCP interno". Testamos o cliente HTTP padrão de dez linguagens contra um listener gopher ativo. Seis bibliotecas de cliente abriram o socket, e todas elas são wrappers finos em torno da libcurl: a linha de comando curl, o ext-curl do PHP, o pycurl do Python, o curl handler do Guzzle, a crate Rust curl, e Faraday no adapter typhoeus. Todas as stacks HTTP nativas que testamos recusaram gopher antes mesmo de abrir um socket: Go net/http, Java HttpURLConnection e HttpClient, Python requests, httpx, aiohttp e urllib3, Node http e fetch, Ruby Net::HTTP, .NET HttpClient, Rust reqwest, e wget. A regra em uma frase: gopher via SSRF é uma história de libcurl.

A versão direta Se o servidor vulnerável busca URLs com a libcurl ou qualquer binding sobre ela, trate um SSRF com gopher como um socket raw para todos os serviços internos e planeje a execução remota de código via Redis ou FastCGI. Se ele faz fetch com uma stack HTTP nativa, gopher está simplesmente fora de cogitação, porque o scheme é rejeitado na camada de URL ou adapter antes de qualquer tentativa de conexão. Não deduza pela linguagem. Uma aplicação PHP em ext-curl está exposta e uma aplicação PHP em stream wrappers não está; uma aplicação Ruby em Net::HTTP é segura e a mesma aplicação em um adapter typhoeus não é.

Por que gopher é o scheme que realmente importa para SSRF

O Server-side request forgery é geralmente descrito como induzir uma aplicação a buscar uma URL escolhida pelo atacante. Por si só, isso alcança serviços HTTP internos, o que é ruim, mas limitado. O gopher é o que remove esse limite. A libcurl trata uma URL gopher como gopher://host:port/<type><selector>, onde o primeiro caractere do path é o tipo de item gopher e tudo após ele é enviado literalmente como o selector, após o URL-decode, seguido por CRLF. Como os bytes passam por URL-decode primeiro, uma quebra de linha percent-encoded no path torna-se uma quebra de linha real no tráfego de rede. Esse é todo o gadget. Ele transforma "buscar uma URL" em "escrever qualquer sequência de bytes em qualquer porta TCP que o servidor consiga alcançar", razão pela qual gopher somado a um Redis interno ou php-fpm é o caminho canônico de um blind SSRF para execução de código. É uma das pontas mais afiadas de uma superfície de ataque externa, e se esconde inteiramente na escolha do cliente HTTP.

O teste: um socket real, não uma suposição

A maioria das referências afirma que "muitas bibliotecas suportam gopher" e passa direto para os payloads. Queríamos que o sucesso ou falha fosse observado, não apenas presumido, então o método é deliberadamente simples. Para cada cliente, iniciamos um TCP listener de execução única no localhost, solicitamos que o cliente busque gopher://127.0.0.1:PORT/_<payload>, e registramos se o listener chega a aceitar uma conexão e quais bytes chegam. Um cliente que recusa o scheme nunca se conecta, portanto o listener não reporta nada. Um cliente que abre o socket nos entrega os bytes exatos no tráfego. Este é um experimento ativo contra nossos próprios containers descartáveis e um listener em localhost, sem tocar em nenhum host de terceiros. Aqui está o núcleo do teste contra curl, pycurl e PHP ext-curl, em três versões diferentes da libcurl:

curl gopher://        (libcurl 7.81.0)  -> CONNECTED  b'AAAA\r\n'
curl gophers://       (libcurl 7.81.0)  -> CONNECTED  (TLS ClientHello on the wire)
curl dict://          (libcurl 7.81.0)  -> CONNECTED  b'CLIENT libcurl 7.81.0\r\nAAAA\r\nQUIT\r\n'
pycurl gopher://      (libcurl 8.21.0)  -> CONNECTED  b'PYCURL\r\nLINE2\r\n'
php ext-curl gopher:// (libcurl 8.14.1) -> CONNECTED  b'PHPCURL\r\nLINE2\r\n'

wget gopher://        (wget 1.21.2)     -> NO-CONNECT
python requests       -> NO-CONNECT     (InvalidSchema: no connection adapters)
python urllib         -> NO-CONNECT     (unknown url type: gopher)
node fetch / http.get -> NO-CONNECT     (scheme rejected before connect)

Vale notar duas coisas diretamente dessa saída. Primeiro, os clientes libcurl abrem o socket em todas as versões que testamos, desde uma antiga 7.81.0 até a atual 8.21.0, logo isso não é uma peculiaridade de uma build específica. Segundo, o gadget de CRLF é real e exato. As linhas de pycurl e PHP foram executadas com o seletor de URL _PYCURL%0d%0aLINE2 e _PHPCURL%0d%0aLINE2, e os bytes recebidos continham um carriage-return line-feed autêntico entre os dois tokens. Confirmamos isso de forma isolada também: a URL gopher://host:port/_TESTPAYLOAD%0d%0aSECONDLINE chega ao socket como b'TESTPAYLOAD\r\nSECONDLINE\r\n'. Trata-se de uma mensagem de protocolo de duas linhas escrita diretamente em um serviço TCP usando apenas uma URL.

A matriz

Linhas marcadas com live foram retestadas contra um socket para esta publicação. O restante foi verificado a partir do código-fonte, registros de protocolos e documentação de manipulação de schemes de cada cliente. O padrão não varia: a coluna "abre um socket" é exatamente o conjunto de bindings da libcurl, além de uma exceção modular em Perl.

O cliente HTTP abre um socket gopher://? Testado em 2026-08
Cliente / bibliotecaLinguagemSocket gopher://Motivo
curl CLI / libcurl (live)CSIMgopher, gophers, dict, ftp, file todos compilados internamente; allowlist padrão de transferência os habilita
wget / wget2 (live)Cnãoapenas http/https/ftp integrados; wget2 suporta apenas https
PHP ext-curl (curl_exec) (live)PHPSIMbinding fino da libcurl; o sink clássico de SSRF para Redis/FastCGI
PHP streams (file_get_contents)PHPnãoapenas wrappers registrados; sem wrapper para gopher/dict (file e ftp sim)
Guzzle (curl handler, padrão)PHPSIMdelega para ext-curl; stream handler não
pycurl (live)PythonSIMbinding direto da libcurl; o mesmo risco que o PHP curl
requests (live)PythonnãoInvalidSchema: no connection adapters
httpx / aiohttp / urllib3PythonnãoUnsupportedProtocol / NonHttpUrlClientError / URLSchemeUnknown
urllib (stdlib) (live)Pythonnãosem opener para gopher no Python 3 (file e ftp sim)
net/httpGonãounsupported protocol scheme "gopher"; apenas round-trippers http/https
HttpURLConnection / HttpClientJavanãohandlers são http/https/ftp/file/jar/mailto; unknown protocol: gopher
Apache HttpClient 4/5 / OkHttpJavanãoregistro de schemes é apenas http/https por design
Net::HTTPRubynãoespecífico para http/https; OpenURI envia gopher para File.open (leitura local, não um pivot TCP)
FaradayRubynão*no Net::HTTP; o adapter typhoeus (libcurl) é a exceção que alcança gopher
http / https / undici / fetch (live)Node.jsnãoespecífico por protocolo; fetch do WHATWG suporta apenas http/https/file/data/blob
HttpClient / HttpWebRequest.NETnãoapenas http/https; o legado GopherWebRequest foi removido no .NET Core
reqwest / ureqRustnãohyper e stacks pure-Rust, apenas http/https
curl crateRustSIMbinding direto da libcurl, mesma ressalva de qualquer outro binding
LWP::UserAgentPerlSIM*apenas se LWP::Protocol::gopher estiver instalado; o LWP é modular por scheme

Conte as linhas SIM e a conclusão é incontestável. Seis bibliotecas de cliente abrem um socket gopher, e todas as seis utilizam libcurl: curl, ext-curl do PHP, pycurl, Guzzle-curl, a crate curl do Rust e Faraday com typhoeus. O sétimo e único resultado positivo sem curl é o LWP::UserAgent do Perl, e até mesmo isso exige que um operador tenha instalado o plugin opcional LWP::Protocol::gopher, logo ele vem desabilitado por padrão. Todo o resto, toda stack HTTP nativa convencional em Go, Java, Python, Node, Ruby, .NET e Rust, rejeita o scheme antes de abrir uma conexão. Não existe payload engenhoso que mude isso, pois a recusa acontece no parser de URL ou no registro de adapters, antes de qualquer socket.

Nuance um: redirect para gopher está desabilitado por padrão

"A aplicação usa curl, então é explorável" é apenas meia verdade em builds modernas, e a metade que falta é onde muito tempo de teste é desperdiçado. Existem duas allowlists separadas na libcurl. A allowlist de transferência controla uma URL passada diretamente, e ela ainda habilita gopher. A allowlist de redirecionamento controla para quais schemes um header Location: pode alternar, e desde a libcurl 7.65.2 em maio de 2019 ela foi restringida apenas para http, https, ftp e ftps. Portanto, um open redirect apontando para gopher:// é recusado por padrão. Observamos isso acontecer na libcurl 7.81.0:

# default: an HTTP 302 to gopher:// is blocked
$ curl -sL http://listener/  ->  Location: gopher://127.0.0.1:PORT/_REDIRPWN
  * Protocol "gopher" not supported or disabled in libcurl
  listener: (never reached)

# widened: the same redirect now opens the gopher socket
$ curl -sL --proto-redir all http://listener/
  listener: GOPHER SOCKET OPENED b'REDIRPWN\r\n'

O redirecionamento só alcança gopher se a própria aplicação ampliou novamente os protocolos de redirecionamento, definindo CURLOPT_REDIR_PROTOCOLS_STR para incluir gopher ou passando para a CLI --proto-redir all. Essa má configuração não é hipotética. CVE-2026-33752 é exatamente essa classe de bug: a biblioteca curl_cffi seguiu redirecionamentos para protocolos arbitrários porque não restringiu a allowlist de redirecionamento, transformando um SSRF http simples de volta em um pivot gopher completo. A interpretação prática para um atacante é que, em um alvo moderno com libcurl, é necessário que o SSRF permita injetar o scheme completo, uma URL gopher:// direta, em vez de depender de um open redirect. A interpretação prática para um defensor é que ampliar protocolos de redirecionamento é uma arma carregada, e o padrão que a desarma está correto desde 2019.

Nuance dois: o payload é CRLF, e é trivial

A razão pela qual gopher importa mais do que dict:// ou file:// é o controle a nível de bytes. A libcurl envia a porção do selector da URL como bytes raw após o URL-decode, de modo que o atacante compõe o tráfego de rede diretamente no path da URL. Um %0d%0a em formato percent-encoded torna-se um carriage-return line-feed real, e qualquer protocolo orientado a linhas processará o resultado como comandos separados:

Nada disso precisa de uma falha de memória ou de uma race condition. Trata-se de uma URL que se decodifica em uma conversa de protocolo, razão pela qual um cliente compatível com gopher transforma um SSRF modesto em algo crítico. O mesmo princípio, um agente ou serviço buscando uma URL sob sua influência, é o que faz com que clientes automatizados que se conectam a endpoints arbitrários mereçam auditoria para a mesma classe de pivot.

O que fazer a respeito

A defesa não é "parar de usar curl". A libcurl é segura quando seu escopo de protocolos é fixado. A falha reside em confiar nos padrões e permitir que o input do usuário escolha o scheme.

Método e limitações, para que os números sejam confiáveis

O conjunto de dados é próprio, construído observando sockets, e é reproduzível.

Nenhum host de terceiros foi contatado em momento algum. Cada conexão nesta pesquisa foi direcionada a um listener iniciado em loopback, o que representa a diferença entre mensurar um comportamento e escanear o sistema de terceiros.

Perguntas frequentes

A biblioteca requests do Python suporta o protocolo gopher?

Não. Em um teste prático, requests gera InvalidSchema: No connection adapters were found para uma URL gopher e nunca abre um socket. O mesmo se aplica a httpx, aiohttp, urllib3, e à biblioteca padrão urllib. A única exceção em Python é pycurl, um binding fino sobre a libcurl, que de fato abre um socket gopher. Se o seu código Python usa requests ou httpx, um payload de SSRF com gopher não consegue alcançar um serviço interno através dele.

Por que o curl suporta gopher mas o wget não?

Conjuntos de protocolos diferentes. A libcurl compila gopher, gophers, dict, ftp e file por padrão, e sua allowlist de transferência os habilita. O wget foi construído apenas para http, https e ftp, e o wget2 suporta apenas https. É uma diferença de compilação e design, não uma opção de runtime, razão pela qual o curl é o pivot clássico para SSRF e o wget não.

Quais protocolos a libcurl suporta para SSRF?

Em uma build padrão, a libcurl pode abrir gopher, gophers, dict, ftp, ftps, file, tftp, ldap e os protocolos de e-mail, além de http e https. Para SSRF, os mais relevantes são gopher e dict, porque ambos colocam bytes controlados pelo atacante em um socket raw. O gopher é o mais poderoso, pois o selector após o caractere de item-type é enviado literalmente, de modo que um CRLF codificado torna-se uma quebra de linha real.

Como o protocolo gopher escala um SSRF para RCE?

Um SSRF comum faz o servidor buscar uma URL http. O gopher transforma isso no envio de bytes arbitrários para qualquer porta TCP acessível, pois a libcurl decodifica a URL do selector e o escreve em formato raw. Uma única URL pode enviar um SET do Redis mais CONFIG SET dir e SAVE para gravar uma webshell, ou um registro FastCGI para o php-fpm. O SSRF é a entrega, gopher é o payload e um serviço interno orientado a linhas é o alvo.

O HttpURLConnection do Java suporta gopher?

Não. java.net.URL e HttpURLConnection registram handlers apenas para http, https, ftp, file, jar e mailto, e uma URL gopher lança unknown protocol: gopher. O moderno HttpClient, Apache HttpClient e OkHttp rejeitam qualquer scheme não-http por design. Java é efetivamente imune a SSRF com gopher, embora o java.net.URL ainda suporte file://, o que é uma preocupação à parte.

Por que a libcurl não segue redirecionamentos para uma URL gopher://?

Desde a libcurl 7.65.2 em maio de 2019, a allowlist de redirecionamento padrão é restrita a http, https, ftp e ftps, de modo que um 302 para gopher:// é recusado e nenhum socket é aberto. O envio direto de uma URL gopher completa ainda funciona, pois a allowlist de transferência é separada. Uma aplicação pode reabrir o caminho de redirecionamento ampliando CURLOPT_REDIR_PROTOCOLS_STR, que é a classe de bug por trás do CVE-2026-33752 no curl_cffi. Em um alvo moderno, insira o scheme gopher completo em vez de depender de um open redirect.

Como enviar CRLF para o Redis através de um payload de SSRF com gopher?

Codifique as quebras de linha. A libcurl decodifica a URL do selector, portanto gopher://127.0.0.1:6379/_SET%20k%20v%0d%0aSAVE%0d%0aQUIT envia SET k v, um CRLF real, SAVE, um CRLF real, depois QUIT. Em nosso teste, _TESTPAYLOAD%0d%0aSECONDLINE chegou como os bytes TESTPAYLOAD\r\nSECONDLINE\r\n. O Redis utiliza um protocolo delimitado por quebras de linha, portanto essa é uma sequência de comandos válida, e é por isso que gopher somado ao Redis é a cadeia clássica de SSRF para RCE.

Leituras relacionadas

Um pivot com gopher funcionaria contra você?

Nosso check de $100 mapeia sua superfície de ataque externa real da mesma forma que um atacante faz, em um escopo que você verificou possuir e autorizou por escrito, com um operador sênior na apresentação dos resultados. Se um Redis interno ou php-fpm está a um SSRF de distância da execução de código é exatamente o tipo de cadeia que procuramos.

Agendar um check de $100