Tutte le ricerche

Quali librerie HTTP effettuano davvero il fetch di un URL gopher://? Una matrice SSRF testata empiricamente

Matrice SSRF gopher: 6 client basati su libcurl aprono un socket gopher, 0 stack HTTP nativi raggiungono gopher, redirect verso gopher disabilitato per default da libcurl 7.65.2 nel 2019.

Solo i client HTTP basati su libcurl apriranno effettivamente un gopher:// socket, e questo singolo fatto determina se una server-side request forgery può essere scalata da "far richiedere un URL al server" a "inviare byte arbitrari a qualsiasi servizio TCP interno." Abbiamo testato il client HTTP predefinito di dieci linguaggi contro un listener gopher attivo. Sei librerie client hanno aperto il socket, e ognuna di esse è un sottile wrapper attorno a libcurl: la riga di comando di curl, ext-curl, di Python pycurl, l'handler curl di Guzzle, il crate curl di Rust, e Faraday sull'adapter typhoeus. Ogni stack HTTP nativo che abbiamo provato ha rifiutato gopher prima ancora che un socket venisse aperto: 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. La regola in una frase: gopher-over-SSRF è una questione di libcurl.

La versione diretta Se il server vulnerabile effettua il fetch degli URL con libcurl o con qualsiasi binding basato su di esso, tratta un SSRF gopher come un raw socket verso ogni servizio interno e pianifica una remote code execution tramite Redis o FastCGI. Se effettua il fetch con uno stack HTTP nativo, gopher è semplicemente fuori discussione, perché lo schema viene rifiutato a livello di URL o di adapter prima che venga tentata qualsiasi connessione. Non tirare a indovinare dal linguaggio. Un'app PHP su ext-curl è esposta mentre un'app PHP su stream wrapper non lo è; un'app Ruby su Net::HTTP è sicura mentre la stessa app su un adapter typhoeus non lo è.

Perché gopher è lo schema che conta per le SSRF

La server-side request forgery viene solitamente descritta come l'indurre un'applicazione a recuperare un URL scelto dall'attaccante. Di per sé questo consente di raggiungere servizi HTTP interni, il che è grave ma limitato. gopher è ciò che rimuove questo limite. libcurl tratta un URL gopher come gopher://host:port/<type><selector>, dove il primo carattere del path è il tipo di elemento gopher e tutto ciò che segue viene inviato testualmente come selettore, dopo la decodifica dell'URL, seguito da CRLF. Poiché i byte vengono prima decodificati dall'URL, un newline percent-encoded nel path diventa un vero a capo sulla rete. Questo è l'intero gadget. Trasforma il "recuperare un URL" in "scrivere qualsiasi sequenza di byte su qualsiasi porta TCP raggiungibile dal server", motivo per cui gopher abbinato a un Redis interno o a php-fpm rappresenta il percorso canonico da una blind SSRF alla code execution. È uno degli aspetti più insidiosi di una superficie di attacco esterna, e si nasconde interamente nella scelta del client HTTP.

Il test: un socket reale, non un'ipotesi

La maggior parte delle fonti afferma che "molte librerie supportano gopher" passando direttamente ai payload. Volevamo che il superamento o il fallimento del test fosse osservato empiricamente, non presunto, quindi il metodo è volutamente semplice. Per ciascun client avviamo un listener TCP one-shot su localhost, chiediamo al client di effettuare il fetch di gopher://127.0.0.1:PORT/_<payload>, e registriamo se il listener accetta una connessione e quali byte arrivano. Un client che rifiuta lo schema non si connette mai, quindi il listener non segnala nulla. Un client che apre il socket ci fornisce gli esatti byte trasmessi sulla rete. Si tratta di un esperimento attivo condotto su nostri container usa-e-getta e su un listener su localhost, senza toccare alcun host di terze parti. Ecco il punto centrale testato su curl, pycurl e PHP ext-curl, su tre diverse versioni di 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)

