Three AWS privilege escalation attacks, with the CloudTrail evidence and the fix
Key takeaways
- A privilege escalation attack in AWS is a sequence of API calls that takes a principal from the permissions it has to permissions it was never given. The calls are ordinary, which is why they are hard to spot.
- Three chains come up again and again in real accounts:
iam:PassRolewithlambda:CreateFunction,iam:UpdateAssumeRolePolicyfollowed bysts:AssumeRole, andiam:CreateAccessKeyon another user. - Each chain leaves specific CloudTrail events. Detection means watching for those events from principals that have no business making them, with the request parameters as the signal.
- Each chain has a narrow fix: a
iam:PassedToServicecondition, a removed permission, or a Deny on the specific action. In most cases it is one policy change. - Detection and prevention are both needed. CloudTrail tells you after the call. Validating the paths in advance tells you which principals could make the call at all.
A privilege escalation attack in AWS is a sequence of API calls that takes a principal from the permissions it holds to permissions it was never granted. There is no exploit in the usual sense. The calls are the same ones administrators make every day, which is what makes them hard to notice and easy to leave open. The AWS Privilege Escalation Map lists every permission that can be used this way. This post takes three of the most common chains and shows, for each, how it works, what CloudTrail records, and the change that breaks it.
Chain one: pass a role to Lambda
iam:PassRole and lambda:CreateFunction held together, a principal creates a function, hands it a privileged role, and the function runs as that role. Neither permission is alarming alone.Permissions. iam:PassRole on a privileged role, together with lambda:CreateFunction. Usually lambda:InvokeFunction as well, though a function can also be triggered through an event source.
How it works. The attacker creates a new Lambda function and passes the privileged role as its execution role. AWS accepts the pass because the role's trust policy allows lambda.amazonaws.com. The attacker then invokes the function, and whatever code they wrote runs with the privileged role's permissions. If that role is an administrator, the account is theirs after one create and one invoke. The same pattern works with EC2, ECS, Glue, CloudFormation, CodeBuild, and any other service that accepts a passed role.
What CloudTrail shows. A CreateFunction management event from lambda.amazonaws.com whose requestParameters.role is the ARN of a privileged role. The iam:PassRole check does not produce an event of its own; the pass is visible in the create call's parameters. The follow-on Invoke is a Lambda data event, so it only appears if the trail has Lambda data events enabled, which is off by default and worth turning on for exactly this reason. With both in place the signal is the pairing: a principal that has never created a function before creates one with a high-value role attached, then invokes it within minutes. Without data events, the signal is the create call itself and the API activity the passed role performs afterwards.
The fix. Scope the pass. Add an iam:PassedToService condition to the iam:PassRole grant so the role can only be passed to the service that needs it, and replace any wildcard in the resource ARN with the specific role. If the principal does not need to pass roles at all, remove the permission. Full write-up: Pass role to Lambda.
Chain two: rewrite a role's trust policy, then assume it
Permissions. iam:UpdateAssumeRolePolicy on a privileged role, plus sts:AssumeRole.
How it works. A role's trust policy decides who may assume it. A principal that can rewrite that document adds itself as a trusted principal, then calls sts:AssumeRole and receives temporary credentials for the role. The original trust relationship is irrelevant once it has been overwritten, so this move takes over any role in the account that the principal can reach with the update call. The rewritten trust policy stays in place until someone notices, which makes this both an escalation and a persistence mechanism.
What CloudTrail shows. An UpdateAssumeRolePolicy event from iam.amazonaws.com with the new policy document in requestParameters.policyDocument, followed by an AssumeRole event from sts.amazonaws.com for the same role, from the principal that was just added. Trust policies change rarely in a healthy account, so any UpdateAssumeRolePolicy event outside a deployment pipeline is worth a look, and one that adds a principal from inside the account is a strong signal.
The fix. Remove iam:UpdateAssumeRolePolicy from any principal that is not the IAM administration path, and put an explicit Deny on it for the privileged roles in the account, with the deployment role as the only exception. Full write-up: Rewrite role trust, then assume.
Chain three: create an access key for another user
Permissions. iam:CreateAccessKey on a user other than the caller.
How it works. The attacker calls iam:CreateAccessKey with another user's name and receives a brand-new long-lived access key for that user. They never need the user's password or existing keys. From then on they can call the API as that user, and the new key survives a password reset and a rotation of the user's other key. This one is the hardest of the three to notice, because a single call produces durable credentials and the user whose identity was taken sees nothing.
What CloudTrail shows. A CreateAccessKey event from iam.amazonaws.com where requestParameters.userName differs from the identity in userIdentity. That mismatch is the signal. Users creating keys for themselves is normal. One principal creating keys for another is rare outside an onboarding script, and the script's role is easy to allowlist. The follow-on use shows up as API calls by the victim user from a new source IP and user agent.
The fix. Remove iam:CreateAccessKey from every principal that is not the identity administration path, and where users must manage their own keys, scope the grant to ${aws:username} so a user can only create keys for themselves. Full write-up: Mint access key for user.
How to detect privilege escalation in AWS
The three chains above share a pattern that generalizes. Privilege escalation leaves ordinary events with unusual parameters, from principals that have no history of making them. Detection in CloudTrail rests on four things.
First, CloudTrail must be on for every account and every region, with the trail delivered to a central account the workloads cannot write to. An attacker who can disable or delete the trail removes the evidence, and StopLogging and DeleteTrail belong on the same watch list as the escalation events.
Second, watch the parameters and not only the event names. CreateFunction on its own is routine, while CreateFunction with an administrator role in the parameters deserves an alert. The same goes for CreateAccessKey when the target user differs from the caller.
Third, baseline per principal. A CI role that creates a function every hour and a developer who has never called IAM in a year are different sources, and the same event from each means different things.
Fourth, pair the events. Escalation is a sequence: create then invoke, update trust then assume, create key then use it from somewhere new. A rule that joins the first event to the second within a short window produces far fewer false positives than either event alone. Lambda, S3, and DynamoDB activity are data events and need to be enabled on the trail before they can be paired.
Detection is the second line
CloudTrail tells you after the call was made. The first line is knowing, before any call, which principals could make it. That means taking every chain on the map and resolving it against the real authorization in the account: identity policies, boundaries, SCPs, trust policies, and conditions. A chain that resolves to allow for a principal is a proven path, and the fix is the change above, applied before anyone tries it. Pathbreak does that resolution read-only across every account, ranks the proven paths by what they reach, and confirms each fix after it ships. The free pathbreak-audit tool lists which principals in a single account hold these permissions, as a starting point.
Frequently asked questions
What is a privilege escalation attack in AWS?
A privilege escalation attack in AWS is a sequence of API calls through which a principal gains permissions it was not granted, for example by passing a privileged role to a service it can create, rewriting a role's trust policy and assuming it, or creating an access key for a more privileged user. Pathbreak validates which principals in your AWS accounts could make these calls.
How do you detect privilege escalation in CloudTrail?
Watch for the specific events each chain produces, such as CreateFunction with a privileged role in the parameters, UpdateAssumeRolePolicy followed by AssumeRole, or CreateAccessKey where the target user differs from the caller. Baseline per principal, pair the first event with the second, and keep the trail in an account the workloads cannot modify. Pathbreak complements CloudTrail detection by showing, before any call is made, which principals could make it.
Is iam:PassRole dangerous on its own?
Not on its own. It becomes a privilege escalation path when combined with a permission that creates or updates a compute resource that accepts a role, such as lambda:CreateFunction or ec2:RunInstances, and when the target role trusts that service. Scoping the pass with iam:PassedToService and a specific role ARN removes the path. Pathbreak checks the trust policy and conditions for each pass when it validates a path.
Which fix removes the most privilege escalation paths?
In most accounts, scoping or removing iam:PassRole on privileged roles removes the largest share, because dozens of services accept a passed role. In the Pathbreak lab account a single Deny on one role removed 3,893 of 4,087 paths. The right first change depends on which principals the proven paths run through.
Find out which principals in your accounts can make these calls today, before CloudTrail has to tell you.
Request a demo Watch the demo