A reproducible, intentionally misconfigured multi-forest Windows Active Directory lab for offensive-security training, CTFs, and red-team practice. One command (python3 deploy.py) downloads the Windows media, builds base images with Packer, wires up the networks, boots the VMs, and runs the full Ansible attack-surface injection from PLAN.md: Kerberoasting, AS-REP roasting, ADCS ESC1–ESC16, ACL abuse, delegation chains, RBCD, ZeroLogon/noPac/Certifried preconditions, Golden/Silver/Diamond tickets, SID-history injection, cross-forest trust abuse, and more.
Every "bug" here is a feature. The misconfigurations are the spec (
PLAN.md). Do not deploy on a network you do not own — treat every VM as hostile.
Acknowledgment: EMPIRE owes a great deal to GOAD — Game of Active Directory, whose multi-forest lab design and attack-path philosophy were an enormous help in shaping this project.
Three forests, up to nine VMs, on a single Linux bridge:
empire.local(root) +eu.empire.local(child) — the Galactic Empire forestrebel.local— external trust (Rebel Alliance)trade.corp— forest trust (Trade Federation)
Eight Windows Server VMs (2019/2022) + one Ubuntu 22.04 Linux member. All share the empire-ctf bridge on 10.10.0.0/16; forests live in different /24 slices (10.10.0.x, 10.10.20.x, 10.10.30.x) so cross-forest traffic routes at the AD/DNS layer, not the network layer.
graph TD
classDef host fill:#000,stroke:#0f0,stroke-width:2px,color:#0f0;
classDef bridge fill:#333,stroke:#fff,stroke-width:1px,color:#fff;
classDef vm fill:#1d2b38,stroke:#00d2ff,stroke-width:2px,color:#fff;
Host["Linux Host<br/>python3 deploy.py<br/>QEMU/KVM · Packer · Ansible · dnsmasq"]:::host
CTF{"empire-ctf<br/>10.10.0.1/16<br/>(ALL forests)"}:::bridge
Host --> CTF
CTF --- coruscant["coruscant.empire.local<br/>10.10.0.10"]:::vm
CTF --- deathstar["deathstar.eu.empire.local<br/>10.10.0.11"]:::vm
CTF --- endor["endor.empire.local<br/>10.10.0.12"]:::vm
CTF --- scarif["scarif.empire.local<br/>10.10.0.13"]:::vm
CTF --- kamino["kamino.empire.local<br/>10.10.0.14"]:::vm
CTF --- mandalore["mandalore.empire.local<br/>10.10.0.15"]:::vm
CTF --- tatooine["tatooine.empire.local<br/>10.10.0.100"]:::vm
CTF --- yavin4["yavin4.rebel.local<br/>10.10.20.10"]:::vm
CTF --- neimoidia["neimoidia.trade.corp<br/>10.10.30.10"]:::vm
graph TD
classDef domain fill:#1d2b38,stroke:#00d2ff,stroke-width:2px,color:#fff;
subgraph EMPIRE Forest
EMPIRE["empire.local<br/>(root domain)"]:::domain
EU["eu.empire.local<br/>(child domain)"]:::domain
EMPIRE -- "Parent / Child" --> EU
end
subgraph REBEL Forest
FIN["rebel.local"]:::domain
end
subgraph TRADE Forest
TRADE["trade.corp"]:::domain
end
EMPIRE <-->|External Trust · BiDirectional · SID filter OFF| FIN
EMPIRE <-->|Forest Trust · BiDirectional · SID filter OFF| TRADE
Trusts are created by the ad_trust role (TrustType=External for empire↔rebel, Forest for empire↔trade, both bidirectional) via the .NET CreateTrustRelationship API, with SID filtering disabled (DF-008) so SID-history injection works across the boundary. Cross-forest name resolution is via conditional forwarders on coruscant.empire.local (the dns role).
| Domain | Forest | IP range | DC (inventory name) | Trust to empire.local |
|---|---|---|---|---|
empire.local |
EMPIRE (root) | 10.10.0.x |
coruscant.empire.local |
— |
eu.empire.local |
EMPIRE (child) | 10.10.0.x |
deathstar.eu.empire.local |
Parent/child, same forest |
rebel.local |
REBEL (root) | 10.10.20.x |
yavin4.rebel.local |
External, bidirectional |
trade.corp |
TRADE (root) | 10.10.30.x |
neimoidia.trade.corp |
Forest, bidirectional |
Specs are hardcoded in providers/qemu/vm-create.sh (VM_DEFS) and the static dnsmasq leases in providers/qemu/network-setup.sh. When you add or rename a VM, all four of vm-create.sh, network-setup.sh, ansible/inventory.yml, and any role/task referencing the hostname must stay in sync.
| Host | IP | RAM | vCPU | VNC | Base image | Role |
|---|---|---|---|---|---|---|
coruscant.empire.local |
10.10.0.10 | 1792 MB | 2 | :5901 | server2022 | Root DC |
deathstar.eu.empire.local |
10.10.0.11 | 1280 MB | 2 | :5902 | server2022 | Child DC |
endor.empire.local |
10.10.0.12 | 1536 MB | 2 | :5903 | server2022 | ADCS / Enterprise CA |
scarif.empire.local |
10.10.0.13 | 1280 MB | 2 | :5904 | server2019 | File server (SMB) |
kamino.empire.local |
10.10.0.14 | 1792 MB | 2 | :5905 | server2022 | SQL Server |
tatooine.empire.local |
10.10.0.100 | 1024 MB | 2 | :5906 | server2022 (Core) | Victim "workstation" |
mandalore.empire.local |
10.10.0.15 | 1280 MB | 2 | :5909 | ubuntu 22.04 | Linux member |
yavin4.rebel.local |
10.10.20.10 | 1280 MB | 2 | :5907 | server2022 | rebel.local DC |
neimoidia.trade.corp |
10.10.30.10 | 1280 MB | 2 | :5908 | server2022 | trade.corp DC |
Profiles: full = all 9 VMs / 3 forests (~12.25 GB allocated). minimal = 7 VMs (empire.local + mandalore, no rebel/trade DCs, ~9.75 GB). single-dc = coruscant only (~1.5 GB smoke test).
| Purpose | Value |
|---|---|
| Domain Administrator (every domain) | SithLord123! |
| DSRM / safe-mode password | SithLord123! |
krbtgt (empire.local) |
KrbtgtEMPIRE2024! |
krbtgt (eu.empire.local) |
KrbtgtEU2024! |
| Cross-forest trust keys | TrustKey2024! |
- Linux host with KVM (Intel VT-x / AMD-V enabled in BIOS)
- ~16 GB free RAM (full) / ~10 GB (minimal) / ~2 GB (single-dc)
- ~100 GB free disk for qcow2 images + Windows ISOs + virtio-win
sudo(bridge creation, dnsmasq, nftables need root)- Internet on first run (Windows ISOs + virtio + Ubuntu cloud image + packages)
- Host packages:
qemu/KVM,libvirt,swtpm,ovmf,packer,ansible,dnsmasq
scripts/setup-deps.sh installs the host packages per distro: Debian/Ubuntu (apt), Fedora/RHEL/Rocky/Alma (dnf), Arch/Manjaro (pacman), openSUSE (zypper). After it adds you to the kvm/libvirt groups you must log out and back in before launching VMs without sudo.
git clone https://github.com/sanchitsahni/Damn-Vunerable-Active-Directory.git EMPIRE
cd EMPIRE
sudo bash scripts/setup-deps.sh # one-time host dependency install
# ... log out / log back in (kvm + libvirt group membership) ...
python3 deploy.py # interactive wizard (recommended first run)
# Non-interactive:
python3 deploy.py --profile full --provider qemu --yes
python3 deploy.py --profile minimal --provider qemu --yes # empire.local + mandalore
python3 deploy.py --profile single-dc --yes # 1-VM smoke test
python3 deploy.py --ram 24 --disk-path /mnt/vms --yes # resource capsYou do not supply your own base image — deploy.py downloads the Windows Server ISOs + virtio-win + the Ubuntu cloud image, then builds the qcow2 base images with Packer automatically.
deploy.py runs 7 phases end-to-end (--phase N / --from-phase restart from any of them):
| # | Phase | What happens |
|---|---|---|
| 0 | media | Download Windows ISOs, virtio-win, Ubuntu cloud image into media/ |
| 1 | packer | Build server2019 / server2022 base qcow2 images |
| 2 | network | Create the empire-ctf bridge + project-local dnsmasq + nftables |
| 3 | VMs | Generate per-VM autounattend.xml + post-install.ps1, clone disks, boot |
| 4 | WinRM | Wait for each VM to finish Windows setup (writes vms/<name>.installed) |
| 5 | ansible | Domain promotion → trusts → ADCS → full vulnerability injection (site.yml) |
| 6 | verify | Layer-1 passive config checks (scripts/verify_vulns.py) |
Expect 45–90 minutes on a full first run (Windows install + packer dominate). Re-running Ansible alone is minutes.
Running python3 deploy.py with no --yes drops into a settings-aware console. Type help (or ?) for the menu:
| Group | Commands |
|---|---|
| Lab | check · install · build · network · vms · provision · resume <n> |
| VMs | status · start · stop · destroy · snapshot · reset · vnc |
| Provision | provision_tags <t> (e.g. kerberos,adcs) · verify |
| Config | settings · set_profile · set_provider · set_ram · set_attacker · set_flag_mode · set_disk |
snapshot after a good provision, then reset to roll every VM back in seconds instead of re-provisioning.
| Flag | Effect |
|---|---|
--yes, -y |
Skip all prompts (CI / cron) |
--profile {full,minimal,single-dc} |
Lab size |
--provider {qemu,virtualbox} |
Hypervisor (default qemu) |
--phase N, -p N |
Start from phase N (0=media … 6=verify) |
--from-phase PHASE |
Run Ansible from this phase to the end, then exit |
--only-phase PHASE |
Run only this Ansible phase, then exit |
--limit HOST |
Ansible --limit (e.g. endor.empire.local) |
--ram GB |
Total RAM budget across all VMs |
--disk-path PATH |
VM disk directory (default ./vms) |
--attacker-ip IP |
Attacker / listener IP baked into payloads |
--flag-mode {ctf,training} |
ctf = flags require exploitation; training = visible at C:\Flags\ |
--base-action {build,skip} |
build = run packer; skip = images already exist |
--destroy |
Tear down all VMs + networks |
--install-cron |
Write crontab + sudoers drop-in and exit |
--log-file PATH |
Append all output to a log file |
Ansible sub-phases (for --from-phase / --only-phase): 1 2 5 6 7 8 8b 9 10 11 13 14 16 17 18 19 20.
# Re-run only Ansible (VMs already up):
cd ansible && ansible-playbook -i inventory.yml playbooks/site.yml -v
# Syntax / dry-run validation:
ansible-playbook -i inventory.yml playbooks/site.yml --syntax-check
ansible-playbook -i inventory.yml playbooks/site.yml --check
# Re-run Ansible from a given phase (helper):
scripts/run-from.sh 8b --limit endor.empire.localConnect to a VM:
vncviewer 127.0.0.1:5901 # coruscant console
evil-winrm -i 10.10.0.10 -u Administrator -p 'SithLord123!' # WinRM (5985 open)
xfreerdp /v:10.10.0.100 /u:Administrator /p:'SithLord123!' # RDP where enabledAttacks run from your own Kali / BlackArch on the host bridge — the box that ran deploy.py. tatooine is a victim, not an attack box. Bring your own impacket, BloodHound, certipy, Rubeus, mimikatz, netexec, Responder, mitm6, ntlmrelayx.
BloodHound collection example (real DC names differ from inventory labels — use
coruscant-fin/coruscant-trade/coruscant-eufor the other forests):bloodhound-python -u Administrator -p 'SithLord123!' -d empire.local \ -dc coruscant.empire.local -ns 10.10.0.10 -c All --zip
Short list — the full spec is PLAN.md:
- Defender disabled, firewall off, UAC weakened on every host
ms-DS-MachineAccountQuota = 10(noPac / Certifried precondition)krbtgtreset to known lab values for deterministic Golden Tickets- ADCS ESC1–ESC16 templates published (
EMPIREUserESC1,EMPIREMachineESC2, …) - Kerberoastable service accounts (
svc_*) with weak passwords - AS-REP roastable accounts (
DoNotRequirePreAuth) - DCSync rights granted to non-admin users
- SID filtering disabled on both cross-forest trusts; trust keys reset to
TrustKey2024! - ZeroLogon precondition, unconstrained/constrained/RBCD delegation, gMSA backdoor
- AdminSDHolder GenericAll backdoor, writable GPO (
EMPIREBackdoorGPO, PER-034) - SMB signing not required, LDAP signing not required, LLMNR on, IPv6 enabled (mitm6)
- …and ~370 more IDs across IA / REC / ENUM / CRED / LAT / PE / PER / DF — see
PLAN.md
Do not "fix" any of these. If something looks broken and is not in PLAN.md, that is a real bug — file it.
python3 deploy.py --destroy --yes # VMs + networks (qcow2 deleted)
# or, inside the console: destroyvms/ and media/ survive a network teardown; delete them manually to reclaim disk.
EMPIRE/
├── deploy.py # Entry point — the only script you run
├── PLAN.md # Authoritative attack-matrix spec (all flag IDs)
├── providers/
│ ├── qemu/
│ │ ├── vm-create.sh # VM_DEFS (MAC/RAM/CPU/VNC/bridge), autounattend +
│ │ │ # post-install generation, VM lifecycle
│ │ └── network-setup.sh # empire-ctf bridge + dnsmasq static leases + nftables
│ └── virtualbox/
│ └── vm-create.sh # VirtualBox provider equivalent
├── packer/ # Packer templates (server2019 / server2022 base images)
├── ansible/
│ ├── inventory.yml # Canonical inventory: 9 hosts × 3 forests
│ └── playbooks/
│ └── site.yml # Master playbook — phased AD setup + vuln injection
│ └── roles/ # 23 roles (5 setup + 18 vuln_*)
├── chains/ # Static attack-path graph + reachability validator
├── scripts/ # Helper scripts (see Scripts reference below)
├── wordlists/ # Lab usernames + passwords
├── vms/ # Generated per-VM state (gitignored)
└── media/ # Windows ISOs + virtio + Ubuntu image (gitignored, ~5 GB)
Ansible roles: setup (ad_domain, child_domain, ad_trust, dns, domain_join) + vulnerability injection (vuln_adcs, vuln_cloud_entra, vuln_cred_access, vuln_cve, vuln_defense_evasion, vuln_exchange, vuln_forest, vuln_ia_surface, vuln_kerberos, vuln_lateral, vuln_linux, vuln_network_protocols, vuln_persistence, vuln_privesc, vuln_recon, vuln_traffic_sim, vuln_victim_exec, vuln_web_apps). The vuln_* roles are the whole point of the lab; the rest is scaffolding.
deploy.py is the entry point; everything below is either invoked by it or run by hand.
Pipeline — invoked automatically by deploy.py:
| Script | Role |
|---|---|
providers/qemu/vm-create.sh |
Generate per-VM autounattend + post-install, create and boot each QEMU/KVM VM |
providers/qemu/network-setup.sh |
Create the empire-ctf bridge + project-local dnsmasq static leases |
providers/virtualbox/vm-create.sh |
VirtualBox provider equivalent (when --provider virtualbox) |
scripts/verify_vulns.py |
Layer-1 passive config verification (also the verify console command) |
ansible/roles/vuln_linux/files/empire_app.py |
Vulnerable web app deployed to mandalore by Ansible |
Manual / optional tools:
| Script | Role |
|---|---|
scripts/setup-deps.sh |
Install host packages per distro — run once before deploy.py |
scripts/run-from.sh |
Re-run the Ansible site.yml from a given phase to the end |
scripts/verify_exploits.sh |
Layer-2 attacker-side exploit verification |
scripts/vps-wg-gateway.sh |
Optional WireGuard gateway for remote VPS access |
scripts/activate-windows.sh |
Massgrave Windows activation helper |
scripts/finalize.sh |
Post-deploy lab finalization / verification |
chains/attack_graph.py + chains/validator.py |
Static attack-path graph + reachability report |
Standalone fallbacks — deploy.py already does these in-process, kept for manual use:
| Script | Role |
|---|---|
scripts/wait-vms.sh, scripts/wait-for-install.sh |
Poll VMs for WinRM / write .installed markers (deploy.py: phase_wait_winrm) |
scripts/setup-sudoers.sh |
Write NOPASSWD sudoers (deploy.py: --install-cron) |
scripts/download-windows.sh |
Download Windows + virtio ISOs (deploy.py: in-process downloader) |
Legacy / utilities:
| Script | Role |
|---|---|
scripts/exploit_graph.py |
Superseded compatibility shim → chains/validator.py |
scripts/check_docs.py, scripts/check_study_flags.py, scripts/generate_missing.py |
Doc / flag consistency helpers |
| Doc | Purpose |
|---|---|
README.md |
This file — setup, lifecycle, repo layout |
PLAN.md |
Authoritative attack-matrix spec — every flag ID, precondition, and intended technique |
| Symptom | Cause / Fix |
|---|---|
Permission denied on /dev/kvm |
Added to kvm/libvirt groups but not re-logged in. Log out and back in. |
| Ansible WinRM connection refused | VM hasn't finished post-install.ps1. deploy.py waits on the vms/<name>.installed marker; watch the VM over VNC if it stalls. |
| Cross-forest trusts missing | Trust creation needs DNS conditional forwarders first; the dns phase runs before ad_trust. Re-run ansible-playbook … --tags dns,trusts (idempotent). |
| VM kernel panic / triple-fault on boot | OVMF/swtpm version mismatch — install both from your distro repos. |
| Packer build fails | Check packer-output/logs/; ensure the Windows ISOs landed in media/ (phase 0). |
EMPIRE is a research and training tool that deliberately produces a trivially exploitable Windows AD environment. Do not deploy it on a network you do not control. The lab password and intentionally vulnerable configurations are public; treat every VM as hostile. The authors accept no responsibility for misuse.
The lab maps Active Directory concepts onto the galactic struggle between the Galactic Empire, the Rebel Alliance, and the Trade Federation.
graph TD
classDef empire fill:#000000,stroke:#ff0000,stroke-width:2px,color:#fff;
classDef rebel fill:#2b5c8f,stroke:#ff9900,stroke-width:2px,color:#fff;
classDef trade fill:#4a4a4a,stroke:#aaaaaa,stroke-width:2px,color:#fff;
classDef highlight fill:#440000,stroke:#ff0000,stroke-width:3px,color:#fff;
subgraph empire_forest["The Galactic Empire — empire.local"]
Coruscant["Coruscant (Root DC)<br/>coruscant.empire.local"]:::empire
DeathStar["The Death Star (Child DC)<br/>deathstar.eu.empire.local"]:::highlight
Scarif["Scarif Citadel (File Server)<br/>scarif.empire.local"]:::empire
Kamino["Kamino Cloning Facility (SQL)<br/>kamino.empire.local"]:::empire
Endor["Endor Shield Generator (CA)<br/>endor.empire.local"]:::empire
Mandalore["Mandalore Mercenary Base (Linux)<br/>mandalore.empire.local"]:::empire
Coruscant -- "Imperial Command" --> DeathStar
Coruscant --- Scarif
Coruscant --- Kamino
Coruscant --- Endor
Coruscant --- Mandalore
end
subgraph rebel_forest["The Rebel Alliance — rebel.local"]
Yavin4["Yavin 4 Base<br/>yavin4.rebel.local"]:::rebel
end
subgraph trade_forest["The Trade Federation — trade.corp"]
Neimoidia["Cato Neimoidia<br/>neimoidia.trade.corp"]:::trade
end
Coruscant <-->|Espionage / External Trust| Yavin4
Coruscant <-->|Treaty / Forest Trust| Neimoidia
empire.local(Galactic Empire): root domain — the seat of the Emperor. Domain Admin here = keys to the galaxy.eu.empire.local(Death Star): child domain. Escaping it to compromise the root is the Death Star plans.rebel.local(Rebel Alliance): external forest, weak link across the trust.trade.corp(Trade Federation): forest trust — forge inter-realm TGTs to cross the boundary.endor(Shield Generator / ADCS): compromise the CA (ESC1/ESC8…) to forge certs for anyone.scarif(Citadel / File server): SMB shares with passwords left in scripts and configs.kamino(Cloning Facility / SQL): SQLi /xp_cmdshellfoothold.mandalore(Mercenary Base / Linux): local privesc + cross-OS pivot.
"Your focus determines your reality." Focus on the attack paths in
PLAN.md. If an exploit fails, check your syntax and targeting — the lab is intentionally vulnerable.
May the Force be with you as you conquer the EMPIRE AD.