Skip to content

Lesson 2 of 9 · 30m

IAM privilege escalation paths that actually get used

PassRole laundering, self-modifying policies, and the indirect paths pentesters reach for first — plus how to close them.

You gave the CI role "just a few IAM permissions." Six commands later it owns the account. This is not hypothetical — Rhino Security Labs catalogued twenty-plus privilege escalation paths in AWS IAM, and penetration testers use them on nearly every engagement because they keep working. Here are the ones that actually get used, and how to close them.

The PassRole family: launder privileges through a service

iam:PassRole lets a principal hand an IAM role to an AWS service. It grants nothing by itself — but pair it with any service that runs code or mutates resources, and the role's permissions become yours:

  • lambda:CreateFunction + iam:PassRole → create a function with a privileged execution role, invoke it, and your code runs as that role.
  • ec2:RunInstances + iam:PassRole → launch an instance with a juicy instance profile, SSM or SSH in, and pull the role's credentials from the metadata service.
  • cloudformation:CreateStack + iam:PassRole → the stack acts with the passed role; a stack that creates a policy, a user, or another role escalates cleanly.
  • glue:CreateDevEndpoint, datapipeline:CreatePipeline, sagemaker:CreateNotebookInstance — same shape, different service.

The escalation in practice:

aws lambda create-function \
  --function-name diag \
  --runtime python3.12 \
  --role arn:aws:iam::123456789012:role/AdminishRole \
  --handler index.handler \
  --zip-file fileb://payload.zip
aws lambda invoke --function-name diag out.json

payload.zip just runs sts:GetCallerIdentity and whatever else you want — as AdminishRole. The defense is twofold: constrain iam:PassRole with the iam:PassedToService condition (only lambda.amazonaws.com, only specific role ARNs), and audit what roles a principal can pass — a "low-priv" role that can pass a high-priv role is not low-priv.

The self-modification family: edit your way to admin

These need no service accomplice:

  • iam:CreatePolicyVersion — write a new version of your own managed policy granting *:*. New versions become the default automatically.
  • iam:SetDefaultPolicyVersion — sneakier: roll the policy back to an older version that was broader. Nobody wrote new permissions; the history did it.
  • iam:AttachUserPolicy / iam:AttachRolePolicy / iam:AttachGroupPolicy — attach AdministratorAccess to yourself or your group.
  • iam:CreateAccessKey — mint credentials for another user, typically the admin user left over from setup. This is the classic: it converts one compromised account into every other account it can touch.
  • iam:CreateLoginProfile / iam:UpdateLoginProfile — set a console password on another user.
  • iam:AddUserToGroup — join the Admins group someone forgot to restrict.

The indirect family: borrow existing privilege

  • lambda:UpdateFunctionCode on an existing function that already runs under a privileged role — swap in your code, wait for a trigger.
  • ssm:SendCommand or ssm:StartSession to instances carrying instance profiles — the instance's role becomes your role.
  • ecs:RegisterTaskDefinition + iam:PassRole + ecs:RunTask — same laundry as Lambda.

Finding them before someone else does

Two tools worth running in a sandbox:

# enumerate-iam: non-destructively probes which IAM calls a key can make
git clone https://github.com/andresriancho/enumerate-iam
python enumerate-iam.py --access-key AKIA... --secret-key ...

# Pacu's privesc module maps escalation paths for the current principal
pacu> run iam__privesc_scan --user-only

Cloudsplaining and aws iam get-account-authorization-details help you review policies at scale for the dangerous actions listed above.

Exercise

In a sandbox account, create a user with only lambda:CreateFunction, lambda:InvokeFunction, and iam:PassRole on a specific role ARN. Walk the Lambda path above until GetCallerIdentity returns the escalated role. Then add the iam:PassedToService condition plus an iam:GetRole-driven review of passable roles, and verify the path dies.

Field notes

  • The single most-abused permission is iam:PassRole. Treat any principal that can pass a role you would not hand them directly as effectively holding that role.
  • Execution roles are routinely created as AdministratorAccess "because the tutorial said so." Audit them like admins, because they are.
  • Alert on AttachUserPolicy, AttachRolePolicy, CreatePolicyVersion, and CreateAccessKey in CloudTrail — these are loud, rare, and nearly always interesting.
  • Permission boundaries are the scalable fix: a boundary can say "you may create roles, but never one more powerful than this," which defangs PassRole delegation for developers.