5 AWS IAM Privilege Escalation Paths and How to Block Them

Five highest-risk AWS escalation paths
AWS IAM privilege escalation is the most common post-compromise attack path in cloud environments, and most of it relies on legitimate IAM API calls that look like normal administration in CloudTrail. The five paths exploited most often are iam:CreatePolicyVersion, iam:PassRole combined with ec2:RunInstances, iam:AttachUserPolicy, lambda:UpdateFunctionCode, and iam:CreateAccessKey. A principal with iam:CreatePolicyVersion can create a new version of any existing IAM policy and set it as the default, including a policy that grants AdministratorAccess, turning one leaked access key into full account takeover in a single API call. Defense has two parts: scope every IAM policy so these permissions never reach broad resources, and alert on the specific API calls that signal escalation. The sections below map each path, its fix, and its detection signature so your team can close the gaps before an attacker maps them.
Each path needs only the permissions named below and a single API call. This table summarizes what an attacker gains from each one.
| Permission combination | Attacker action | Potential impact |
|---|---|---|
iam:CreatePolicyVersion | Creates a new default policy version granting * on * | Full administrator access |
iam:PassRole + ec2:RunInstances | Launches an EC2 instance with a privileged instance profile and reads its credentials | Inherits the target role’s permissions |
iam:AttachUserPolicy | Attaches AdministratorAccess to the attacker’s own user | Full administrator access |
lambda:UpdateFunctionCode | Replaces a privileged function’s code to run actions under its role | Inherits the function role’s permissions |
iam:CreateAccessKey | Generates a new long-lived access key for an admin user | Inherits the target user’s permissions |
These permissions are not rare in practice. Many teams grant iam:PassRole or iam:CreatePolicyVersion at Resource: * to keep deployment pipelines moving, which is exactly the over-grant that turns a foothold into a takeover. The permissions in this table are also mutually reinforcing: once an attacker holds any one of them, the others often become reachable through the roles and policies they can now read or modify. Treating these permissions as high-risk and scoping them tightly is the single most effective control, and it belongs inside any broader data security posture management program that inventories where privileged access actually lives.
Block each path with policy scoping
The fix for every path in the table is the same principle applied at different points: never let a high-risk IAM permission touch a broad resource set. Scope iam:PassRole so it can only be passed to a named service and a named role. Add a condition such as StringEquals: iam:PassedToService: ec2.amazonaws.com and restrict Resource to specific non-admin role ARNs, because otherwise a principal with iam:PassRole and ec2:RunInstances can launch a new EC2 instance with an instance profile that has more privileges than the attacker’s own role and then read that profile’s temporary credentials from the instance metadata. Never grant iam:CreatePolicyVersion without bounding which policy ARNs it can touch, and never grant iam:AttachUserPolicy, iam:PutUserPolicy, iam:AttachRolePolicy, or iam:PutRolePolicy without resource-level restrictions that exclude administrative roles and managed policies. Scope iam:CreateAccessKey to the principal’s own user at most, and restrict lambda:UpdateFunctionCode to CI/CD service accounts bound to specific function ARNs. These restrictions mirror the shared responsibility split: the provider secures the platform, but IAM policy content is entirely the customer’s job.
Detect escalation in CloudTrail
Detection matters because the attacks are designed to blend in. These calls do not look like attacks in CloudTrail logs; they look like routine IAM administration, so only targeted alerting surfaces them. Build rules that alert when CreatePolicyVersion carries requestParameters.setAsDefault set to true, when CreateAccessKey targets a user different from the calling principal, and when RunInstances attaches an instance profile ARN you consider sensitive from an identity that is not your standard infrastructure automation role. Add real-time alerts for UpdateFunctionCode and any deployment event fired by an identity other than your designated CI/CD service account. Speed is decisive: the window between when an attacker escalates and when they begin exfiltration is often less than ten minutes, so these alerts must page a human rather than land in a daily digest. Pair the alerts with CloudTrail log file integrity validation so an attacker who gained enough access cannot silently tamper with the trail, and forward the events to your SIEM with the source identity preserved.
Audit permissions before attackers do
Before you can scope and alert, you need to know where the dangerous permissions already live. Run a structured audit in this order.
- Run PMapper to map IAM relationships as a graph and enumerate escalation paths account-wide with
python3 -m principalmapper graph --createfollowed by the analysis command. - Run Prowler and Cloudsploit to flag high-risk permissions and configuration drift across the account.
- Review every principal that holds
iam:CreatePolicyVersion,iam:PassRole,iam:CreateAccessKey, orlambda:UpdateFunctionCodeagainst the scoping rules above. - Remediate by replacing broad grants with resource-scoped, condition-bound policies, then re-run the tools to confirm the paths are closed.
- Re-run the audit on a fixed cadence and after every privilege change, not only after an incident.
The value of this loop is that PMapper maps IAM relationships as a graph and finds the escalation paths, while Prowler and Cloudsploit flag high-risk permissions, giving you the same view an attacker would build for themselves. Closing those paths proactively is far cheaper than recovering from a full account compromise.