Alle onderzoeken

Welke HTTP-libraries halen daadwerkelijk een gopher://-URL op? Een empirisch geteste SSRF-matrix

SSRF gopher-matrix: 6 clients op basis van libcurl openen een gopher-socket, 0 native HTTP-stacks bereiken gopher, redirect naar gopher staat standaard uitgeschakeld sinds libcurl 7.65.2 in 2019.

Alleen HTTP-clients die zijn gebouwd op libcurl openen daadwerkelijk een gopher:// socket, en dat ene feit bepaalt of een server-side request forgery kan worden geëscaleerd van "laat de server een URL ophalen" naar "stuur willekeurige bytes naar elke interne TCP-service." We hebben de standaard HTTP-client van tien programmeertalen getest tegen een live gopher-listener. Zes client-libraries openden de socket, en stuk voor stuk zijn ze een dunne wrapper rond libcurl: de curl-commandline, PHP's ext-curl, Python's pycurl, Guzzle's curl-handler, de Rust curl crate, en Faraday op de typhoeus-adapter. Elke native HTTP-stack die we probeerden weigerde gopher voordat er ooit een socket werd geopend: Go net/http, Java HttpURLConnection en HttpClient, Python requests, httpx, aiohttp en urllib3, Node http en fetch, Ruby Net::HTTP, .NET HttpClient, Rust reqwest, en wget. De wet in één zin: gopher-via-SSRF is een libcurl-verhaal.

De directe samenvatting Als de kwetsbare server URL's ophaalt met libcurl of een binding daaromheen, behandel een gopher-SSRF dan als een raw socket naar elke interne service en houd rekening met remote code execution via Redis of FastCGI. Als er wordt opgehaald met een native HTTP-stack, is gopher simpelweg van de baan, omdat het scheme al op URL- of adapterniveau wordt geweigerd voordat er een verbinding wordt geprobeerd. Ga niet af op de programmeertaal. Een PHP-app op ext-curl is kwetsbaar en een PHP-app op stream-wrappers niet; een Ruby-app op Net::HTTP is veilig en dezelfde app op een typhoeus-adapter niet.

Waarom gopher het scheme is dat ertoe doet voor SSRF

Server-side request forgery wordt meestal omschreven als het manipuleren van een applicatie om een door de aanvaller gekozen URL op te halen. Op zichzelf bereikt dat interne HTTP-services, wat ernstig is maar begrensd. gopher neemt die begrenzing weg. libcurl behandelt een gopher-URL als gopher://host:port/<type><selector>, waarbij het eerste pad-teken het gopher-itemtype is en alles daarna letterlijk als selector wordt verzonden, na URL-decodering, gevolgd door CRLF. Omdat de bytes eerst worden URL-gedecodeerd, wordt een percent-encoded newline in het pad een echte newline op het netwerk. Dat is de complete gadget. Het verandert "haal een URL op" in "schrijf elke willekeurige bytereeks naar elke TCP-poort die de server kan bereiken", en daarom is gopher plus een interne Redis of php-fpm de canonieke route van een blind SSRF naar code execution. Het is een van de scherpere randen van een extern aanvalsoppervlak, en het schuilt volledig in de keuze van de HTTP-client.

De test: een echte socket, geen aanname

De meeste bronnen beweren dat "veel libraries gopher ondersteunen" en gaan direct door naar payloads. We wilden dat het slagen of falen werd waargenomen in plaats van aangenomen, dus de methode is bewust eenvoudig. Voor elke client starten we een one-shot TCP-listener op localhost, vragen we de client om gopher://127.0.0.1:PORT/_<payload> op te halen, en registreren we of de listener ooit een verbinding accepteert en welke bytes er binnenkomen. Een client die het scheme weigert maakt nooit verbinding, dus rapporteert de listener niets. Een client die de socket opent, levert ons exact de bytes op de wire. Dit is een actief experiment tegen onze eigen wegwerpcontainers en een localhost-listener, zonder enig hostsysteem van derden te raken. Hier is de kern daarvan tegen curl, pycurl en PHP ext-curl, over drie verschillende libcurl-versies:

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)

