Skip to main content
All articles

SSRF to cloud takeover: what Capital One taught us, and why half of EC2 still has not learned it

One HTTP request to 169.254.169.254 turned an SSRF into 100 million records. Seven years on, IMDSv2 fixes it and adoption is roughly half. How the attack works, why v2 stops it, and how to enforce it.

7 min read0 views

Server-side request forgery gets reported as a medium. "The application can be made to fetch a URL." On a server with no interesting internal network, that is fair.

On a cloud instance, it is not, because there is a specific address on every instance that hands out credentials to anything that asks. Reaching it converts "the application fetches a URL" into "the attacker holds your application's IAM role."

The link-local address

Every major cloud provider runs a metadata service on 169.254.169.254, a link-local address reachable only from the instance itself. It exists so software can discover its own configuration and, crucially, obtain temporary credentials for its attached role without anyone shipping a static key.

That last part is genuinely good design. Static keys in configuration files are worse. But it means an HTTP client running on the instance can ask a local address for credentials, and SSRF gives an attacker exactly that client.

The AWS shape, in the original version of the protocol:

GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
→ the role name

GET http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>
→ AccessKeyId, SecretAccessKey, Token, Expiration

Two plain GET requests. No headers, no authentication, no POST body. Anything that will fetch a URL for you will fetch those.

Also worth knowing, because it is often more useful than the credentials themselves:

/latest/user-data                          # the boot script -- routinely contains secrets
/latest/dynamic/instance-identity/document # account ID, region, instance type

Other providers, same idea, different paths, GCP and Azure require a header (Metadata-Flavor: Google, Metadata: true), which incidentally makes them harder to reach through the many SSRF primitives that cannot set headers. That header requirement is not a coincidence; it is the same lesson AWS learned.

Capital One

  1. Roughly 100 million customer records. An SSRF vulnerability was used to reach the metadata service, retrieve credentials for the role attached to the instance, and use them against S3.

What makes it the canonical case is how ordinary each step was:

  • The SSRF was in a web application, of the sort found routinely.
  • The metadata service responded to an unauthenticated GET, exactly as designed.
  • The role had permissions well beyond what that instance needed.
  • The resulting API calls came from valid credentials, so nothing looked anomalous at the identity layer.

The regulatory outcome ran to tens of millions of dollars, and AWS shipped IMDSv2 in November 2019, months after the breach became public.

What IMDSv2 changes

IMDSv2 is session-oriented. You must first PUT to obtain a token, then send that token as a header on every request:

PUT /latest/api/token
  X-aws-ec2-metadata-token-ttl-seconds: 21600
→ <token>

GET /latest/meta-data/...
  X-aws-ec2-metadata-token: <token>

Three properties do the work, and it is worth being precise because "it needs a token now" undersells it:

  1. It requires a PUT. Most SSRF primitives issue GETs. An <img> tag, a URL preview fetcher, a webhook validator, a PDF renderer loading a remote asset, none of them can be made to issue a PUT.
  2. It requires setting a request header. Most SSRF primitives cannot control headers.
  3. It sets a low TTL on the response, so the request will not survive a network hop, which specifically defeats the SSRF-through-a-proxy variants and open reverse proxy misconfigurations.

Together these mean the typical SSRF primitive cannot reach IMDSv2 at all. Not "it is harder". The common shapes structurally cannot do it.

The gap: a full-featured SSRF, where the attacker controls method and headers (some proxy misconfigurations, some URL-fetching features, and any SSRF that has already become remote code execution), can still complete the handshake. IMDSv2 raises the bar from "any URL fetch" to "a fairly capable request forgery". That is a large win and not a complete one, which is why least privilege on the role remains load-bearing.

The adoption problem

IMDSv2 has existed since 2019. Reported figures put enforcement across EC2 instances at roughly half, better than the single-digit percentages of the early years, and still leaving an enormous population of instances answering unauthenticated GETs.

And it remains actively exploited: F5 Labs documented a coordinated campaign in March 2025 specifically targeting EC2-hosted sites with SSRF in order to reach IMDSv1.

The reason for slow adoption is instructive. IMDSv2 is not the default for existing instances, older SDK versions do not speak it, and enforcing it can break software that still issues v1 requests. So it stays optional, and optional security controls stay off. If you take one action from this post, make it checking whether your instances enforce it.

