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.
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.”
What the link-local address actually gives you
On a classic instance metadata service (IMDSv1), the path that matters is the role credentials tree. A plain GET, no auth, no token:
GET /latest/meta-data/iam/security-credentials/ HTTP/1.1
Host: 169.254.169.254That returns the role name attached to the instance. Append it:
GET /latest/meta-data/iam/security-credentials/app-server-role HTTP/1.1
Host: 169.254.169.254and 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.
PUT /latest/api/token HTTP/1.1
Host: 169.254.169.254
X-aws-ec2-metadata-token-ttl-seconds: 21600The 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 IMDSv2PUTfrom 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:
- Pull the credentials through the SSRF and confirm they parse as STS creds (key id, secret, token, expiry).
- 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.
- 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.