Twee zaken vallen direct af te lezen uit die output. Ten eerste openen de libcurl-clients de socket op elke versie die we hebben geprobeerd, van een oude 7.81.0 tot een huidige 8.21.0, dus dit is geen eigenaardigheid van één specifieke build. Ten tweede is de CRLF-gadget reëel en exact. De pycurl- en PHP-rijen werden opgehaald met de URL-selector _PYCURL%0d%0aLINE2 en _PHPCURL%0d%0aLINE2, en de binnengekomen bytes bevatten een daadwerkelijke carriage-return line-feed tussen de twee tokens. We hebben dit ook geïsoleerd bevestigd: de URL gopher://host:port/_TESTPAYLOAD%0d%0aSECONDLINE komt op de socket aan als b'TESTPAYLOAD\r\nSECONDLINE\r\n'. Dat is een protocolbericht van twee regels dat rechtstreeks in een TCP-service wordt geschreven door niets meer dan een URL.

De matrix

Rijen gemarkeerd met live zijn voor dit artikel opnieuw getest tegen een socket. De rest is geverifieerd op basis van broncode, protocolregisters en de gedocumenteerde scheme-afhandeling van elke client. Het patroon wijkt niet af: de kolom "opent een socket" is exact de set libcurl-bindings, plus één modulaire uitzondering in Perl.

Opent de HTTP-client een gopher://-socket? Getest 2026-08
Client / libraryTaalgopher:// socketWaarom
curl CLI / libcurl (live)CJAgopher, gophers, dict, ftp, file allemaal meegecompileerd; standaard transfer-allowlist schakelt ze in
wget / wget2 (live)Cneealleen http/https/ftp ingebouwd; wget2 is https-only
PHP ext-curl (curl_exec) (live)PHPJAdunne libcurl-binding; de klassieke SSRF-naar-Redis/FastCGI-sink
PHP streams (file_get_contents)PHPneealleen geregistreerde wrappers; geen gopher/dict-wrapper (file en ftp wel)
Guzzle (curl-handler, standaard)PHPJAdelegeert naar ext-curl; stream-handler doet dat niet
pycurl (live)PythonJAdirecte libcurl-binding; hetzelfde risico als PHP curl
requests (live)PythonneeInvalidSchema: no connection adapters
httpx / aiohttp / urllib3PythonneeUnsupportedProtocol / NonHttpUrlClientError / URLSchemeUnknown
urllib (stdlib) (live)Pythonneegeen gopher-opener in Python 3 (file en ftp wel)
net/httpGoneeunsupported protocol scheme "gopher"; alleen http/https round-trippers
HttpURLConnection / HttpClientJavaneehandlers zijn alleen http/https/ftp/file/jar/mailto; unknown protocol: gopher
Apache HttpClient 4/5 / OkHttpJavaneescheme-register is opzettelijk alleen http/https
Net::HTTPRubyneespecifiek voor http/https; OpenURI stuurt gopher naar File.open (lokaal lezen, geen TCP-pivot)
FaradayRubynee*op Net::HTTP; de typhoeus (libcurl)-adapter is de uitzondering die gopher bereikt
http / https / undici / fetch (live)Node.jsneeprotocolspecifiek; WHATWG fetch is alleen http/https/file/data/blob
HttpClient / HttpWebRequest.NETneealleen http/https; de verouderde GopherWebRequest is verwijderd in .NET Core
reqwest / ureqRustneehyper en pure-Rust-stacks, alleen http/https
curl crateRustJAdirecte libcurl-binding, hetzelfde voorbehoud als elke andere binding
LWP::UserAgentPerlJA*alleen als LWP::Protocol::gopher is geïnstalleerd; LWP is modulair per scheme

Tel de JA-rijen en het verhaal is overduidelijk. Zes client-libraries openen een gopher-socket, en alle zes zijn ze libcurl: curl, PHP ext-curl, pycurl, Guzzle-curl, de Rust curl-crate en Faraday-op-typhoeus. De zevende en enige niet-curl positieve uitkomst is Perl's LWP::UserAgent, en zelfs die vereist dat een beheerder de optionele LWP::Protocol::gopher-plugin heeft geïnstalleerd, dus staat standaard uit. Al het andere, elke gangbare native HTTP-stack in Go, Java, Python, Node, Ruby, .NET en Rust, weigert het scheme voordat er een verbinding wordt geopend. Er is geen slimme payload die dat verandert, omdat de weigering plaatsvindt bij de URL-parser of het adapter-register, ruim vóór enige socket.

