Prancer Blog / Live-Fire Engagements
Inside the SwarmHack 200-IP AWS Lab: 18 Surfaces, 45 Findings, Zero Cloud-Native Detections
SwarmHack ran a full autonomous kill chain against 200 hosts across 5 AWS network tiers. 45 findings, 19 Exploited, 66 crown jewels, a 4-hop IAM trust-chain compromise — and zero GuardDuty attack detections.
Prancer Research · 2026-08-25 · 14 min
Numbers first, narrative second. In a single continuous engagement, SwarmHack swept 18 distinct application surfaces across 200 hosts in a purpose-built AWS lab. The autonomous swarm produced 45 findings — 19 Exploited (13 Critical), 25 Observed, 1 Simulated, captured 66 crown jewels, completed 4 of 4 IAM escalation hops, and did all of it while AWS GuardDuty raised zero attack-related detections.
This post is the engagement report — the surfaces, the chain, the evidence, and the detection gap.
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin:24px 0;"> <div style="border:1px solid rgba(217,160,54,0.35);background:rgba(217,160,54,0.06);border-radius:8px;padding:14px 16px;"> <div style="font-family:ui-monospace,monospace;font-size:11px;letter-spacing:.15em;text-transform:uppercase;color:#D9A036;">Surfaces</div> <div style="font-size:28px;font-weight:800;color:#F1F5F9;line-height:1.1;margin-top:4px;">18</div> </div> <div style="border:1px solid rgba(217,160,54,0.35);background:rgba(217,160,54,0.06);border-radius:8px;padding:14px 16px;"> <div style="font-family:ui-monospace,monospace;font-size:11px;letter-spacing:.15em;text-transform:uppercase;color:#D9A036;">Findings</div> <div style="font-size:28px;font-weight:800;color:#F1F5F9;line-height:1.1;margin-top:4px;">45</div> </div> <div style="border:1px solid rgba(229,72,77,0.35);background:rgba(229,72,77,0.06);border-radius:8px;padding:14px 16px;"> <div style="font-family:ui-monospace,monospace;font-size:11px;letter-spacing:.15em;text-transform:uppercase;color:#E5484D;">Exploited</div> <div style="font-size:28px;font-weight:800;color:#F1F5F9;line-height:1.1;margin-top:4px;">19</div> <div style="font-size:11px;color:#94A3B8;margin-top:2px;">13 Critical</div> </div> <div style="border:1px solid rgba(217,160,54,0.35);background:rgba(217,160,54,0.06);border-radius:8px;padding:14px 16px;"> <div style="font-family:ui-monospace,monospace;font-size:11px;letter-spacing:.15em;text-transform:uppercase;color:#D9A036;">Crown Jewels</div> <div style="font-size:28px;font-weight:800;color:#F1F5F9;line-height:1.1;margin-top:4px;">66</div> </div> <div style="border:1px solid rgba(217,160,54,0.35);background:rgba(217,160,54,0.06);border-radius:8px;padding:14px 16px;"> <div style="font-family:ui-monospace,monospace;font-size:11px;letter-spacing:.15em;text-transform:uppercase;color:#D9A036;">IAM Hops</div> <div style="font-size:28px;font-weight:800;color:#F1F5F9;line-height:1.1;margin-top:4px;">4/4</div> </div> <div style="border:1px solid rgba(229,72,77,0.35);background:rgba(229,72,77,0.06);border-radius:8px;padding:14px 16px;"> <div style="font-family:ui-monospace,monospace;font-size:11px;letter-spacing:.15em;text-transform:uppercase;color:#E5484D;">GuardDuty</div> <div style="font-size:28px;font-weight:800;color:#F1F5F9;line-height:1.1;margin-top:4px;">0</div> <div style="font-size:11px;color:#94A3B8;margin-top:2px;">attack detections</div> </div> </div>
The environment
A customer engagement in a dedicated AWS account, us-east-1, VPC 10.0.0.0/22, 200 target hosts across five isolated network tiers. Every vulnerability was a specific, nameable misconfiguration seeded by versioned bootstrap scripts — so every finding maps to an exact remediation. GuardDuty and Security Hub were enabled for the duration of the attack. VPC Flow Logs streamed to CloudWatch.
| Tier | Subnet | Hosts | Route | Role |
| --- | --- | --- | --- | --- |
| Edge (internet-facing) | 10.0.0.0/24 | 50 | IGW | Deliberately vulnerable public web apps |
| Internal (services) | 10.0.1.0/24 | 50 | none | DB / cache / API with weak credentials |
| Infra (orchestration) | 10.0.2.0/24 | 50 | none | Jenkins / Docker / k3s misconfigurations |
| Sensitive (crown jewels) | 10.0.3.0/24 | 50 | no IGW | Flags, secrets, over-privileged IAM |
| Control | 10.0.4.0/25 | ~31 | NAT only | Coordinator + 30 agent hosts |
Network topology
The sensitive tier has no route to the internet. The only way in is through the tiers in front of it — which is exactly the path SwarmHack took.
<figure style="margin:24px 0;"> <svg viewBox="0 0 980 560" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="AWS lab VPC network topology across five subnet tiers" style="width:100%;height:auto;background:rgba(15,23,42,0.4);border:1px solid rgba(148,163,184,0.15);border-radius:8px;"> <style> .t-cap{fill:#94A3B8;font:11px ui-monospace,monospace;letter-spacing:.15em;text-transform:uppercase} .t-tier{fill:#F1F5F9;font:600 13px ui-sans-serif,system-ui} .t-sub{fill:#94A3B8;font:11px ui-monospace,monospace} .t-host{fill:#CBD5E1;font:11px ui-monospace,monospace} .t-deny{fill:#E5484D;font:600 11px ui-monospace,monospace} </style> <text x="20" y="26" class="t-cap">VPC 10.0.0.0/22 · us-east-1 · 200 targets · 5 isolated tiers</text> <rect x="20" y="48" width="140" height="40" rx="20" fill="rgba(148,163,184,0.08)" stroke="rgba(148,163,184,0.45)" stroke-width="1.5" /> <text x="52" y="73" class="t-tier">Internet</text> <path d="M164 68 L246 68" stroke="#D9A036" stroke-width="2" marker-end="url(#tarr)" /> <rect x="250" y="48" width="90" height="40" rx="6" fill="rgba(217,160,54,0.10)" stroke="#D9A036" stroke-width="1.5" /> <text x="277" y="73" class="t-tier">IGW</text> <path d="M295 90 L295 112" stroke="#D9A036" stroke-width="2" marker-end="url(#tarr)" /> <rect x="20" y="112" width="560" height="80" rx="8" fill="rgba(217,160,54,0.06)" stroke="#D9A036" stroke-width="1.5" /> <text x="34" y="134" class="t-tier">Edge — 50 hosts</text> <text x="34" y="152" class="t-sub">10.0.0.0/24 · internet-routed</text> <text x="34" y="172" class="t-host">struts2 .146:8080 · tomcat .103:8080 · httpd .113 · juiceshop .100:3000</text> <text x="34" y="186" class="t-host">webgoat .196:8080 · drupal .128 · bwapp .149 · dvwa .169 · mutillidae .122</text> <text x="604" y="146" class="t-sub">33 findings</text> <text x="604" y="164" class="t-deny">9 Exploited</text> <path d="M120 194 L120 224" stroke="#E5484D" stroke-width="2" marker-end="url(#tarr2)" /> <path d="M400 194 L400 336" stroke="#E5484D" stroke-width="2" marker-end="url(#tarr2)" /> <rect x="20" y="224" width="560" height="72" rx="8" fill="rgba(148,163,184,0.06)" stroke="rgba(148,163,184,0.45)" stroke-width="1.5" /> <text x="34" y="246" class="t-tier">Internal — 50 hosts</text> <text x="34" y="264" class="t-sub">10.0.1.0/24 · no internet route</text> <text x="34" y="284" class="t-host">mysql .100:3306 (weak creds) · redis .104:6379 (no auth) · apigw .110</text> <text x="604" y="256" class="t-sub">3 findings</text> <text x="604" y="274" class="t-deny">2 Exploited</text> <rect x="20" y="336" width="560" height="72" rx="8" fill="rgba(148,163,184,0.06)" stroke="rgba(148,163,184,0.45)" stroke-width="1.5" /> <text x="34" y="358" class="t-tier">Infra — 50 hosts</text> <text x="34" y="376" class="t-sub">10.0.2.0/24 · no internet route</text> <text x="34" y="396" class="t-host">jenkins .106:8080 (script console) · docker .113:2375 · k3s kubelet .104:10250</text> <text x="604" y="368" class="t-sub">4 findings</text> <text x="604" y="386" class="t-deny">3 Exploited</text> <path d="M400 410 L400 442" stroke="#E5484D" stroke-width="2" stroke-dasharray="5 4" marker-end="url(#tarr2)" /> <text x="412" y="432" class="t-deny">sts:AssumeRole</text> <rect x="20" y="442" width="560" height="86" rx="8" fill="rgba(229,72,77,0.08)" stroke="#E5484D" stroke-width="1.5" /> <text x="34" y="464" class="t-tier">Sensitive — 50 hosts · crown jewels</text> <text x="34" y="482" class="t-deny">10.0.3.0/24 · NO IGW, NO NAT — unreachable from the internet</text> <text x="34" y="502" class="t-host">vault .112:22 (sudo NOPASSWD) · broadrole .106:22 (over-privileged role)</text> <text x="34" y="516" class="t-host">S3 crown-jewels/ bucket · /root/FLAG.txt · seeded secrets</text> <text x="604" y="474" class="t-sub">5 findings</text> <text x="604" y="492" class="t-deny">5 Exploited</text> <text x="604" y="512" class="t-sub">66 crown jewels</text> <rect x="720" y="336" width="240" height="72" rx="8" fill="rgba(148,163,184,0.04)" stroke="rgba(148,163,184,0.3)" stroke-width="1.5" stroke-dasharray="4 4" /> <text x="734" y="358" class="t-tier">Control — ~31 hosts</text> <text x="734" y="376" class="t-sub">10.0.4.0/25 · NAT only</text> <text x="734" y="396" class="t-host">coordinator + 30 agent hosts</text> <defs> <marker id="tarr" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto"> <path d="M0 0 L10 5 L0 10 z" fill="#D9A036" /> </marker> <marker id="tarr2" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto"> <path d="M0 0 L10 5 L0 10 z" fill="#E5484D" /> </marker> </defs> </svg> <figcaption style="font-size:12px;color:#94A3B8;margin-top:8px;">The lab VPC. Gold edges are internet-reachable; red edges are the lateral and identity paths SwarmHack proved.</figcaption> </figure>
Findings by tier
Sensitive was 5-for-5. Edge produced the most findings by raw count, but the highest exploit density lived deeper in the network — exactly where a real attacker profits.
<figure style="margin:24px 0;"> <svg viewBox="0 0 720 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Findings by tier bar chart" style="width:100%;height:auto;background:rgba(15,23,42,0.4);border:1px solid rgba(148,163,184,0.15);border-radius:8px;"> <style> .lbl{fill:#CBD5E1;font:12px ui-sans-serif,system-ui} .num{fill:#F1F5F9;font:600 12px ui-monospace,monospace} .cap{fill:#94A3B8;font:11px ui-monospace,monospace;letter-spacing:.1em;text-transform:uppercase} .grid{stroke:rgba(148,163,184,0.12);stroke-width:1} </style> <text x="20" y="24" class="cap">Findings per tier · exploited vs observed</text> <line x1="140" y1="60" x2="700" y2="60" class="grid"/> <line x1="140" y1="120" x2="700" y2="60" class="grid" style="stroke:none"/> <line x1="140" y1="120" x2="700" y2="120" class="grid"/> <line x1="140" y1="180" x2="700" y2="180" class="grid"/> <line x1="140" y1="240" x2="700" y2="240" class="grid"/> <line x1="140" y1="290" x2="700" y2="290" stroke="rgba(148,163,184,0.35)" stroke-width="1"/> <text x="130" y="82" class="lbl" text-anchor="end">Edge</text> <rect x="140" y="70" width="288" height="24" fill="rgba(217,160,54,0.25)" stroke="#D9A036" stroke-width="1"/> <rect x="140" y="70" width="108" height="24" fill="#E5484D"/> <text x="440" y="87" class="num">33 findings · 9 exploited</text> <text x="130" y="142" class="lbl" text-anchor="end">Sensitive</text> <rect x="140" y="130" width="60" height="24" fill="#E5484D" stroke="#E5484D" stroke-width="1"/> <text x="210" y="147" class="num">5 findings · 5 exploited</text> <text x="130" y="202" class="lbl" text-anchor="end">Infra</text> <rect x="140" y="190" width="48" height="24" fill="rgba(217,160,54,0.25)" stroke="#D9A036" stroke-width="1"/> <rect x="140" y="190" width="36" height="24" fill="#E5484D"/> <text x="200" y="207" class="num">4 findings · 3 exploited</text> <text x="130" y="262" class="lbl" text-anchor="end">Internal</text> <rect x="140" y="250" width="36" height="24" fill="rgba(217,160,54,0.25)" stroke="#D9A036" stroke-width="1"/> <rect x="140" y="250" width="24" height="24" fill="#E5484D"/> <text x="188" y="267" class="num">3 findings · 2 exploited</text> <rect x="20" y="298" width="12" height="12" fill="#E5484D"/> <text x="38" y="308" class="lbl">Exploited</text> <rect x="120" y="298" width="12" height="12" fill="rgba(217,160,54,0.25)" stroke="#D9A036" stroke-width="1"/> <text x="138" y="308" class="lbl">Observed / other</text> </svg> </figure>
Evidence grade — the honesty gate
Every SwarmHack finding is stamped with an evidence grade. Exploited means a live round-trip with captured target output. Observed means a probe response confirmed the condition but no target output was captured. Simulated means the payload was constructed but not delivered. There is no scoring inflation: an Exploited label without captured output is downgraded automatically, and Critical severity is reserved for proven evidence.
<figure style="margin:24px 0;"> <svg viewBox="0 0 640 260" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Evidence grade donut chart" style="width:100%;height:auto;background:rgba(15,23,42,0.4);border:1px solid rgba(148,163,184,0.15);border-radius:8px;"> <style> .lbl{fill:#CBD5E1;font:13px ui-sans-serif,system-ui} .num{fill:#F1F5F9;font:700 16px ui-monospace,monospace} .big{fill:#F1F5F9;font:800 36px ui-sans-serif,system-ui} .cap{fill:#94A3B8;font:11px ui-monospace,monospace;letter-spacing:.15em;text-transform:uppercase} </style> <text x="20" y="24" class="cap">Evidence grade · 45 findings total</text> <g transform="translate(150,145)"> <circle r="80" fill="none" stroke="rgba(148,163,184,0.15)" stroke-width="24"/> <circle r="80" fill="none" stroke="#E5484D" stroke-width="24" stroke-dasharray="212 502" transform="rotate(-90)"/> <circle r="80" fill="none" stroke="#D9A036" stroke-width="24" stroke-dasharray="279 502" stroke-dashoffset="-212" transform="rotate(-90)"/> <circle r="80" fill="none" stroke="#64748B" stroke-width="24" stroke-dasharray="11 502" stroke-dashoffset="-491" transform="rotate(-90)"/> <text text-anchor="middle" y="6" class="big">45</text> <text text-anchor="middle" y="26" class="cap">Findings</text> </g> <g transform="translate(300,80)"> <rect x="0" y="0" width="14" height="14" fill="#E5484D"/> <text x="24" y="12" class="lbl">Exploited</text> <text x="220" y="12" class="num" text-anchor="end">19 · 42%</text> <rect x="0" y="40" width="14" height="14" fill="#D9A036"/> <text x="24" y="52" class="lbl">Observed</text> <text x="220" y="52" class="num" text-anchor="end">25 · 56%</text> <rect x="0" y="80" width="14" height="14" fill="#64748B"/> <text x="24" y="92" class="lbl">Simulated</text> <text x="220" y="92" class="num" text-anchor="end">1 · 2%</text> <text x="0" y="132" class="cap">13 of 19 exploited findings are Critical</text> </g> </svg> </figure>
The 4-hop IAM trust chain
The lab seeded a deliberate IAM trust path from the internet-facing edge all the way to the crown-jewel S3 bucket. SwarmHack walked every hop autonomously — IMDSv2 credential harvest, Secrets Manager reads, sts:AssumeRole, S3 exfiltration — using read-only IAM primitives that produced live session keys and real flag files as proof.
<figure style="margin:24px 0;"> <svg viewBox="0 0 780 300" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Four-hop IAM trust chain" style="width:100%;height:auto;background:rgba(15,23,42,0.4);border:1px solid rgba(148,163,184,0.15);border-radius:8px;"> <style> .box-title{fill:#F1F5F9;font:600 13px ui-sans-serif,system-ui} .box-sub{fill:#CBD5E1;font:11px ui-monospace,monospace} .hop{fill:#D9A036;font:700 11px ui-monospace,monospace;letter-spacing:.1em;text-transform:uppercase} .cap{fill:#94A3B8;font:11px ui-monospace,monospace;letter-spacing:.15em;text-transform:uppercase} </style> <text x="20" y="24" class="cap">Kill chain · 4 hops from unauthenticated internet to crown-jewel S3</text> <rect x="20" y="60" width="170" height="90" rx="6" fill="rgba(217,160,54,0.08)" stroke="#D9A036" stroke-width="1.5"/> <text x="30" y="80" class="hop">Hop 1</text> <text x="30" y="100" class="box-title">Edge web RCE</text> <text x="30" y="118" class="box-sub">Struts S2-045</text> <text x="30" y="134" class="box-sub">→ IMDSv2 harvest</text> <text x="30" y="148" class="box-sub">→ edge-role creds</text> <path d="M195 105 L215 105" stroke="#D9A036" stroke-width="2" marker-end="url(#arr)"/> <rect x="220" y="60" width="170" height="90" rx="6" fill="rgba(217,160,54,0.08)" stroke="#D9A036" stroke-width="1.5"/> <text x="230" y="80" class="hop">Hop 2</text> <text x="230" y="100" class="box-title">Secrets Manager</text> <text x="230" y="118" class="box-sub">lab/internal-db-creds</text> <text x="230" y="134" class="box-sub">→ svc_internal:</text> <text x="230" y="148" class="box-sub">InternalDB-PwnMe-…</text> <path d="M395 105 L415 105" stroke="#D9A036" stroke-width="2" marker-end="url(#arr)"/> <rect x="420" y="60" width="170" height="90" rx="6" fill="rgba(229,72,77,0.08)" stroke="#E5484D" stroke-width="1.5"/> <text x="430" y="80" class="hop" style="fill:#E5484D">Hop 3</text> <text x="430" y="100" class="box-title">Jenkins RCE → STS</text> <text x="430" y="118" class="box-sub">script console + crumb</text> <text x="430" y="134" class="box-sub">→ AssumeRole</text> <text x="430" y="148" class="box-sub">sensitive-reader</text> <path d="M595 105 L615 105" stroke="#E5484D" stroke-width="2" marker-end="url(#arr2)"/> <rect x="620" y="60" width="150" height="90" rx="6" fill="rgba(229,72,77,0.15)" stroke="#E5484D" stroke-width="1.5"/> <text x="630" y="80" class="hop" style="fill:#E5484D">Hop 4</text> <text x="630" y="100" class="box-title">Crown-jewel S3</text> <text x="630" y="118" class="box-sub">crown-jewels/</text> <text x="630" y="134" class="box-sub">flags.txt</text> <text x="630" y="148" class="box-sub">exfiltrated</text> <rect x="20" y="190" width="240" height="80" rx="6" fill="rgba(229,72,77,0.08)" stroke="#E5484D" stroke-width="1.5"/> <text x="30" y="210" class="hop" style="fill:#E5484D">Sensitive tier · parallel path</text> <text x="30" y="230" class="box-title">SSH labuser → sudo NOPASSWD</text> <text x="30" y="248" class="box-sub">uid=0(root) · /root/FLAG.txt read</text> <text x="30" y="264" class="box-sub">+ IMDS session → S3 crown bucket</text> <rect x="290" y="190" width="240" height="80" rx="6" fill="rgba(229,72,77,0.08)" stroke="#E5484D" stroke-width="1.5"/> <text x="300" y="210" class="hop" style="fill:#E5484D">Infra tier · parallel path</text> <text x="300" y="230" class="box-title">Docker API 2375 → container escape</text> <text x="300" y="248" class="box-sub">alpine mount /:/host</text> <text x="300" y="264" class="box-sub">uid=0(root) on the host</text> <rect x="560" y="190" width="210" height="80" rx="6" fill="rgba(229,72,77,0.08)" stroke="#E5484D" stroke-width="1.5"/> <text x="570" y="210" class="hop" style="fill:#E5484D">GuardDuty</text> <text x="570" y="230" class="box-title">0 attack detections</text> <text x="570" y="248" class="box-sub">across container escape,</text> <text x="570" y="264" class="box-sub">AssumeRole, S3 exfil</text> <defs> <marker id="arr" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto"> <path d="M0 0 L10 5 L0 10 z" fill="#D9A036"/> </marker> <marker id="arr2" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto"> <path d="M0 0 L10 5 L0 10 z" fill="#E5484D"/> </marker> </defs> </svg> </figure>
The full attack path
Five independent entry points, one objective. Every edge below carries live evidence — a command marker, a session key, or a flag file.
<figure style="margin:24px 0;"> <svg viewBox="0 0 900 600" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Full attack path graph from five entry points to crown-jewel exfiltration" style="width:100%;height:auto;background:rgba(15,23,42,0.4);border:1px solid rgba(148,163,184,0.15);border-radius:8px;"> <style> .p-cap{fill:#94A3B8;font:11px ui-monospace,monospace;letter-spacing:.15em;text-transform:uppercase} .p-stage{fill:#D9A036;font:700 11px ui-monospace,monospace;letter-spacing:.12em;text-transform:uppercase} .p-t{fill:#F1F5F9;font:600 12px ui-sans-serif,system-ui} .p-s{fill:#94A3B8;font:10.5px ui-monospace,monospace} .p-e{fill:#E5484D;font:600 10.5px ui-monospace,monospace} </style> <text x="20" y="24" class="p-cap">Attack path · 19 Exploited findings · 66 crown jewels captured</text> <text x="20" y="52" class="p-stage">1 · Initial access</text> <text x="20" y="186" class="p-stage">2 · Execution and root</text> <text x="20" y="330" class="p-stage">3 · Credential access</text> <text x="20" y="474" class="p-stage">4 · Impact</text> <rect x="20" y="62" width="164" height="62" rx="6" fill="rgba(217,160,54,0.08)" stroke="#D9A036" stroke-width="1.5" /> <text x="32" y="82" class="p-t">Struts 2 S2-045</text> <text x="32" y="98" class="p-s">10.0.0.146:8080</text> <text x="32" y="114" class="p-e">CVE-2017-5638 RCE</text> <rect x="196" y="62" width="164" height="62" rx="6" fill="rgba(217,160,54,0.08)" stroke="#D9A036" stroke-width="1.5" /> <text x="208" y="82" class="p-t">Tomcat PUT JSP</text> <text x="208" y="98" class="p-s">10.0.0.103:8080</text> <text x="208" y="114" class="p-e">CVE-2017-12615 RCE</text> <rect x="372" y="62" width="164" height="62" rx="6" fill="rgba(217,160,54,0.08)" stroke="#D9A036" stroke-width="1.5" /> <text x="384" y="82" class="p-t">Apache mod_cgi</text> <text x="384" y="98" class="p-s">10.0.0.113</text> <text x="384" y="114" class="p-e">CVE-2021-42013 RCE</text> <rect x="548" y="62" width="164" height="62" rx="6" fill="rgba(217,160,54,0.08)" stroke="#D9A036" stroke-width="1.5" /> <text x="560" y="82" class="p-t">Docker API 2375</text> <text x="560" y="98" class="p-s">10.0.2.113 · unauth</text> <text x="560" y="114" class="p-e">mount / into container</text> <rect x="724" y="62" width="156" height="62" rx="6" fill="rgba(217,160,54,0.08)" stroke="#D9A036" stroke-width="1.5" /> <text x="736" y="82" class="p-t">SSH default creds</text> <text x="736" y="98" class="p-s">10.0.3.112 · .106</text> <text x="736" y="114" class="p-e">labuser accepted</text> <path d="M102 126 L102 152 L280 152 L280 196" stroke="#E5484D" stroke-width="1.5" fill="none" marker-end="url(#parr)" /> <path d="M278 126 L278 196" stroke="#E5484D" stroke-width="1.5" fill="none" marker-end="url(#parr)" /> <path d="M454 126 L454 152 L300 152 L300 196" stroke="#E5484D" stroke-width="1.5" fill="none" marker-end="url(#parr)" /> <path d="M630 126 L630 196" stroke="#E5484D" stroke-width="1.5" fill="none" marker-end="url(#parr)" /> <path d="M802 126 L802 166 L676 166 L676 196" stroke="#E5484D" stroke-width="1.5" fill="none" marker-end="url(#parr)" /> <rect x="196" y="198" width="248" height="70" rx="6" fill="rgba(229,72,77,0.10)" stroke="#E5484D" stroke-width="1.5" /> <text x="208" y="220" class="p-t">Web-app command execution</text> <text x="208" y="238" class="p-s">marker SWMHK12019CK; uid=0(ro…</text> <text x="208" y="254" class="p-s">PUT test-swarmhack-*.jsp → 201</text> <rect x="556" y="198" width="288" height="70" rx="6" fill="rgba(229,72,77,0.10)" stroke="#E5484D" stroke-width="1.5" /> <text x="568" y="220" class="p-t">Host root</text> <text x="568" y="238" class="p-s">container escape · sudo NOPASSWD → uid=0</text> <text x="568" y="254" class="p-s">/root/FLAG.txt read · SWARMHACK-fe04176a</text> <path d="M320 270 L320 340" stroke="#E5484D" stroke-width="1.5" marker-end="url(#parr)" /> <path d="M700 270 L700 340" stroke="#E5484D" stroke-width="1.5" marker-end="url(#parr)" /> <path d="M444 233 L556 233" stroke="#E5484D" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#parr)" /> <rect x="196" y="342" width="248" height="86" rx="6" fill="rgba(217,160,54,0.08)" stroke="#D9A036" stroke-width="1.5" /> <text x="208" y="364" class="p-t">IMDSv2 → instance role</text> <text x="208" y="382" class="p-s">edge-role session keys</text> <text x="208" y="398" class="p-s">Secrets Manager: internal-db-creds</text> <text x="208" y="414" class="p-s">→ mysql 3306 · redis 6379 reached</text> <rect x="556" y="342" width="288" height="86" rx="6" fill="rgba(217,160,54,0.08)" stroke="#D9A036" stroke-width="1.5" /> <text x="568" y="364" class="p-t">Jenkins script console → STS</text> <text x="568" y="382" class="p-s">10.0.2.106:8080 · crumb + Groovy exec</text> <text x="568" y="398" class="p-s">infra-role → sts:AssumeRole</text> <text x="568" y="414" class="p-s">→ sensitive-reader-role</text> <path d="M320 430 L320 462 L420 462 L420 484" stroke="#E5484D" stroke-width="1.5" fill="none" marker-end="url(#parr)" /> <path d="M700 430 L700 462 L560 462 L560 484" stroke="#E5484D" stroke-width="1.5" fill="none" marker-end="url(#parr)" /> <rect x="196" y="486" width="500" height="76" rx="6" fill="rgba(229,72,77,0.16)" stroke="#E5484D" stroke-width="1.5" /> <text x="208" y="508" class="p-t">Crown-jewel exfiltration — s3://…/crown-jewels/</text> <text x="208" y="526" class="p-s">66 objects retrieved · flags, secrets, seeded customer records</text> <text x="208" y="544" class="p-e">GuardDuty attack detections raised during the whole chain: 0</text> <defs> <marker id="parr" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto"> <path d="M0 0 L10 5 L0 10 z" fill="#E5484D" /> </marker> </defs> </svg> <figcaption style="font-size:12px;color:#94A3B8;margin-top:8px;">Solid edges are proven transitions (Exploited). The dashed edge is the lateral hop from an exploited web host into the infra tier.</figcaption> </figure>
Kill chain — surface by surface
Edge tier — 10 surfaces, 33 findings, 9 Exploited
Real round-trips on the class-defining bugs — no version-match handwaving where actual exploitation was possible.
- Struts 2 (S2-045) on
10.0.0.146:8080— OGNL inContent-Typeexecuted a shell; response contained the markerSWMHK12019CK; uid=0(ro…. Critical / Exploited. - Apache 2.4.49 traversal → RCE (CVE-2021-42013) on
10.0.0.113— live-confirmed traversal and RCE probe. Critical / Exploited. - Tomcat PUT JSP upload (CVE-2017-12615) on
10.0.0.103:8080—PUT test-swarmhack-*.jsp/ → 201; the follow-upGETexecuted the JSP. Critical / Exploited. - Juice Shop default admin + SQLi auth-bypass on the same REST login — JWT session token captured on both. High / Exploited each.
- WebGoat seeded account
swmhk-ca47b1fd:Swhk-123→ HTTP 302 +JSESSIONID, plus open self-registration → authenticated session. Critical / Exploited. - Drupal unauthenticated
install.phpon10.0.0.128— HTTP 200, 14,027 bytes of admin content, no auth. Critical / Exploited.
The 24 other edge findings are Observed-grade: sensitive-file exposure (/web.config, /phpinfo.php), version-intelligence CVE matches on Apache/PHP builds, and cookie-security issues. Observed never counts as exploitation — the grade earns its own line in the report.
Internal tier — 3 surfaces, 3 findings, 2 Exploited
- MySQL 5.7.44 on
10.0.1.100:3306— default credentials accepted via native-password handshake. High / Exploited. - Redis 7.4.10 on
10.0.1.104:6379— unauthenticatedPING → +PONG. Critical / Exploited. - The API gateway on
10.0.1.110had no dedicated no-auth-API agent this run — logged as an honest miss.
Infra tier — 3 surfaces, 4 findings, 3 Exploited
- Jenkins script console RCE on
10.0.2.106:8080— CSRF-crumb dance, boundedprintlnmarker:SWARMHACK-fe04176aechoed by the console. Critical / Exploited. - Jenkins → IAM trust-chain pivot —
sts:AssumeRolesessionswarmhack-lab-demoassumedswarmhack-sensitive-reader-role; with the assumed credentials,secretsmanager get-secret-value lab/crown-jewelsreturnedcrown_jewel_api_key=CROWN-JEWEL-FLAG-7f3a9, thens3 cp crown-jewels/flags.txtexfiltrated tagged flag lines. Critical / Exploited. - Docker Engine container escape on
10.0.2.113:2375— anonymous API; escape container mounted/:/hostand returneduid=0(root) gid=0(root) groups=0(root). The escape container was removed after proof. Critical / Exploited. - k3s kubelet on
10.0.2.104:10250— anonymous/podsdisclosed a privilegedroot-hostpathpod; independently re-verified. The finding was dropped by a merge bug in the run's mission-report (filed and being fixed) — the exposure itself is real and in the agent telemetry.
Sensitive tier — 2 surfaces, 5 findings, 5 Exploited
- Passwordless sudo root on
vault · 10.0.3.112:22—sudo -n -l→(ALL) NOPASSWD: ALL; escalation produceduid=0(root) … context=unconfined_u:unconfined_r:unconfined_t;/root/FLAG.txtread →FLAG-CROWN-VAULT-ip-10-0-3-112.ec2.internal. Critical / Exploited. - Over-privileged instance role on the same host reads the crown-jewel S3 bucket directly (
s3://swarmhack-lab-crown-…/flags.txt). Critical / Exploited. - Broadrole host — separate SSH foothold, same S3 chain via
s3:*instance role. Critical / Exploited.
Live-built capabilities
Five agents shipped mid-engagement to close the remaining gaps — all committed under feat/aws-lab-gap-agents:
jenkins_console— anonymous console detection, CSRF-crumb dance, boundedprintlnmarker round-trip.docker_api_escape— 2375 API → alpine container mounting/:/host→idas host root → container removed after.kubelet_anonymous— anonymous 10250/podsinventory, privileged hostPath pod audit, legacy/runprobe.os_privesc— over an SSH foothold:sudo -n -lNOPASSWD parse, SUID GTFOBins audit, writable cron; root proved by flag read.aws_pivot— IMDSv2 token-dance credential harvest, Secrets Manager read,sts:AssumeRolechain, S3 exfil; SSH and Jenkins-console transports with sentinel-extracted output.
The engineering lessons that came out of the run — Jenkins CSRF crumbs, universal IMDSv2 token dance, GOAP precondition unblocking the console transport, Groovy '''-quoting to stop $-interpolation eating bash variables, sentinel-wrapped console output, and bypassing a seeded fake static-credentials file with AWS_SHARED_CREDENTIALS_FILE=/dev/null — are all in the tree.
Detection gap
Across the entire attack — internet-to-crown-jewel port scans, unauthenticated Jenkins RCE, container escape, IMDS credential harvest, AssumeRole trust-chain traversal, and S3 exfiltration — AWS GuardDuty raised zero findings. The only two GuardDuty findings in the engagement window are unrelated low-severity SSH-scanner noise against two edge hosts.
"Detected by our cloud-native tooling" is a promise buyers hear often. In a controlled, GuardDuty-enabled environment executing a real end-to-end attack, that promise did not hold. Detection has to be validated the same way exploitation is — with evidence, not vendor narrative.
Vetting and honest misses
Every Exploited-grade claim was cross-checked against the seeded ground truth (bootstrap scripts + IAM seed): marker strings, credential pairs, flag strings, and IAM artifacts must appear in the finding's own evidence. The three live IAM session keys are real working credentials harvested from instance metadata; they expired within hours and were used for read-only proof only.
Honest misses this run:
edge-bodgeitproduced no findings — the surface scored zero and it says so in the report.- The internal API gateway needs a dedicated no-auth-API agent (roadmap).
- DVWA / bWAPP / mutillidae stayed at Observed grade this run.
- IMDS credential harvest landed on 2 of 3 qualifying footholds — a metadata token-rate sensitivity, fixed after the engagement.
Downgrades and misses shipping in the report is the point. An engagement report without them is a marketing artifact, not evidence.
Takeaways
- Coverage that goes deep, not wide. 200 hosts, 18 surfaces, one autonomous run — with real exploitation on the class-defining bugs at every tier.
- Grades that hold up. Exploited, Observed, Simulated stay distinct in the ledger. Critical severity is reserved for proven evidence. Version-match guesses cannot masquerade as exploitations.
- Full-chain proof, end to end. Unauth edge RCE → service credentials → host root → IAM credential holder — walked and captured, with the exfiltrated flag strings in the report.
- Detection is not a substitute for validation. GuardDuty did not raise the alarm on any part of this attack. If your program depends on cloud-native detection catching a real end-to-end chain, prove it in your environment before you promise it to a board.
See SwarmHack for the platform, or book a demo to run a scoped engagement against your own estate.