Module 03 — IAM Attack Paths¶
Type 3 · Blast-Radius Trace (+ Type 4 · Audit→Build→Verify) — model the account as a graph and predict which ordinary permissions chain to admin. (Secondary: Audit→Build→Verify — find the minimum cut-set, implement it, and re-run the graph to prove the edge is gone.) Go to the hands-on lab →
Last reviewed: 2026-06
Cloud & Container Security — privilege escalation in the cloud isn't a vulnerability; it's a path through a graph that legitimate permissions drew for you.
In 60 seconds
Privilege escalation in the cloud isn't a vulnerability — it's a path through a graph that legitimate permissions drew for you. Rhino's "21 methods" are 21 combinations of permissions AWS grants exactly as intended that nonetheless let a low-privilege principal become admin, and the killer property is that they're invisible to flat policy review: no single step looks alarming, the composition is total compromise. Model the account as a directed graph — principals are nodes, an edge means "A can become B" — and privesc collapses to reachability to an admin node. The deliverable is the minimum cut-set (the smallest edge removal that disconnects all paths), implemented and re-run to prove the path is gone.
The research¶
In 2018, Rhino Security Labs published a catalogue that should be uncomfortable reading for anyone
who signs off on IAM policies: "AWS IAM Privilege Escalation — 21 Methods".
Not 21 bugs in AWS — 21 combinations of permissions that AWS grants exactly as intended and that
nonetheless let a low-privilege principal become administrator. iam:CreatePolicyVersion lets you
write a new version of a policy you're attached to — so set it to Allow *:*. iam:PassRole plus a
launch action (ec2:RunInstances, lambda:CreateFunction) lets you hand an admin role to a compute
resource you control and become it. iam:AttachUserPolicy lets you attach AdministratorAccess to
yourself. Each is one line in a policy. None is a CVE. Every one is admin.
The catalogue's real lesson isn't the list — it's that these paths are invisible to policy review.
A human reading flat policy documents one at a time cannot see that user A can assume role B, which can
pass role C to a Lambda, which has iam:*. No single step looks alarming; the composition is total
compromise. So this module turns on a prediction, and the naive guess is reliably wrong:
Given a menu of ordinary-looking permissions, which ones chain to admin — and how many hops away is "admin" from a principal that has none of the obvious red-flag grants?
Your job¶
By the end of this module you'll model an entire account as a directed graph and predict its blast
radius: principals are nodes, and an edge from A to B means A holds a permission that lets it become
B. You'll build the graph with pmapper/cloudfox, walk a real multi-hop chain from a
low-privilege user to admin, find the minimum cut-set — the smallest set of edges whose removal
disconnects every path to admin — then implement the cut and re-run the graph to prove the edge is
gone. That last beat is what separates an assessment from a report: in IAM, "fixed" means the path no
longer exists in the graph, not that someone wrote a recommendation.
Call it before you read on¶
Don't scroll. Write down your gut answers — being wrong here is the teaching event, and you'll grade yourself in the lab.
Q1. Of these four permissions, which chain to admin:
iam:CreatePolicyVersion,s3:GetObject,iam:PassRole,cloudwatch:GetMetricData? (Two are escalation primitives; two are inert.)Q2.
dev-alicehas no admin grant, noiam:*, and can't attach a policy to herself. She can onlysts:AssumeRoleone Lambda role. Is she safe — and if not, how many hops to admin?Q3. Two separate paths run from
dev-aliceto admin. You scopeiam:PassRoleon the role in path 1. Is the account fixed?
The graph, revealed¶
Hold your answers against these.
Q1 — escalation is a permission type, not a permission name. iam:CreatePolicyVersion and
iam:PassRole are escalation primitives because they let a principal change what it (or a resource it
controls) is allowed to do — they create edges in the graph. iam:CreatePolicyVersion lets you author
Allow *:* into a policy you're already attached to; iam:PassRole lets you donate a more-powerful
role to compute you launch. s3:GetObject and cloudwatch:GetMetricData read data — they're reach, but
they're terminal: they grant no new permissions, so they draw no edges. The skill Rhino's catalogue
trains is reading a permission and asking "does this let the holder grant itself or a resource more
power?" If yes, it's an edge. The 21 methods are 21 answers to that one question.
The mental model
The account is a directed graph: principals are nodes, an edge from A to B means "A holds a
permission that lets it become B," and privilege escalation is just reachability to an is_admin
node. "Find all privesc paths" becomes graph search from every principal to every admin node —
milliseconds, not an afternoon of squinting at JSON.
The gotcha
The danger is never one over-grant — it's the edges between principals. dev-alice with no
iam:*, no admin, and a single sts:AssumeRole can still be two hops from admin. Flat,
policy-by-policy review cannot see multi-hop chains; that blind spot is the entire reason pmapper
exists.
Q2 — she is two hops from admin, and nothing she holds looks dangerous. dev-alice can assume
LambdaRole (hop 1, a plain sts:AssumeRole the trust policy permits). That role holds
iam:PassRole and lambda:UpdateFunctionConfiguration — so she updates an existing Lambda's execution
role to AdminRole and invokes it (hop 2). Now her code runs with iam:* and s3:*. No
single principal in that chain has an obvious over-grant; the escalation lives in the edges between
them, which is exactly why flat policy review misses it and why pmapper exists.
flowchart LR
A(["dev-alice<br/>no iam:*, no admin"])
L["LambdaRole<br/>iam:PassRole, UpdateFunctionConfiguration"]
F["Lambda function<br/>(re-pointed to AdminRole)"]
Adm["AdminRole<br/>iam:* s3:*"]
A -- "hop 1: sts:AssumeRole" --> L
L -- "hop 2: PassRole + update + invoke" --> F
F -- "runs as" --> Adm
The mental model to
keep: the account is a directed graph, an edge is a permission that lets one principal become another,
and privilege escalation is just reachability to an admin node. Once you see it that way, "find all
privesc paths" becomes "graph search from every principal to every is_admin node" — milliseconds, not
an afternoon of squinting at JSON.
Q3 — no, and this is the whole reason "cut-set" is the right word. Breaking one edge severs one
path; if a second path reaches admin through different edges, the account is still compromised. The
minimum cut-set is the smallest set of edges whose removal disconnects all paths from the source
to every admin node — a classic graph operation, and the actual deliverable of an IAM assessment. The
graph tells you which edge is cheapest to cut (usually scoping an iam:PassRole Resource from *
to the one role legitimately needed, or adding an iam:PassedToService condition, or removing an
over-broad principal from a trust policy); the discipline of the minimal, targeted change tells you
not to cut more than disconnects the graph. And — the beat that makes the verdict yours — you don't stop
at naming the cut. You apply it and re-run the analysis: a fixed account is one where the path finder
reports no paths to admin, proven, not promised.
Go deeper: why 'cut-set,' not 'cut'
Severing one edge breaks one path. If a second path reaches admin through different edges, the
account is still compromised — so the deliverable is the minimum cut-set: the smallest set of edges
whose removal disconnects all source-to-admin paths. The graph tells you which edge is cheapest
(usually scoping an iam:PassRole Resource from *, adding an iam:PassedToService condition, or
pruning a trust policy); the discipline of the minimal change tells you not to cut more than the
graph requires.
AI caveat
A model is a fast first-pass on single-hop edges and a strong drafter of the CISO summary — but it
reliably misses three-hop chains and hallucinates edges from services it doesn't fully model (Lambda
execution, ECS task roles). Treat its path list — and every claimed absence of a path — as a
hypothesis, and validate against the pmapper/cloudfox graph. You own the graph.
Learn (~4 hrs)¶
Richer than a foundations module: the graph model here is the backbone of the Phase-1 project and the capstone, so the time is well spent. Read Rhino's catalogue first — it's the source the tools encode.
The escalation catalogue (~1.5 hrs) - Rhino Security Labs — AWS IAM Privilege Escalation — 21 Methods (~1 hr) — the primary research this whole module rests on. Read the PassRole, CreatePolicyVersion, AttachUserPolicy, and AssumeRole sections closely (those are the edges in the lab graph); skim the rest as a reference for what a privesc edge looks like. - MITRE ATT&CK T1548 — Abuse Elevation Control Mechanism: Cloud and T1078.004 — Valid Accounts: Cloud Accounts (~30 min) — the technique IDs your finding cites; map each lab hop to one.
The graph model and the tools (~1.5 hrs)
- tecRacer — Map out your IAM with PMapper (~30 min) — a walkthrough of why modeling IAM as a directed graph is the right abstraction: it works a concrete multi-hop chain (a developer edits a Lambda, borrows its existing role, mints an admin policy) that flat, policy-by-policy review would never surface.
- pmapper — README (NCC Group) (~40 min) — read "how it works," then pmapper graph create, pmapper analysis, and pmapper query. This is the tool that turns the model above into a query.
- BishopFox — cloudfox README (permissions, role-trusts) (~20 min) — the enumeration accelerator you'll use to corroborate the graph against the live policies.
The scenario range (~1 hr)
- CloudGoat — README and the iam_privesc_by_* scenarios (Rhino Security Labs) (~1 hr) — the same author's attack range; read one iam_privesc_by_rollback or iam_privesc_by_key_rotation scenario writeup so you've seen a privesc path run end-to-end in a real account, where IAM is actually enforced. The stretch re-runs the lab there.
Key concepts¶
- The account is a directed graph: principals are nodes, an edge means "A can become B," privesc is reachability to an
is_adminnode - An escalation primitive is any permission that lets the holder grant itself/a resource more power (
iam:PassRole,iam:CreatePolicyVersion,iam:AttachUserPolicy,sts:AssumeRole); reach-only permissions draw no edges - Multi-hop paths are invisible to flat policy review and obvious to graph search — this is why
pmapper/cloudfoxexist - A minimum cut-set is the smallest set of edges whose removal disconnects all paths to admin; one cut edge is not a fix if a second path survives
- A fix is proven only when the re-run graph reports no paths to admin — implement the cut, don't just recommend it
- ATT&CK mapping: T1548 (elevation) and T1078.004 (valid accounts)
AI acceleration¶
Give a model the principals, their policies, and the role trust policies and ask it to enumerate every
path to admin. It's a fast first-pass on single-hop edges and a strong drafter of the CISO summary — but
it has a known failure mode this module exists to expose: it reliably misses three-hop chains and
hallucinates edges from services it doesn't fully model (Lambda execution, ECS task roles). Treat its
path list as a hypothesis, never the source of truth: validate every reported path against the
pmapper/cloudfox graph, and confirm every absence of a path the same way. The judgment the model
can't do for you is the minimum cut — the smallest edge removal that disconnects all paths without
breaking the principal's real job — and the re-run that proves it. You direct it; you own the graph.
Check yourself
- Of
iam:CreatePolicyVersion,s3:GetObject,iam:PassRole,cloudwatch:GetMetricData— which draw edges in the graph, and what single question separates an escalation primitive from an inert permission? dev-alicehas no admin grant, noiam:*, and can only assume one Lambda role. Why is she not safe, and where does the escalation actually live?- You scope
iam:PassRoleon the role in one of two paths fromdev-aliceto admin. Is the account fixed, and what makes "cut-set" the right word?
Comments
Sign in with GitHub to comment. Choose the type: Feedback (errors or suggestions on this page) · Hints (help for fellow learners — no spoilers) · General (anything else).