The engagement letter arrived with a confident phrase that I had learned to read as a dare:
"Howler Logistics has invested heavily in a Zero Trust architecture. Internal segmentation, conditional access, smart-card-only admin tiers, EDR on every endpoint, and an AD CS deployment we re-architected last summer. We don't believe a foothold can become Domain Admin. Prove us wrong."
The scoping document was thirty-two pages long. It listed PAW workstations, tier-0 jump boxes, just-in-time elevation, FIDO2 keys for every administrator. Their head of identity used the word "deny-by-default" four times on the kickoff call.
Eleven days later, I was reading the password hash for krbtgt out of a terminal.
This is the story of how that happened, and more importantly, what the "Zero Trust" diagram on their wall failed to model.
There is no zero-day in this post. There is no kernel exploit. There is one obscure misconfiguration on a certificate template that nobody had touched since 2017, and a chain of small trusts that, taken together, were not zero at all.
The most interesting thing about this engagement is the part of Zero Trust that actually worked. Howler had done something that 99% of enterprises don't. They hardened the Active Directory directory itself. LDAP read permissions on every high-privileged object, Domain Admins, Enterprise Admins, the DCs themselves, and every account with sensitive ACLs had been restricted.
A normal domain user could not enumerate them. BloodHound, the tool every attacker reaches for first, came back with a half-blind graph. The "see what an attacker would see" part of Zero Trust was, for once, doing its job.
This is the story of how that defense lasted exactly until I forged a certificate. If you build, audit, or defend an AD CS deployment, I think you will find the climax of this story uncomfortable.
Day one. I plugged into a switch port in a windowless room labeled "Wired Test Net - DO NOT USE FOR PRODUCTION" and pulled DHCP from the 10.84.20.0/22 user VLAN. No NAC kicked me off. That was the first surprise; I'll come back to it.
I started where everyone starts: do you have any RPC services that will talk to a stranger?
No credentials. No prompt. The domain controller HOWLER-DC01, running on a fully patched Server 2022, hardened by a Big Four consultancy six months prior, politely handed me every user and service principal in HOWLER.CORP.
This is CVE-less. It is "Restrict Unauthenticated RPC Clients" left at its default. Microsoft has shipped a Group Policy fix for it for over a decade. It is also the seed of every story like this one.
Three thousand eight hundred and fifty usernames in a text file. The "Zero Trust" architecture does not require an attacker to introduce themselves before asking who lives at this address.
Now I have identities. Did anyone use a password that I could guess?
Modern guidance has rotated away from forced password expiry. Howler had not gotten the memo. Their policy was a 90-day rotation, complexity-enforced, lockout at five failures. I picked the second-most-predictable scheme on Earth (the most predictable is <Company>123!).
Three plus signs. One spray, three valid accounts. One of them - svc-print-collector - was a service account whose password had not been changed since the printer fleet was installed in 2019 and had clearly been set by a human who liked the current month.
A password-spray detection rule that watches a single account is useless. They had the rules. It triggered on nothing because no individual account saw more than one failure.
Foothold acquired. Now I needed to know where the bodies were buried.
I dropped into a Linux box and ran the canonical reconnaissance pass. This is the part of the engagement where I always pour a coffee, because BloodHound’s UI is what tells you whether the next two weeks will be tedious or interesting.
This time, the coffee got cold.
Read those WARNING lines in the above screenshot again, because this is the moment the engagement got hard.
In a normal Active Directory deployment, s.carter - a member of Domain Users - can read the membership of Domain Admins. They can read the nTSecurityDescriptor of pretty much every object in the directory. They can ask the DC who is currently logged into which workstation. This is the default, and it is what makes BloodHound the recon tool of choice. The directory is a glasshouse, and every authenticated user gets a flashlight.
Howler had, painstakingly, boarded up the windows.
When I opened the BloodHound graph in Neo4j, here is what I saw and didn't see:
A graph with no edges is a list. Howler had implemented something I had never seen executed properly in a production AD: they had stripped the default Pre-Windows 2000 Compatible Access group of read rights, removed Authenticated Users from the read ACEs on tier-0 OUs, applied custom Deny Read ACEs on every privileged group object, and routed the legitimate need for AD reads (their HR sync, their monitoring, their helpdesk tools) through a single tightly-scoped group called `AD View All Objects`.
If you wanted to know who was a Domain Admin at Howler, you had to be in AD View All Objects. From Domain Users, the directory looked like a phone book where every interesting name had been redacted with a black bar.
This was the "Zero Trust" property that Howler was most proud of, and rightly so. The single most consequential first move for a real attacker, map the forest, had been blunted. BloodHound's pathfinder, the tool that turns "I have a foothold" into "and here is the four-hop path to Domain Admin," was looking at a graph with no edges.
So the question shifted. Forget who is a Domain Admin? I couldn't see. The question became: what can I see, and is any of it a way out?
Two threads survived the lockdown:
The first was certificate templates. AD CS template objects live in the Configuration partition of the directory. By design, they have to be readable by everyone, because a domain computer that is enrolling a certificate has to be able to discover what templates are available. Howler's hardening had not, and could not, restrict this. The 47 templates were fully visible.
AD CS - Microsoft's certificate authority role - has been a quiet treasure trove for offensive researchers since SpecterOps published the "Certified Pre-Owned" paper in 2021. The vulnerabilities were given the catchy ESC1 through ESC8 names. Patching them requires not a software update but a configuration audit, and configuration audits are boring, and so most enterprises skip them.
I had no password for azuresync2. I had no hash. I had a hint and a forgotten template. The shape of the chain wasn't in BloodHound. It was in the gap between what Howler had locked down and what they hadn't realized they needed to.
AD CS, Microsoft's certificate authority role, has been a quiet treasure trove for offensive researchers since SpecterOps published the "Certified Pre-Owned" paper in 2021. The vulnerabilities were given the catchy ESC1 through ESC8 names. Patching them requires not a software update but a configuration audit, and configuration audits are boring, and so most enterprises skip them.
I ran:
Read that vulnerability line twice.
In plain English: any computer that has joined the domain can request a certificate, choose any username it likes as the subject, and that certificate will let it log in as that user.
This template, called MacEnroll, was created in 2017 to let macOS endpoints auto-enroll when joining the domain. It had been forgotten. It was the only crack I needed.
But there was a problem. To abuse ESC1, I needed to be a computer. A user account couldn't enroll. I needed a machine account TGT. And I had no machines.
I spent that evening doing what I always do when I have an idea but no machine to run it from: scanning for low-hanging RCE.
Among Howler's seventeen domain-joined servers, four were running an old IBM application platform on TCP/8080. Banner-grabbed, fingerprinted, and (the thing that should have made someone scream) authenticated only by an admin/admin default that the deployment guide had warned about in 2014.
nt authority\system. On a domain-joined host. Which means I was now, effectively, the machine account APP-LEGACY01$.
I had my computer.
This is one of the parts of the chain that "Zero Trust" most badly mismodels. A flat phrase like "never trust, always verify" gives no architectural guidance about what to do with the dozens of unloved application servers that exist in every enterprise, the ones that nobody owns, that run as SYSTEM, and whose machine accounts can authenticate to the domain like first-class citizens.
Now I needed the Kerberos TGT for APP-LEGACY01$. With SYSTEM, that's a memory read away. I dropped a small PowerShell loader called dumper.ps1 that walks LSASS's Kerberos cache and writes any TGTs it finds to disk as base64 blobs:
Four tickets came back. I took the machine TGT for APP-LEGACY01$, exfiltrated the base64 blob, and converted it on my Linux attack box into a ccache file Linux Kerberos tooling can read:
I now had a machine identity authenticated to the forest. The MacEnroll template would talk to me.
This is the move that makes the post.
The MacEnroll template's EnrolleeSuppliesSubject flag means: whoever requests this certificate may write any name they want into the Subject Alternative Name field. The CA does not validate it. It signs whatever you ask for. The resulting certificate, because it has the Client Authentication EKU, is valid for logging in to anything that trusts the domain CA, which is everything.
So, I asked the CA, in the voice of APP-LEGACY01$, for a certificate in the name of azuresync2:
There it was. A perfectly valid X.509 certificate, signed by Howler's enterprise CA, bearing the name azuresync2@howler.corp, issued to me, the machine that does not own that name, on the authority of a template that nobody had inventoried since 2017.
I tried the obvious next step: trade the certificate for a Kerberos TGT and the user's NTLM hash via PKINIT. And here, the engagement got more interesting, because Howler had disabled PKINIT pre-authentication as part of their Zero Trust hardening. Smart. It is one of the standard mitigations for ESC-class abuses.
I sat with that error for an hour. PKINIT is the normal way to turn a certificate into a logon. With it off, the certificate looked like a dead end.
It was not.
There is a second, less-traveled path: Schannel authentication over LDAPS. A domain controller will, by default, accept a TLS client certificate as authentication when you connect to LDAP-over-TLS on port 636. There is no Kerberos involved at all. The DC validates that the cert chains to its trusted CA, reads the SAN, and binds your LDAP session as that user.
A tool called passthecert.py automates this. It takes a PFX, splits the cert and key, opens an LDAPS connection, and gives you an interactive LDAP shell:
The DC believed I was azuresync2. Because, in every way that the Schannel trust chain measured, I was.
And the moment I ran whoami, something else happened that I want to draw attention to. Watch what get_user_groups returns:
There it is. `AD View All Objects`.
The lockdown that had made BloodHound useless from s.carter, the boarded-up windows on every privileged group, the stripped read rights, the redacted ACLs was, from this LDAP session, gone. azuresync2 was a member of the one group that Howler had whitelisted as allowed to see the directory. The Zero Trust ACLs that had hidden Domain Admins, sessions, group memberships, and ACEs from a normal user were, to this session, transparent.
I had not just authenticated as a privileged user. I had authenticated as a user with the keys to the directory itself. Every fact about Howler's AD that had been hidden from me for the last six days was now one LDAP query away.
This is the part of the chain I want to underline, because it is the part that the architecture diagram on Howler's wall did not contemplate: the access controls on directory visibility were enforced at the identity layer, but the certificate template was a way to mint a new identity. The Zero Trust ACLs assumed that the set of identities was fixed and well-governed. The cert template let me add to it.
A defense built on "only trusted users can see the directory" collapses the moment an attacker can issue a trusted user identity.
Now that I could see, I looked.
azuresync2 had DCSync rights but was not, itself, a Domain Admin. I had two choices: (a) DCSync immediately, get krbtgt, mint a Golden Ticket, and walk in as anyone; or (b) do the cleaner thing, issue a second certificate, this time for an actual Domain Admin, and authenticate as them directly.
Option (b) leaves cleaner forensics behind for a real defender to find later. I picked (b) for the engagement report.
I queried the LDAP shell for current Domain Admins, a query that, an hour earlier, had returned an empty result.
azuresync (no number) was a Domain Admin. The kind of accident that happens when one engineer creates a temporary account during a migration, and another engineer creates a second temporary account because they couldn't find the first one.
I issued a certificate for azuresync exactly the same way as before. Same template, same trick, different SAN:
Then I authenticated as azuresync:
Domain Admin. Enterprise Admin. Eleven days from a switch port to the top of the forest.
For audit trail, I created a clearly-named user, added it to Domain Admins, and informed the customer’s blue team out-of-band:
For completeness and because clients sometimes ask, "what would the next attacker do?" I performed the DCSync. I had earned the right to:
Every secret in the directory, replicated to my laptop over the official Microsoft DRSUAPI protocol. From the DC’s point of view, this looked exactly like a legitimate domain controller doing its job.
There was a second path I had spotted in BloodHound on day three, and I want to mention it briefly because it is the path most enterprises overlook: SCCM's Network Access Account. Stored as a DPAPI blob in WMI on every SCCM client, retrievable by any local SYSTEM, the NAA is reused across the fleet and frequently has admin rights on tier-1 servers. From a single SYSTEM foothold, I could read it; from there, any host where Domain Admin j.morgan had logged in interactively would have that admin's credentials in clear text in LSA secrets. The path was: SYSTEM on any client → NAA credentials → admin login on `WS-FIN-228` → LSA secret dump → Domain Admin. I did not need it. It was there.
Let me be direct about what worked, because the failure is harder to see otherwise.
The directory hardening was excellent. The fact that BloodHound came back with no edges from a normal user account is, in my experience, almost unheard of in a real enterprise.
Stripping Authenticated Users from sensitive ACEs, removing Pre-Windows 2000 Compatible Access from the read path, gating directory visibility behind a single audited group — that is the work of a team that has read [the Microsoft Tier Model](https://learn.microsoft.com/en-us/security/privileged-access-workstations/privileged-access-access-model) and actually implemented it. If Howler had done nothing else, this single piece of work would have made them harder to compromise than 90% of Fortune 500 networks.
Now re-read the architecture summary from the kickoff call:
Internal segmentation: yes, but the user VLAN reached the DC's RPC and LDAP ports, the AD CS server's RPC interface, and the OpenAdmin console. Segmentation that doesn't gate identity protocols is wallpaper.
The architecture was not wrong. It was incomplete. Howler had built a directory I could not read and an admin tier I could not log into. What they had not done, and this is the lesson, is govern the set of trusted identities. A certificate template with EnrolleeSuppliesSubject is, functionally, a trapdoor that issues identity on demand. As long as that trapdoor existed, the carefully restricted ACL list became something I could append myself to.
You cannot enforce "only trusted users can see the directory" if any computer in the directory can mint a trusted user.
The single line that summarizes my report to Howler was this:
Your CA is part of your trust boundary. If you do not audit it the way you audit your firewalls, it is a hole the size of your forest.
If you are responsible for an AD environment and this story made you uneasy, here is what I would do this week, in priority order:
Three things made this engagement feel different from the dozens like it I have run.
The first is that, technically, every individual control Howler had deployed was working as designed. The CA wasn't compromised. The DC wasn't unpatched. The conditional access policy enforced exactly what it was written to enforce. The failure was that no single human had ever mapped the graph of how trust flowed between those controls, and so a path existed that no individual control was positioned to block.
The second is that the climax the passthecert.py step used a Microsoft-supported authentication path. There was no exploit. The DC behaved correctly. I asked it to authenticate me using a feature it advertised, with a certificate it had itself issued, and it did. This is the part of the chain that is hardest to detect and hardest to fix, because it is not a bug.
The third is the simplest and the most uncomfortable: nothing I did was novel. Every step in this post has been public for at least three years. ESC1 was published in June 2021. Pass-the-certificate was published in 2022. The OpenAdmin RCE is a 2017 CVE. The "Zero Trust" architecture I bypassed had been designed in 2024.
If your defenses are calibrated against last decade’s playbooks, your red team is going to look like a time traveler. And the next attacker, the one who isn’t writing a research blog about it, already knows everything in this post.
If you run an AD CS deployment and want expert eyes on your certificate templates before an attacker finds the gap, connect with us.