Instance Metadata & SSRF-to-Cloud-Takeover
After this chapter you will be able to turn a server-side request — yours or the provider's — into stolen workload credentials, and recognise when that SSRF reaches past the tenant into the cloud provider's own internal endpoints.
A company runs a firewall in front of its website. Like any firewall, it can be told to go fetch something from a web address. An attacker discovers they can choose that address — and instead of pointing it at a normal website, they point it at a special address that only works from inside the company's own cloud server. That address answers, helpfully, with a working key to the company's cloud account. No password is asked for. A few minutes later, the attacker is reading the personal data of a hundred million customers.
This actually happened. The strange thing is not that the firewall could be tricked — that is an old, well-understood bug. The strange thing is what answered on the other end, and why it handed over a key to anyone who simply asked from the right place.
The problem
Every virtual machine in the cloud needs to know things about itself: its own name, its region, the start-up script it should run. It also needs an identity — a way to prove to the cloud's APIs that it is allowed to read a storage bucket or write to a queue. The provider solves both needs with one piece of plumbing: a tiny built-in web server that the VM can query about itself. That server is the instance metadata service, and it is the subject of this chapter.
Every cloud VM can ask the platform about itself by making an ordinary HTTP request to a non-routable, link-local address that exists only from inside that VM — the magic constant 169.254.169.254, identical across AWS, Azure, GCP, Oracle and Alibaba. There is no password and no TLS: being able to send the packet is the authorization. It was built so cloud-init and the SDKs could bootstrap a VM without baking secrets into the image.
If the metadata service only returned inert facts — instance ID, region, hostname — it would be a curiosity. It returns more than that. It returns live credentials.
The metadata service does not just describe the workload — it returns short-lived credentials for the cloud identity attached to that workload, and mints a fresh one every time it is asked. The role you learned to escalate in Chapter 3 is delivered here. One server-side HTTP request to the right path equals one stolen cloud identity.
What goes wrong is now easy to state. Anything that can make a workload send an HTTP request to 169.254.169.254 — a server-side request forgery (SSRF), an XXE external entity, an open redirect followed server-side, a misconfigured proxy — can vend that workload's cloud credentials. You already know how to find an SSRF. The only new knowledge is the URL.
Here is the AWS request and its response — the exact shape the firewall SSRF from the cold open retrieved:[7]
# Step 1 — discover the role name attached to the instance
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
# -> WAF-Role
# Step 2 — vend a live credential for it
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/WAF-Role
{
"Code": "Success",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIA................",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCY................",
"Token": "IQoJb3JpZ2luX2VjE......",
"Expiration": "2026-05-16T18:42:11Z"
}
Export those three fields and you are that role — no login flow, no MFA, no further trust to satisfy. The credential is short-lived, but you need it only long enough to AssumeRole into something durable: the privilege-escalation machinery of Chapter 3.
IMDS credentials expire, which protects you against a static key copied out of a config file. They are no defence against a live SSRF, because the endpoint mints a fresh credential every time it is asked. Expiry only matters once your access to the vending machine is cut.
Why it matters: this is not a tenant pentest
In a traditional pentest, an SSRF that reaches a metadata service is a serious finding — and it stops there. It steals that customer's credentials, inside that customer's account. The firewall breach from the cold open was exactly this: catastrophic, 106 million records, and still entirely contained within one tenant.[7]
The cloud-provider angle is different, and it is the reason this chapter exists. Two shifts in perspective:
- The metadata endpoint is a wormhole between planes. It lives in the data plane — a thing your VM talks to, no different in principle from a local DNS resolver. Yet the token it dispenses is honoured by the global control plane. A humble data-plane endpoint, asked the right question, hands you an identity you can drive against the provider's management APIs.
- The SSRF can live inside a provider-operated service. When the vulnerable HTTP client belongs to AWS or Azure — a template renderer, a managed bot's data connector — the credential it vends is not a customer's. It is the provider's. And a provider service's identity is frequently scoped to see every tenant.
Run the metadata service through the book's six-part lens and the danger is plain. Plane: data plane by location, control plane by consequence. Isolation boundary: barely one — the service is authenticated by network position alone, so any primitive that borrows the VM's network position borrows its authority. Identity propagation: this is the moment a workload's identity gets attached, and the moment you can steal it. Shared component: inside a provider service, the metadata endpoint belongs to a fleet shared by every tenant. Provider "magic": plumbing the tenant was never meant to think about — and magic that works invisibly can be invoked invisibly. Detection surface: thin; a request to a link-local address never leaves the host.
The methods at a glance
The rest of the chapter breaks down four techniques, in roughly increasing severity. The first three are things you can do with the SSRF/XXE skills you already have; the fourth is the provider-side payoff.
| Technique | What it abuses | What you get |
|---|---|---|
| 1. SSRF / XXE to workload credentials | Any GET-capable primitive pointed at 169.254.169.254 | The workload's cloud identity token |
| 2. Forging metadata responses | IMDS speaks plaintext HTTP — it can be MITM'd, not just read | Control over what the node believes about itself |
| 3. Bypassing the hardened service | Redirects and header smuggling defeat IMDSv2 / required-header controls | Credentials despite anti-SSRF defences |
| 4. SSRF into the provider's internals | An SSRF inside a provider-operated service | The provider's identity → other tenants' resources |
The three cloud providers are not interchangeable. The pattern is universal; the details are not. Learn the pattern, then look up the vendor.
| AWS EC2 | Azure VM | GCP Compute | Oracle OCI | |
|---|---|---|---|---|
| Endpoint | 169.254.169.254 | 169.254.169.254 | 169.254.169.254 / metadata.google.internal | 169.254.169.254 |
| Credential path | /latest/meta-data/iam/security-credentials/<role> | /metadata/identity/oauth2/token?resource=... | /computeMetadata/v1/instance/service-accounts/default/token | /opc/v2/instance/ (identity certs) |
| Identity returned | STS session for instance-profile role | OAuth token for managed identity | OAuth token for service account | Instance-principal x509 certs |
| Anti-SSRF control | IMDSv2: session token + hop-limit 1 + no XFF (opt-in enforce) | Metadata: true header; rejects XFF | Metadata-Flavor: Google header | /opc/v2/ token vs deprecated /opc/v1/ (none) |
| Still bypassable by | XXE, full-request SSRF, RCE, proxy; v1 if not enforced | header smuggling, redirect bypass | header-injecting SSRF | services still on /opc/v1/ |
Technique 1 — From SSRF and XXE to workload credentials
The simplest version is the firewall version: a classic SSRF in a customer workload, pointed at 169.254.169.254. You can already do this; the only new knowledge was the URL. Two variations are worth your attention because single-tenant pentest experience does not prepare you for them.
The SSRF source need not be a server-side HTTP client. An XXE delivers the same primitive. A SYSTEM entity with an http:// scheme is, functionally, a blind SSRF — and again the relevant question is which URL. The most instructive corpus example puts that XXE inside a provider's own service.
In 2021, Orca Security's Tzah Pahima found that AWS CloudFormation — the service that turns a tenant's declarative template into provisioned infrastructure — renders templates through an XML parser that processed external entities.[1]#005 Critically, that parser does not run on the tenant's VM. It runs on CloudFormation's own rendering host, an AWS infrastructure machine shared across every customer.
- Submit a template containing an external entity with a
file://URL — read local files off the renderer host (service binaries, configs naming internal AWS endpoints, AWS-employee data). - Swap the scheme to
http://— the same primitive becomes a blind SSRF as the CloudFormation host, reaching that host's metadata identity (an internal AWS service credential, not any tenant's). - Prove whose identity you hold without poking AWS internals: use the leaked credentials to presign an S3 URL and fetch your own bucket through the SSRF.
- Read the resulting CloudTrail entry — the caller's
userIdentityshows an internal AWS service principal, not CloudFormation's public-facing principal.
Step 1 is the whole exploit in four lines of XML. The submitted template carries a DOCTYPE that declares an external entity: when CloudFormation's parser expands &xxe;, it dereferences the entity's SYSTEM URL and splices the fetched bytes into the rendered document — which the service then reports back. Point that URL at file:///etc/passwd and the renderer reads its own disk for you; the exfiltration channel is whatever part of the template the service echoes.
<!DOCTYPE r [
<!ENTITY xxe SYSTEM "file:///etc/passwd"> <!-- local file read off the renderer host -->
<!-- swap the scheme for step 2: -->
<!-- <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/"> -->
]>
<root>&xxe;</root>
The parser does not care which scheme follows SYSTEM. Keep file:// and you read files; change it to http://169.254.169.254/… (step 2) and the identical primitive issues a blind GET from the renderer host — that is the SSRF that reaches CloudFormation's own IMDS. Orca assessed the host held credentials and configuration that "could be abused to bypass tenant boundaries, giving privileged access to any resource in AWS." AWS shipped a fix to all regions within days.[1]#005 The template parser is the feature and the attack surface at once — and it sits on a multi-tenant fleet.
The SSRF "engine" can be the victim's own browser session. AWS SageMaker's managed Jupyter notebooks ship a "security extension" meant to block notebook code from reaching IMDS. Research described an XSS in the notebook chained to CSRF that subverts that extension and issues a request to the EC2 instance's metadata service, lifting the AWS token of the notebook's attached IAM role.[2]#011 The teaching point is sharp: a provider can put a guard in front of IMDS, but if the guard runs in the same trust context the attacker just took over, it is decoration.
The credential-vending pattern is not limited to one address. The ECS Task Metadata Service lives at 169.254.170.2 (found via $ECS_CONTAINER_METADATA_URI_V4) and vends task-role credentials over a plain, unauthenticated GET. Newer microVM metadata services on managed AI runtimes have shipped the same way. Each new compute substrate can re-introduce the old bug.
Technique 2 — Forging what metadata says
So far you have read from the metadata service. There is an inverse attack: because IMDS speaks plaintext HTTP, you can also forge what it says back — and the components that consume those answers trust them. Inside a provider-operated service, that inversion is an escape route off the tenant's sandbox and onto the provider's own host.
Wouter ter Maat and Ezequiel Pereira chained their way to a shell inside Google Cloud SQL — a fully managed database service — and then used a forged metadata response to escape the tenant's container onto Google's own managed host VM.[3]#215 The attacker here is nothing more than an ordinary Cloud SQL customer; the container they break out of is part of Google's provider-operated fleet.
- A SQL injection in Cloud SQL's CSV-export custom query yields the MySQL
FILEprivilege, and parameter injection into the export'smysqldumpcall (--plugin-dir/--default-auth) loads a malicious client plugin — a reverse shell inside the Cloud SQL container. - The container runs with host networking, so it shares the managed host VM's network position — and therefore its path to
169.254.169.254. - The GCE host fetches authorised SSH keys at runtime from the metadata server over plaintext HTTP. Using a modified
rshijack, the attacker spoofs that metadata response — no ARP poisoning needed — and injects their own SSH key. - SSH onto the host VM as the injected user;
sudogives root. The tenant database container is now escaped onto Google's managed host.
The broken assumption is that a plaintext metadata fetch can be trusted on a multi-tenant substrate: anything sharing the host's network position can answer in the real service's place. The researchers also noted that Cloud SQL's Private IP mode exposes MySQL to the entire 10.0.0.0/8 range, so escaped instances can reach across to other customers' databases. The same forge-the-reply primitive recurs wherever a managed host consumes metadata over plain HTTP.
Technique 3 — Bypassing the hardened service
The firewall breach forced AWS's hand. In November 2019 — months after the incident — AWS shipped IMDSv2, a re-engineered protocol designed to make the SSRF-to-credential chain hard.[8] It is good engineering. It is also, emphatically, not a cure, and a CSP security engineer who treats it as one will miss live attack paths.
IMDSv1 is a single, plain, unauthenticated GET — any primitive that can cause a GET to 169.254.169.254 steals credentials. IMDSv2 makes metadata access a two-step, session-oriented protocol: a PUT with a request header obtains a session token, and every data request must then carry that token header. It also sets a default response hop limit of 1 and rejects requests bearing X-Forwarded-For.
# Step 1 — obtain a session token (a PUT, with a request header)
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
# Step 2 — every data request must carry the token header
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/
Why this defeats most SSRF: a textbook SSRF gives the attacker a GET with no control over method or headers. It can neither issue the PUT nor attach the token header. But the limits matter, because the chapter's credibility depends on stating them:
- Opt-in by enforcement. For years IMDSv2 shipped alongside v1. An instance launched without
HttpTokens=requiredstill answers v1. The fix is a configuration, not a removal. - It only stops GET-only, header-blind SSRF. A full-request SSRF where the attacker controls method and headers — typically via a "make an HTTP request" feature in a managed service — can complete the v2 handshake itself. So can XXE, RCE, or a proxy that forwards arbitrary methods.
- It is AWS-only. Azure IMDS requires a
Metadata: trueheader; GCP requiresMetadata-Flavor: Google. Real friction against a blindGET— but, as we are about to see, smuggle-able. - Adjacent surfaces never got it. ECS task metadata and the newer microVM endpoints are still plain-
GETsurfaces.
The redirect bypass. When a provider service does validate the SSRF target — "you may not request internal or RFC 1918 hosts" — it almost always validates the URL string the tenant supplied, then hands that string to an HTTP client that follows redirects. So the attacker hosts a public URL that answers with a 301 whose Location points at IMDS:
# attacker-controlled server, lab-scoped
class H(http.server.BaseHTTPRequestHandler):
def do_GET(s):
s.send_response(301)
s.send_header('Location',
'http://169.254.169.254/metadata/instance?api-version=2021-12-13')
s.end_headers()
The allow-list inspected a perfectly public address and approved it; the HTTP client then followed the redirect onto 169.254.169.254. In 2024 this exact bypass appeared in three independent disclosures against three different Azure services from the same research team.[4]#258 The defensive lesson writes itself: validate the connection target — the resolved IP at socket time, on every hop — not the user-supplied string.
Header smuggling. Azure IMDS demands Metadata: true and rejects any request bearing X-Forwarded-For. When a service's HTTP-request feature lets the user set headers but auto-appends its own X-Forwarded-For, an attacker can terminate the Metadata header value with newline characters, pushing the service-injected X-Forwarded-For down past the header block and into the request body — where IMDS simply ignores it.[5]#254 "Require a header" is a soft control when the attacker has even partial control of the request structure.
Technique 4 — When the SSRF reaches the provider's internals
Here is the chapter's thesis. An in-tenant SSRF that only hits 169.254.169.254 steals the tenant's own credentials — the firewall-breach scale. The course-relevant escalation is an SSRF inside a provider-operated service. There, the very same SSRF reaches targets a tenant should never touch: the metadata identity of a provider-owned host, provider-internal management APIs, and host-agent channels — the host-to-guest channel like the Azure WireServer, which gets its own full treatment in Chapter 5.
Azure AI Health Bot is a managed service for building healthcare chatbots. Its "Data Connections" feature lets a bot call external APIs. Tenable found direct requests to 169.254.169.254 were filtered — so they reached for the redirect bypass.[6]#256
- Point a data connection at an attacker-controlled host that answers with a
301tohttp://169.254.169.254/metadata/instance?api-version=2021-12-13. The service followed the redirect; the filter never saw the internal host. - From IMDS, vend a managed-identity access token scoped to
management.azure.com— not the calling tenant's identity, but the Health Bot service's own. - Call the control-plane API with that token to enumerate subscriptions and their resources.
GET https://management.azure.com/subscriptions?api-version=2020-01-01
Authorization: Bearer <managed-identity token>
GET https://management.azure.com/subscriptions/<id>/resources?api-version=2020-10-01
Authorization: Bearer <managed-identity token>
The response listed, in Tenable's words, "hundreds and hundreds of resources belonging to other customers." The failed boundary is subscription/tenant isolation; the broken assumption is that the Health Bot service principal is scoped to the tenant that invoked it. It was not. This is the precise moment an SSRF stops being a web bug and becomes a provider-side, cross-tenant bug.
The Health Bot finding did not stand alone. Two sibling 2024 disclosures from the same team show the pattern is structural:
Copilot Studio (CVE-2024-38206). Copilot Studio Topics expose an HttpRequestAction with a user-controllable URL and headers. Tenable bypassed the IP filter with the same attacker-hosted 301, then smuggled past the Metadata/X-Forwarded-For controls with the newline trick. The loot: managed-identity tokens, access to two Azure subscriptions, a Cosmos DB instance with master read/write keys, and visibility into internal 10.0.x.0/24 subnets — rated a Critical Information Disclosure.[5]#254
Azure Machine Learning. The data-connection endpoints on ml.azure.com and ai.azure.com had internal-host filtering; a 301/302 from an attacker server bypassed it. The PoC reached http://127.0.0.1:4191/metrics# — a Linkerd service-mesh metrics endpoint — leaking the identities of other internal hosts and services on the shared, region-scoped ML infrastructure; Microsoft documented the fix in an MSRC blog.[4]#258 Note this SSRF never reached IMDS at all — it reached region-shared internal infrastructure, which is its own prize.
An older case shows the pattern is neither new nor Azure-specific. Azure Stack DataService (CVE-2019-1234): the User Portal's "deploy template from URL" feature was a GET with no host validation, IPv6-capable so it slipped past IPv4-only filtering, reaching an unauthenticated internal DataService API whose GetVmScreenshot method returned screenshots of arbitrary tenant and infrastructure VMs.[9]#059 SSRF in a provider portal → provider-internal API → cross-tenant disclosure, years before the 2024 trio.
The irreducible flaw — an internal HTTP endpoint authenticated only by network position, vending credentials, reachable by an unprivileged tenant workload — is not confined to 169.254.169.254. The Azure host-agent channel (WireServer at 168.63.129.16) fits the same description and has been abused to reach cluster-admin on AKS with no web bug at all. That host-to-guest channel is Chapter 5's subject.
- On any foothold, identify the metadata endpoint and its version. Try a plain
GETto169.254.169.254; if it answers, you have IMDSv1 reach. On AWS, test whetherHttpTokens=requiredis enforced. - Enumerate every metadata-class surface, not just IMDS: ECS task metadata at
169.254.170.2($ECS_CONTAINER_METADATA_URI_V4), microVM metadata on managed runtimes, GCPmetadata.google.internal. - If the target is a provider-operated service, hunt for every place it makes a server-side request — data connections, "fetch template from URL", HTTP-action steps, webhook callbacks.
- Against a validated SSRF, test the redirect bypass (host a public URL that
301s to the internal target) and header smuggling (newline-terminate a required header to displace an injected one). - Remember XXE, RCE and full-request SSRF reach a hardened IMDSv2 — header-blindness is what IMDSv2 punishes.
- After stealing a token, fingerprint whose identity it is before acting — the BreakingFormation presigned-S3 trick. Confirm whether you hold a tenant identity or a provider service identity.
- Enumerate the token's scope, then hand it to Chapter 3's privesc machinery to convert short-lived metadata credentials into durable, escalated access.
- Enforce IMDSv2. Set
HttpTokens=requiredfleet-wide — the v2 protocol exists, but only enforcement closes v1. Set the response hop limit to 1 so a container cannot reach the host's IMDS. - Block pod egress to the magic addresses. NetworkPolicy or host firewall rules denying
169.254.169.254,169.254.170.2and the Azure host-agent address from workloads with no reason to reach them. - Validate SSRF targets at the socket, not the string. Resolve the destination and check the actual connection IP on every redirect hop, or disable redirect-following for fetch features.
- Scope service identities tightly. The Health Bot finding was severe because the service principal saw across tenants. A managed service's identity should be scoped to the tenant it is acting for.
- Detect anomalous metadata access — IMDS calls from web-facing processes, spikes in request volume, and the strongest single signal: a credential minted by IMDS being used from an IP that is not its instance.
- Audit new services for the regression: any managed compute substrate that ships a metadata endpoint without session-token enforcement has re-introduced IMDSv1.
- The metadata service is a credential vending machine: it mints live, short-lived identity tokens for the workload, authenticated only by network position. One server-side request equals one stolen cloud identity.
- SSRF and XXE are the same primitive against IMDS — the new knowledge is not the vulnerability class but the target URL.
- Metadata is plaintext HTTP, so it can be forged as readily as read; components that consume the answers trust them.
- IMDSv2 is good but not a cure. It stops GET-only, header-blind SSRF; it is opt-in, AWS-only, and leaks under XXE, RCE, full-request SSRF, or the redirect bypass.
- The chapter's thesis: an SSRF inside a provider-operated service steals the provider's identity — a credential that can enumerate other tenants. One SSRF becomes a cross-tenant breach.
- A stolen metadata token is an acquisition. Hand it to Chapter 3's privesc machinery; the host-agent channels it can reach internally are Chapter 5's; container-to-node escapes are Chapter 6's.
References
- Tzah Pahima, Orca Security, "BreakingFormation: Orca Security Research Team Discovers AWS CloudFormation Vulnerability". Archived: local copy · Original: orca.security. Corpus #005.
- Security Boulevard, "AWS SageMaker Jupyter Notebook Instance Takeover (XSS/CSRF/security extension)". Archived: local copy (metadata stub) · Original: securityboulevard.com. Corpus #011.
- Wouter ter Maat & Ezequiel Pereira, "Dropping a shell in Google Cloud SQL: how to contact Google SRE". Archived: local copy · Original: offensi.com. Corpus #215.
- Tenable Research, "Azure Machine Learning SSRF" (TRA-2024-22). Archived: local copy · Original: tenable.com · MSRC: microsoft.com/msrc. Corpus #258.
- Tenable Research, "SSRF'ing the Web with the Help of Copilot Studio" (CVE-2024-38206). Archived: local copy · Original: tenable.com. Corpus #254.
- Tenable Research, "Compromising Microsoft's AI Healthcare Chatbot Service". Archived: local copy · Original: tenable.com. Corpus #256.
- Brian Krebs, "What We Can Learn from the Capital One Hack". Original: krebsonsecurity.com. See also Riyaz Walikar / Appsecco, "An SSRF, privileged AWS keys and the Capital One breach": blog.appsecco.com.
- AWS Security Blog, "Add defense in depth against open firewalls, reverse proxies, and SSRF vulnerabilities with enhancements to the EC2 Instance Metadata Service" (IMDSv2 design). Original: aws.amazon.com.
- Check Point Research, "Remote Cloud Execution — Critical Vulnerabilities in Azure Cloud Infrastructure" (Azure Stack DataService SSRF, CVE-2019-1234). Archived: local copy. Corpus #059.