4 min readSSRF · CLOUD · API-SECURITY

SSRF to cloud metadata: turning a fetch into IAM credentials

How a server-side request that reaches 169.254.169.254 becomes role credentials, why IMDSv2 changes the rules, the bypasses that still work, and how to prove impact read-only without touching anything destructive.

attacker vulnerable fetch SERVER-SIDE 169.254.169.254 LINK-LOCAL IAM role creds STS CREDENTIALS MITIGATION IMDSv2 TOKEN + HOP-LIMIT
SSRF that reaches the metadata endpoint and walks out with role credentials

Every cloud instance carries a small lie: a magic address, 169.254.169.254, that answers as if it were a normal HTTP server but hands out the host’s identity. If I can make a backend issue a request I control the destination of, and that backend runs in a cloud account, the metadata service is the first place I point it. Not because it is exotic, but because the payoff jumps from “the server fetched a URL” to “I am now the server’s IAM role.”

On a classic instance metadata service (IMDSv1), the path that matters is the role credentials tree. A plain GET, no auth, no token:

http
GET /latest/meta-data/iam/security-credentials/ HTTP/1.1
Host: 169.254.169.254

That returns the role name attached to the instance. Append it:

http
GET /latest/meta-data/iam/security-credentials/app-server-role HTTP/1.1
Host: 169.254.169.254

and you get back JSON with an AccessKeyId, a SecretAccessKey, and a Token. Those are temporary STS credentials scoped to whatever the instance role can do, which on a lot of backends is far more than anyone intended.

Why IMDSv2 was supposed to end this

IMDSv2 added two things that break naive SSRF. First, a session token: you PUT to /latest/api/token with a TTL header, get a token back, then send it on every subsequent request. A GET-only SSRF, the most common kind, can’t do the PUT, so it can’t even start. Second, and this is the part people miss, a hop limit. The token response carries a default IP TTL of 1, so a packet that crosses a single proxy or container boundary dies before it returns.

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

The reason IMDSv2 isn’t a clean win for defenders: it has to be enforced, not just available. Plenty of instances run with IMDSv2 set to optional, which means v1 still answers. And plenty of SSRF primitives can issue a PUT, not just a GET.

The bypasses that survive

The metadata IP is a flat filter target, so the first thing a defender does is blocklist 169.254.169.254 and metadata hostnames. None of that holds up.

  • Alternate encodings of the IP. The link-local address has decimal, octal, and IPv6-mapped forms. 2852039166, 0251.0376.0251.0376, and [::ffff:169.254.169.254] all resolve to the same place but slip a naive string blocklist.
  • DNS rebinding. Point a hostname you control at a public IP for the validation fetch, then re-answer the second lookup with the link-local address. The allowlist check and the actual request resolve the name twice and get different answers. This is the cleanest bypass against resolve-then-fetch validators.
  • Redirect chains. If the fetcher follows redirects, give it a public URL that 302s to the metadata endpoint. The validator sees the public host; the HTTP client follows the hop to the link-local address.
  • Gopher and non-HTTP schemes. Where the SSRF primitive accepts a scheme you control, gopher:// lets you write raw bytes to a socket, which is how you craft the IMDSv2 PUT from a primitive that looked GET-only.

Proving impact without doing harm

This is where a lot of reports overreach. You do not need to touch anything destructive to prove a stolen-credential SSRF, and you shouldn’t. The clean, defensible proof is read-only:

  1. Pull the credentials through the SSRF and confirm they parse as STS creds (key id, secret, token, expiry).
  2. Make exactly one identity call with them, the cloud equivalent of “who am I.” It returns the account ID and the role ARN and changes nothing.
  3. Optionally, one read-only enumeration call (list, not get; describe, not modify) to show the role’s reach.

That trio (creds out, identity confirmed, scope shown) is a complete, non-destructive impact story. The account ID and role ARN in the response are what turn a triager’s “so what” into a severity bump. I never read real data to “prove” it; the identity call already shows the credentials are live and mine.

The transferable lesson: SSRF severity is a function of where the request lands, and the metadata endpoint is the highest-value landing zone in any cloud account. Steer a fetch at the link-local address, defeat whatever IMDS posture is in place, and stop the moment the identity call confirms the creds.