Exploiting Cloud Metadata via SSRF: 169.254.169.254 on AWS, GCP, and Azure
How SSRF turns into stolen cloud credentials through the 169.254.169.254 metadata endpoint. IMDSv1 vs IMDSv2, the GCP header requirement, Azure quirks, and why IMDSv2 still gets popped.
The reason SSRF gets a critical rating in cloud environments and a medium everywhere else comes down to one address: 169.254.169.254. Reach it from a vulnerable server and you usually walk away with temporary IAM credentials, which is a far worse day for the defender than reading an internal admin page. This is the payoff stage of the SSRF series, so I'll assume you already have a confirmed fetch primitive and focus on what to do with it.
Why this address, and why link-local
169.254.0.0/16 is the link-local range, defined in RFC 3927. It's not routable off the local segment, which is exactly why cloud providers picked it for the instance metadata service. Every VM can reach its own metadata at 169.254.169.254 without any of that traffic ever touching a real network. Convenient for the platform. Also a perfect target, because it's a well-known fixed address that responds with no authentication beyond, in some cases, a header.
The metadata service hands out instance configuration, user-data scripts (which teams love to stuff secrets into), and most importantly the credentials of whatever IAM role is attached to the instance. Those credentials are what your code uses to talk to S3, DynamoDB, and the rest. Steal them and you're acting as the instance.
AWS: IMDSv1 is a free win
If the instance allows IMDSv1, this is a single unauthenticated GET. Walk the tree:
http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/meta-data/iam/security-credentials/
The second path lists the role name. Append it:
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>
You get JSON back:
{
"Code": "Success",
"AccessKeyId": "ASIA...",
"SecretAccessKey": "...",
"Token": "...",
"Expiration": "2026-06-08T18:00:00Z"
}
Export those into your environment and you're authenticated as the role:
export AWS_ACCESS_KEY_ID=ASIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...
aws sts get-caller-identity
get-caller-identity is the first thing to run, always. It confirms the creds work and tells you the role ARN, which tells you what to enumerate next. Don't start firing s3 ls blindly, you'll generate noise and possibly trip GuardDuty, which specifically flags credentials used from an IP that isn't the instance's. That detection is real and it's the main reason smash-and-grab credential theft gets caught.
Other AWS paths worth grabbing while you're there:
http://169.254.169.254/latest/user-data # often has secrets, bootstrap scripts
http://169.254.169.254/latest/dynamic/instance-identity/document # account ID, region
http://169.254.169.254/latest/meta-data/iam/info
AWS: IMDSv2 and why it's not the wall people claim
IMDSv2 made the service session-oriented. You first PUT to get a token, then send that token as a header on every read:
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/
The defense rests on two things a basic SSRF can't do: send a PUT, and set a custom request header. Plus IMDSv2 sets a default response hop limit of 1, so a token can't be relayed through a proxy hop.
Here's the part the "just enable IMDSv2" advice skips. IMDSv2 only helps if your SSRF is limited to simple GETs with no header control. The moment the vulnerable feature lets you choose the method or inject headers, and a surprising number do, IMDSv2 falls. A full-featured HTTP proxy SSRF, a gopher:// primitive that lets you write the raw request including a PUT line and arbitrary headers, or a fetcher that forwards user-controlled headers, all defeat it. I've popped IMDSv2 instances through a gopher payload that wrote the entire two-step token dance into the socket. The hop limit of 1 matters more in practice than the session requirement, because it blocks the common "container app talks to host metadata through an extra network hop" case.
So IMDSv2 raises the bar from "any SSRF" to "SSRF with method or header control." That's a real improvement. It is not immunity, and pen test reports that mark the finding resolved purely because IMDSv2 is on are wrong.
GCP: don't forget the header
GCP's metadata server lives at the same IP and at metadata.google.internal, but it refuses any request without a specific header:
curl -H "Metadata-Flavor: Google" \
http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token
curl -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token?alt=json"
That header requirement is GCP's built-in SSRF mitigation, and it predates IMDSv2 by years. It means a plain GET-only SSRF can't read GCP metadata at all. You need header injection or a fetcher that sets it. The token you get back is an OAuth2 bearer token, not long-lived keys, and you use it directly against googleapis.com. Also grab:
http://metadata.google.internal/computeMetadata/v1/project/project-id
http://metadata.google.internal/computeMetadata/v1/instance/attributes/ # startup scripts, ssh-keys
The old ?recursive=true&alt=json trick dumps a lot at once. Worth trying.
Azure: header plus api-version
Azure's Instance Metadata Service also wants a header (Metadata: true) and additionally requires an api-version query parameter, which trips people up because leaving it off returns an error that looks like the endpoint is dead:
curl -H "Metadata: true" \
"http://169.254.169.254/metadata/instance?api-version=2021-02-01"
curl -H "Metadata: true" \
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
The identity token path is the prize, it returns a bearer token for the managed identity attached to the VM. Same header-injection requirement as GCP, so GET-only SSRF doesn't reach it.
Getting the headers in when the fetcher won't set them
The recurring theme: GCP and Azure need headers, and IMDSv2 needs a header plus a non-GET method. When the vulnerable feature only does GET with fixed headers, your options are:
A gopher:// payload, if you can change the scheme. Gopher lets you write the raw TCP bytes, so you compose the entire HTTP request by hand, request line, headers, blank line, and the metadata service answers it. This is the most reliable way to satisfy the header requirements, and it's why protocol smuggling matters so much in cloud SSRF. I covered the gopher mechanics in the filter bypass post.
Header injection via CRLF in the URL, if the client is sloppy enough to let %0d%0a through into the request. Increasingly rare against modern clients, still worth a shot.
An open redirect or request-smuggling chain through a host that will add or preserve your header. Situational.
For encoding the 169.254.169.254 address into forms that slip past filters before you ever get to the header problem, the generator does the decimal, hex, octal, and IPv6-mapped conversions, the same 2852039166 and [::ffff:169.254.169.254] variants from the cheat sheet.
What defenders should actually do
Enabling IMDSv2 and setting the hop limit to 1 is the baseline, not the finish line. The controls that hold up:
Set the metadata options to http-tokens: required so IMDSv1 is fully disabled, not merely available, and confirm with aws ec2 describe-instance-metadata-options rather than trusting the launch template. Scope IAM roles to least privilege, because the realistic question is not "can SSRF reach metadata" but "how bad are the creds if it does", and an instance role with s3:GetObject on one bucket is a contained incident while *:* is a breach. Block egress to 169.254.169.254 at the network layer for workloads that genuinely don't need it. And alert on credential use from outside the instance, the GuardDuty InstanceCredentialExfiltration finding exists precisely for this and it's one of the few detections that reliably catches post-SSRF credential abuse.
That wraps the core series. The throughline across all three posts is the same: SSRF is rarely the whole exploit, it's the pivot, and on cloud infrastructure the pivot lands on 169.254.169.254 more often than anywhere else.