Skip to content
Home/Blog/Mapping AWS IAM Privilege Escalation: When a Permi...
Back to blog

Mapping AWS IAM Privilege Escalation: When a Permission Grants a Permission

2026-07-05

10 min

awscloud-securityiam

My project Domino exists because of one uncomfortable realization: in AWS, the path from a low-privileged identity to full account control is usually not a vulnerability. It is a feature, used as intended, one permission at a time. IAM is expressive enough that a principal who can modify permissions can often grant themselves more of them, and the shortest path to admin is frequently a chain nobody drew on purpose. Domino models IAM as a directed graph and proves those chains exist. This post is the thinking behind it.

The core idea is simple once you see it. Most IAM privilege escalation is a principal using a permission it has to obtain a permission it wants. Rhino Security Labs cataloged around twenty-one distinct methods for this years ago, and they still land in real accounts today because they are not bugs to be patched. They are policies to be configured correctly. Here are the ones I check first.

The self-grant family

The bluntest paths are the ones where a principal can edit its own permissions directly.

  • iam:AttachUserPolicy or iam:AttachRolePolicy. If you can attach a managed policy to yourself, you attach AdministratorAccess. Done. It is one API call.
  • iam:PutUserPolicy or iam:PutRolePolicy. Same outcome with an inline policy you write yourself, granting * on *.
  • iam:CreatePolicyVersion and iam:SetDefaultPolicyVersion. More subtle. If you can create a new version of a customer-managed policy and set it as default, you rewrite the rules of a policy that is already attached to you, without touching the attachment at all.

These feel too easy, and that is the point. Nobody grants iam:AttachUserPolicy intending to grant admin, but that is what it is, transitively.

The PassRole family, which is where it gets interesting

The paths I actually worry about run through iam:PassRole, because they hop across services and hide the escalation inside normal-looking automation.

iam:PassRole lets a principal hand an IAM role to an AWS service. On its own that is fine. Combined with a compute service, it becomes escalation. If I can pass a privileged role to a service that runs code, the code runs as that role:

  • Pass a role to EC2 (iam:PassRole + ec2:RunInstances): launch an instance with an admin instance profile, then use its credentials.
  • Pass a role to Lambda (iam:PassRole + lambda:CreateFunction + lambda:InvokeFunction): create a function with a privileged execution role and invoke it.
  • Modify an existing privileged function (lambda:UpdateFunctionCode on a function that already has an admin role): you do not even need PassRole, you just change what the privileged function does.

The reason these matter more than the self-grant paths is that they look like legitimate infrastructure work. A developer who can deploy Lambda functions and pass execution roles is doing their job. The same permissions are an escalation path, and telling the two apart is the hard part.

Why I model it as a graph

Any one of these is a single hop. The dangerous reality is that they chain. A principal with sts:AssumeRole reaches a second role, which can iam:CreateAccessKey for a third identity, which can pass a role to Lambda, which is admin. No single permission looks alarming in a review. The path is the finding.

That is exactly why a graph is the right model and a permissions spreadsheet is not. Domino treats every principal, role, and policy as a node and every "can do X to Y" as an edge, then searches for paths from a starting identity to an admin-equivalent capability. It surfaces the multi-hop routes a human reviewer would never trace by hand, because humans do not enumerate every transitive edge across hundreds of roles. Tools like Pacu approach the offensive side of this with a library of privesc modules, and the graph view is the natural way to see how those modules compose.

Detecting and closing the paths

Because half my work is detection engineering, the defensive side is not an afterthought here, it is the payoff. AWS IAM privilege escalation is very observable if you are looking, because every one of these actions is a CloudTrail event.

  • Alert on the self-grant APIs. AttachUserPolicy, AttachRolePolicy, PutUserPolicy, PutRolePolicy, CreatePolicyVersion, and SetDefaultPolicyVersion are rare and high-signal. In most environments a human attaching a policy to themselves, or a new default policy version appearing, should page someone.
  • Watch PassRole in context. PassRole paired with RunInstances, CreateFunction, or UpdateFunctionCode, especially passing a role more privileged than the caller, is the shape of a PassRole escalation. The event that matters is a low-privileged principal passing a high-privileged role.
  • Flag credential creation for others. CreateAccessKey or CreateLoginProfile targeting an account that is not the caller is a classic lateral/escalation move.

The durable fixes are structural, not detective:

  • Least privilege on the IAM-modifying actions. Almost nobody needs iam:AttachUserPolicy or iam:PutUserPolicy at runtime. These belong to a tightly controlled deployment path, not to application or developer roles.
  • Constrain PassRole. Scope iam:PassRole with a resource restriction and a PassedToService condition so a principal can only pass the specific roles it legitimately needs, to the specific services that need them.
  • Use permission boundaries and SCPs. A permission boundary caps what an identity can ever reach even if it grants itself a policy. Service Control Policies put a ceiling on the whole account. Both turn "reachable in theory" into "denied in practice."
  • Run IAM Access Analyzer and re-check the graph after every change. The same graph search that finds a path proves it is gone once you cut an edge.

The lesson from building Domino

What Domino taught me is that cloud privilege escalation is a modeling problem more than an exploitation problem. The individual moves are documented, boring, and use the API exactly as designed. The difficulty is that a real account has hundreds of principals and thousands of edges, and the admin path hides in the composition. You do not defend this by memorizing twenty-one tricks. You defend it by being able to ask, continuously, "from this identity, is there any path to admin," and by making the answer no. That question is a graph search, which is why I built one.

Resources