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.

No exploit. No CVE. Just IAM doing exactly what you told it to.

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 combination — iam: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