Alle Research-Artikel

Welche HTTP-Bibliotheken rufen tatsächlich eine gopher://-URL ab? Eine empirisch getestete SSRF-Matrix

SSRF-gopher-Matrix: 6 libcurl-basierte Clients öffnen einen gopher-Socket, 0 native HTTP-Stacks erreichen gopher, Redirects zu gopher standardmäßig seit libcurl 7.65.2 im Jahr 2019 deaktiviert.

Nur HTTP-Clients, die auf libcurl basieren, öffnen tatsächlich einen gopher://-Socket, und genau dieser Umstand entscheidet darüber, ob eine Server-Side Request Forgery von "der Server ruft eine URL ab" zu "beliebige Bytes an jeden internen TCP-Dienst senden" eskaliert werden kann. Wir haben die Standard-HTTP-Clients von zehn Programmiersprachen gegen einen aktiven gopher-Listener getestet. Sechs Client-Bibliotheken öffneten den Socket, und jede einzelne davon ist ein schlanker Wrapper um libcurl: die curl-Kommandozeile, PHPs ext-curl, Pythons pycurl, Guzzles curl-Handler, das Rust-Crate curl und Faraday mit dem typhoeus-Adapter. Jeder native HTTP-Stack, den wir ausprobiert haben, verweigerte gopher, noch bevor ein Socket geöffnet wurde: Go net/http, Java HttpURLConnection und HttpClient, Python requests, httpx, aiohttp und urllib3, Node http und fetch, Ruby Net::HTTP, .NET HttpClient, Rust reqwest sowie wget. Die Faustregel in einem Satz: gopher-over-SSRF ist reine libcurl-Sache.

Die Kurzfassung Wenn der verwundbare Server URLs mit libcurl oder einem darauf aufbauenden Binding abruft, behandeln Sie eine gopher-SSRF wie einen Raw-Socket zu jedem internen Dienst und planen Sie eine Remote Code Execution über Redis oder FastCGI ein. Wenn er URLs mit einem nativen HTTP-Stack abruft, ist gopher schlicht kein Thema, da das Schema auf URL- oder Adapter-Ebene abgewiesen wird, bevor überhaupt ein Verbindungsaufbau versucht wird. Verlassen Sie sich nicht auf die Sprache. Eine PHP-App mit ext-curl ist verwundbar, eine PHP-App mit Stream-Wrappern nicht. Eine Ruby-App mit Net::HTTP ist sicher, dieselbe App mit einem typhoeus-Adapter nicht.

Warum gopher das entscheidende Schema für SSRF ist

Server-Side Request Forgery wird üblicherweise so beschrieben, dass eine Anwendung dazu verleitet wird, eine vom Angreifer gewählte URL abzurufen. Für sich genommen erreicht das interne HTTP-Dienste, was problematisch, aber begrenzt ist. gopher hebt diese Begrenzung auf. libcurl behandelt eine gopher-URL als gopher://host:port/<type><selector>, wobei das erste Pfadzeichen der gopher-Item-Typ ist und alles dahinter nach dem URL-Decoding unverändert als Selector gesendet wird, gefolgt von CRLF. Da die Bytes zuerst URL-dekodiert werden, wird ein prozentkodierter Zeilenumbruch im Pfad zu einem echten Zeilenumbruch auf der Leitung. Das ist das gesamte Gadget. Es verwandelt "eine URL abrufen" in "eine beliebige Bytefolge an jeden TCP-Port senden, den der Server erreichen kann". Genau deshalb ist gopher in Kombination mit einem internen Redis oder php-fpm der klassische Pfad von einer Blind-SSRF zur Codeausführung. Es ist eine der schärfsten Kanten einer externen Angriffsfläche, und sie verbirgt sich vollständig in der Wahl des HTTP-Clients.

Der Test: ein echter Socket, keine bloße Behauptung

Die meisten Quellen behaupten pauschal, dass "viele Bibliotheken gopher unterstützen", und gehen direkt zu Payloads über. Wir wollten das Bestehen oder Scheitern beobachten und nicht nur annehmen. Die Methode ist daher bewusst simpel gehalten. Für jeden Client starten wir einen One-Shot-TCP-Listener auf localhost, weisen den Client an, gopher://127.0.0.1:PORT/_<payload> abzurufen, und protokollieren, ob der Listener jemals eine Verbindung akzeptiert und welche Bytes ankommen. Ein Client, der das Schema verweigert, baut nie eine Verbindung auf, sodass der Listener nichts meldet. Ein Client, der den Socket öffnet, liefert uns exakt die übertragenen Bytes. Dies ist ein aktives Experiment in unseren eigenen Wegwerf-Containern und einem Localhost-Listener, ohne externe Hosts zu berühren. Hier ist der Kern des Tests gegen curl, pycurl und PHP ext-curl, über drei verschiedene libcurl-Versionen hinweg:

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)