Testing for it

Everywhere the application takes a URL: webhooks, "import from URL", avatar-by-URL, PDF and screenshot generators, link previews, SSO metadata endpoints, XML parsers with external entities, image proxies, and anything that renders HTML server-side.

Standard bypasses, because naive filters block the literal string 169.254.169.254:

http://169.254.169.254/
http://[::ffff:169.254.169.254]/          # IPv6-mapped
http://2852039166/                        # decimal
http://0251.0376.0251.0376/               # octal
http://169.254.169.254.nip.io/            # DNS resolving to it
http://metadata.google.internal/          # GCP by name

Also test redirect-based bypasses. The application fetches a URL you control, which 302s to the metadata address. A filter that only inspects the initial URL misses this entirely, and it is one of the most common real-world bypasses.

Then test whether you have a blind SSRF: point it at a collaborator host you control and watch for DNS or HTTP. Blind SSRF still reaches IMDSv1, because you do not need to see the response to exfiltrate it, send it onward in a subsequent request, or exfiltrate via DNS.

If you retrieve credentials during an authorised test, confirm scope before using them: aws sts get-caller-identity establishes what you hold without touching data. What the role can do is the finding; enumerating it is a separate, explicitly-authorised step.

Fixing it

In order of impact:

  1. Enforce IMDSv2. HttpTokens=required on every instance, in the launch template, and as an organisation-wide SCP so a new instance cannot be launched without it. Also set HttpPutResponseHopLimit=1, with a hop limit of 1, a container on the instance cannot reach the metadata service through the host's network namespace, which closes a large secondary path.
  2. Least privilege on instance roles. Capital One's severity came from what the role could do, not from the SSRF. Ask of every role: if an attacker had these credentials for one hour, what is the worst outcome? Scope until that answer is tolerable.
  3. Block egress to the link-local range from application containers that have no reason to talk to it. 169.254.0.0/16 in the network policy is a one-line control.
  4. Fix the SSRF. Allowlist destination hosts; resolve the hostname, validate the resulting IP against a deny list of private ranges, and connect to that IP, resolving twice is a DNS-rebinding hole. Do not follow redirects, or re-validate at every hop. Never write a blocklist of string patterns; the bypass table above is a small sample of why.
  5. Prefer credential mechanisms with a narrower blast radius, IRSA on EKS, workload identity federation, or per-pod identities rather than one broad instance role shared by everything on the host.

Detection

  • Any application-originated request to 169.254.169.254. Your application does not need to call the metadata service; the SDK does, and it does so from a different code path. Requests carrying an HTTP Referer, a browser user-agent, or arriving with an unusual process ancestry are the signal.
  • PUT /latest/api/token from an unexpected process.
  • CloudTrail: instance-role credentials used from an IP that is not the instance. This is the highest-fidelity detection available for this attack and it is often already in your logs, AWS reports the source IP for calls made with role credentials, and a role credential appearing from outside your VPC is unambiguous.
  • Outbound requests from the application to private ranges generally: 10/8, 172.16/12, 192.168/16, 127/8.
  • GetCallerIdentity immediately followed by broad enumeration, ListBuckets, ListRoles, DescribeInstances, is what post-SSRF orientation looks like.

Take this away

SSRF on a cloud instance is not a medium. It is a credential disclosure vulnerability that happens to be spelled as a URL fetch.

Two settings decide the outcome: whether IMDSv2 is enforced, and what the instance role is allowed to do. Both are checkable this afternoon, on every instance you own, without waiting for anyone to fix an application bug.


Further reading

Was this useful?

Share

Tags

  • ssrf
  • cloud security
  • detection engineering
  • penetration testing

Comments

Loading comments…

Leave a comment

Comments are read and approved by hand before they appear, so yours will not show up straight away. Your email address is optional, is never published, and is only used if we need to reply to you directly.

0/5000

Related service

Cloud Penetration Testing (AWS)

Identity, storage and workload misconfiguration review across your cloud estate.

If you want to know whether what you have just read applies to your own systems, that is the engagement that answers it.

SSRF to cloud takeover: IMDS and Capital One