Chapter 6 — Cloud IAM in depth: policy evaluation and privilege-escalation paths
In one sentence
An IAM misconfiguration is a privilege-escalation path when a principal has a permission that lets it grant itself, or assume, more permission — and finding every such edge in an account is a graph problem with well-known primitives.
Where we are
Chapter 5 showed the identity plane. This chapter is the offensive and defensive core of cloud security: how a low-privileged principal becomes account-admin through a chain of individually-reasonable permissions.
How it works
AWS policy evaluation, precisely (commit this to memory):
1. Start: implicit DENY.
2. Is there an explicit DENY that matches? (in ANY of: SCP, identity
policy, resource policy, permission boundary, session policy, VPC
endpoint policy) -> DENY. Nothing overrides an explicit deny.
3. SCP: for a member account, the action must be allowed by SCPs.
(Management account is not restricted by SCPs.)
4. Resource-based policy: an Allow here can grant access even with no
identity policy (same account). Cross-account: need Allow on BOTH
sides.
5. Identity-based policy: an Allow here grants access (subject to the
rest).
6. Permission boundary: if attached, the action must ALSO be allowed by
it (it's a ceiling, not a grant).
7. Session policy: if present (AssumeRole with --policy), must ALSO
allow.
FINAL: allowed only if some Allow applies AND no Deny applies AND every
applicable ceiling (SCP, boundary, session) also allows.
The privilege-escalation primitives (AWS — the classic set; each is an edge "principal with permission X -> effectively admin or another role"):
GRANT YOURSELF MORE (on your own user/role):
iam:CreatePolicyVersion (+ SetAsDefault) -> rewrite an attached policy
iam:SetDefaultPolicyVersion -> roll to an old permissive version
iam:AttachUserPolicy / AttachRolePolicy -> attach AdministratorAccess
iam:PutUserPolicy / PutRolePolicy -> inline an allow-* policy
iam:AddUserToGroup -> join an admin group
iam:CreateAccessKey (on another user) -> steal a more-privileged user
iam:CreateLoginProfile / UpdateLoginProfile-> set a console password on a
privileged user
iam:UpdateAssumeRolePolicy -> make a privileged role trust you
PASS A ROLE TO A COMPUTE SERVICE YOU CONTROL (iam:PassRole + ...):
+ ec2:RunInstances -> launch an instance with an
admin role, read its creds
+ lambda:CreateFunction + Invoke (or an
event-source mapping) -> run code as an admin role
+ glue:CreateDevEndpoint / sagemaker /
cloudformation / datapipeline / ecs -> same idea, different service
lambda:UpdateFunctionCode (existing fn
with a good role) -> overwrite its code
ASSUME:
sts:AssumeRole where a role's trust policy is `Principal: *` or
over-broad -> just assume it
GCP equivalents:
iam.serviceAccounts.actAs + (compute.instances.create /
cloudfunctions.create / run.services.create / ...) -> run as a
more-privileged service account (the default Compute SA is Editor!)
iam.serviceAccounts.getAccessToken / signJwt / signBlob -> mint tokens
for another SA directly
iam.serviceAccountKeys.create -> long-lived key for another SA
resourcemanager.projects.setIamPolicy (or iam.roles.update) -> grant
yourself Owner
deploymentmanager / cloudbuild jobs run as a Google-managed SA that is
often project Editor -> submit a job that escalates
Azure equivalents:
Microsoft.Authorization/roleAssignments/write (Owner or User Access
Administrator) -> assign yourself Owner
Assign a privileged MANAGED IDENTITY to a VM / automation account you
control, then pull its token from IMDS
Entra: Application Administrator / Cloud Application Administrator ->
add a secret to a privileged service principal -> auth as it
Entra: Privileged Role Administrator -> grant yourself Global Admin
VM run-command / custom-script-extension -> local exec -> MI token
Defence:
[ ] NO wildcards on iam:* actions in identity policies. Especially not
iam:PassRole with Resource:* -- scope PassRole to specific role ARNs
and add an iam:PassedToService condition.
[ ] Permission boundaries on every non-admin human and CI role -> caps
the ceiling so even a privesc primitive can't exceed the boundary.
[ ] SCPs that deny: iam:CreateUser, iam:CreateAccessKey (outside a
break-glass path), cloudtrail:StopLogging, disabling GuardDuty/
Config, leaving the org, modifying the log-archive bucket, using
regions you don't operate in, root actions.
[ ] Role trust policies: never `Principal: "*"`; use exact account/role
ARNs + `sts:ExternalId` for third parties (confused-deputy).
[ ] Analyse continuously: run PMapper / cloudsplaining / Access Analyzer
"unused access" in CI; fail on new privesc edges.
[ ] Prefer permission sets via IAM Identity Center (SSO) over IAM users;
zero standing admin (JIT elevation).
Worked example
A three-hop escalation in a sandbox, and the one condition that kills it.
START: you have creds for `ci-deployer`, intended to deploy Lambda.
Its policy: Allow lambda:*, iam:PassRole on Resource "*".
HOP 1 enumerate: `aws iam list-roles` -> find `ops-admin-role`
(AdministratorAccess), trusted by lambda.amazonaws.com.
HOP 2 create a function passing that role:
aws lambda create-function --function-name x --runtime python3.12
--role arn:aws:iam::ACCT:role/ops-admin-role
--handler x.h --zip-file fileb://x.zip
(x.zip: a handler that calls `boto3` to attach
AdministratorAccess to `ci-deployer`, or just returns STS creds)
HOP 3 invoke it: `aws lambda invoke --function-name x out.json`
-> code runs AS ops-admin-role -> you are now account admin.
THE KILL: change `ci-deployer`'s policy so PassRole is
Resource: "arn:aws:iam::ACCT:role/lambda-exec-*" AND
Condition: { StringEquals: { "iam:PassedToService":
"lambda.amazonaws.com" } }
Now `ci-deployer` can only pass narrowly-named exec roles to Lambda,
and `ops-admin-role` (not matching the name pattern) can't be passed.
Hop 2 fails with AccessDenied. Path broken with a resource scope + one
condition. Re-run PMapper -> the edge is gone.
Real-world scenario: a real cross-tenant privilege-escalation CVE
In 2021, Wiz Research disclosed "ChaosDB": a chain of vulnerabilities
in Azure Cosmos DB's built-in Jupyter Notebook feature (Notebooks)
that let an attacker, with no prior access to a victim's account at
all, obtain the PRIMARY KEYS of ARBITRARY OTHER CUSTOMERS' Cosmos DB
instances -- full read/write/delete access to their databases.
The root cause was a misconfiguration in how the Notebooks feature
handled certificate validation and container isolation, letting a
researcher escape their own container and reach a management
interface that (this chapter's exact theme) had FAR TOO MUCH implicit
trust in "whatever called this API is authorized for whatever it
asks." Microsoft's own response included rotating a very large number
of customer keys as the remediation -- itself a real demonstration of
why Chapter 44's "can you actually rotate this key without an outage"
question matters at Azure's own scale, not just yours.
THE LESSON FOR THIS CHAPTER: this was not a leaked credential or a
phished admin -- it was a genuine PRIVILEGE-ESCALATION PRIMITIVE built
into a managed service's own feature surface, discovered by external
researchers rather than the vendor. It's a useful reminder that
Ch 6's privesc-hunting discipline (continuous graphing, not a one-time
audit) applies to what your CLOUD PROVIDER exposes to you, not only to
the IAM policies you author yourself.
Practice (75 min) — sandbox account only
- Deploy IAM Vulnerable (BishopFox) or a CloudGoat IAM scenario into your sandbox.
- With the provided low-priv creds, escalate to admin. Do it two different ways (a "grant yourself" primitive and a "PassRole + compute" primitive).
- Run
cloudsplainingagainstget-account-authorization-detailsoutput and PMapper (pmapper graph createthenpmapper query "who can do iam:PutUserPolicy with *"andpmapper argquery --preset privesc). Confirm the tools find the paths you used. - Apply the fixes: scope
PassRole, addiam:PassedToService, attach a permission boundary, add SCP denies. Re-run PMapper — paths gone. - Add a GuardDuty detector; re-run one escalation; note which finding fires
(
PrivilegeEscalation:IAMUser/AdministrativePermissions) and its latency. - Tear down (
cloudgoat destroy/terraform destroy); check the budget.
Common mistakes
iam:PassRolewithResource: "*". The single most common cloud privesc enabler. Always scope it and addiam:PassedToService.- Trust policies with
Principal: "*"or a bare account root without a condition — anyone in that account (or anyone, with*) can assume it. - Relying on "they only have
lambda:*" / "justiam:Get*". Read permissions enable enumeration; a single write primitive completes the chain. - No permission boundaries. Without a ceiling, one privesc primitive is game over. With one, the blast radius is capped.
- Analysing IAM once. Policies drift every sprint. Put PMapper/cloudsplaining in CI.
- Forgetting the management account. SCPs don't restrict it — nothing should run there, and access to it should be break-glass only.
Check yourself
- Recite AWS policy evaluation order. What can never be overridden?
- Give three "grant yourself more" privesc primitives and three "PassRole + service" ones.
- What two things scope
iam:PassRolesafely? - What does a permission boundary do, and why does it blunt privesc even when a primitive exists?
- Name the GCP permission that is the rough equivalent of
iam:PassRole, and why the default Compute service account makes it dangerous. - Which account in an AWS org is not restricted by SCPs, and what follows from that?
Further reading
- Primary: Rhino Security Labs, "AWS IAM Privilege Escalation – Methods and
Mitigation" (Spencer Gietzen) — the canonical primitive list; and the
aws_escalatetool. - Tools: PMapper (NCC Group), Cloudsplaining (Salesforce), AWS IAM Access
Analyzer (external access + unused access + policy validation),
iamlive(generate least-privilege policies from observed calls). - GCP: "Privilege Escalation in GCP" (Rhino Security Labs /
GCP_IAM_ Privilege_Escalationrepo); Google's "IAM securely" best-practices. - Azure: "Azure Privilege Escalation" write-ups (XPN, SpecterOps
AzureHound/BloodHoundAzure edges); MicroBurst tooling. - Practice:
flaws2.cloud(attacker + defender tracks),cloudgoat,IAM Vulnerable,pwnedlabs.io.