Aus dieser Ausgabe lassen sich zwei Dinge direkt ablesen. Erstens öffnen die libcurl-Clients den Socket in jeder getesteten Version, von einem älteren 7.81.0 bis zum aktuellen 8.21.0. Es handelt sich also nicht um eine Eigenheit eines einzelnen Builds. Zweitens ist das CRLF-Gadget real und präzise. Die pycurl- und PHP-Zeilen wurden mit dem URL-Selector _PYCURL%0d%0aLINE2 und _PHPCURL%0d%0aLINE2 abgerufen, und die empfangenen Bytes enthielten ein echtes Carriage Return Line Feed zwischen den beiden Tokens. Wir haben dies auch isoliert bestätigt: Die URL gopher://host:port/_TESTPAYLOAD%0d%0aSECONDLINE landet auf dem Socket als b'TESTPAYLOAD\r\nSECONDLINE\r\n'. Das ist eine zweizeilige Protokollnachricht, die allein über eine URL direkt in einen TCP-Dienst geschrieben wird.

Die Matrix

Zeilen mit der Kennzeichnung live wurden für diesen Beitrag erneut an einem Socket getestet. Der Rest ist anhand von Quellcode, Protokoll-Registries und dem dokumentierten Schema-Handling des jeweiligen Clients verifiziert. Das Muster ist eindeutig: Die Spalte "öffnet einen Socket" umfasst exakt die Menge der libcurl-Bindings plus einen modularen Ausreißer in Perl.

Öffnet der HTTP-Client einen gopher://-Socket? Getestet 2026-08
Client / BibliothekSprachegopher://-SocketGrund
curl CLI / libcurl (live)CJAgopher, gophers, dict, ftp, file standardmäßig einkompiliert. Standard-Transfer-Allowlist aktiviert sie
wget / wget2 (live)Cneinnur http/https/ftp integriert. wget2 unterstützt ausschließlich https
PHP ext-curl (curl_exec) (live)PHPJAschlankes libcurl-Binding. die klassische SSRF-zu-Redis/FastCGI-Senke
PHP-Streams (file_get_contents)PHPneinnur registrierte Wrapper. kein gopher/dict-Wrapper (file und ftp vorhanden)
Guzzle (curl-Handler, Standard)PHPJAdelegiert an ext-curl. Stream-Handler tut dies nicht
pycurl (live)PythonJAdirektes libcurl-Binding. dieselbe Falle wie PHP curl
requests (live)PythonneinInvalidSchema: no connection adapters
httpx / aiohttp / urllib3PythonneinUnsupportedProtocol / NonHttpUrlClientError / URLSchemeUnknown
urllib (Standardbibliothek) (live)Pythonneinkein gopher-Opener in Python 3 (file und ftp vorhanden)
net/httpGoneinunsupported protocol scheme "gopher", nur http/https-Round-Tripper
HttpURLConnection / HttpClientJavaneinHandler nur für http/https/ftp/file/jar/mailto. unknown protocol: gopher
Apache HttpClient 4/5 / OkHttpJavaneinSchema-Registry ist konstruktionsbedingt ausschließlich für http/https ausgelegt
Net::HTTPRubyneinspezifisch für http/https. OpenURI leitet gopher an File.open weiter (lokales Lesen, kein TCP-Pivot)
FaradayRubynein*auf Net::HTTP. Der typhoeus-(libcurl)-Adapter ist die Ausnahme, die gopher erreicht
http / https / undici / fetch (live)Node.jsneinprotokollspezifisch. WHATWG-fetch unterstützt nur http/https/file/data/blob
HttpClient / HttpWebRequest.NETneinnur http/https. Der veraltete GopherWebRequest wurde in .NET Core entfernt
reqwest / ureqRustneinhyper- und Pure-Rust-Stacks, nur http/https
curl-CrateRustJAdirektes libcurl-Binding, dieselbe Einschränkung wie bei jedem anderen Binding
LWP::UserAgentPerlJA*nur wenn LWP::Protocol::gopher installiert ist. LWP ist pro Schema modular aufgebaut

Zählt man die JA-Zeilen, ist das Ergebnis unübersehbar. Sechs Client-Bibliotheken öffnen einen gopher-Socket, und alle sechs basieren auf libcurl: curl, PHP ext-curl, pycurl, Guzzle-curl, das Rust-curl-Crate und Faraday-auf-typhoeus. Das siebte und einzige Nicht-curl-Positivergebnis ist Perls LWP::UserAgent, und selbst dafür muss ein Administrator das optionale Plugin LWP::Protocol::gopher installiert haben, sodass es standardmäßig deaktiviert ist. Alles andere, jeder gängige native HTTP-Stack in Go, Java, Python, Node, Ruby, .NET und Rust, weist das Schema ab, bevor eine Verbindung geöffnet wird. Es gibt kein cleveres Payload, das daran etwas ändert, da die Verweigerung im URL-Parser oder in der Adapter-Registry vor jedem Socket-Aufbau stattfindet.

