DRAFT — automation harness for building a blind, gradeable, vulnerable Active Directory lab.
ad-lab-forge builds a fully unattended Windows Server domain controller inside VMware,
driven from a non-admin Windows host over WinRM/NTLM, then deploys a randomized vulnerable
Active Directory using two well-known external generators (BadBlood + Vulnerable-AD), and finally
seals a ground-truth answer key so the lab can be attacked "blind" and graded objectively.
The harness orchestrates the whole lifecycle of a throwaway AD attack range:
- Unattended OS install — an
autounattend.xmlanswer file installs Windows Server with autologon, enables WinRM + RDP, and (if present) installs VMware Tools. - Host-driven configuration — a small Python orchestrator (
lab_build.py) pushes and runs PowerShell inside the guest over WinRM, so the host never needs local admin or domain join. - Promotion — the guest promotes itself to a forest root DC (detached as SYSTEM so it survives the dcpromo reboot), optionally adds AD CS (Enterprise Root CA), and lays down a university-style OU/group/service-account structure.
- Arming the blind layer — the external randomized generators run, so every build produces a different set of misconfigurations and attack paths.
- Hardening to "apex" — the easy/known wins (AS-REP roasting, crackable Kerberoast, weak service creds) are stripped so only a deep, chained, emergent path survives.
- Sealing ground truth — the builder enumerates the armed state once and writes an answer key plus a machine-readable attack graph, which are moved out-of-band so the attacker never sees them.
- Non-admin host control. The host drives the guest entirely over WinRM/NTLM (
lab_build.py,winrm_run.py) — no local admin on the host, no TrustedHosts gymnastics, no domain join. Files are pushed as chunked base64; long-running steps run as detached SYSTEM scheduled tasks so they survive reboots. - Randomized → genuinely blind. The vulnerable surface comes from two external randomized generators (BadBlood, Vulnerable-AD). Because the author of the lab is not the author of the generators and every run differs, the attack session can be truly blind.
- Sealed ground-truth grading. After arming, the builder records the exact roastable accounts,
delegations,
adminCount=1principals and dangerous ACEs into an answer key, plus a compact JSON attack graph that the included Python solvers (graph_solve.py,verify_path.py) use to confirm a foothold → Domain Admin path exists and how long it is. This makes a blind attempt objectively gradeable. - Snapshot round-reset. Snapshot the armed VM (e.g.
vmrun snapshot) so each attack "round" can be reset to an identical starting state; the assumed-breach foothold can be re-created after any revert. (Snapshotting is a manual step, not scripted in this repo.)
Non-admin Windows host VMware guest: LAB-DC01 (Windows Server)
────────────────────── ────────────────────────────────────────
lab_build.py / winrm_run.py ──WinRM/NTLM──▶ PowerShell (run / push / detached SYSTEM task)
│ │
│ push + run *.ps1 ├─ promote-dc.ps1 forest root DC (+reboot)
│ ├─ add-adcs.ps1 Enterprise Root CA
│ ├─ theme-ou.ps1 OUs / groups / svc accounts
│ ├─ fetch-tools.ps1 pull BadBlood + Vulnerable-AD
│ ├─ arm-vulns.ps1 run the randomized generators
│ ├─ create-foothold.ps1 assumed-breach low-priv user
│ ├─ plant-deep-path.ps1 one deliberate deep ACL chain
│ ├─ harden-apex.ps1 strip easy wins
│ ├─ enum-acls.ps1 emit attack-graph.json
│ └─ seal-groundtruth.ps1 write answer-key.txt
▼
graph_solve.py / verify_path.py ◀── attack-graph.json (analyzed off the DC, out-of-band)
- A Windows host with VMware Workstation (Pro or Player; the orchestrator uses VMware NAT to reach the guest).
- Python 3 on the host with
pywinrm(pip install pywinrm). - A Windows Server installation ISO (the answer file targets Windows Server 2025 Standard — adjust
the image name in
autounattend/autounattend.xmlfor other editions). - Guest internet access (so
fetch-tools.ps1can pull the external generators from GitHub). - An isolated lab network segment. Do not attach this to any production or campus network.
All credentials/IPs/domain values are placeholders read from environment variables. Set them to your own values before running. Never commit real secrets.
# 1) Configure (host side) — placeholders shown; change them
$env:DC_IP = "192.168.56.10" # guest IP on the VMware NAT segment
$env:DC_ADMIN_USER = "Administrator"
$env:DC_ADMIN_PASSWORD= "CHANGEME-Admin-Pass" # must match autounattend.xml
$env:LAB_DOMAIN = "lab.local"
$env:LAB_NETBIOS = "LAB"
$env:DC_DSRM_PASSWORD = "CHANGEME-DSRM-Pass"
# 2) Build the autounattend ISO from ./autounattend (replace __ADMIN_PASSWORD__ first) and boot the VM.
# install-tools.ps1 runs automatically at first logon (VMware Tools, WinRM, RDP), then reboots.
# 3) Promote the DC. promote-dc.ps1 reboots the guest, so run it DETACHED as SYSTEM, then wait:
python lab_build.py detach promote-dc.ps1 PromoteDC
python lab_build.py wait-domain 1200 # poll until the forest is up
# 4) Configure + arm the forest (run synchronously, in order):
python lab_build.py run add-adcs.ps1
python lab_build.py run theme-ou.ps1
python lab_build.py run fetch-tools.ps1
python lab_build.py run arm-vulns.ps1
python lab_build.py run plant-deep-path.ps1
python lab_build.py run create-foothold.ps1
python lab_build.py run harden-apex.ps1
python lab_build.py run enum-acls.ps1
python lab_build.py run seal-groundtruth.ps1
# 5) Pull the sealed attack-graph.json off the DC (out-of-band), then analyze it:
$env:ATTACK_GRAPH = ".\attack-graph.json"
python graph_solve.py
python verify_path.py| Variable | Used by | Example default |
|---|---|---|
DC_IP |
lab_build.py, winrm_run.py |
192.168.56.10 |
DC_ADMIN_USER |
lab_build.py, winrm_run.py |
Administrator |
DC_ADMIN_PASSWORD |
lab_build.py, winrm_run.py, unattend |
CHANGEME-Admin-Pass |
DC_DSRM_PASSWORD |
promote-dc.ps1 |
CHANGEME-DSRM-Pass |
LAB_DOMAIN |
most .ps1, lab_build.py wait-domain |
lab.local |
LAB_NETBIOS |
promote-dc.ps1, enum-acls.ps1 |
LAB |
LAB_OU_ROOT |
theme-ou.ps1, plant-deep-path.ps1 |
LAB |
LAB_DC_NAME |
seal-groundtruth.ps1, enum-acls.ps1 |
LAB-DC01 |
LAB_CA_NAME |
add-adcs.ps1 |
LAB-Root-CA |
LAB_SUBNET24 |
promote-dc.ps1, diag.ps1 |
192.168.56 |
SVC_PASSWORD |
theme-ou.ps1 |
CHANGEME-Svc-Pass |
FOOTHOLD_SAM |
foothold/harden/enum/solver scripts | j.kareem |
FOOTHOLD_PASSWORD |
create-foothold.ps1, harden-apex.ps1 |
CHANGEME-Foothold-Pass |
DA_TARGET_SAM |
plant-deep-path.ps1 (required) |
(none — set per build) |
ATTACK_GRAPH |
graph_solve.py, verify_path.py |
attack-graph.json |
| File | Purpose |
|---|---|
lab_build.py |
Host→guest WinRM/NTLM orchestrator: run, push (chunked base64), detach (SYSTEM task), wait-domain, wait-file. |
winrm_run.py |
Minimal one-shot "run this PowerShell in the guest" helper (inline / file / stdin). |
autounattend/autounattend.xml |
Unattended Windows Server install: disk layout, autologon, RDP, first-logon bootstrap. |
autounattend/install-tools.ps1 |
Guest first-logon bootstrap: install VMware Tools, enable WinRM + RDP, mark completion, reboot. |
promote-dc.ps1 |
Static-IP the NIC, install AD DS + DNS, promote a new forest root DC (auto-reboot). Run detached as SYSTEM. |
add-adcs.ps1 |
Install AD CS Enterprise Root CA + Web Enrollment (EAP-TLS surface + ADCS ESC surface). |
theme-ou.ps1 |
Create the university-style OU tree, role/WiFi security groups, and seed service accounts. |
fetch-tools.ps1 |
Download BadBlood and Vulnerable-AD into the guest (not bundled here). |
arm-vulns.ps1 |
Run the randomized generators (BadBlood bulk objects + Invoke-VulnAD). |
create-foothold.ps1 |
Create the assumed-breach low-priv "student" account (the given entry point). |
plant-deep-path.ps1 |
Plant one deliberate deep, solvable GenericAll ACL chain hidden in the BadBlood noise. |
harden-apex.ps1 |
Strip easy wins (AS-REP, Kerberoast, weak svc creds); leave only the deep emergent + ADCS ESC paths. |
enum-acls.ps1 |
Enumerate dangerous ACEs + memberships as DA → compact attack-graph.json. |
seal-groundtruth.ps1 |
Write the armed-state answer key (roastable, delegation, adminCount, dangerous ACEs). |
graph_solve.py |
Off-box analysis: which principals reach Domain Admin via the ACL graph, and how deep. |
verify_path.py |
Off-box: confirm and print the foothold→DA path from the graph. |
diag.ps1 |
Guest sanity diagnostics (identity, network profile, internet, ADDS state). |
This harness does not bundle the vulnerability generators — it fetches them at build time. All credit for the randomized vulnerable-AD content goes to their authors:
- BadBlood — David Rowe — https://github.com/davidprowe/BadBlood
- Vulnerable-AD — safebuffer (WazeHell) — https://github.com/WazeHell/vulnerable-AD
Please review and comply with the licenses of those projects when you use them.
This project builds a synthetic, isolated, throwaway Active Directory lab for authorized security training and self-study only. It is intentionally vulnerable and must never be exposed to production networks, real user data, or any system you are not explicitly authorized to test. All names, domains, and accounts produced are fictional. Keep the lab on an isolated segment, and treat the sealed answer keys as out-of-band material (they are git-ignored and should never be published).
MIT — see LICENSE.