IAM Privilege Escalation Paths in AWS: A Bug Hunter's Field Guide

IAM Privilege Escalation Paths in AWS: A Bug Hunter's Field Guide

Individually harmless-looking IAM permissions that chain together into full account compromise, and the exact commands used to find and prove each path.

HackerSavanna Security Team

Security research and platform engineering at HackerSavanna.

5 min read3 views

AWS IAM privilege escalation almost never comes from a single catastrophic misconfiguration. It comes from a chain of individually reasonable-looking permissions that combine into something nobody intended. This is a field guide to the most common escalation paths, written for the moment you've already landed low-privilege credentials (through an SSRF, a leaked key, or a scoped-down IAM user) and need to figure out how far they actually reach.

Start here: know what you can do

The first move with any newly obtained AWS credentials, before touching anything else, is a permissions inventory:

bash197 Bytes
1aws sts get-caller-identity
2aws iam list-attached-user-policies --user-name <name>
3aws iam list-user-policies --user-name <name>
4aws iam get-policy-version --policy-arn <arn> --version-id <version>

If direct IAM enumeration is denied (a well-configured account will often deny iam:List* and iam:Get* to low-privilege principals), tools like Pacu or a manual permutation of iam:SimulatePrincipalPolicy calls can help map out what's actually allowed without needing broad list permissions yourself.

Path one: iam:PassRole plus a service that will use it

This is the escalation path that shows up most often in real assessments, precisely because iam:PassRole sounds harmless in isolation. It just lets a principal hand a role to an AWS service. The danger is entirely about which role and which service.

json227 Bytes
1{
2 "Effect": "Allow",
3 "Action": ["iam:PassRole"],
4 "Resource": "arn:aws:iam::123456789012:role/HighPrivilegeRole"
5},
6{
7 "Effect": "Allow",
8 "Action": ["lambda:CreateFunction", "lambda:InvokeFunction"],
9 "Resource": "*"
10}

Combined, these two statements let you create a Lambda function, attach the high-privilege role to it, and then invoke code that runs as that role:

bash269 Bytes
1aws lambda create-function \
2 --function-name escalate \
3 --runtime python3.12 \
4 --role arn:aws:iam::123456789012:role/HighPrivilegeRole \
5 --handler lambda_function.handler \
6 --zip-file fileb://payload.zip
7
8aws lambda invoke --function-name escalate output.json

The Lambda function's code, running with the attached role's permissions, can now do anything that role is allowed to do, including creating new IAM users, attaching admin policies, or reading secrets far outside your original scope. The same pattern applies to ec2:RunInstances with an instance profile, glue:CreateJob, datapipeline:CreatePipeline, and several other services that accept a role parameter at creation time.

Path two: policy versioning tricks

IAM managed policies support up to five versions, and only one is "default" (active) at a time. If a principal has iam:CreatePolicyVersion but not full iam:PutUserPolicy, they can still escalate:

bash166 Bytes
1aws iam create-policy-version \
2 --policy-arn arn:aws:iam::123456789012:policy/SomeAttachedPolicy \
3 --policy-document file://admin-policy.json \
4 --set-as-default

If that policy is already attached to your user or a role you can assume, you've just rewritten its permissions to whatever you want, without ever touching an attachment API. This is a favorite because policy version changes are logged less prominently than new attachments in a lot of default CloudTrail dashboards, making it a quieter escalation path during an actual assessment.

Path three: sts:AssumeRole with an overly permissive trust policy

A role's trust policy determines who can assume it, separately from what the role itself is permitted to do. Trust policies get misconfigured surprisingly often, especially in accounts with a history of "just get it working" cross-account setups:

json156 Bytes
1{
2 "Version": "2012-10-17",
3 "Statement": [
4 {
5 "Effect": "Allow",
6 "Principal": { "AWS": "*" },
7 "Action": "sts:AssumeRole"
8 }
9 ]
10}

A wildcard Principal on a trust policy means any AWS account, including one you control, can assume that role if you know its ARN. Role ARNs aren't secret, they show up in CloudFormation templates, Terraform state files accidentally committed to public repos, and IAM policy documents you already have read access to. Enumerate role names, then try assuming each one:

bash112 Bytes
1aws sts assume-role \
2 --role-arn arn:aws:iam::123456789012:role/candidate-role \
3 --role-session-name pentest

Path four: iam:CreateAccessKey on another user

If you can create access keys for a different, higher-privilege IAM user, you don't need to touch any policy at all. You just mint new, valid credentials for that user directly:

bash57 Bytes
1aws iam create-access-key --user-name high-privilege-user

This permission alone, without anything else, is a complete privilege escalation path and is worth checking for explicitly during enumeration rather than assuming it's bundled safely with other "user management" permissions.

Mapping this efficiently

Doing this by hand across dozens of attached policies doesn't scale. Purpose-built tools automate the graph traversal:

  • Pacu runs an iam__privesc_scan module that checks your current credentials against a comprehensive list of known escalation patterns automatically.
  • PMapper builds a full graph of principals, roles, and reachable permissions across an account, useful when you have broader read access and want to visualize every path at once, not just the ones reachable from a single starting identity.

Reporting this responsibly

A privilege escalation chain report should document:

  • The starting permission set (exactly what the low-privilege credential was allowed to do)
  • Each step of the chain, with the specific API calls used
  • The final privilege level achieved, demonstrated with a low-risk, easily reversible action (like sts:get-caller-identity after assuming an escalated role) rather than anything destructive
  • A clear statement that no data was accessed or modified beyond what was necessary to prove the chain

Closing the gaps

  • Treat iam:PassRole as a genuinely dangerous permission and always scope it to specific role ARNs via a Resource condition, never wildcard it alongside a service that accepts arbitrary roles.
  • Restrict iam:CreatePolicyVersion and iam:SetDefaultPolicyVersion to break-glass administrative principals only.
  • Audit trust policies for wildcard or overly broad Principal blocks on a recurring schedule, not just at creation time.
  • Run AWS IAM Access Analyzer continuously. It's built specifically to surface unintended external and cross-account access paths before an attacker, or a researcher, finds them first.

None of these individual permissions look dangerous on a policy review checklist. The danger is entirely in the combination, which is exactly why automated graph-based analysis consistently outperforms manual policy-by-policy review for this bug class.

Share: