DEV Community

Rocky
Rocky

Posted on

The IAM Policy That Passes Every Review, Then Hands Over the Whole Account

The scope on this cloud engagement is deliberately narrow: one IAM user, assumed already compromised through a phishing sim, no admin rights attached anywhere in the account. Before touching the console, the client hands over the user's policy for review, expecting a quick "looks fine." Read action by action, it does. No iam:*, no AdministratorAccess, no wildcard resource on anything scary-looking. Two lines stand out only because they're unusual together: iam:PassRole scoped to a single role ARN, and lambda:CreateFunction plus lambda:InvokeFunction. Neither one means anything alarming in isolation. Combined, they hand this low-privilege user the exact permissions of whatever that one role can do.

iam:PassRole exists because plenty of AWS services need to run under a role you specify: EC2 needs an instance profile, Lambda needs an execution role, and PassRole is the permission that lets a principal attach one of those roles to a resource it's creating. That's the entire, legitimate point of the permission. The problem shows up the moment a principal can also create something that runs code and pass a role to it. Write a Lambda function, pass it that one role via iam:PassRole, invoke it, and the code inside that function executes with the role's permissions, not the calling user's. If the role sitting behind that ARN is more privileged than the user who can pass it, the user just escalated, entirely through documented AWS API calls, with nothing exploited and no misconfiguration in Lambda itself.

This is why it survives so many policy reviews. Reviewers check each permission against a mental blocklist: does this include admin, does this include a wildcard on IAM, does this touch anything with "Admin" or "Full" in the managed policy name. iam:PassRole scoped to one ARN doesn't trip any of that, because scoping it to a specific role is the recommended way to grant it. What the checklist approach misses is that PassRole was never dangerous on its own. It becomes dangerous the instant it sits in the same policy as any action that lets the same principal launch or run something under an attacker-chosen role: Lambda, EC2 with an instance profile, CloudFormation with a service role, Glue jobs, Data Pipeline, the list of "compute plus a role" services is long and every one of them is a version of the same escalation path.

The fix isn't removing PassRole, since real infrastructure needs it constantly. It's never granting it as a standalone capability without asking what the paired role can actually do and whether this principal should be able to become that role. AWS added iam:PassedToService and iam:AssociatedResourceArn condition keys specifically so PassRole can be scoped to "only when passed to this exact service, for this exact resource," which closes the generic version of the path even when the permission itself stays granted. A permission boundary on the user or role is the other real control, capping what any role it can ever assume is allowed to do regardless of what gets passed where.

Finding this pattern reliably, across AWS, Azure, and GCP's own versions of the same "who can attach a privileged identity to something they control" problem, is exactly the kind of IAM reasoning Codelivly's Cloud Penetration Testing Book for Beginners: 60+ Labs walks through hands-on instead of leaving as a line item to memorize. It's the difference between a policy review that checks boxes and one that actually finds the account takeover sitting in plain sight.

Top comments (0)