Even more privileged ADCS ESC_CES
For a long time, when people talked about NTLM relay to AD CS, the conversation usually focused on classic Web Enrollment on the following endpoints:
/certsrv/
/certsrv/certfnsh.asp
But AD CS has one more component that can provide additional vector for privilege escalation
AD CS consists of following components, for those who what to know more:
- Certification Authority
- The core CA service that issues, renews, revokes, and manages certificates. - Certification Authority Web Enrollment
- Classic /certsrv/ web interface for requesting and retrieving certificates. - Online Responder
- Provides OCSP responses for certificate revocation checking. - Network Device Enrollment Service
- NDES/SCEP service used by network devices to enroll certificates. - Certificate Enrollment Web Service / Certificate Enrollment Policy Web Service
- Web-service based enrollment/policy components:
- CES: Certificate Enrollment Web Service
- CEP: Certificate Enrollment Policy Web Service
When looking deeper at AD CS role services, there is another component that can also be exposed over HTTP(S):
Certificate Enrollment Services, or CES
CES commonly lives at endpoints like:
https://ca.example.local/CA_CES_Kerberos/service.svc/CES
https://ca.example.local/CA_CES_NTLM/service.svc/CES
This is interesting because CES can also accept certificate enrollment requests. In other words, if an attacker can authenticate to CES as a principal that is allowed to enroll a template, CES may also be able to return a certificate.
That led to the obvious question:
If Web Enrollment can be abused through NTLM relay to obtain certificates, can CES be abused in a similar way?
To answer that, I started comparing the actual HTTP requests used by classic Web Enrollment and CES. I looked at protocols, authentication negotiation, request bodies, response formats, and the protections around both endpoints.
Since CES is normally exposed over HTTPS and often has its own trusted TLS certificate, intercepting and understanding the traffic required setting up tools like mitmproxy or Burp and dealing with the HTTPS layer properly.
The result is that Web Enrollment and CES both perform certificate enrollment, but they are completely different HTTP protocols.
Web Enrollment uses a normal HTTP form POST.
The request content type is:
Content-Type: application/x-www-form-urlencoded
The client submits a CSR with a request like:
POST /certsrv/certfnsh.asp HTTP/1.1
Host: ca.example.local
Content-Type: application/x-www-form-urlencoded
Authorization: NTLM <token>
Mode=newreq&CertRequest=<CSR>&CertAttrib=CertificateTemplate:User&TargetStoreFlags=0&SaveCert=yes&ThumbPrint=
The important fields are:
CertRequest=<PKCS10 CSR>
CertAttrib=CertificateTemplate:<template>
For example:
CertAttrib=CertificateTemplate:User
If a SAN/UPN is included, the attribute can look like:
CertAttrib=CertificateTemplate:User%0d%0aSAN:upn=user@example.local
In Web Enrollment, the CSR is sent in the form field:
CertRequest=
The certificate template is sent in:
CertAttrib=CertificateTemplate:<template>
Classic Web Enrollment usually returns HTML.
If the request is accepted, the response often contains a request ID or a link like:
certnew.cer?ReqID=42
The client then fetches the certificate separately:
GET /certsrv/certnew.cer?ReqID=42
So the flow is usually:
1. Generate private key and CSR
2. POST CSR to /certsrv/certfnsh.asp
3. Parse HTML response for ReqID
4. GET /certsrv/certnew.cer?ReqID=<id>
5. Combine private key and certificate
6. Save PFX
Certificate Enrollment Services is a different AD CS role service. It exposes a WCF service endpoint, usually under a path like:
/CA_CES_Kerberos/service.svc/CES
/CA_CES_NTLM/service.svc/CES
A typical URL is:
https://ca.example.local/CA_CES_Kerberos/service.svc/CES
Unlike Web Enrollment, CES does not accept a simple application/x-www-form-urlencoded request.
CES uses SOAP/WSTEP. WSTEP is short for Web Services Trust Enrollment Protocol.
In practice, the client sends a WS-Trust SOAP RequestSecurityToken message.
CES uses:
Content-Type: application/soap+xml; charset=utf-8
A CES enrollment request is sent to:
POST /CA_CES_Kerberos/service.svc/CES HTTP/1.1
Host: ca.example.local
Content-Type: application/soap+xml; charset=utf-8
Authorization: Negotiate <token>
The body is XML it contains a SOAP envelope, a simplified version looks like this:
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope
xmlns:a="http://www.w3.org/2005/08/addressing"
xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Header>
<a:Action s:mustUnderstand="1">
http://schemas.microsoft.com/windows/pki/2009/01/enrollment/RST/wstep
</a:Action>
<a:MessageID>urn:uuid:...</a:MessageID>
<a:To s:mustUnderstand="1">
https://ca.example.local/CA_CES_Kerberos/service.svc/CES
</a:To>
</s:Header>
<s:Body>
<RequestSecurityToken
PreferredLanguage="en-US"
xmlns="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
<TokenType>
http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3
</TokenType>
<RequestType>
http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue
</RequestType>
<BinarySecurityToken
ValueType="http://schemas.microsoft.com/windows/pki/2009/01/enrollment#PKCS10"
EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary"
xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
BASE64_PKCS10_CSR
</BinarySecurityToken>
<AdditionalContext
xmlns="http://schemas.xmlsoap.org/ws/2006/12/authorization">
<ContextItem Name="CertificateTemplate">
<Value>User</Value>
</ContextItem>
</AdditionalContext>
</RequestSecurityToken>
</s:Body>
</s:Envelope>
CSR is not sent as a form field. It is embedded inside a WS-Security element:
<BinarySecurityToken
ValueType="http://schemas.microsoft.com/windows/pki/2009/01/enrollment#PKCS10"
EncodingType="...#base64binary">
BASE64_PKCS10_CSR
</BinarySecurityToken>
The certificate template is not sent as CertAttrib. It is sent inside AdditionalContext:
<AdditionalContext xmlns="http://schemas.xmlsoap.org/ws/2006/12/authorization">
<ContextItem Name="CertificateTemplate">
<Value>User</Value>
</ContextItem>
</AdditionalContext>
This is one of the most important differences.
Web Enrollment:
CertAttrib=CertificateTemplate:User
CES:
<ContextItem Name="CertificateTemplate">
<Value>User</Value>
</ContextItem>
CES usually returns the issued certificate material directly inside the SOAP response. The response contains one or more:
<BinarySecurityToken>
Common returned token types include:
X509v3 certificate
PKCS7 response
So unlike Web Enrollment, CES normally does not require a separate request to get certificate:
GET /certsrv/certnew.cer?ReqID=<id>
The flow is usually:
1. Generate private key and CSR
2. Build SOAP RequestSecurityToken
3. POST SOAP to /service.svc/CES
4. Parse SOAP response
5. Extract X509v3 and/or PKCS7 BinarySecurityToken
6. Combine private key and certificate
7. Save PFX
Authentication and Enrollment
Both Web Enrollment and CES can be protected by IIS Windows Authentication.
That means the authentication layer may involve:
NTLM
Negotiate
Kerberos
However, default behavior is different between endpoints.
Classic Web Enrollment often accepts:
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
While CES, by default, uses the following:
WWW-Authenticate: Negotiate
I tested this via this random script just to make sure:

In my mind, this meant that I could negotiate NTLM authentication, and that turned out to be true. I built a tool that sends the same SOAP requests to the CES endpoint using Negotiate.
Here a proof-of-concept

It work with both NTLM and Kerberos mode for Negotiate.
Interception and Relay
So Why Web Enrollment Tooling Does Not Automatically Work Against CES?
ntlmrelay tool expects to send something like:
Mode=newreq&CertRequest=...&CertAttrib=CertificateTemplate:User
to:
/certsrv/certfnsh.asp
A CES endpoint does not understand that request it expects are SOAP XML containing a WS-Trust request:
<RequestSecurityToken>
<BinarySecurityToken>CSR</BinarySecurityToken>
<AdditionalContext>
<ContextItem Name="CertificateTemplate">
<Value>User</Value>
</ContextItem>
</AdditionalContext>
</RequestSecurityToken>
sent to:
/service.svc/CES
The concept is similar: submit CSR, request template, receive certificate, but the protocol is completely different.
With all that in mind, and based on what I’ve learned so far, I tried to build a tool that could intercept an authentication request and relay it to the CES endpoint to obtain a certificate.
Disclamer: Tool is heavily based on existing impacket ntlmrealy tool I just modified request handling.
Here is our intercept and relay tool

Getting that juicy user authentication:

And easy Un-PAC-The-Hash and Pass-The-Certificate

Tools are available on: https://github.com/ADHDMurky/ADCS_Tools/
Sorry for the long post here is a picture of Murloc