Nuance één: redirect naar gopher staat standaard uit

"De app gebruikt curl, dus het is exploiteerbaar" is op een moderne build slechts de halve waarheid, en het ontbrekende deel is waar mensen tijdens een assessment tijd verliezen. Er zijn twee afzonderlijke allowlists in libcurl. De transfer-allowlist regelt een URL die direct wordt meegegeven, en staat gopher nog steeds toe. De redirect-allowlist bepaalt naar welke schemes een Location:-header mag overschakelen, en sinds libcurl 7.65.2 in mei 2019 is deze ingeperkt tot uitsluitend http, https, ftp en ftps. Een open-redirect die wijst naar gopher:// wordt standaard dus geweigerd. We zagen dit gebeuren op 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'

De redirect bereikt gopher alleen als de applicatie zelf de redirect-protocollen opnieuw heeft verruimd, door CURLOPT_REDIR_PROTOCOLS_STR in te stellen met gopher of door de CLI --proto-redir all mee te geven. Die misconfiguratie is niet hypothetisch. CVE-2026-33752 is exact deze categorie kwetsbaarheid: de curl_cffi-library volgde redirects naar willekeurige protocollen omdat het de redirect-allowlist niet beperkte, waardoor een eenvoudige http-SSRF weer veranderde in een volledige gopher-pivot. De praktische les voor een aanvaller is dat je bij een actueel libcurl-doelwit het volledige scheme moet kunnen plaatsen, een directe gopher://-URL, in plaats van te vertrouwen op een open-redirect. De praktische les voor een verdediger is dat het verruimen van redirect-protocollen een geladen wapen is, en de standaardinstelling die dit ontwapent is al sinds 2019 correct.

Nuance twee: de payload is CRLF, en het is triviaal

De reden dat gopher belangrijker is dan dict:// of file:// is de controle op byteniveau. libcurl verzendt het selector-gedeelte van de URL als raw bytes na URL-decodering, waardoor de aanvaller het netwerkverkeer samenstelt in het URL-pad. Een percent-encoded %0d%0a wordt een echte carriage-return line-feed, en elk regelgeoriënteerd protocol verwerkt het resultaat als afzonderlijke commando's:

Niets hiervan vereist een geheugenfout of een race condition. Het is een URL die decodeert naar een protocolconversatie, en daarom maakt een client met gopher-ondersteuning van een bescheiden SSRF een ernstige kwetsbaarheid. Ditzelfde principe, een agent of service die een URL ophaalt die jij beïnvloedt, maakt machine-clients die verbinding maken met willekeurige endpoints het controleren waard op dezelfde klasse van pivots.

Wat hiertegen te doen

De verdediging is niet "stop met het gebruik van curl." libcurl functioneert prima wanneer het protocolbereik expliciet is vastgezet. De fout zit in het vertrouwen op standaardinstellingen en het toestaan dat gebruikersinvoer het scheme bepaalt.

Methode en beperkingen, zodat de cijfers betrouwbaar zijn

De dataset is van onszelf, opgebouwd door sockets te observeren, en is reproduceerbaar.

Er is op geen enkel moment contact opgenomen met hosts van derden. Elke verbinding in dit onderzoek was gericht op een listener die we zelf op loopback hebben gestart, wat precies het verschil is tussen het meten van gedrag en het scannen van andermans systemen.

Veelgestelde vragen

Ondersteunt Python's requests-library het gopher-protocol?

Nee. In een live test genereert requests een InvalidSchema: No connection adapters were found voor een gopher-URL en opent het nooit een socket. Hetzelfde geldt voor httpx, aiohttp, urllib3, en de standaardlibrary urllib. De enige Python-uitzondering is pycurl, een dunne binding over libcurl, die wel een gopher-socket opent. Als je Python-code requests of httpx gebruikt, kan een gopher-SSRF-payload via die weg geen interne service bereiken.

