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:

  1. Create a new Lambda function.
  2. Attach an existing IAM role that already has AdministratorAccess — any role in the account, since PassRole isn’t restricted to specific ARNs.
  3. Write a short handler that imports the AWS SDK and performs whatever action the attacker wants.
  4. 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 the Resource element to the exact ARN(s) of the roles that specific account is actually meant to pass — not *.
  • Use the iam:PassedToService condition key to further restrict which service a role can be passed to (e.g., only lambda.amazonaws.com), so even an approved role can’t be attached to a service it was never intended for.
  • Use iam:AssociatedResourceArn where 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:PassRole review as seriously as reviewing for AdministratorAccess itself — 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 combinationiam:PassRole and lambda:CreateFunction are 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.

📚 References