Ci sono due aspetti degni di nota direttamente da quell'output. Primo, i client basati su libcurl aprono il socket su ogni versione testata, da una vecchia 7.81.0 all'attuale 8.21.0, quindi non si tratta di un comportamento anomalo di una singola build. Secondo, il gadget CRLF è reale ed esatto. Le righe di pycurl e PHP sono state interrogate con il selettore URL _PYCURL%0d%0aLINE2 e _PHPCURL%0d%0aLINE2, e i byte arrivati contenevano un vero carriage-return line-feed tra i due token. Lo abbiamo confermato anche in isolamento: l'URL gopher://host:port/_TESTPAYLOAD%0d%0aSECONDLINE arriva sul socket come b'TESTPAYLOAD\r\nSECONDLINE\r\n'. Si tratta di un messaggio di protocollo su due righe scritto direttamente in un servizio TCP tramite nient'altro che un URL.

La matrice

Le righe contrassegnate come live sono state ritestate su un socket per questo post. Le restanti sono state verificate dal codice sorgente, dai registri di protocollo e dalla gestione documentata degli schemi di ciascun client. Il pattern è costante: la colonna "apre un socket" coincide esattamente con l'insieme dei binding di libcurl, più un caso isolato modulare in Perl.

Il client HTTP apre un socket gopher://? Testato 2026-08
Client / libreriaLinguaggioSocket gopher://Motivazione
curl CLI / libcurl (live)Cgopher, gophers, dict, ftp, file tutti compilati all'interno; l'allowlist di trasferimento predefinita li abilita
wget / wget2 (live)Cnosolo http/https/ftp integrati; wget2 supporta solo https
PHP ext-curl (curl_exec) (live)PHPsottile binding di libcurl; il classico sink da SSRF a Redis/FastCGI
Stream PHP (file_get_contents)PHPnosolo wrapper registrati; nessun wrapper gopher/dict (file e ftp sì)
Guzzle (handler curl, default)PHPdelega a ext-curl; l'handler stream no
pycurl (live)Pythonbinding diretto di libcurl; stessa insidia di PHP curl
requests (live)PythonnoInvalidSchema: no connection adapters
httpx / aiohttp / urllib3PythonnoUnsupportedProtocol / NonHttpUrlClientError / URLSchemeUnknown
urllib (stdlib) (live)Pythonnonessun opener gopher in Python 3 (file e ftp sì)
net/httpGonounsupported protocol scheme "gopher"; solo round-tripper http/https
HttpURLConnection / HttpClientJavanogli handler sono solo http/https/ftp/file/jar/mailto; unknown protocol: gopher
Apache HttpClient 4/5 / OkHttpJavanoil registro degli schemi è solo http/https per design
Net::HTTPRubynospecifico per http/https; OpenURI invia gopher a File.open (lettura locale, non un pivot TCP)
FaradayRubyno*su Net::HTTP; l'adapter typhoeus (libcurl) è l'eccezione che raggiunge gopher
http / https / undici / fetch (live)Node.jsnospecifico per protocollo; fetch di WHATWG supporta solo http/https/file/data/blob
HttpClient / HttpWebRequest.NETnosolo http/https; il legacy GopherWebRequest è stato rimosso in .NET Core
reqwest / ureqRustnostack hyper e pure-Rust, solo http/https
crate curlRustbinding diretto di libcurl, stesso caveat di ogni altro binding
LWP::UserAgentPerlSÌ*solo se LWP::Protocol::gopher è installato; LWP è modulare per schema

Contando le righe con SÌ il quadro è inequivocabile. Sei librerie client aprono un socket gopher, e tutte e sei usano libcurl: curl, PHP ext-curl, pycurl, Guzzle-curl, il crate curl di Rust e Faraday su typhoeus. Il settimo e unico riscontro positivo non-curl è quello di Perl LWP::UserAgent, e anche in quel caso richiede che l'operatore abbia installato il plugin opzionale LWP::Protocol::gopher, quindi è disattivato per default. Tutto il resto, ogni stack HTTP nativo diffuso in Go, Java, Python, Node, Ruby, .NET e Rust, rifiuta lo schema prima di aprire una connessione. Non esiste alcun payload ingegnoso in grado di cambiare questo comportamento, perché il rifiuto avviene nel parser degli URL o nel registro degli adapter, a monte di qualsiasi socket.

