JSTGTECH
← Back to blog

The IAM Permissions That Quietly Grant Admin, One Hop at a Time

4 min read

Nobody attaches AdministratorAccess to a compromised access key and calls it a breach report. Real IAM privilege escalation looks boring: a handful of narrow, plausible-sounding permissions that individually pass a code review, and together let a low-privilege identity mint itself a new one with full account control. Security researcher Spencer Gietzen catalogued the first broad list of these paths back in 2018, and the list has only grown since — recent cloud-posture surveys still find escalation-capable policy combinations in the large majority of scanned AWS accounts. The mechanism is unglamorous and that’s exactly why it keeps working: no exploit code, no CVE, just IAM doing precisely what its policy language says.

Root cause

Every one of these paths reduces to the same shape: an identity can create or modify a permission set, or hand an existing powerful role to a resource it controls. iam:CreatePolicyVersion lets you rewrite the default version of a policy already attached to you — so if you can touch a policy at all, you can silently swap its contents for "Action": "*" without ever calling AttachUserPolicy or AttachRolePolicy, the actions most detections actually watch for. iam:PassRole is the more common route: on its own it does nothing, but paired with permission to launch a compute resource — lambda:CreateFunction

  • lambda:InvokeFunction, ec2:RunInstances, glue:CreateDevEndpoint, sagemaker:CreatePresignedNotebookInstanceUrl, or cloudformation:CreateStack — it lets you pass a role with far more privilege than your own to a service that will run code as that role on your behalf. CloudFormation is the sharpest version of this: a principal with cloudformation:CreateStack and iam:PassRole can hand the stack an administrator role and have it provision an AdministratorAccess-attached IAM user as a stack resource, turning a single API call into full account takeover. None of these permissions look dangerous read in isolation on a policy diff; the danger is entirely in the combination, which is exactly what makes them survive manual review.

Blast radius

Because escalation here is pure policy manipulation rather than a memory corruption bug or a lateral-movement chain, there’s no patch window and no detection signature tied to a specific exploit — the “exploit” is a sequence of fully authorized, individually mundane API calls. That also means the blast radius scales with how the initial low-privilege credential was obtained, not with any skill on the attacker’s part: a leaked CI access key, an SSRF against the instance metadata service, or a phished developer session all land at the same starting line, and from there the privilege-escalation path is a checklist, not a research project. Documented incidents have gone from a stolen token to administrator-equivalent access in well under a day once the attacker finds one exploitable permission combination — and because the resulting admin role was created through ordinary IAM and CloudFormation calls, it often doesn’t stand out from routine infrastructure activity in a CloudTrail review unless someone is specifically looking for the pattern.

Remediation

Start by finding these paths before an attacker does: **AWS IAM Access Analyzer**‘s policy-generation and unused-access findings won’t flag escalation chains directly, but open-source tools built for exactly this — Rhino Security Labs’ pacu privesc scanner and the aptly named aws-privilege-escalation policy simulators — walk every principal’s effective permissions against the known combination list and tell you which identities are one API call from admin. Where you can, replace the dangerous permissions outright: scope iam:PassRole with a iam:PassedToService condition key so a role can only be passed to the specific service that needs it, never to CloudFormation or Lambda generically; and avoid granting iam:CreatePolicyVersion or iam:SetDefaultPolicyVersion to anything short of your break-glass admin role, since both let a principal rewrite its own effective permissions without touching the attach/detach APIs that most alerting rules key on. Permission boundaries are the structural fix — they cap the maximum permissions any role or user created by a given principal can ever have, so even a successful iam:CreateRole + iam:PassRole chain can’t produce a role more powerful than the boundary allows, regardless of what policy gets attached to it. At the organization level, an SCP denying iam:CreatePolicyVersion, iam:PutRolePolicy, and iam:PutUserPolicy for everything except a designated deployment role removes the whole permission-authoring surface from ordinary accounts. And because none of this looks anomalous in isolation, alert specifically on the sequence — CloudTrail Lake or a SIEM rule that fires when a single principal calls iam:PassRole followed within minutes by cloudformation:CreateStack, lambda:CreateFunction, or ec2:RunInstances — since the individual calls are routine but that pairing, from a principal that shouldn’t need it, is the tell.

The uncomfortable finding underneath all of this is that least-privilege IAM isn’t just about trimming unused permissions — it’s about knowing which combinations of permissions are equivalent to *, because IAM’s policy language has no concept of “these two grants together are more dangerous than either alone.” A policy reviewer checking each statement in isolation will pass every one of these chains. Only a tool, or a reviewer, checking for the specific combinations will catch it.

Related posts