Waarom ondersteunt curl gopher wel, maar wget niet?

Verschillende protocolsets. libcurl compileert standaard gopher, gophers, dict, ftp en file mee, en de transfer-allowlist schakelt deze in. wget is uitsluitend gebouwd voor http, https en ftp, en wget2 is https-only. Dit is een verschil in compileeropties en ontwerp, geen runtime-schakelaar, en daarom is curl het klassieke SSRF-pivotpunt en wget niet.

Welke protocollen ondersteunt libcurl voor SSRF?

Op een standaardbuild kan libcurl naast http en https ook gopher, gophers, dict, ftp, ftps, file, tftp, ldap en de mailprotocollen openen. Voor SSRF zijn gopher en dict het belangrijkst, omdat beide de aanvaller willekeurige bytes op een raw socket laten plaatsen. gopher is het krachtigst, omdat de selector na het itemtype-teken letterlijk wordt verzonden, waardoor een gecodeerde CRLF een echte regelafbreking wordt.

Hoe escaleert het gopher-protocol een SSRF naar RCE?

Een gewone SSRF zorgt ervoor dat de server een http-URL ophaalt. gopher zet dat om in het verzenden van willekeurige bytes naar elke bereikbare TCP-poort, doordat libcurl de selector URL-decodeert en raw verstuurt. Een enkele URL kan een Redis SET plus CONFIG SET dir en SAVE verzenden om een webshell te plaatsen, of een FastCGI-record naar php-fpm sturen. De SSRF is het transport, gopher is de payload, en een regelgeoriënteerde interne service is het doelwit.

Ondersteunt Java's HttpURLConnection gopher?

Nee. java.net.URL en HttpURLConnection registreren uitsluitend handlers voor http, https, ftp, file, jar en mailto, en een gopher-URL levert een unknown protocol: gopher op. Het modernere HttpClient, Apache HttpClient en OkHttp weigeren elk niet-http-scheme opzettelijk. Java is in de praktijk immuun voor gopher-SSRF, al ondersteunt java.net.URL nog steeds file://, wat een afzonderlijk risico vormt.

Waarom volgt libcurl geen redirect naar een gopher://-URL?

Sinds libcurl 7.65.2 in mei 2019 is de standaard redirect-allowlist beperkt tot http, https, ftp en ftps, waardoor een 302 naar gopher:// wordt geweigerd en er geen socket opent. Directe invoer van een volledige gopher-URL werkt nog steeds, omdat de transfer-allowlist afzonderlijk functioneert. Een applicatie kan het redirect-pad heropenen door CURLOPT_REDIR_PROTOCOLS_STR te verruimen, wat precies de kwetsbaarheid was achter CVE-2026-33752 in curl_cffi. Geef op een modern doelwit het volledige gopher-scheme op in plaats van te vertrouwen op een open-redirect.

Hoe stuur je CRLF naar Redis via een gopher-SSRF-payload?

Codeer de newlines. libcurl URL-decodeert de selector, dus gopher://127.0.0.1:6379/_SET%20k%20v%0d%0aSAVE%0d%0aQUIT verzendt SET k v, een echte CRLF, SAVE, een echte CRLF, en daarna QUIT. In onze test kwam _TESTPAYLOAD%0d%0aSECONDLINE binnen als de bytes TESTPAYLOAD\r\nSECONDLINE\r\n. Redis gebruikt een door regeleinden gescheiden protocol, waardoor dit een geldige commandoreeks vormt; daarom is gopher plus Redis de klassieke SSRF-naar-RCE-keten.

Aanbevolen artikelen

Zou een gopher-pivot werken tegen jouw omgeving?

Onze check van $100 brengt jouw daadwerkelijke externe aanvalsoppervlak in kaart zoals een aanvaller dat doet, binnen een scope waarvan schriftelijk is vastgesteld dat jij de eigenaar bent en waarvoor toestemming is verleend, met toelichting door een senior specialist. Of een interne Redis of php-fpm één SSRF verwijderd is van code execution is precies het soort keten waarnaar we zoeken.

Boek een check van $100