Prima sfumatura: il redirect verso gopher è disabilitato per default

"L'applicazione usa curl, quindi è sfruttabile" è vero solo a metà su una build moderna, e la metà mancante è dove si perde tempo durante un engagement. Esistono due allowlist distinte in libcurl. L'allowlist di trasferimento governa gli URL passati direttamente, e include ancora gopher. L'allowlist di redirect governa verso quali schemi può reindirizzare un header Location:, e a partire da libcurl 7.65.2 di maggio 2019 è ristretta ai soli http, https, ftp e ftps. Di conseguenza, un open redirect che punta a gopher:// viene rifiutato per default. Lo abbiamo osservato direttamente su 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'

Il redirect raggiunge gopher solo se l'applicazione ha riallargato i protocolli di redirect autonomamente, impostando CURLOPT_REDIR_PROTOCOLS_STR per includere gopher o passando alla CLI --proto-redir all. Tale configurazione errata non è ipotetica. CVE-2026-33752 appartiene esattamente a questa classe di bug: la libreria curl_cffi seguiva redirect verso protocolli arbitrari perché non vincolava l'allowlist di redirect, trasformando una semplice SSRF http in un pivot gopher completo. La deduzione pratica per un attaccante è che su un target libcurl moderno è necessario che la SSRF permetta di inserire l'intero schema, un URL diretto gopher://, anziché fare affidamento su un open redirect. La deduzione pratica per chi difende è che ampliare i protocolli di redirect è estremamente pericoloso, e l'impostazione predefinita che lo neutralizza è corretta dal 2019.

Seconda sfumatura: il payload è CRLF, ed è banale

La ragione per cui gopher è più rilevante di dict:// o file:// è il controllo a livello di byte. libcurl invia la porzione del selettore dell'URL come raw byte dopo la decodifica dell'URL, consentendo all'attaccante di comporre il traffico di rete direttamente nel path dell'URL. Un percent-encoded %0d%0a diventa un vero carriage-return line-feed, e qualsiasi protocollo basato su righe analizzerà il risultato come comandi separati:

Nulla di tutto questo richiede un bug di memoria o una race condition. Si tratta di un URL che viene decodificato in una conversazione di protocollo, motivo per cui un client che supporta gopher trasforma una modesta SSRF in una minaccia critica. Lo stesso meccanismo, un agente o un servizio che effettua il fetch di un URL influenzato dall'utente, è ciò che rende i client automatizzati che si connettono a endpoint arbitrari meritevoli di verifica per la stessa classe di pivot.

Come rimediare

La contromisura non è "smettere di usare curl". libcurl funziona perfettamente quando il suo perimetro di protocolli è vincolato. L'errore sta nell'affidarsi alle impostazioni predefinite e permettere all'input dell'utente di scegliere lo schema.

Metodo e limitazioni, per dati affidabili

Il dataset è nostro, generato osservando i socket, ed è riproducibile.

Nessun host di terze parti è stato contattato in alcuna fase. Ogni connessione in questa ricerca è avvenuta verso un listener avviato su loopback, che è la differenza tra misurare un comportamento e scansionare il sistema di qualcun altro.

Domande frequenti

La libreria requests di Python supporta il protocollo gopher?

No. In un test reale, requests solleva InvalidSchema: No connection adapters were found per un URL gopher e non apre mai un socket. Lo stesso vale per httpx, aiohttp, urllib3, e per il modulo della standard library urllib. L'unica eccezione in Python è pycurl, un sottile binding su libcurl, che apre effettivamente un socket gopher. Se il tuo codice Python usa requests o httpx, un payload SSRF gopher non può raggiungere un servizio interno tramite esso.

