Finding SSRF That Actually Matters
Server-Side Request Forgery is one of the most over-reported and under-exploited bug classes in bounty. A researcher fires a Burst Collaborator payload, sees a DNS hit, and files "Blind SSRF." Triage closes it as informational. Both sides are right and both sides lose.
The difference between a $150 informational and a critical is what the request can reach and what it can do when it gets there. This is the methodology we use to close that gap.
Map every place the server fetches a URL
Before touching payloads, inventory the sinks. SSRF lives anywhere the application takes something you control and turns it into an outbound request:
- Obvious: webhook URLs, "import from URL," avatar-by-URL, PDF/screenshot generators, link previews, RSS/feed readers, SSO metadata (SAML/OIDC discovery), address/geo lookups.
- Non-obvious: file parsers that resolve remote entities (SVG, XML, Office docs), image libraries that follow redirects, payment-provider callbacks the server re-fetches, "health check this endpoint" admin features, and any field labelled callback, return_url, next, dest, or webhook.
Instrument each with an out-of-band domain you control that logs both DNS and HTTP, with the request path echoing which sink fired it (https://oob.example/sink-avatar-<id>). You want to know not just that it called out, but how — did it follow redirects, send a body, present credentials, honor a custom port?
Fingerprint the fetcher
A blind hit tells you the door opened. Now learn the shape of the room:
- Does it follow redirects? Point the URL at your server, respond
302tohttp://169.254.169.254/. If the second hop lands, you have redirect-based filter bypass for free. - Which schemes? Try
http,https,gopher://,dict://,file://,ftp://. Agopherthat survives lets you speak raw TCP to internal services (Redis, unauthenticated HTTP APIs, SMTP). - Does it leak the response? "Full" SSRF returns the fetched body to you (an import that shows a preview). "Blind" doesn't. For blind, you escalate through timing and out-of-band channels.
Escalation targets, in order of payout
Once you can steer the request, go where the impact is:
- Cloud metadata.
169.254.169.254(AWS/GCP/Azure),100.100.100.200(Alibaba). On IMDSv1 this is instant credentials. On IMDSv2 you need thePUTtoken dance — which is exactly whygopher/full-control SSRF matters. Grab the role, then the temporary keys. - Internal services with no auth. Kubernetes API on
:6443/:10250(kubelet), Consul:8500, etcd:2379, internal admin panels, Elasticsearch:9200, Redis:6379via gopher. Enumerate127.0.0.1and the RFC1918 ranges the app can see. - The link-local and DNS tricks when a naive filter blocks
127.0.0.1: decimal (2130706433), IPv6 ([::1],[::ffff:127.0.0.1]), the0.0.0.0bind,localtest.me-style resolvers, and DNS rebinding when the app resolves twice (validate-then-fetch).
Defeating the filter, not the developer
Most SSRF filters are a blocklist of 127.0.0.1/localhost plus a regex. They fall to:
- Redirects (validated the first URL, fetched the second).
- DNS rebinding — your domain answers a public IP during validation, then
169.254.169.254during the fetch. Set the TTL to zero and win the race. - Encoding — mixed case, trailing dots, unicode,
@-based userinfo confusion (http://expected.com@169.254.169.254/), and parser differentials where the validator and the HTTP client disagree on what the host is.
Parser differentials are where the money is. The validator sees expected.com; libcurl sees 169.254.169.254. Test every combination of @, #, ?, backslashes, and whitespace against the specific HTTP client the stack uses.
Report the impact, not the callback
A report that survives triage doesn't say "the server made a DNS request." It says: here is the metadata endpoint reached, here is the IAM role, here are the scoped credentials, here is one benign call proving access. Show the chain, stop at proof, never touch data you weren't invited to.
That is the whole game — SSRF is never the finding. Where SSRF goes is the finding.