Lion Cipher Lab
Steal cloud metadata credentials via SSRF — and defeat it with IMDSv2
- Cloud Security
- Intermediate
- ~60 min
- Free · no account required
Why it matters
The single most valuable thing an attacker can reach through a Server-Side Request Forgery on a cloud server is not a database or an admin page — it is the Instance Metadata Service, or IMDS: a fixed internal address (169.254.169.254) that every major cloud runs, which hands the machine's temporary credentials to any process that asks. Those credentials are what the server uses to call the cloud's own APIs — to read storage buckets, list databases, spin up machines — so stealing them is stealing the server's identity. This is not theoretical. The Capital One breach of 2019, which exposed data…
What you'll learn
- explaining the cloud metadata service (IMDS): a fixed internal address (169.254.169.254) that returns a machine's configuration and, dangerously, temporary IAM credentials to any process that asks
- defining an IAM role and temporary credentials: a short-lived AccessKeyId / SecretAccessKey / Token an instance uses to call cloud APIs, which an attacker can reuse if stolen
- running a local mock IMDS on 127.0.0.1 that imitates the AWS IMDSv1 and IMDSv2 endpoints with FAKE credentials — never touching a real cloud provider
- reproducing the IMDSv1 attack: a single unauthenticated GET through an SSRF that returns temporary credentials — the exact shape of the Capital One 2019 breach
- hardening the mock to IMDSv2: requiring a PUT to /latest/api/token and an X-aws-ec2-metadata-token header on every GET, and proving a URL-only SSRF can no longer reach it
- reasoning about the hop-limit defense (HttpPutResponseHopLimit=1) and why it stops a token response from surviving a proxy or container hop
- layering the real fixes: enforce IMDSv2 (HttpTokens=required), set the hop limit to 1, block egress to 169.254.169.254, and least-privilege the instance role so a stolen credential is worthless
- mapping the same idea to GCP (Metadata-Flavor: Google header at metadata.google.internal) and Azure IMDS (Metadata: true header at 169.254.169.254)
Steps in this lab
- Why 169.254.169.254 is the crown jewel — and the ground rules
- Set up: confirm Python, make a folder, install Flask and requests
- Build the mock metadata service — IMDSv1 (hands out fake credentials)
- Build the VULNERABLE fetcher (the SSRF bug, on purpose)
- The IMDSv1 attack: one GET, and the credentials fall out
- The defense: upgrade the mock to IMDSv2 (PUT-then-GET, token-bound)
- Re-run the attack: the URL-only SSRF now fails
- The hop-limit defense — and layering the real fixes
- When IMDSv2 does NOT save you — and the other clouds
- What you did — and the reflex to keep