Nuance eins: Redirects zu gopher sind standardmäßig deaktiviert

"Die Anwendung nutzt curl, also ist sie ausnutzbar" stimmt auf modernen Systemen nur zur Hälfte, und an der fehlenden Hälfte wird oft wertvolle Zeit im Assessment verschwendet. In libcurl gibt es zwei getrennte Allowlists. Die Transfer-Allowlist regelt URLs, die direkt übergeben werden, und sie lässt gopher weiterhin zu. Die Redirect-Allowlist regelt, zu welchen Schemas ein Location:-Header weiterleiten darf, und seit libcurl 7.65.2 im Mai 2019 ist sie auf http, https, ftp und ftps beschränkt. Ein Open-Redirect auf gopher:// wird standardmäßig abgewiesen. Wir haben das unter libcurl 7.81.0 überprüft:

# 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'

Der Redirect erreicht gopher nur dann, wenn die Anwendung die erlaubten Redirect-Protokolle selbst wieder erweitert hat, indem sie CURLOPT_REDIR_PROTOCOLS_STR so setzt, dass gopher enthalten ist, oder der CLI --proto-redir all übergibt. Diese Fehlkonfiguration ist keineswegs hypothetisch. CVE-2026-33752 ist genau diese Art von Schwachstelle: Die curl_cffi-Bibliothek folgte Redirects in beliebige Protokolle, weil sie die Redirect-Allowlist nicht einschränkte, wodurch aus einer einfachen http-SSRF wieder ein vollständiger gopher-Pivot wurde. Für Angreifer bedeutet das in der Praxis, dass bei einem aktuellen libcurl-Ziel die SSRF die Platzierung des gesamten Schemas erlauben muss, also eine direkte gopher://-URL, anstatt sich auf einen Open-Redirect zu verlassen. Für Verteidiger bedeutet das, dass das Erweitern von Redirect-Protokollen ein enormes Risiko birgt und die Standardeinstellung, die dies verhindert, seit 2019 korrekt greift.

Nuance zwei: Das Payload ist CRLF und denkbar einfach

Der Grund, warum gopher relevanter ist als dict:// oder file://, liegt in der Kontrolle auf Byte-Ebene. libcurl sendet den Selector-Teil der URL nach dem URL-Decoding als Raw-Bytes, sodass der Angreifer den Netzwerkverkehr direkt im URL-Pfad zusammenstellt. Ein prozentkodiertes %0d%0a wird zu einem echten Carriage Return Line Feed, und jedes zeilenbasierte Protokoll parst das Ergebnis als separate Befehle:

Nichts davon erfordert einen Speicherfehler oder eine Race Condition. Es handelt sich um eine URL, die sich in eine Protokollkommunikation dekodiert. Deshalb verwandelt ein gopher-fähiger Client eine einfache SSRF in ein kritisches Problem. Dasselbe Prinzip, dass ein Agent oder Dienst eine von Ihnen beeinflusste URL abruft, macht auch maschinelle Clients, die sich mit beliebigen Endpunkten verbinden, zu lohnenden Prüfzielen für dieselbe Pivot-Klasse.

Minderungsmaßnahmen

Die Lösung lautet nicht "curl verbieten". libcurl ist unbedenklich, wenn sein Protokollumfang fest eingeschränkt wird. Das eigentliche Problem besteht darin, sich auf Standardwerte zu verlassen und Benutzereingaben das Schema wählen zu lassen.

Methodik und Grenzen zur Nachvollziehbarkeit der Daten

Der Datensatz stammt aus unseren eigenen Messungen an realen Sockets und ist reproduzierbar.

Zu keinem Zeitpunkt wurde ein externer Host kontaktiert. Jede Verbindung in dieser Untersuchung erfolgte zu einem Listener, den wir auf dem Loopback-Interface gestartet haben. Genau das unterscheidet eine Verhaltensmessung vom Scannen fremder Systeme.

Häufig gestellte Fragen

Unterstützt die Python-Bibliothek requests das gopher-Protokoll?

Nein. Im Live-Test wirft requests bei einer gopher-URL InvalidSchema: No connection adapters were found und öffnet zu keinem Zeitpunkt einen Socket. Dasselbe gilt für httpx, aiohttp, urllib3 sowie die Standardbibliothek urllib. Die einzige Ausnahme in Python ist pycurl, ein schlankes Binding um libcurl, das tatsächlich einen gopher-Socket öffnet. Wenn Ihr Python-Code requests oder httpx verwendet, kann ein gopher-SSRF-Payload darüber keinen internen Dienst erreichen.