Perché curl supporta gopher mentre wget no?

Insiemi di protocolli differenti. libcurl compila al suo interno gopher, gophers, dict, ftp e file per default, e la sua allowlist di trasferimento li abilita. wget è stato creato solo per http, https e ftp, mentre wget2 supporta solo https. Si tratta di una differenza di progettazione e in fase di compilazione, non di uno switch a runtime, motivo per cui curl è il classico vettore di pivot SSRF e wget non lo è.

Quali protocolli supporta libcurl per le SSRF?

Su una build standard, libcurl può aprire gopher, gophers, dict, ftp, ftps, file, tftp, ldap e i protocolli di posta, oltre a http e https. Per le SSRF quelli rilevanti sono gopher e dict, poiché entrambi inviano byte controllati dall'attaccante su un raw socket. gopher è il più potente, perché il selettore dopo il carattere del tipo di elemento viene inviato testualmente, trasformando un CRLF codificato in un vero a capo.

In che modo il protocollo gopher scala una SSRF a RCE?

Una comune SSRF fa richiedere al server un URL http. gopher trasforma questa richiesta nell'invio di byte arbitrari a qualsiasi porta TCP raggiungibile, poiché libcurl decodifica l'URL del selettore e lo scrive in formato raw. Un singolo URL può inviare a Redis SET oltre a CONFIG SET dir e SAVE per depositare una webshell, oppure un record FastCGI a php-fpm. La SSRF è il vettore di consegna, gopher è il payload e un servizio interno basato su righe è il target.

HttpURLConnection di Java supporta gopher?

No. java.net.URL e HttpURLConnection registrano gestori solo per http, https, ftp, file, jar e mailto, e un URL gopher solleva unknown protocol: gopher. I moderni HttpClient, Apache HttpClient e OkHttp rifiutano qualsiasi schema non-http per design. Java è di fatto immune a SSRF gopher, anche se java.net.URL supporta ancora file://, che costituisce una problematica distinta.

Perché libcurl non segue un redirect verso un URL gopher://?

A partire da libcurl 7.65.2 di maggio 2019, l'allowlist di redirect predefinita include solo http, https, ftp e ftps, quindi un 302 verso gopher:// viene rifiutato e non viene aperto alcun socket. L'invio diretto di un URL gopher completo funziona ancora, poiché l'allowlist di trasferimento è separata. Un'applicazione può riaprire il percorso di redirect ampliando CURLOPT_REDIR_PROTOCOLS_STR, che è la classe di bug alla base di CVE-2026-33752 in curl_cffi. Su un target moderno, inserisci l'intero schema gopher anziché affidarti a un open redirect.

Come si invia un CRLF a Redis tramite un payload SSRF gopher?

Codificando i newline. libcurl decodifica l'URL del selettore, quindi gopher://127.0.0.1:6379/_SET%20k%20v%0d%0aSAVE%0d%0aQUIT invia SET k v, un vero CRLF, SAVE, un vero CRLF, quindi QUIT. Nel nostro test, _TESTPAYLOAD%0d%0aSECONDLINE è arrivato come sequenza di byte TESTPAYLOAD\r\nSECONDLINE\r\n. Redis adotta un protocollo delimitato da newline, quindi questa è una sequenza di comandi valida, motivo per cui gopher abbinato a Redis è la classica catena da SSRF a RCE.

Letture correlate

Un pivot gopher funzionerebbe contro la tua infrastruttura?

Il nostro check da $100 mappa la tua reale superficie di attacco esterna esattamente come farebbe un attaccante, su un perimetro verificato di tua proprietà e autorizzato per iscritto, con un operatore senior per l'analisi dei risultati. Verificare se un Redis o un php-fpm interno sia a una sola SSRF di distanza dall'esecuzione di codice è esattamente il tipo di concatenazione che cerchiamo.

Prenota un check da $100