iam:PassRole + Wildcard: The Privilege Escalation Hiding in Plain Sight
A developer account with nothing but lambda:CreateFunction and a wildcard iam:PassRole can walk itself up to full Administrator — no exploit, no misconfigured bucket, just permissions doing exactly what they were asked to do.
Hi everyone 👋
This one doesn’t show up as a vulnerability scan finding or a CVE — it’s a permissions grant that looks completely reasonable in a policy review, right up until someone connects the dots.
😱 The convenient habit
To make deployments easier, it’s extremely common to grant developer accounts iam:PassRole against * (a wildcard covering every role in the account). The reasoning is simple and, on its own, sounds harmless: developers need to attach an IAM role to whatever they deploy — a Lambda function, an EC2 instance — and wildcarding PassRole means nobody has to update a policy every time a new role gets created.
🔍 How the escalation actually works
An account with only two permissions — lambda:CreateFunction and iam:PassRole on * — can walk itself up to Administrator, with no other access required:
- Create a new Lambda function.
- Attach an existing IAM role that already has
AdministratorAccess— any role in the account, sincePassRoleisn’t restricted to specific ARNs. - Write a short handler that imports the AWS SDK and performs whatever action the attacker wants.
- Invoke the function. The code now runs with the permissions of the attached role — Administrator — regardless of what permissions the calling account actually has.
This exact path is one of several documented by Rhino Security Labs’ AWS IAM privilege escalation research — and there are variants that don’t even need lambda:InvokeFunction: pairing PassRole with lambda:AddPermission (to let something else trigger it) or lambda:CreateEventSourceMapping (to wire up an event source that invokes it) gets to the same place.
❗ Why this is easy to miss in a policy review
iam:PassRole doesn’t grant access to do anything by itself — it only grants permission to hand a role to a service. Reviewed in isolation, it looks like a narrow, administrative permission. It only becomes dangerous in combination with whatever service-creation permission (lambda:CreateFunction, ec2:RunInstances, and others) the same account already holds — and those two permissions are rarely reviewed together, because they usually live in different policy statements, sometimes attached at different times by different people.
🛠️ The fix
- Never wildcard
iam:PassRole. Restrict theResourceelement to the exact ARN(s) of the roles that specific account is actually meant to pass — not*. - Use the
iam:PassedToServicecondition key to further restrict which service a role can be passed to (e.g., onlylambda.amazonaws.com), so even an approved role can’t be attached to a service it was never intended for. - Use
iam:AssociatedResourceArnwhere applicable, to allow passing a role only when it’s being associated with a specific resource, rather than any resource of that type. - Treat
iam:PassRolereview as seriously as reviewing forAdministratorAccessitself — because combined with the right service-creation permission, it effectively is that.
💡 What we learned
- A permission can be “safe” in isolation and dangerous in combination —
iam:PassRoleandlambda:CreateFunctionare each individually reasonable things to grant a developer; together, unscoped, they’re an escalation path. - Convenience-driven wildcards age badly. The wildcard was added to avoid updating a policy every time a new role appears — but every new
AdministratorAccess-capable role created afterward is automatically in scope for anyone holding that wildcard, with nobody having explicitly decided that. - This isn’t a misconfiguration in the “forgot to lock a bucket” sense — it’s two correctly-functioning permissions composing into something nobody intended. Reviewing IAM policies for this means reviewing combinations, not just individual statements.