Warum unterstützt curl gopher, wget jedoch nicht?

Unterschiedlicher Protokollumfang. libcurl kompiliert standardmäßig gopher, gophers, dict, ftp und file ein, und seine Transfer-Allowlist aktiviert diese Protokolle. wget wurde ausschließlich für http, https und ftp entwickelt, und wget2 unterstützt nur https. Dies ist ein Entwurfs- und Build-Unterschied, kein Schalter zur Laufzeit. Deshalb ist curl der klassische SSRF-Pivot und wget nicht.

Welche Protokolle unterstützt libcurl für SSRF?

In einem Standard-Build kann libcurl neben http und https auch gopher, gophers, dict, ftp, ftps, file, tftp, ldap sowie die Mail-Protokolle öffnen. Für SSRF sind gopher und dict besonders relevant, da beide es ermöglichen, vom Angreifer kontrollierte Bytes auf einen Raw-Socket zu schreiben. gopher ist am wirkungsvollsten, da der Selector nach dem Item-Typ-Zeichen unverändert übertragen wird und ein kodiertes CRLF somit zu einem echten Zeilenumbruch wird.

Wie eskaliert das gopher-Protokoll eine SSRF zu einer RCE?

Eine gewöhnliche SSRF veranlasst den Server, eine http-URL abzurufen. gopher verwandelt dies in das Senden beliebiger Bytes an jeden erreichbaren TCP-Port, da libcurl den Selector URL-dekodiert und im Rohformat überträgt. Eine einzelne URL kann ein Redis-SET zusammen mit CONFIG SET dir und SAVE senden, um eine Webshell abzulegen, oder einen FastCGI-Record an php-fpm übertragen. Die SSRF dient als Transportweg, gopher als Payload und ein zeilenbasierter interner Dienst als Angriffsziel.

Unterstützt Javas HttpURLConnection gopher?

Nein. java.net.URL und HttpURLConnection registrieren Handler ausschließlich für http, https, ftp, file, jar und mailto. Eine gopher-URL wirft unknown protocol: gopher. Das moderne HttpClient, Apache HttpClient und OkHttp verwerfen Nicht-http-Schemas konstruktionsbedingt. Java ist gegen gopher-SSRF praktisch immun, wenngleich java.net.URL weiterhin file:// unterstützt, was ein separates Problem darstellt.

Warum folgt libcurl keinem Redirect auf eine gopher://-URL?

Seit libcurl 7.65.2 im Mai 2019 umfasst die standardmäßige Redirect-Allowlist nur noch http, https, ftp und ftps. Ein 302-Redirect auf gopher:// wird daher abgewiesen und es wird kein Socket geöffnet. Die direkte Übergabe einer vollständigen gopher-URL funktioniert weiterhin, da die Transfer-Allowlist separat verwaltet wird. Eine Anwendung kann den Redirect-Pfad wieder öffnen, indem sie CURLOPT_REDIR_PROTOCOLS_STR erweitert, was genau die Schwachstellenklasse hinter CVE-2026-33752 in curl_cffi war. Setzen Sie auf modernen Zielen das gesamte gopher-Schema ein, anstatt sich auf einen Open-Redirect zu verlassen.

Wie sendet man CRLF über ein gopher-SSRF-Payload an Redis?

Indem man die Zeilenumbrüche enkodiert. libcurl URL-dekodiert den Selector, sodass gopher://127.0.0.1:6379/_SET%20k%20v%0d%0aSAVE%0d%0aQUIT nacheinander SET k v, ein echtes CRLF, SAVE, ein weiteres CRLF und schließlich QUIT sendet. In unserem Test kam _TESTPAYLOAD%0d%0aSECONDLINE auf dem Socket als Bytefolge TESTPAYLOAD\r\nSECONDLINE\r\n an. Da Redis ein zeilenbasiertes Protokoll verwendet, stellt dies eine valide Befehlsfolge dar. Aus diesem Grund ist gopher in Kombination mit Redis die Paradekette für SSRF-zu-RCE.

Weiterführende Artikel

Wäre ein gopher-Pivot bei Ihnen erfolgreich?

Unser $100-Check bildet Ihre reale externe Angriffsfläche aus Angreiferperspektive ab, bezogen auf einen schriftlich autorisierten und verifizierten Scope, inklusive Nachbesprechung durch einen Senior Operator. Ob ein interner Redis- oder php-fpm-Dienst nur eine SSRF von der Codeausführung entfernt ist, gehört genau zu den Angriffsketten, nach denen wir suchen.

$100-Check buchen