✓ Human-authored analysis; AI used for formatting and proofreading.
Rhino Security Labs built CloudGoat to train red teams on AWS attack paths. Each scenario deploys real, exploitable infrastructure with a documented escalation route. The scenarios are adversary-designed that is built by pentesters, for pentesters.
We pointed a static analyzer at one of those scenarios. Not at the running infrastructure. At a JSON snapshot of the configuration. Without credentials, API calls or exploit execution.
The analyzer found the documented attack path.
The scenario
iam_privesc_by_attachment is a one-hop privilege escalation. A low-privilege IAM user named raynor has one dangerous permission: iam:AttachUserPolicy. An admin-grade managed policy (AdministratorAccess) exists in the account. The attack is one API call:
aws iam attach-user-policy \
--user-name raynor \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess
After that call, raynor has full administrator access. No exploit chain. No vulnerability. Just a permission that shouldn't exist on that principal. The same structural pattern as the Datadog iam:CreateAccessKey scenario, but through a different IAM mechanism.
The scenario tests whether a security tool can detect the latent configuration that enables this, before the attach call happens. The dangerous state isn't the exploit. It's the pre-existing permission that makes the exploit possible.
What the analyzer found
13 findings across 28 assets. The one that matters:
CTL.IAM.ESCALATE.ATTACHUSERPOLICY.001 on user raynor:
The control identified that raynor has iam:AttachUserPolicy scoped to reach a policy that grants administrator access. The finding describes the exact mechanism: the user can attach a managed policy to themselves, and an attachable admin policy exists in the account. One API call to full admin.
The analyzer also assembled a compound chain: iam_privesc_by_attachment at critical severity, linking the AttachUserPolicy permission to the existence of the attachable admin policy. The chain represents the attack path as a composition with not just "this user has a dangerous permission" but "this permission, combined with this policy's existence, creates a one-hop escalation to admin."
Three engines, same answer
The finding was exported as structured facts and fed to two additional formal engines:
| Engine | Foundation | Result | Time |
|---|---|---|---|
| Stave CEL | Predicate evaluation + chain engine | Chain assembled: iam_privesc_by_attachment [critical] |
— |
| Soufflé | Datalog enumeration |
can_escalate_via_attach(raynor, attach_user_policy) derived |
<1s |
| Z3 | SMT satisfiability |
sat — existence proof confirmed |
0.28s |
Three engines built on different mathematical foundations. All three identified the same principal (raynor), the same mechanism (attach_user_policy), and the same escalation target (admin). Zero disagreements.
The Soufflé result is independently derived with custom Datalog rules (privesc-reachability.dl) that define reachability over the IAM permission graph. The rules weren't copied from the analyzer's control logic. They encode the same security property in a different formal language and arrive at the same conclusion from the same facts.
What this validates
CloudGoat is adversary-designed. The scenario wasn't built to test scanners. It was built to train attackers. The attack path is documented, tested, and known to work. Finding it from static configuration means the analyzer identifies the same structural risk a red team would exploit, without executing the exploit.
Three things this proves that the other labs (Datadog, NCC Group SadCloud) didn't:
Compound chain detection on an adversary-designed path. The Datadog lab produced two correct findings but chains: null — the endpoints were identified but not linked. The NCC Group lab produced one compound chain (cloudwatch_detection_broken) from a CSPM misconfiguration. CloudGoat produced a compound chain on a privilege escalation path designed by pentesters. The chain engine correctly linked the permission (AttachUserPolicy on raynor) to the target (attachable admin policy) into a single critical-severity compound finding.
The enrichment gap is real and solvable. The first evaluation pass produced 14 findings but 0 chains. The existing ATTACHUSERPOLICY.001 control was in the catalog, but it couldn't fire because the observation didn't include the IAM user's policy-shape properties. The observation builder needed enrichment: seven escalation properties (attach_user_policy_self.present, attach_role_policy.present, etc.) added to the IAM user asset. After enrichment, the control fired immediately. The control was always correct. The observation was incomplete. This confirms the architecture: the evaluator checks what it's given. The collector's job is to give it the right inputs.
Labs find bugs that unit tests don't. Five engine bugs surfaced during this lab. A false positive from missing evidence treated as confirmed exposure, a risk-signals path that ignored asset-type scope, a content-addressed cache that didn't key on eval version, and two related issues in control metadata. None of these would have been caught by unit tests or by the CSPM-focused SadCloud lab. CloudGoat's scenario structure and the sandbox account's pre-existing resources created the specific conditions that exposed them. All five were fixed, and the final evaluation is: 13 findings, 1 chain, 0 false positives.
The numbers
Scenario: CloudGoat iam_privesc_by_attachment
Assets captured: 28
Findings: 13
Compound chains: 1 (critical)
False positives: 0 (was 1 before PREFIX fix, now 0)
Engines confirmed: 3 (CEL, Soufflé, Z3)
Engine disagreements: 0
Bugs found and fixed: 5
Credentials required: 0 (snapshot-based)
Lab testing progress
Vendor Status Detected Engines
──────────── ────── ──────── ───────
Datadog 2 of 4 labs 7/7 CEL only
Bishop Fox COMPLETE 30/30 CEL only
NCC Group COMPLETE 57/57 CEL + Soufflé + Z3
Rhino Security 1 of 6 13/13 CEL + Soufflé + Z3
Overall: 3.2 of 4 vendors
107 findings verified
3 engines confirmed
0 disagreements across engines
The remaining 5 CloudGoat scenarios test progressively harder paths: cross-service escalation (STS, ECS, Lambda), multi-hop chains, and parallel attack paths. The iam_privesc_by_attachment scenario was the simplest with one hop, one mechanism, one principal. The compound chain engine's value increases as the paths get longer and the compositions get more complex.
The tool is Stave. Static analysis without credentials or agents. Files in, findings out.
Top comments (0)