<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Parag Mali - tag: bloodhound</title><description>Posts tagged bloodhound.</description><link>https://paragmali.com/</link><language>en-US</language><lastBuildDate>Sun, 07 Jun 2026 04:13:08 GMT</lastBuildDate><atom:link href="https://paragmali.com/tags/bloodhound/rss.xml" rel="self" type="application/rss+xml"/><item><title>AD Is a Graph: How BloodHound Made Defenders Think Like Attackers</title><link>https://paragmali.com/blog/ad-is-a-graph-how-bloodhound-made-defenders-think-like-attac/</link><guid isPermaLink="true">https://paragmali.com/blog/ad-is-a-graph-how-bloodhound-made-defenders-think-like-attac/</guid><description>From Lambert&apos;s 2015 essay to Microsoft Security Exposure Management in 2024 -- how the attack-path graph became the default model for Active Directory security.</description><pubDate>Tue, 26 May 2026 00:00:00 GMT</pubDate><content:encoded>
**AD is a graph.** In April 2015 John Lambert named the missing model in two sentences. In August 2016 Andy Robbins, Rohan Vazarkar, and Will Schroeder shipped the tool that made it operational: BloodHound treats Active Directory as a directed graph of privilege relationships, queries it with Neo4j&apos;s Cypher, and turns weeks of red-team whiteboard work into a 200 ms shortest-path lookup. By November 2024 Microsoft itself shipped the same mental model as Microsoft Security Exposure Management. This article is half tool history (1998 academic attack graphs through OpenGraph 2025) and half graph-theory exposition: property graphs, BFS shortest paths, the edge taxonomy, and the open problem of *weighting* what is currently treated as one BFS hop per privilege.
&lt;h2&gt;1. How do I get from this user to Domain Admin?&lt;/h2&gt;
&lt;p&gt;In 2014, a red-team analyst with a help-desk account inside a 40,000-user Active Directory forest asks the question every red-team analyst asks: &lt;em&gt;is there a path from here to Domain Admins?&lt;/em&gt; The answer takes two analysts five days of PowerView scripts, hand-drawn whiteboard diagrams, and per-host RDP probing. The same question in 2024 is a ninety-character Cypher query that returns in 200 milliseconds.&lt;/p&gt;
&lt;p&gt;What happened in those ten years is the story of one sentence -- &lt;em&gt;defenders think in lists, attackers think in graphs&lt;/em&gt; -- becoming, in turn, a tool, a discipline, and a Microsoft product.&lt;/p&gt;
&lt;p&gt;The 2014 reality was a stack of CSV files. PowerView, the PowerShell enumeration toolkit Will Schroeder first published in August 2014 [@powersploit-repo], could dump every group membership, every &lt;a href=&quot;https://paragmali.com/blog/windows-access-control-25-years-of-attacks/&quot; rel=&quot;noopener&quot;&gt;Access Control Entry&lt;/a&gt;, and every active session from a low-privilege account [@powertools-powerview]. The outputs were rows. Hundreds of thousands of rows. Composing them into a coherent attack path was a job for a marker and a whiteboard, and the join keys were Distinguished Names that wrapped twice across an analyst&apos;s notebook page. Five days to map a single 40,000-user forest was not unusual. It was the price of doing business.&lt;/p&gt;
&lt;p&gt;The 2024 reality is a query. The analyst loads SharpHound&apos;s JSON dump into a Neo4j graph, opens the BloodHound web interface in a browser, types&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cypher&quot;&gt;MATCH p = shortestPath(
  (u:User {name:&apos;HELPDESK@CORP.LOCAL&apos;})-[*1..]-&amp;gt;(g:Group {name:&apos;DOMAIN ADMINS@CORP.LOCAL&apos;})
) RETURN p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and clicks Run. The graph renders. The shortest path is highlighted. The pivot points are circled. Time elapsed: 200 milliseconds on the database, plus a second for the browser to draw the SVG [@bloodhound-ce-repo].&lt;/p&gt;
&lt;p&gt;Whatever happened in those ten years has to be more than a software release. It has to be a change in how the entire community models the problem. The change has a date and a sentence. Both arrived in April 2015. Let us start with the sentence.&lt;/p&gt;
&lt;h2&gt;2. From ACL lists to graphs -- why the model matters&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;An access-control list is not the same as a graph -- and the difference is everything.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Consider five accounts in a hypothetical &lt;code&gt;CORP.LOCAL&lt;/code&gt; forest. Bob, a help-desk operator, has been granted &lt;code&gt;ForceChangePassword&lt;/code&gt; on Carol&apos;s account by a long-departed administrator who once needed to delegate password resets. Carol is a member of &lt;code&gt;Server Operators&lt;/code&gt;. &lt;code&gt;Server Operators&lt;/code&gt;, by default, can log on locally to Domain Controllers and back up the directory database. The Domain Controller hosts the &lt;a href=&quot;https://paragmali.com/blog/krbtgt-the-account-that-owns-active-directory/&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;krbtgt&lt;/code&gt; account&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Three rows in three different audit reports. One attack path.&lt;/p&gt;
&lt;p&gt;A per-object Access Control List audit looks at Bob&apos;s row, sees a &lt;code&gt;ForceChangePassword&lt;/code&gt; ACE, and flags it as &quot;an over-broad delegation.&quot; It looks at Carol&apos;s row, sees that she belongs to &lt;code&gt;Server Operators&lt;/code&gt;, and flags it as &quot;a privileged group membership.&quot; It looks at the Domain Controller and sees that &lt;code&gt;Server Operators&lt;/code&gt; has logon rights, which is the default. Nothing in the audit composes the three facts. Reachability is not a property the report computes.&lt;/p&gt;

A set of nodes (vertices) and directed edges (arrows) between them. Each edge points from one node to another. A *path* is a sequence of edges that can be traversed in the direction of their arrows. A node B is *reachable* from a node A if some path leads from A to B.

A property of two nodes A and B in a directed graph: B is reachable from A if there exists a sequence of edges leading from A to B, regardless of length. Reachability is fundamentally a graph property and cannot be answered by inspecting any single edge in isolation.
&lt;p&gt;Now draw the same five accounts as a directed graph. Bob is a node. Carol is a node. &lt;code&gt;Server Operators&lt;/code&gt; is a node. The Domain Controller is a node. The &lt;code&gt;krbtgt&lt;/code&gt; account is a node. There is an edge from Bob to Carol labelled &lt;code&gt;ForceChangePassword&lt;/code&gt;. There is an edge from Carol to &lt;code&gt;Server Operators&lt;/code&gt; labelled &lt;code&gt;MemberOf&lt;/code&gt;. There is an edge from &lt;code&gt;Server Operators&lt;/code&gt; to the Domain Controller labelled &lt;code&gt;CanRDP&lt;/code&gt;. There is an edge from the Domain Controller to &lt;code&gt;krbtgt&lt;/code&gt; labelled &lt;a href=&quot;https://paragmali.com/blog/two-checkmarks-and-the-keys-to-the-kingdom-how-active-direct/&quot; rel=&quot;noopener&quot;&gt;&lt;code&gt;DCSync&lt;/code&gt;&lt;/a&gt;. Trace the arrows. Bob can reach &lt;code&gt;krbtgt&lt;/code&gt;.&lt;/p&gt;

flowchart LR
    Bob([Bob, help-desk]) --&amp;gt;|ForceChangePassword| Carol([Carol])
    Carol --&amp;gt;|MemberOf| SO([Server Operators])
    SO --&amp;gt;|CanRDP| DC([Domain Controller])
    DC --&amp;gt;|DCSync| Krb([krbtgt])
&lt;p&gt;The graph form makes reachability visually obvious. The list form does not. This is not a presentation difference. It is a data-model difference, and it is the difference that decides whether a tool can answer the question &lt;em&gt;&quot;is there a path from A to B?&quot;&lt;/em&gt; at all.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Reachability is a property of the graph that the list does not, in general, express. This is not a UX difference. It is a data-model difference, and it is the entire reason BloodHound exists.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The sentence that named this gap appeared on April 26, 2015. John Lambert, then a Distinguished Engineer in the Microsoft Threat Intelligence Center, published a short essay to a personal GitHub repository [@lambert-2015-defenders-lists-attackers-graphs]. No peer review. No formal venue. Two declarative opening sentences that every defender attack-path product since has cited approvingly.&lt;/p&gt;

Defenders don&apos;t have a list of assets -- they have a graph. Assets are connected to each other by security relationships. As long as defenders use a list and attackers use a graph, attackers win. -- John Lambert, April 26, 2015.
&lt;p&gt;Lambert then enumerated five concrete classes of &quot;security dependencies&quot; that constitute edges in any real network: shared local-admin passwords; logon scripts on file servers; print-driver propagation from print servers; certificate authorities that mint smart-card logon certificates; and database administrators who run code as a privileged DB process. The essay closed with a defender prescription: &lt;em&gt;&quot;The first step is to visualize your network by turning your lists into graphs.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If the diagnosis is so obvious in retrospect, why was every mainstream AD audit tool from 2000 through 2015 list-shaped? The answer is that the academic literature had the right model but the wrong substrate, and the operator community had the right substrate but the wrong model. The two did not meet until April 2015.&lt;/p&gt;
&lt;h2&gt;3. Attack graphs before BloodHound, 1998 to 2015&lt;/h2&gt;
&lt;p&gt;The phrase &lt;em&gt;attack graph&lt;/em&gt; is older than Active Directory itself.&lt;/p&gt;
&lt;p&gt;In September 1998, two researchers at Sandia National Laboratories presented a paper titled &lt;em&gt;A Graph-Based System for Network-Vulnerability Analysis&lt;/em&gt; at the New Security Paradigms Workshop in Charlottesville, Virginia. The authors, Cynthia Phillips and Laura Painton Swiler, proposed that a network&apos;s worst-case attack was a &lt;em&gt;graph traversal&lt;/em&gt; problem [@phillips-swiler-1998-nspw]. Nodes encoded network states (the set of attacker privileges across the set of hosts). Edges encoded atomic attack steps (an exploit that, given prerequisite privileges, granted new ones). The shortest path from &quot;attacker outside the network&quot; to &quot;attacker has goal asset&quot; was the worst-case attack.&lt;/p&gt;
&lt;p&gt;Phillips and Swiler observed in a now-canonical sentence that &lt;em&gt;&quot;the security of a network is more than the sum of the security of its hosts.&quot;&lt;/em&gt; It is the conceptual ancestor of every attack-path tool that followed.&lt;/p&gt;
&lt;p&gt;Four years later, at IEEE Symposium on Security and Privacy 2002, Oleg Sheyner (then a CMU PhD student) along with Joshua Haines and Richard Lippmann (MIT Lincoln Lab), Somesh Jha (Wisconsin), and Jeannette Wing (CMU) made the construction automatic [@sheyner-et-al-2002-attack-graphs]. They encoded the network and attacker model as an NuSMV model-checker specification, treated the negation of the security goal as a temporal-logic property, and let the model checker generate every counterexample. The union of counterexamples was the attack graph.&lt;/p&gt;
&lt;p&gt;They also proved that the &lt;em&gt;minimum&lt;/em&gt; set of edges whose removal disconnects the attacker from the goal is NP-hard, but admits an O(log n) approximation -- the first asymptotic bound for the defender-hardening problem.&lt;/p&gt;
&lt;p&gt;Sheyner&apos;s companion 2004 thesis -- &lt;em&gt;Scenario Graphs and Attack Graphs&lt;/em&gt;, CMU-CS-04-122 -- remains the most readable book-length treatment of the academic-attack-graph generation [@sheyner-2004-thesis].&lt;/p&gt;
&lt;p&gt;By 2005 the academic line had a scale solution. Xinming Ou, Sudhakar Govindavajhala, and Andrew Appel at Princeton released &lt;strong&gt;MulVAL&lt;/strong&gt; at USENIX Security 2005 [@mulval-usenix-2005], encoding network state and attacker rules as Datalog facts and running them through XSB Prolog. From the paper&apos;s abstract: &lt;em&gt;&quot;Once the information is collected, the analysis can be performed in seconds for networks with thousands of machines.&quot;&lt;/em&gt; NetSPA at MIT Lincoln Lab (2006) and TVA / CAULDRON at George Mason (2003 to 2005) achieved similar scale through different mechanisms.&lt;/p&gt;
&lt;p&gt;In parallel, on the Windows operator side, Sean Metcalf was running adsecurity.org and documenting AD misconfiguration patterns one writeup at a time [@adsecurity-org]. Microsoft was rolling out its Enhanced Security Administrative Environment (&quot;Red Forest&quot;) tiered-administration model -- the predecessor that the Enterprise Access Model later replaced [@ms-enterprise-access-model] -- which was implicitly graph-aware (the entire ESAE prescription is a tier diagram) but never exposed the tier graph as a queryable structure. ESAE was a deployment blueprint, not a tool.&lt;/p&gt;

gantt
    title Three parallel tracks converging on the attack-path graph
    dateFormat YYYY
    axisFormat %Y&lt;pre&gt;&lt;code&gt;section Academic CVE line
Phillips and Swiler NSPW   :a1, 1998, 1999
Sheyner et al. IEEE SP     :a2, 2002, 2003
MulVAL USENIX              :a3, 2005, 2006
NetSPA TVA                 :a4, 2006, 2008

section Windows operator line
adsecurity.org writeups    :b1, 2014, 2016
PowerView                  :b2, 2014, 2016
Lambert essay              :milestone, b3, 2015-04-26, 1d
BloodHound DEF CON 24      :b4, 2016, 2018
AzureHound BloodHound 4.0  :b5, 2020, 2023
BloodHound CE 5.0          :b6, 2023, 2024
ADCS attack paths          :b7, 2024, 2025
Butterfly v6.3             :b8, 2024, 2026
OpenGraph v8.0             :b9, 2025, 2026

section Microsoft defender stack
ESAE Red Forest blueprint  :c1, 2015, 2018
Azure ATP LMPs preview     :c2, 2018, 2020
Defender CSPM attack paths :c3, 2022, 2024
MSEM GA                    :milestone, c4, 2024-11-19, 1d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The April 2015 essay sat between the two tracks. Lambert was inside Microsoft. He had read the academic literature. He worked next to the threat-intelligence teams who watched real intrusions unfold. The essay was the moment the two communities&apos; vocabularies met. The diagnosis was right. The cure was sixteen months away. It would not come from Microsoft, and it would not come from academia. It would come from a red-team consultancy and a free graph database, and it would debut on a Saturday in August at a hacker convention in Las Vegas.&lt;/p&gt;
&lt;h2&gt;4. Defenders evaluating Active Directory as a list of ACLs&lt;/h2&gt;
&lt;p&gt;If you were a Microsoft-aligned AD security engineer in 2013, your job was to read ACLs. One object at a time. Down a list.&lt;/p&gt;
&lt;p&gt;The mainstream defender toolchain of the era was, almost without exception, list-shaped. Microsoft shipped &lt;code&gt;dsacls.exe&lt;/code&gt; and PowerShell&apos;s &lt;code&gt;Get-Acl&lt;/code&gt;; both produced row-oriented output that an analyst read sequentially. The commercial AD-audit market -- NetWrix Auditor [@netwrix-auditor], ManageEngine ADAudit Plus [@manageengine-adaudit-plus], Quest ActiveRoles [@quest-activeroles], and others -- produced HTML reports with one section per directory object, one row per non-default Access Control Entry, and severity classifications based on a fixed checklist of &quot;dangerous rights&quot; (&lt;code&gt;GenericAll&lt;/code&gt;, &lt;code&gt;WriteDacl&lt;/code&gt;, &lt;code&gt;WriteOwner&lt;/code&gt;, and a handful of others).&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s own &lt;em&gt;Best Practices for Securing Active Directory&lt;/em&gt; document codified this approach. Its central recommendation was &lt;em&gt;per-object delegation review&lt;/em&gt;: walk the directory tree, evaluate each object&apos;s ACL against a hardening checklist, and remediate non-default ACEs that exceed the documented privilege model [@ms-best-practices-ad]. The document is excellent at what it sets out to do. What it does not do -- because the format does not permit it -- is compose multi-hop reachability.&lt;/p&gt;
&lt;p&gt;This was the failure mode Lambert described. A help-desk operator with &lt;code&gt;ForceChangePassword&lt;/code&gt; on a junior service account appears as one row in the audit. The junior service account&apos;s &lt;code&gt;MemberOf Server Operators&lt;/code&gt; membership appears in a different report section. The &lt;code&gt;Server Operators&lt;/code&gt; group&apos;s logon rights on the Domain Controller appear in the Domain Controller&apos;s own report. Three findings in three places, with no machinery to compose them. The data model cannot represent the question.&lt;/p&gt;

A reader trained on Phillips and Swiler, Sheyner et al., MulVAL, NetSPA, and TVA might object that *the field already had* graph-based attack-path analysis a decade before BloodHound. True -- in academia. The academic line solved the scale problem (MulVAL: *&quot;seconds for networks with thousands of machines&quot;*) but spoke the wrong vocabulary. Its atomic attack step was a CVE exploit -- a buffer overflow, a format-string bug, a daemon remote code execution. The dominant AD attack primitive is not a CVE. A user with `WriteDacl` on a group is not exploiting any vulnerability; they are using the system as designed. None of MulVAL, NetSPA, or TVA developed an AD-style privilege-graph input format, and the operator community never adopted them. The academic line was substrate-mismatched and delivered as research-PDF tarballs rather than as `git clone`-able tools.
&lt;p&gt;Two communities, both wrong in different ways. The academic one had the right algorithm with the wrong substrate. The operator one had the right substrate with no algorithm at all. The fix was to fuse them. That happened on August 6, 2016.&lt;/p&gt;
&lt;h2&gt;5. The breakthrough -- BloodHound and Six Degrees of Domain Admin&lt;/h2&gt;
&lt;p&gt;Saturday, August 6, 2016. 1:00 PM. DEF CON 24, Track 2, Paris and Bally&apos;s hotel-casinos in Las Vegas [@defcon-24-archive]. Three speakers from Veris Group&apos;s adaptive threat division step on stage: Andy Robbins, Rohan Vazarkar, Will Schroeder. The talk is titled &lt;em&gt;Six Degrees of Domain Admin: Using Graph Theory to Accelerate Red Team Operations&lt;/em&gt; [@defcon24-bloodhound-slides]. The Veris Group team would spin out as SpecterOps the following year, but the August 2016 attribution -- Robbins (&lt;code&gt;@_wald0&lt;/code&gt;), Vazarkar (&lt;code&gt;@CptJesus&lt;/code&gt;), and Schroeder (&lt;code&gt;@harmj0y&lt;/code&gt;) -- is the canonical one [@bloodhound-legacy-repo].&lt;/p&gt;
&lt;p&gt;The talk demonstrated three design decisions that in retrospect look obvious and at the time were not.&lt;/p&gt;
&lt;p&gt;First, &lt;strong&gt;model Active Directory as a directed property graph&lt;/strong&gt;. Nodes are typed security principals (User, Computer, Group, Domain, OU, GPO). Edges are typed privilege relationships (MemberOf, AdminTo, HasSession, GenericAll, GenericWrite, WriteDacl, ForceChangePassword, and others). This was Lambert&apos;s framing made concrete: every ACE that grants a dangerous right becomes an edge from the trustee to the target.&lt;/p&gt;
&lt;p&gt;Second, &lt;strong&gt;reuse Neo4j as the engine&lt;/strong&gt;. Do not build a custom graph database; piggyback on Cypher&apos;s pattern-matching query language. The cost of building a path-finding engine from scratch was non-trivial; the cost of standing up Neo4j was a single Docker container.&lt;/p&gt;
&lt;p&gt;Third, &lt;strong&gt;ship a collector that emits typed JSON edges, not raw NTSecurityDescriptors&lt;/strong&gt;. The collector&apos;s value is the &lt;em&gt;interpretation&lt;/em&gt; of an ACE as a graph edge -- the mapping from a binary security descriptor to the typed edge that says &quot;trustee X has GenericWrite on object Y&quot; is the hard part, and a defender re-creating that mapping per query would lose. SharpHound (initially &lt;code&gt;SharpHound.ps1&lt;/code&gt;, then a C# binary) does the interpretation once at collection time and writes the edges to disk [@bloodhound-ce-repo].&lt;/p&gt;

SharpHound&apos;s enumeration calls are visibly descended from PowerView. The November 2020 SpecterOps blog announcing BloodHound 4.0 acknowledges the lineage explicitly, naming Schroeder&apos;s joint authorship of both projects and crediting PowerView as the data-collection precursor [@specterops-2020-bloodhound-4]. PowerView&apos;s August 2014 release was the substrate that made BloodHound&apos;s August 2016 synthesis possible. The chain is unbroken: enumeration in 2014, framing in April 2015, graph synthesis in August 2016.
&lt;p&gt;The five-day-of-whiteboard-work figure comes from the SpecterOps team&apos;s own internal benchmark from the original DEF CON 24 talk. The 200 ms query latency is the typical 2024 figure on a mid-size enterprise forest of roughly 10^5 nodes and 10^6 edges, retained as the company&apos;s marketing framing across subsequent blog posts.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; BloodHound&apos;s breakthrough was neither an algorithm nor an architecture. It was the decision to &lt;em&gt;interpret&lt;/em&gt; Active Directory&apos;s access-control data as a typed graph and ship that interpretation as a tool the operator community could actually run.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Crucially, the choice to use Neo4j&apos;s free &lt;code&gt;shortestPath()&lt;/code&gt; function rather than building a custom path-finder was a &lt;em&gt;delivery&lt;/em&gt; decision as much as a &lt;em&gt;technical&lt;/em&gt; one. Neo4j already did breadth-first shortest paths. The team did not need to invent anything. The hard work was in the edge taxonomy and the collector, not in the graph database.&lt;/p&gt;
&lt;p&gt;The talk made a promise the rest of the article must now cash: that there is a real algorithm under the hood, that the algorithm has a name, and that the name is not Dijkstra.&lt;/p&gt;
&lt;h2&gt;6. The algorithmic core -- property graphs, Cypher, and shortest paths&lt;/h2&gt;
&lt;p&gt;If you have never written a Cypher query in your life, the next ninety seconds is the entirety of the syntax you need.&lt;/p&gt;

A graph in which both nodes and edges are typed and can carry key-value properties. A node might have type `User` and properties `name=&apos;BOB@CORP.LOCAL&apos;`, `enabled=true`, `pwdlastset=1719234234`. An edge might have type `GenericWrite` and properties `source=&apos;ACE&apos;`, `isacl=true`. This is the data model Neo4j implements and the model BloodHound uses.
&lt;p&gt;A Cypher pattern is a parenthesised node, a bracketed edge, and a parenthesised node, with arrows showing direction. &lt;code&gt;(u:User)-[:MemberOf]-&amp;gt;(g:Group)&lt;/code&gt; reads &quot;find a node &lt;code&gt;u&lt;/code&gt; of type User connected by a MemberOf edge to a node &lt;code&gt;g&lt;/code&gt; of type Group.&quot; A full query has four parts: &lt;code&gt;MATCH&lt;/code&gt; for the pattern, optional &lt;code&gt;WHERE&lt;/code&gt; for filters, &lt;code&gt;RETURN&lt;/code&gt; for the output. That&apos;s it.Cypher patterns visually mimic ASCII graph drawings: parentheses are nodes, square brackets are edges, and the arrow direction matches the edge direction. The syntax was deliberately designed to look like the diagram you would sketch on a whiteboard.&lt;/p&gt;

The pattern-matching query language originally created for the Neo4j graph database. Cypher&apos;s syntax is declarative: you describe the shape of the data you want, and the engine plans the traversal. Since April 2024 Cypher has been the basis of the ISO/IEC 39075:2024 GQL standard -- the first ISO-standardised graph query language [@iso-39075-2024-gql] [@opencypher-home].
&lt;p&gt;Here is the canonical BloodHound query, with annotations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cypher&quot;&gt;MATCH p = shortestPath(
  (u:User {name:&apos;BOB@CORP.LOCAL&apos;})       // start node: Bob
    -[*1..]-&amp;gt;                            // any number of typed edges
  (g:Group {name:&apos;DOMAIN ADMINS@CORP.LOCAL&apos;})  // end node: Domain Admins
)
RETURN p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;shortestPath()&lt;/code&gt; wrapper tells Neo4j to short-circuit at the first solution. The variable-length quantifier &lt;code&gt;[*1..]&lt;/code&gt; says &quot;one or more edges of any type.&quot; The &lt;code&gt;p =&lt;/code&gt; binds the entire matched path to the variable &lt;code&gt;p&lt;/code&gt;, which the &lt;code&gt;RETURN&lt;/code&gt; then emits as a sequence of node-edge-node triples that the BloodHound frontend renders as an SVG.&lt;/p&gt;
&lt;p&gt;What does &lt;code&gt;shortestPath()&lt;/code&gt; actually run? Here is where the misconception that BloodHound uses Dijkstra needs to die. The current Neo4j Cypher manual is explicit that &lt;code&gt;shortestPath()&lt;/code&gt; runs an unweighted &lt;strong&gt;bidirectional traversal&lt;/strong&gt; -- BFS in the classical sense -- between the source and target nodes [@neo4j-cypher-shortest-paths]. Not Dijkstra. Not A*. BFS.&lt;/p&gt;

A graph-traversal algorithm that explores all nodes at the current depth before proceeding to the next depth. Starting from the source, it visits all 1-hop neighbours, then all 2-hop neighbours, and so on. For unweighted graphs, BFS is guaranteed to find a shortest path (in number of edges) the first time it reaches the target. Worst-case time is O(V + E) where V is the node count and E is the edge count -- the trivial information-theoretic lower bound for any algorithm that must read the input.
&lt;p&gt;Cypher does support weighted shortest paths via the Neo4j Graph Data Science library, but the BloodHound CE distribution does not enable it. There is no natural cost metric on Active Directory privilege edges in 2026; every edge is treated as one hop.&lt;/p&gt;
&lt;p&gt;Why BFS rather than Dijkstra? Dijkstra is BFS&apos;s generalisation to weighted graphs. If your edges have natural costs -- road distances, link latencies, dollar prices -- Dijkstra (with a Fibonacci heap, $O(E + V\log V)$) gives you shortest paths under that cost metric. Active Directory privilege edges do not have a natural cost metric. &lt;code&gt;MemberOf&lt;/code&gt;, &lt;code&gt;GenericAll&lt;/code&gt;, and &lt;code&gt;CanRDP&lt;/code&gt; are all &quot;the attacker can take this step.&quot; Some are easier than others, but quantifying &lt;em&gt;how much&lt;/em&gt; easier is itself an unsolved problem (see Section 11). Treating every edge as one hop is the load-bearing simplification that makes the model tractable.&lt;/p&gt;

Of a binary relation R: the relation R+ that contains the pair (a, c) whenever there is a chain a R b R ... R c. For a graph, the transitive closure tells you, for every pair of nodes, whether one is reachable from the other. BloodHound&apos;s `shortestPath()` queries can be thought of as on-demand evaluation of the transitive closure restricted to one source-target pair.
&lt;p&gt;The per-query complexity is $O(V + E)$, the standard BFS bound from any algorithms textbook. On a mid-size enterprise forest -- roughly 10^5 nodes and 10^6 edges -- a single user-to-group shortest path returns in sub-second wall-clock time.&lt;/p&gt;
&lt;p&gt;The variable-length quantifier &lt;code&gt;[*1..N]&lt;/code&gt; for general path enumeration is a different matter. Cyclic graphs admit exponentially many paths in N, and Neo4j&apos;s documentation explicitly warns that quantified path patterns can return exponentially many results in the worst case [@neo4j-cypher-variable-length]. The &lt;code&gt;shortestPath()&lt;/code&gt; short-circuit avoids this by returning on the first hit; &lt;code&gt;allShortestPaths()&lt;/code&gt; enumerates only paths tied for shortest; unbounded enumeration is intractable on any non-trivial graph.&lt;/p&gt;
&lt;p&gt;A concrete demonstration is in order. The runnable snippet below is a 35-line implementation of unweighted BFS over a six-node toy graph. It returns the shortest path from a &lt;code&gt;helpdesk&lt;/code&gt; user to the &lt;code&gt;domain-admin&lt;/code&gt; group. This is, structurally, the same algorithm Neo4j&apos;s &lt;code&gt;shortestPath()&lt;/code&gt; runs. The numerical answer (&quot;path of length 4&quot;) is the same number BloodHound would report on the same graph.&lt;/p&gt;
&lt;p&gt;{`
// Edges modelled as a typed adjacency list.
const edges = {
  helpdesk:        [{ to: &apos;carol&apos;,          via: &apos;ForceChangePassword&apos; }],
  carol:           [{ to: &apos;serverops&apos;,      via: &apos;MemberOf&apos; }],
  serverops:       [{ to: &apos;dc01&apos;,           via: &apos;CanRDP&apos; }],
  dc01:            [{ to: &apos;krbtgt&apos;,         via: &apos;DCSync&apos; }],
  krbtgt:          [{ to: &apos;domain-admin&apos;,   via: &apos;GoldenTicket&apos; }],
  domain_admin:    []
};&lt;/p&gt;
&lt;p&gt;function shortestPath(start, goal) {
  const queue = [[start]];           // queue holds candidate paths
  const seen  = new Set([start]);    // each node enqueued at most once&lt;/p&gt;
&lt;p&gt;  while (queue.length) {
    const path = queue.shift();      // BFS = FIFO
    const node = path[path.length - 1];
    if (node === goal) return path;
    for (const e of (edges[node] || [])) {
      if (!seen.has(e.to)) {
        seen.add(e.to);
        queue.push([...path, e.to]);
      }
    }
  }
  return null;                       // unreachable
}&lt;/p&gt;
&lt;p&gt;const path = shortestPath(&apos;helpdesk&apos;, &apos;domain-admin&apos;);
console.log(&apos;Path:&apos;, path.join(&apos; -&amp;gt; &apos;));
console.log(&apos;Length:&apos;, path.length - 1, &apos;hops&apos;);
`}&lt;/p&gt;
&lt;p&gt;The algorithm fits on a single screen. The hard work of BloodHound is not in this loop. The hard work is in deciding &lt;em&gt;which edges to insert into the graph in the first place&lt;/em&gt;. That decision -- the edge taxonomy -- is what makes BloodHound a security tool rather than a graph-database demo.&lt;/p&gt;

sequenceDiagram
    participant Client as Cypher client
    participant Planner as Cypher planner
    participant Engine as BFS engine
    participant Graph as Property graph
    Client-&amp;gt;&amp;gt;Planner: MATCH shortestPath(u to g)
    Planner-&amp;gt;&amp;gt;Engine: plan(start=u, end=g, bidirectional=true)
    Engine-&amp;gt;&amp;gt;Graph: expand 1-hop frontier from u
    Engine-&amp;gt;&amp;gt;Graph: expand 1-hop frontier from g
    Graph--&amp;gt;&amp;gt;Engine: neighbours of u, neighbours of g
    Engine-&amp;gt;&amp;gt;Graph: expand 2-hop frontier (both sides)
    Engine-&amp;gt;&amp;gt;Graph: expand 3-hop frontier (both sides)
    Graph--&amp;gt;&amp;gt;Engine: frontiers intersect at node m
    Engine--&amp;gt;&amp;gt;Planner: reconstruct path u to m to g
    Planner--&amp;gt;&amp;gt;Client: return path
&lt;h2&gt;7. The edge taxonomy -- what Active Directory actually looks like as a graph&lt;/h2&gt;
&lt;p&gt;The BloodHound graph is not &lt;em&gt;every privilege relationship in Active Directory.&lt;/em&gt; It is the set of relationships that the SpecterOps team has decided -- by iterative discovery, often in response to specific community-reported abuse primitives -- to model. The taxonomy has grown roughly monotonically since 2016; the rate has accelerated since the BloodHound CE 5.0 reboot in August 2023 [@specterops-2023-bloodhound-ce].&lt;/p&gt;
&lt;p&gt;Eight families dominate the 2026 graph. They map, family by family, onto the substrates of a modern hybrid enterprise.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Group membership.&lt;/strong&gt; &lt;code&gt;MemberOf&lt;/code&gt; is the simplest edge. Cypher&apos;s variable-length quantifier (&lt;code&gt;[:MemberOf*1..]&lt;/code&gt;) walks transitive memberships in one expression, which is why nested-group reachability is a one-liner.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. ACL write-equivalents.&lt;/strong&gt; &lt;code&gt;GenericAll&lt;/code&gt;, &lt;code&gt;GenericWrite&lt;/code&gt;, &lt;code&gt;WriteDacl&lt;/code&gt;, &lt;code&gt;WriteOwner&lt;/code&gt;, &lt;code&gt;Owns&lt;/code&gt;, &lt;code&gt;AllExtendedRights&lt;/code&gt;, &lt;code&gt;ForceChangePassword&lt;/code&gt;, &lt;code&gt;AddSelf&lt;/code&gt;, &lt;code&gt;AddMember&lt;/code&gt;, and &lt;code&gt;AddKeyCredentialLink&lt;/code&gt; (the shadow-credentials primitive). Each names a specific dangerous-right pattern on a directory object&apos;s security descriptor. SharpHound&apos;s interpreter scans the &lt;code&gt;nTSecurityDescriptor&lt;/code&gt; attribute and emits one typed edge per matching ACE.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Sessions.&lt;/strong&gt; &lt;code&gt;HasSession&lt;/code&gt; is the dynamic edge that goes stale fastest. SharpHound enumerates active sessions via &lt;code&gt;NetSessionEnum&lt;/code&gt; and &lt;code&gt;SAMR&lt;/code&gt;; the resulting edges describe &quot;user U is currently logged into computer C.&quot; The graph is whatever the most recent collection captured.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Remote execution rights.&lt;/strong&gt; &lt;code&gt;AdminTo&lt;/code&gt;, &lt;code&gt;CanRDP&lt;/code&gt;, &lt;code&gt;ExecuteDCOM&lt;/code&gt;, &lt;code&gt;CanPSRemote&lt;/code&gt;, &lt;code&gt;SQLAdmin&lt;/code&gt;. Each describes a code-execution primitive granted to a principal on a computer object.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. &lt;a href=&quot;https://paragmali.com/blog/kerberos-in-windows-the-other-half-of-ntlmless/&quot; rel=&quot;noopener&quot;&gt;Kerberos delegation&lt;/a&gt;.&lt;/strong&gt; &lt;code&gt;AllowedToDelegate&lt;/code&gt; (constrained delegation), &lt;code&gt;AllowedToAct&lt;/code&gt; (resource-based constrained delegation, via &lt;code&gt;msDS-AllowedToActOnBehalfOfOtherIdentity&lt;/code&gt;), unconstrained delegation surfaced as node properties on Computer objects, and -- from BloodHound CE v6.3 in December 2024 [@bloodhound-v6-3-release] -- a &lt;code&gt;CoerceToTGT&lt;/code&gt; edge that replaces the older &lt;code&gt;UnconstrainedDelegation&lt;/code&gt; finding for BHE customers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. &lt;a href=&quot;https://paragmali.com/blog/certified-pre-owned-ad-cs-and-active-directorys-second-trust/&quot; rel=&quot;noopener&quot;&gt;ADCS&lt;/a&gt; edges (early-access January 2024).&lt;/strong&gt; &lt;code&gt;ADCSESC1&lt;/code&gt; through &lt;code&gt;ADCSESC10&lt;/code&gt;, plus the &lt;strong&gt;&lt;code&gt;CoerceAndRelayNTLMToADCS&lt;/code&gt; edge for ESC8&lt;/strong&gt; [@specterops-2024-adcs-bloodhound]. These edges land in BloodHound roughly thirty months after Will Schroeder and Lee Christensen first published the ESC1 to ESC8 catalog in &lt;em&gt;Certified Pre-Owned: Abusing Active Directory Certificate Services&lt;/em&gt; [@specterops-2021-certified-preowned]. Each ADCS edge in BloodHound is the most complex in the taxonomy because each is composed from multiple raw facts (see the traversable / non-traversable discussion below).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7. Azure / Entra ID edges&lt;/strong&gt; (via AzureHound, November 20, 2020) [@specterops-2020-bloodhound-4]. &lt;code&gt;AZGlobalAdmin&lt;/code&gt;, &lt;code&gt;AZRoleAssignment&lt;/code&gt;, &lt;code&gt;AZContains&lt;/code&gt;, &lt;code&gt;AZOwns&lt;/code&gt;, &lt;code&gt;AZUserAccessAdministrator&lt;/code&gt;, &lt;code&gt;AZAddSecret&lt;/code&gt;, &lt;code&gt;AZMGAddOwner&lt;/code&gt;, plus AzureRM-side resource roles. Microsoft Entra &lt;a href=&quot;https://paragmali.com/blog/privileged-identity-management-how-a-two-state-role-assignme/&quot; rel=&quot;noopener&quot;&gt;Privileged Identity Management (PIM)&lt;/a&gt; role coverage was added in BloodHound v8.0 in July 2025 [@specterops-2025-opengraph].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8. OpenGraph custom edges (v8.0, July 29, 2025).&lt;/strong&gt; User-defined edges for arbitrary substrates: GitHub, Snowflake, Microsoft SQL Server, ServiceNow, Tailscale, Duo. The schema is intentionally generic so that a community contributor can ship edges for any system whose privilege model can be drawn as a graph [@bloodhound-opengraph-library].&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Family&lt;/th&gt;
&lt;th&gt;Representative edges&lt;/th&gt;
&lt;th&gt;Underlying AD mechanism&lt;/th&gt;
&lt;th&gt;What it gives the attacker&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Group membership&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MemberOf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;member&lt;/code&gt; attribute on group object&lt;/td&gt;
&lt;td&gt;Inherits all permissions held by the group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACL write-equivalents&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GenericAll&lt;/code&gt;, &lt;code&gt;GenericWrite&lt;/code&gt;, &lt;code&gt;WriteDacl&lt;/code&gt;, &lt;code&gt;WriteOwner&lt;/code&gt;, &lt;code&gt;ForceChangePassword&lt;/code&gt;, &lt;code&gt;AddKeyCredentialLink&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Specific dangerous-right ACE patterns in &lt;code&gt;nTSecurityDescriptor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Take control of the target principal (reset password, modify object, plant shadow credentials)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HasSession&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NetSessionEnum&lt;/code&gt; and &lt;code&gt;SAMR&lt;/code&gt; enumeration on member computers&lt;/td&gt;
&lt;td&gt;Pivot via credential theft from the logged-in user&apos;s memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote execution&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AdminTo&lt;/code&gt;, &lt;code&gt;CanRDP&lt;/code&gt;, &lt;code&gt;ExecuteDCOM&lt;/code&gt;, &lt;code&gt;CanPSRemote&lt;/code&gt;, &lt;code&gt;SQLAdmin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Local-admin membership, RDP / DCOM / WinRM / SQL group rights&lt;/td&gt;
&lt;td&gt;Run arbitrary code as the target principal on the target host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kerberos delegation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AllowedToDelegate&lt;/code&gt;, &lt;code&gt;AllowedToAct&lt;/code&gt;, &lt;code&gt;CoerceToTGT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Constrained and resource-based delegation attributes&lt;/td&gt;
&lt;td&gt;Forge service tickets and impersonate other accounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADCS composite&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ADCSESC1&lt;/code&gt; through &lt;code&gt;ADCSESC10&lt;/code&gt;, &lt;code&gt;CoerceAndRelayNTLMToADCS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Certificate template misconfigurations plus CA trust plus enrollment ACEs&lt;/td&gt;
&lt;td&gt;Obtain a certificate usable for authentication as a privileged account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure / Entra&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AZGlobalAdmin&lt;/code&gt;, &lt;code&gt;AZRoleAssignment&lt;/code&gt;, &lt;code&gt;AZAddSecret&lt;/code&gt;, &lt;code&gt;AZOwns&lt;/code&gt;, &lt;code&gt;AZMGAddOwner&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Entra role assignments, AzureRM RBAC&lt;/td&gt;
&lt;td&gt;Cross the on-prem to cloud boundary; pivot via tenant or subscription privileges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenGraph&lt;/td&gt;
&lt;td&gt;User-defined&lt;/td&gt;
&lt;td&gt;Any substrate the contributor models&lt;/td&gt;
&lt;td&gt;Anything the contributed schema encodes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The ADCS family deserves a closer look because it introduced an important new modelling vocabulary.&lt;/p&gt;

A *traversable* edge is one the shortest-path query can step through directly: `MemberOf`, `ForceChangePassword`, `CanRDP`. A *non-traversable* edge is a precondition relationship that is only exploitable when several others appear together. A certificate template&apos;s `Enroll` ACE is non-traversable on its own; combined with eight other facts about the template, the issuing CA, and the domain&apos;s trust posture, it composes into `ADCSESC1`. The post-processor scans for the full pattern and synthesises a single traversable edge that the BFS can then treat as one hop [@specterops-2024-adcs-bloodhound].
&lt;p&gt;For ESC1 the pattern has nine numbered prerequisites: six template and CA requirements, two enterprise-CA trust facts, and one implicit constraint. None of the nine raw facts is exploitable in isolation. All nine together are. The post-processor&apos;s job is to walk the candidate sub-graphs, check every requirement, and write the composed &lt;code&gt;ADCSESC1&lt;/code&gt; edge when the pattern holds.&lt;/p&gt;
&lt;p&gt;This is a non-trivial graph-modelling contribution because it gives the field a vocabulary for &quot;an edge that is real only as a join over several facts.&quot; It also generalises beyond ADCS: any future attack primitive composed from a fixed pattern of raw facts can be modelled the same way.&lt;/p&gt;
&lt;p&gt;ESC8 -- the &lt;a href=&quot;https://paragmali.com/blog/ntlmless-the-death-of-ntlm-in-windows/&quot; rel=&quot;noopener&quot;&gt;NTLM relay&lt;/a&gt; primitive against an HTTP-enrollment certificate authority -- is the most delicate case, and the one most commonly mis-modelled in early secondary writeups.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The &lt;code&gt;CoerceAndRelayNTLMToADCS&lt;/code&gt; edge is a &lt;strong&gt;Group(&lt;code&gt;Authenticated Users&lt;/code&gt;) to Computer(coerced target)&lt;/strong&gt; edge, not a Computer to Computer edge as some early secondary writeups described. The relay-target CA and the certificate template are carried as &lt;em&gt;edge metadata&lt;/em&gt;, not as additional graph nodes. The canonical edge documentation is explicit: &lt;em&gt;Source: Authenticated Users [Group] / Destination: Computer / Traversable: Yes&lt;/em&gt; [@bloodhound-coerce-relay-edge].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The schema correction matters because the source-principal choice affects every shortest-path query that crosses ESC8. If the edge is mis-modelled as Computer to Computer, queries that begin from a low-privilege user account miss the path entirely. The Group-to-Computer schema correctly captures that &lt;em&gt;any authenticated principal&lt;/em&gt; can coerce.&lt;/p&gt;
&lt;p&gt;What the graph does not yet model is also worth naming. The &lt;code&gt;SpecterOps/TierZeroTable&lt;/code&gt; README states it verbatim: &lt;em&gt;&quot;DISCLAIMER: The table does not include all Tier Zero assets yet.&quot;&lt;/em&gt; [@specterops-tier-zero-table] Several edge classes remain partially or fully out of scope; the full enumeration appears in Section 11 (open problems). Coverage expansion is iterative and community-fed; OpenGraph (Section 9) is the structural answer to &quot;where does the graph end?&quot;&lt;/p&gt;

flowchart TD
    subgraph OnPrem[&quot;On-prem Active Directory&quot;]
        Members[&quot;MemberOf, Owns&quot;]
        ACL[&quot;ACL write-equivalents&lt;br /&gt;GenericAll, WriteDacl, ForceChangePassword,&lt;br /&gt;AddKeyCredentialLink&quot;]
        Sessions[&quot;HasSession&quot;]
        Exec[&quot;AdminTo, CanRDP, ExecuteDCOM,&lt;br /&gt;CanPSRemote, SQLAdmin&quot;]
        Krb[&quot;Kerberos delegation&lt;br /&gt;AllowedToDelegate, AllowedToAct, CoerceToTGT&quot;]
    end
    subgraph Entra[&quot;Entra ID and AzureRM&quot;]
        EntraRoles[&quot;AZGlobalAdmin, AZRoleAssignment,&lt;br /&gt;AZAddSecret, AZUserAccessAdministrator,&lt;br /&gt;PIM roles&quot;]
        AzureRM[&quot;AZContains, AZOwns,&lt;br /&gt;AzureRM resource roles&quot;]
    end
    subgraph ADCS[&quot;ADCS composite edges&quot;]
        ESC[&quot;ADCSESC1 to ADCSESC10,&lt;br /&gt;CoerceAndRelayNTLMToADCS&quot;]
    end
    subgraph Open[&quot;OpenGraph user-defined&quot;]
        Custom[&quot;GitHub, Snowflake, SQL Server,&lt;br /&gt;ServiceNow, Tailscale, Duo, custom&quot;]
    end
    OnPrem --&amp;gt; Cypher((Cypher query layer))
    Entra --&amp;gt; Cypher
    ADCS --&amp;gt; Cypher
    Open --&amp;gt; Cypher
&lt;p&gt;If this is what one community modelled, the natural question is: what did Microsoft model? And when?&lt;/p&gt;
&lt;h2&gt;8. The defender adoption -- Microsoft catches up, 2018 to 2024&lt;/h2&gt;
&lt;p&gt;The defender vendor whose product BloodHound was mapping is also a defender vendor with a graph product of its own. Three of them, in fact, shipped in three different years for three different substrates. They are easy to confuse; press releases sometimes do.&lt;/p&gt;
&lt;p&gt;The first arrived on November 27, 2018, when Tali Ash (then a Program Manager on the Azure Advanced Threat Protection team) announced a preview feature called &lt;em&gt;Lateral Movement Paths&lt;/em&gt; (LMPs) in a Microsoft tech-community post [@ms-azure-atp-lmp-2018]. LMPs were a graph-shaped visualisation, but a constrained one: restricted to &quot;sensitive accounts&quot; (a configurable set defaulting to Domain Admins and similar) plus non-sensitive accounts that had shared a session on the same host as a sensitive account.&lt;/p&gt;
&lt;p&gt;The portal rendered one- and two-hop credential-theft pivots as a static SVG. There was no Cypher equivalent, no LMP-export API, and no way to write a custom query. Azure ATP was rebranded &lt;strong&gt;Microsoft Defender for Identity&lt;/strong&gt; in 2020, and the LMP feature came along under the new name [@ms-mdi-lmp-docs].&lt;/p&gt;
&lt;p&gt;Several secondary sources date Lateral Movement Paths to &quot;June 2019,&quot; which corresponds to the general-availability and rebrand window rather than the original preview announcement. The primary Microsoft tech-community post is November 27, 2018; treat the year-not-month for any third-party claim and prefer the November 2018 preview date as the canonical first ship.&lt;/p&gt;
&lt;p&gt;The second arrived in October 2022, when Microsoft Defender for Cloud&apos;s Defender CSPM plan added a &lt;em&gt;cloud security graph&lt;/em&gt; with attack-path analysis (public preview at Ignite October 2022; generally available March 28, 2023) [@ms-defender-cloud-attack-path]. This product is a &lt;em&gt;cloud&lt;/em&gt; attack-path graph: Azure plus AWS plus GCP asset inventory, with inferred edges for permissions, network reachability, vulnerability presence, and internet exposure. It is explicitly &lt;em&gt;not&lt;/em&gt; the Active Directory identity graph; it covers the multi-cloud workload surface.&lt;/p&gt;
&lt;p&gt;A common mistake conflates this 2022-2023 product (Defender for &lt;em&gt;Cloud&lt;/em&gt;) with Microsoft Defender for &lt;em&gt;Identity&lt;/em&gt; (the LMP product from November 2018). The substrate, the team, and the year are all different. Worth flagging here because secondary writeups repeat the confusion often.&lt;/p&gt;

The Microsoft Defender XDR product family is a naming minefield. *Defender for Identity* (MDI) is the on-prem AD identity-threat product; LMPs are its graph view. *Defender for Cloud* (MDC) is the multi-cloud workload-protection product; its CSPM plan ships cloud-security-graph attack-path analysis. *Defender for Endpoint* (MDE) is the EDR product; it does not ship its own attack-path graph but feeds telemetry into MSEM. *Microsoft Security Exposure Management* (MSEM, GA November 19, 2024) is the unified exposure-graph layer that subsumes the others. Four products, four substrates, four ship dates. The naming overlap is unfortunate but the distinctions are real.
&lt;p&gt;The third arrived on November 19, 2024, at the Ignite 2024 keynote in Chicago. Satya Nadella, in the opening keynote, announced that &lt;strong&gt;Microsoft Security Exposure Management&lt;/strong&gt; (MSEM) had reached general availability [@ms-ignite-2024-msem]. MSEM is the product whose attack-path model is &lt;em&gt;structurally equivalent&lt;/em&gt; to BloodHound&apos;s: cross-substrate (identity plus endpoint plus multi-cloud), first-class attack-path objects with choke-point and blast-radius dashboards, and continuous data feed via the Defender XDR signal plane.&lt;/p&gt;

Microsoft&apos;s unified exposure-graph product, generally available November 19, 2024, at Ignite 2024 in Chicago. MSEM ingests telemetry from Defender for Endpoint, Defender for Identity, Defender for Cloud, Entra ID, and the Defender XDR plane into a single graph. Attack paths are first-class objects with three dashboard views: an attack-path list, choke-point analysis (small sets of nodes whose compromise enables disproportionately many downstream paths), and blast-radius (downstream reach of a selected node) [@ms-msem-attack-paths].
&lt;p&gt;The MSEM docs page introduces the model verbatim: &lt;em&gt;&quot;Attack paths in Microsoft Security Exposure Management help you to proactively identify and visualize potential routes that attackers can exploit using vulnerabilities, gaps, and misconfigurations across endpoints, cloud environments, and hybrid infrastructures.&quot;&lt;/em&gt; And, on choke points: &lt;em&gt;&quot;By focusing on these choke points, you can reduce risk by addressing high-impact assets.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The query interface is the Defender XDR portal plus KQL (Kusto Query Language), not Cypher. The graph engine is proprietary; Microsoft does not publish per-query latency numbers or the underlying algorithms. But the model -- nodes, typed edges, attack paths as the unit of analysis, choke-point and blast-radius views -- is the model BloodHound shipped at DEF CON 24 in August 2016.&lt;/p&gt;
&lt;p&gt;The arc takes eight years. From the August 6, 2016 BloodHound talk to the November 19, 2024 MSEM general-availability announcement is eight years and three months. The defender vendor whose product the original BloodHound was mapping ships a defender product whose attack-path model is structurally equivalent to the one a red-team consultancy shipped in a conference talk eight years earlier.&lt;/p&gt;
&lt;p&gt;Microsoft adopted the model. The community kept extending it. By 2026 the frontier is no longer &lt;em&gt;&quot;does the graph exist?&quot;&lt;/em&gt; It is &lt;em&gt;&quot;how do we make the graph weighted, complete, and substrate-independent?&quot;&lt;/em&gt; That is the state of the art.&lt;/p&gt;
&lt;h2&gt;9. State of the art -- Tier Zero, ADCS edges, and OpenGraph, 2023 to 2026&lt;/h2&gt;
&lt;p&gt;By the time MSEM shipped, SpecterOps had already moved past &lt;em&gt;&quot;is there a graph?&quot;&lt;/em&gt; and was asking three sharper questions. Where does the graph end? How do we model attack primitives that compose from raw facts? And does the AD-specific schema even matter?&lt;/p&gt;
&lt;p&gt;The first question is what &lt;em&gt;&quot;Tier Zero&quot;&lt;/em&gt; means. On June 22, 2023, Jonas Bülow Knudsen, Elad Shamir, and Justin Kohler at SpecterOps published &lt;em&gt;What is Tier Zero -- Part 1&lt;/em&gt;, which reframed Microsoft&apos;s tiered-administration concept -- introduced in the 2012-2014 Securing Privileged Access guidance and renamed the Enterprise Access Model with the &quot;Control Plane&quot; vocabulary in December 2020 [@ms-enterprise-access-model] -- as a property &lt;em&gt;of the graph&lt;/em&gt; [@specterops-2023-tier-zero].&lt;/p&gt;
&lt;p&gt;A Tier Zero asset (see Definition below) reframes Microsoft&apos;s tiered concept from &lt;em&gt;the set of things in the high-privilege tier&lt;/em&gt; to &lt;em&gt;the set of things from which the high-privilege tier is reachable&lt;/em&gt;. Microsoft&apos;s own Tier 0 definition -- &lt;em&gt;&quot;Direct Control of enterprise identities... and all the assets in it&quot;&lt;/em&gt; -- becomes a graph property. The two formulations are equivalent if and only if the graph is complete. If the graph is incomplete (which it is), the Tier Zero set computed from the graph is the floor, not the ceiling.&lt;/p&gt;

Any node in the attack-path graph whose compromise lets an attacker reach an administrative privilege in the forest. The companion `SpecterOps/TierZeroTable` GitHub project is the community-maintained inventory; the README discloses that the table is the floor, not the ceiling [@specterops-tier-zero-table].
&lt;p&gt;The Tier Zero definition is the answer to &lt;em&gt;&quot;shortest path to what?&quot;&lt;/em&gt; -- the target side of every BloodHound shortest-path query. Without a defined Tier Zero set, the question has no endpoint.&lt;/p&gt;
&lt;p&gt;The second question is how to model attack primitives that compose. The January 24, 2024 SpecterOps blog by Knudsen formalised this with the traversable / non-traversable edge distinction discussed in Section 7. The mechanism generalises: any attack primitive whose exploitability is a conjunction of raw facts can be encoded as a composed edge that the post-processor synthesises when the pattern is present.&lt;/p&gt;
&lt;p&gt;ESC1, walked through in Section 7, is the canonical example: nine numbered prerequisites that the post-processor checks before writing the composed &lt;code&gt;ADCSESC1&lt;/code&gt; edge [@specterops-2024-adcs-bloodhound]. Subsequent posts in the series extended the same machinery to ESC3, ESC4, ESC6, ESC7, ESC8 (the &lt;code&gt;CoerceAndRelayNTLMToADCS&lt;/code&gt; edge), ESC9, ESC10, and -- on February 14, 2024 -- ESC13 [@specterops-2024-esc13].&lt;/p&gt;
&lt;p&gt;In December 2024 BloodHound v6.3 introduced an early-access &quot;improved analysis algorithm&quot; internally referred to as &lt;strong&gt;Butterfly&lt;/strong&gt; [@bloodhound-v6-3-release]. Butterfly is the first production attempt at &lt;em&gt;bi-directional impact&lt;/em&gt; analysis. Pre-v6.3 BloodHound Enterprise quantified risk as &quot;&lt;em&gt;who can reach this node?&lt;/em&gt;&quot; (incoming attack-path count). v6.3 also quantifies &quot;&lt;em&gt;who can this node reach if compromised?&lt;/em&gt;&quot; (outgoing blast radius).&lt;/p&gt;
&lt;p&gt;The release notes describe the outcome but not the algorithm: &lt;em&gt;&quot;Improve risk scoring fidelity for all finding types... Measure risk at each individual finding... Support the inclusion of hybrid paths in risk scoring (Azure assets will now contribute to measured risk in AD and vice versa).&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The same release also announced that BloodHound Enterprise had begun migrating off Neo4j onto PostgreSQL as the &lt;em&gt;graph&lt;/em&gt; database, with the release notes reporting &lt;em&gt;&quot;&amp;gt;50% improvement in the time it takes to perform post-processing during the Analysis process.&quot;&lt;/em&gt; Cypher continues to be the query language; the engine underneath changed.&lt;/p&gt;
&lt;p&gt;The third question -- whether the AD-specific schema matters -- got its answer on July 29, 2025, when SpecterOps released BloodHound v8.0 with &lt;strong&gt;OpenGraph&lt;/strong&gt; [@specterops-2025-opengraph]. OpenGraph decouples the graph engine from the AD-specific schema. Users (and SpecterOps partners) define their own node and edge kinds and ingest attack-path data from arbitrary substrates. The initial release included GitHub organisations, Snowflake role hierarchies, Microsoft SQL Server logins, ServiceNow groups, and Tailscale ACLs. Subsequent community contributions extended the library.&lt;/p&gt;

BloodHound OpenGraph is a foundational shift toward... identity risk management across the entire enterprise. -- Justin Kohler, SpecterOps Chief Product Officer, July 29, 2025.
&lt;p&gt;OpenGraph is the closing observation of an arc that began with Phillips and Swiler in 1998: &lt;em&gt;the model is the abstraction; the substrate is whatever your enterprise runs.&lt;/em&gt; The same &lt;code&gt;shortestPath()&lt;/code&gt; that finds Active Directory attack paths now finds attack paths over a GitHub organisation, a Snowflake role hierarchy, or a Microsoft SQL Server login graph, with no engine change. The 2026 BloodHound release (v9.1.0, May 6, 2026, per the public release-notes index [@bloodhound-release-notes-index]) extends OpenGraph and adds incremental edge updates -- the first step toward a streaming graph rather than a snapshot.&lt;/p&gt;
&lt;h2&gt;10. Competing approaches -- BloodHound versus Microsoft versus the alternatives&lt;/h2&gt;
&lt;p&gt;In 2026 no single product covers every substrate. The field is plural.&lt;/p&gt;
&lt;p&gt;A practitioner choosing among attack-path tools answers four questions, in order. What substrate do you need to cover? Self-host or SaaS? Snapshot or continuous? Open query language or vendor portal? The table below assembles the answers on the dimensions a 2026 practitioner actually uses.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Substrate&lt;/th&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Query language&lt;/th&gt;
&lt;th&gt;Deployment&lt;/th&gt;
&lt;th&gt;Licensing&lt;/th&gt;
&lt;th&gt;Edge weighting&lt;/th&gt;
&lt;th&gt;ADCS coverage&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;BloodHound CE 9.x&lt;/td&gt;
&lt;td&gt;AD + Entra + AzureRM + OpenGraph&lt;/td&gt;
&lt;td&gt;Neo4j + Postgres app DB&lt;/td&gt;
&lt;td&gt;Cypher&lt;/td&gt;
&lt;td&gt;Self-hosted Docker Compose&lt;/td&gt;
&lt;td&gt;Apache-2.0&lt;/td&gt;
&lt;td&gt;No (unweighted BFS)&lt;/td&gt;
&lt;td&gt;Yes -- ESC1 through ESC10 + ESC8 composite&lt;/td&gt;
&lt;td&gt;Authorised offensive testing + DIY blue team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BloodHound Enterprise&lt;/td&gt;
&lt;td&gt;Same as CE&lt;/td&gt;
&lt;td&gt;PostgreSQL-as-graph (in-progress migration off Neo4j)&lt;/td&gt;
&lt;td&gt;Cypher&lt;/td&gt;
&lt;td&gt;SaaS&lt;/td&gt;
&lt;td&gt;Commercial&lt;/td&gt;
&lt;td&gt;Bi-directional (Butterfly v6.3+); weighting function not public&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Continuous AD/Entra attack-surface management at enterprise scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adalanche&lt;/td&gt;
&lt;td&gt;AD (on-prem; LDIF or live LDAP)&lt;/td&gt;
&lt;td&gt;In-memory Go&lt;/td&gt;
&lt;td&gt;AQL (GQL-like)&lt;/td&gt;
&lt;td&gt;Single Go binary&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (per README)&lt;/td&gt;
&lt;td&gt;Offline / air-gapped analysis from LDIF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft Security Exposure Management&lt;/td&gt;
&lt;td&gt;Defender XDR signal: identity + endpoint + multi-cloud + Entra&lt;/td&gt;
&lt;td&gt;Proprietary&lt;/td&gt;
&lt;td&gt;KQL + portal&lt;/td&gt;
&lt;td&gt;SaaS&lt;/td&gt;
&lt;td&gt;Microsoft licensing&lt;/td&gt;
&lt;td&gt;Implicit (filter against exploitability oracle)&lt;/td&gt;
&lt;td&gt;Indirect via MDI signals&lt;/td&gt;
&lt;td&gt;Hybrid Microsoft-substrate unified exposure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MDI Lateral Movement Paths&lt;/td&gt;
&lt;td&gt;On-prem AD (sensitive-account paths only)&lt;/td&gt;
&lt;td&gt;Proprietary&lt;/td&gt;
&lt;td&gt;None -- portal only&lt;/td&gt;
&lt;td&gt;SaaS&lt;/td&gt;
&lt;td&gt;Microsoft licensing&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;Implicit via separate MDI alerts&lt;/td&gt;
&lt;td&gt;Default-on credential-hopping detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Defender for Cloud CSPM attack-path analysis&lt;/td&gt;
&lt;td&gt;Multi-cloud (Azure + AWS + GCP)&lt;/td&gt;
&lt;td&gt;Proprietary&lt;/td&gt;
&lt;td&gt;Cloud Security Explorer + KQL&lt;/td&gt;
&lt;td&gt;SaaS&lt;/td&gt;
&lt;td&gt;Microsoft licensing&lt;/td&gt;
&lt;td&gt;Implicit&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;Multi-cloud workload protection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PingCastle / Semperis DSP / ADAudit Plus&lt;/td&gt;
&lt;td&gt;On-prem AD (+ limited Entra)&lt;/td&gt;
&lt;td&gt;None -- list-of-findings&lt;/td&gt;
&lt;td&gt;None -- HTML / portal&lt;/td&gt;
&lt;td&gt;Self-hosted or SaaS&lt;/td&gt;
&lt;td&gt;Commercial / mixed&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;Single-finding hygiene only&lt;/td&gt;
&lt;td&gt;Compliance auditing and change tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;A few rows deserve commentary.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; BloodHound CE ships under &lt;strong&gt;Apache-2.0&lt;/strong&gt; per the current repository [@bloodhound-ce-repo]. The GPL-3.0 license you may see in older treatments applies only to the deprecated BloodHound Legacy v4 repository [@bloodhound-legacy-repo], which was last updated in 2023 and is no longer maintained. The licensing difference is material: GPL-3.0 is copyleft, Apache-2.0 is permissive. Downstream use cases that need permissive licensing should rely on the current CE.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Older blog posts and conference talks frequently call BloodHound CE GPL-3.0. The CE-Legacy LICENSE block does carry the GPL-3.0 copyright header, which is the source of the confusion. The &lt;em&gt;current&lt;/em&gt; CE codebase at github.com/SpecterOps/BloodHound is Apache-2.0; the GPL-3.0 LICENSE applies only to the deprecated Legacy v4 repository.&lt;/p&gt;
&lt;p&gt;Adalanche, by Lars Karlslund, is the load-bearing counter-example to the claim that &quot;the graph model requires Neo4j&quot; [@adalanche-repo]. Adalanche reads AD data from an LDIF dump or live LDAP, builds the graph entirely in process memory, and exposes a web GUI plus an Adalanche Query Language (AQL) -- described in the README as &lt;em&gt;&quot;a GQL-like language that allows for complex queries.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The README&apos;s headline claim is verbatim: &lt;em&gt;&quot;Adalanche gives instant results, showing you what permissions users and groups have in an Active Directory.&quot;&lt;/em&gt; The trade is no continuous monitoring, no multi-user web app, and a smaller community in exchange for zero deployment friction. The model is identical; the engine is replaceable.&lt;/p&gt;
&lt;p&gt;MSEM is the closest Microsoft analogue to BloodHound (see Section 8 for substrate and query interface). Reasonable defenders run &lt;em&gt;both&lt;/em&gt; MSEM and BloodHound (CE or Enterprise) on the same forest. The tools are complementary rather than substitutionary: MSEM brings the EDR plus workload-protection telemetry that BloodHound does not natively ingest, while BloodHound brings the precise AD edge semantics that SpecterOps&apos;s research community has validated. Running both is not double-counting.&lt;/p&gt;
&lt;p&gt;The hygiene scanners -- PingCastle [@pingcastle], Semperis DSP [@semperis-dsp], ManageEngine ADAudit Plus [@manageengine-adaudit-plus] -- are the surviving descendants of the per-object ACL-inspection generation, with risk-scoring layered on top. They are valuable for compliance auditing and change tracking. They do not expose a queryable attack-path graph. The compliance auditor and the attack-path analyst are different personas with different tools.&lt;/p&gt;
&lt;p&gt;If the field is plural and every tool has a gap, what is the shape of the problem that no tool yet solves? The next section is the honest answer.&lt;/p&gt;
&lt;h2&gt;11. Theoretical limits and open problems&lt;/h2&gt;
&lt;p&gt;Some of the gaps in attack-path analysis are engineering gaps. Others are not.&lt;/p&gt;
&lt;p&gt;The single most consequential open problem is &lt;strong&gt;edge weighting&lt;/strong&gt;. BloodHound&apos;s BFS treats every edge as one hop. In reality, &lt;code&gt;MemberOf&lt;/code&gt; is effectively free; &lt;code&gt;ForceChangePassword&lt;/code&gt; requires the attacker to log in as the changed principal afterwards; &lt;code&gt;AddKeyCredentialLink&lt;/code&gt; requires shadow-credential infrastructure; &lt;code&gt;CoerceAndRelayNTLMToADCS&lt;/code&gt; requires an active SMB-coercion primitive, NTLM relay tooling, and an ESC8-vulnerable certificate authority. A shortest-&lt;em&gt;hop&lt;/em&gt; path is not in general the shortest-&lt;em&gt;exploitation-cost&lt;/em&gt; path.&lt;/p&gt;
&lt;p&gt;BloodHound Enterprise v6.3 shipped the Butterfly analysis as the first production attempt to relax this assumption. As the v6.3 release notes acknowledge (see Section 9), Butterfly&apos;s weighting function is not publicly documented [@bloodhound-v6-3-release].&lt;/p&gt;
&lt;p&gt;Academic intuition suggests weighting edges by an exploitation-success probability and computing &lt;em&gt;most-likely-exploited&lt;/em&gt; paths via shortest paths under negative-log-probability weights. The complexity ceiling is well-known: Dijkstra (with non-negative weights) runs in $O(E + V\log V)$ time with a Fibonacci heap; Bellman-Ford handles negative weights at $O(VE)$. Either fits comfortably inside the per-query budget BloodHound already operates within.&lt;/p&gt;
&lt;p&gt;The unsolved part is the &lt;em&gt;empirical calibration&lt;/em&gt;: what numerical weight is the right weight on a &lt;code&gt;ForceChangePassword&lt;/code&gt; edge versus a &lt;code&gt;CoerceAndRelayNTLMToADCS&lt;/code&gt; edge? There is no published peer-reviewed answer.&lt;/p&gt;
&lt;p&gt;The second open problem is &lt;strong&gt;coverage&lt;/strong&gt;. The &lt;code&gt;TierZeroTable&lt;/code&gt; README is the authoritative self-disclosure: file-system ACLs on member servers, fine-grained GPO delegation, on-host service-account permissions, some Entra conditional-access logic, and cross-tenant Entra B2B trust paths remain partially or fully out of scope [@specterops-tier-zero-table]. This is an &lt;em&gt;engineering&lt;/em&gt; problem -- more collectors, more edge definitions -- rather than an algorithmic one. OpenGraph is the structural answer: shift coverage from &quot;what edges has SpecterOps modelled?&quot; to &quot;what edges has the community contributed to the shared library?&quot; [@bloodhound-opengraph-library].&lt;/p&gt;
&lt;p&gt;The third is &lt;strong&gt;graph privacy&lt;/strong&gt;. A continuously-collected, complete AD privilege graph shipped to a third-party SaaS backend is, in adversarial hands, a pre-computed attack plan for the customer&apos;s forest. Tenant isolation, encryption at rest, SOC 2 and FedRAMP attestation, and customer-managed key encryption do not eliminate the structural risk: a compromised SaaS backend yields the customer&apos;s graph regardless of compliance posture.&lt;/p&gt;
&lt;p&gt;Cryptographic approaches -- homomorphic graph queries, secure multi-party computation for path enumeration -- exist in the theoretical literature but are not in production attack-path products at time of writing. Adalanche and self-hosted BloodHound CE remain the privacy-preserving options at the cost of forgoing continuous monitoring.&lt;/p&gt;
&lt;p&gt;The fourth is the &lt;strong&gt;the graph is alive&lt;/strong&gt; problem. Session edges (&lt;code&gt;HasSession&lt;/code&gt;) go stale in hours. New ACEs, new group memberships, new sessions appear continuously. SharpHound&apos;s snapshot model is yesterday&apos;s view; continuous collectors (BloodHound Enterprise, MSEM agent streams) trade stealth for freshness. The May 6, 2026 release notes describe BloodHound CE&apos;s first move toward a streaming model: &lt;em&gt;&quot;incremental edge updates that reduce unnecessary writes during post-processing&quot;&lt;/em&gt; [@bloodhound-release-notes-index]. No production attack-path tool yet ships a fully streaming graph.&lt;/p&gt;
&lt;p&gt;The fifth is &lt;strong&gt;combinatorial intractability&lt;/strong&gt;. These are not engineering gaps; they are complexity-theoretic facts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Counting all attack paths is #P-complete.&lt;/strong&gt; Leslie Valiant&apos;s 1979 result on the complexity of counting solutions to combinatorial problems applies directly: counting the simple paths between two nodes in a general graph cannot be done in polynomial time unless P = #P [@valiant-1979-permanent]. BloodHound&apos;s path-count UI is necessarily an approximation or a length-truncation; this is the theoretical reason why.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The minimum-edge-cut &quot;defender hardening&quot; problem is NP-hard.&lt;/strong&gt; Choose the smallest set of edges whose removal disconnects the attacker from the goal. Sheyner et al. 2002 proved the result is NP-hard but admits an $O(\log n)$ approximation [@sheyner-et-al-2002-attack-graphs]. BHE choke-point ranking and MSEM choke-point analysis necessarily implement heuristic approximations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Finding regular simple paths is NP-complete.&lt;/strong&gt; Mendelzon and Wood 1995 proved that finding a simple path matching a regular expression over edge labels in a graph database is NP-complete [@mendelzon-wood-1995-dblp]. Cypher&apos;s &lt;code&gt;shortestPath()&lt;/code&gt; does not enforce simple-path semantics, which is why it remains in P; quantified path patterns with &lt;code&gt;DIFFERENT RELATIONSHIPS&lt;/code&gt; semantics (available since Neo4j 5.x and documented in the current Cypher manual [@neo4j-cypher-variable-length]) do enforce simple paths and so cross into the NP-complete regime.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; BloodHound&apos;s per-query algorithm (BFS, $O(V + E)$) is optimal up to constants. The frontier of the field is no longer the algorithm. It is what &lt;em&gt;question&lt;/em&gt; we ask the algorithm: weighted? regular-path? simple-path? bi-directional? cross-substrate? Each open question is a different model, not a different implementation.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Three further honest framings deserve a mention.&lt;/p&gt;
&lt;p&gt;First, the &lt;strong&gt;standardisation of OpenGraph edge taxonomies&lt;/strong&gt; is unsettled. Without community convergence on edge naming, different contributors may model the same substrate with incompatible schemas. Historical precedent (MITRE ATT&amp;amp;CK technique IDs, CVE identifiers) suggests that convergence happens when a single high-trust curator becomes the de-facto registry; whether SpecterOps will operate OpenGraph as a vendor-neutral standards body or as a SpecterOps-owned artefact is a governance question, not an algorithmic one.&lt;/p&gt;
&lt;p&gt;Second, the &lt;strong&gt;adversarial robustness of the collector&lt;/strong&gt; is an open question: SharpHound runs as an authenticated principal, and an attacker with prior compromise can poison the collection. There is no closed-form defence.&lt;/p&gt;
&lt;p&gt;Third, the &lt;strong&gt;absence of any public head-to-head benchmark&lt;/strong&gt; of BloodHound CE versus BHE versus Adalanche versus MSEM on the same forest under controlled conditions is structural: Microsoft does not publish per-query latency, SpecterOps publishes only relative improvement claims, and the academic line uses 2005-era hardware figures that are not comparable.&lt;/p&gt;
&lt;h2&gt;12. Practical guide -- running BloodHound today&lt;/h2&gt;
&lt;p&gt;If the previous sections sold you on the model, the next few paragraphs are the minimum you need to stand it up.&lt;/p&gt;
&lt;p&gt;Stand up BloodHound CE with the Docker Compose file in the repository [@bloodhound-ce-repo]. The stack is four containers: a PostgreSQL application database (users, roles, sessions, audit logs, saved queries); a Neo4j graph database holding the property graph; a Go REST API; and a React plus Sigma.js single-page frontend. Five minutes to first boot on a developer laptop. The repository README is the authoritative deployment reference.&lt;/p&gt;
&lt;p&gt;Run &lt;strong&gt;SharpHound&lt;/strong&gt; on a domain-joined Windows host as the collection identity. The default invocation -- &lt;code&gt;SharpHound.exe --CollectionMethods all,GPOLocalGroup&lt;/code&gt; -- enumerates every group membership, every recognised ACL pattern, every active session, every local-admin relationship, and every Kerberos delegation. Run &lt;strong&gt;AzureHound&lt;/strong&gt; with appropriate Entra ID credentials for Entra and AzureRM coverage. Both emit JSON dumps in the same envelope; the BloodHound CE upload tab in the web UI ingests both.&lt;/p&gt;
&lt;p&gt;Open the web UI. The stock pre-built queries are a reasonable starting palette: &lt;em&gt;Find Shortest Paths to Domain Admins&lt;/em&gt;, &lt;em&gt;Find Principals with DCSync Rights&lt;/em&gt;, &lt;em&gt;Find Computers with Unsupported Operating Systems&lt;/em&gt;, and &lt;em&gt;Shortest Paths from Owned Principals to High-Value Targets&lt;/em&gt; (after marking some accounts as owned). Custom Cypher goes in the Cypher tab at the top right; the Section 6 query is a good template.&lt;/p&gt;
&lt;p&gt;The most important interpretation discipline: treat the result as a &lt;em&gt;risk register&lt;/em&gt;, not a vulnerability list. A finding is &quot;Bob can reach Domain Admins via a four-hop path.&quot; The &lt;em&gt;edge&lt;/em&gt; is &quot;Bob has &lt;code&gt;GenericWrite&lt;/code&gt; on Carol.&quot; Closing the edge breaks the finding &lt;em&gt;and&lt;/em&gt; every other path that passed through it. Edges are the unit of remediation, not findings. SpecterOps&apos;s own 2021 customer-anonymous essay &lt;em&gt;Active Directory Attack Paths -- Is It Always This Bad?&lt;/em&gt; reports findings from hundreds of engagements, and the recurring observation across forests is that a small number of high-blast-radius edges explain most of the discovered paths [@specterops-2021-ad-attack-paths].&lt;/p&gt;
&lt;p&gt;A few pitfalls are worth naming. &lt;code&gt;HasSession&lt;/code&gt; collection generates measurable LDAP and SAMR traffic that Microsoft Defender for Identity alerts on; coordinate with the blue team or expect detections. The stealth collection mode trades coverage for traffic volume.&lt;/p&gt;
&lt;p&gt;Unconstrained variable-length Cypher queries (&lt;code&gt;MATCH p=(a)-[*]-&amp;gt;(b)&lt;/code&gt;) can pin Neo4j&apos;s heap; CE&apos;s &quot;protected Cypher&quot; cost limits help but do not eliminate the problem, so prefer &lt;code&gt;shortestPath()&lt;/code&gt; or bound the path length explicitly. The wildcard-principal post-processing for &lt;code&gt;Authenticated Users&lt;/code&gt; and &lt;code&gt;Everyone&lt;/code&gt; requires v6.0 or later to be correct; older versions miscount these edges [@bloodhound-v6-0-release]. And the &lt;code&gt;CoerceAndRelayNTLMToADCS&lt;/code&gt; edge is Group to Computer, not Computer to Computer, as discussed in Section 7.&lt;/p&gt;

```cypher
MATCH (n)
WHERE n.system_tags CONTAINS &apos;admin_tier_0&apos;
WITH collect(n) AS tier_zero
MATCH p = shortestPath((u:User {enabled:true})-[*1..6]-&amp;gt;(t))
WHERE t IN tier_zero AND NOT u.system_tags CONTAINS &apos;admin_tier_0&apos;
RETURN u.name AS source, t.name AS target, length(p) AS hops
ORDER BY hops ASC
LIMIT 25
```&lt;p&gt;This returns up to 25 enabled non-Tier-Zero users with the shortest paths into Tier Zero. The &lt;code&gt;[*1..6]&lt;/code&gt; bound prevents the pathological cyclic-graph cost explosion. Bound length aggressively until you have indexed your graph.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;

BloodHound is dual-use. Authorised defensive use on your own forest, or contracted penetration testing within written scope, is the standard legal posture. Running it against a directory you are not authorised to assess is unlawful in most jurisdictions: the Computer Fraud and Abuse Act in the United States; the Computer Misuse Act 1990 in the United Kingdom; equivalents in most EU jurisdictions. The dual-use posture is fundamental to the tool; the legal posture depends on you.
&lt;p&gt;The tool is the easy part. The hard part is what you do with the answer.&lt;/p&gt;
&lt;h2&gt;13. Frequently asked questions&lt;/h2&gt;
&lt;p&gt;The misconceptions worth disposing of, in order of how often they recur.&lt;/p&gt;

No. BloodHound is the SpecterOps-maintained, evolving set of edge families. File-system ACLs on member servers, fine-grained GPO delegation, on-host service-account permissions, and some Entra conditional-access logic remain partially or fully out of scope. The `SpecterOps/TierZeroTable` README is explicit about this limitation [@specterops-tier-zero-table]. Coverage expansion is iterative and community-fed; OpenGraph is the structural answer to scope generalisation.

The original BloodHound (2016) shipped Neo4j only. The modern BloodHound CE uses *both*: PostgreSQL as the application database (users, roles, sessions, audit logs) and Neo4j as the graph layer [@bloodhound-ce-repo]. BloodHound Enterprise has begun migrating entirely off Neo4j onto PostgreSQL-as-graph (announced in v6.3, December 2024) [@bloodhound-v6-3-release]; Cypher continues to be the query language on the new backend. The model is engine-independent; Adalanche proves the same point by doing it all in process memory in Go [@adalanche-repo].

Authorised defensive use on your own forest, yes. Contracted penetration testing within written scope, yes. Running it against a directory you are not authorised to assess is unlawful in most jurisdictions: the Computer Fraud and Abuse Act in the United States, the Computer Misuse Act 1990 in the United Kingdom, and equivalents in most EU jurisdictions. The dual-use posture is fundamental to the tool; legal compliance is the operator&apos;s responsibility.
&lt;h2&gt;Epilogue&lt;/h2&gt;
&lt;p&gt;The 2014 analyst with the whiteboard and the 2024 analyst with the Cypher query are doing the same work. The unit of analysis has shifted, and once the unit shifts, the field does not go back.&lt;/p&gt;
&lt;p&gt;John Lambert diagnosed it in two sentences in April 2015 [@lambert-2015-defenders-lists-attackers-graphs]. Andy Robbins, Rohan Vazarkar, and Will Schroeder shipped it as BloodHound in August 2016 [@bloodhound-legacy-repo]. SpecterOps extended it through AzureHound in 2020, the CE 5.0 web architecture in 2023, the Tier Zero formalisation in 2023, ADCS composed edges in 2024, Butterfly bi-directional analysis in 2024, and OpenGraph in 2025 [@specterops-2025-opengraph].&lt;/p&gt;
&lt;p&gt;Microsoft validated the model with Lateral Movement Paths in 2018, cloud security graph attack-path analysis in 2022 to 2023, and Microsoft Security Exposure Management at Ignite in 2024 [@ms-msem-attack-paths]. The community that shipped the graph won; the community that kept shipping lists is selling compliance reports to the auditors.&lt;/p&gt;
&lt;p&gt;The frontier in 2026 is not whether to model attacks as a graph -- that argument is settled. The frontier is how to make the graph weighted (so the shortest path approximates the easiest), how to make it complete (so the unmodelled edges shrink toward zero), and how to make it substrate-independent (so the next enterprise primitive worth modelling -- whatever it turns out to be -- can be ingested without changing the engine). Each of these is a research direction with its own asymptotic ceiling, its own engineering practice, and its own community of contributors.&lt;/p&gt;
&lt;p&gt;What started as a sentence in a 1,100-word essay on a personal GitHub repository is now an ISO-standardised query language [@iso-39075-2024-gql], a shipped Microsoft product family, an open-source repository with hundreds of thousands of downloads, and a discipline taught at most major security conferences. The graph wins because the graph is the right model. The right model wins because, eventually, the right model always does.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;bloodhound-attack-path-graph&quot; keyTerms={[
  { term: &quot;Attack-path graph&quot;, definition: &quot;A directed graph whose nodes are security principals and resources and whose edges are privilege relationships an attacker can traverse. Reachability in the graph models multi-hop privilege escalation.&quot; },
  { term: &quot;Directed property graph&quot;, definition: &quot;A graph in which both nodes and edges have types and can carry key-value properties. The data model BloodHound uses.&quot; },
  { term: &quot;Cypher&quot;, definition: &quot;The pattern-matching query language for Neo4j and now the basis of the ISO/IEC 39075:2024 GQL standard.&quot; },
  { term: &quot;Bidirectional BFS&quot;, definition: &quot;Breadth-first search executed simultaneously from source and target, meeting in the middle. The algorithm Neo4j&apos;s shortestPath() runs and the algorithm BloodHound inherits.&quot; },
  { term: &quot;Tier Zero&quot;, definition: &quot;Any node in the attack-path graph whose compromise lets an attacker reach an administrative privilege in the forest. The endpoint of every BloodHound shortest-path query.&quot; },
  { term: &quot;Traversable / non-traversable edge&quot;, definition: &quot;A traversable edge can be stepped through directly by a path query; a non-traversable edge is a precondition fact that, with others, composes into a traversable edge during post-processing. ADCS edges are the canonical example.&quot; },
  { term: &quot;OpenGraph&quot;, definition: &quot;BloodHound&apos;s v8.0 (July 2025) generalisation that decouples the graph engine from the AD-specific schema, admitting user-defined node and edge kinds for arbitrary substrates.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>active-directory</category><category>bloodhound</category><category>graph-theory</category><category>attack-paths</category><category>specterops</category><category>identity-security</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>Two Checkmarks and the Keys to the Kingdom: How Active Directory&apos;s Replication Protocol Became the Longest-Lived Credential Attack on Windows</title><link>https://paragmali.com/blog/two-checkmarks-and-the-keys-to-the-kingdom-how-active-direct/</link><guid isPermaLink="true">https://paragmali.com/blog/two-checkmarks-and-the-keys-to-the-kingdom-how-active-direct/</guid><description>MS-DRSR was designed for domain controllers to replicate secrets to each other. Its access check gates on an ACL, not on whether the caller is a DC. Eleven years after Mimikatz proved it, no patch can fix it.</description><pubDate>Thu, 21 May 2026 00:00:00 GMT</pubDate><content:encoded>
Active Directory&apos;s replication protocol (MS-DRSR) lets any domain controller pull every secret in the directory from any other domain controller, and the access check gates on an ACL, not on whether the caller is actually a DC. Mimikatz&apos;s `lsadump::dcsync` (August 11, 2015) and `lsadump::dcshadow` (January 2018) are the read and write sides of this loophole; both remain in active operational use eleven years later because the protocol cannot be amended without breaking AD replication. Credential Guard cannot help -- the secret moves from a remote DC&apos;s NTDS.dit over the network, never touching the attacker&apos;s LSASS. The 2026 defender response is four parallel detection layers (posture, behavioral, network, graph) because no single layer catches every case, and the architectural fifth layer that would close the class is not coming.
&lt;h2&gt;1. Two Checkmarks and the Keys to the Kingdom&lt;/h2&gt;
&lt;p&gt;A non-admin service account, created during a 2017 file-server migration and never cleaned up, holds exactly two access-control entries on the &lt;code&gt;contoso.local&lt;/code&gt; domain object: &lt;code&gt;Replicating Directory Changes&lt;/code&gt; and &lt;code&gt;Replicating Directory Changes All&lt;/code&gt;. From a Windows 11 workstation -- not a domain controller, not a tier-zero admin host, just a desk -- the attacker opens a Mimikatz prompt and types:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lsadump::dcsync /domain:contoso.local /user:krbtgt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sixty seconds later they have the &lt;code&gt;krbtgt&lt;/code&gt; NT hash and &lt;a href=&quot;https://paragmali.com/blog/kerberos-in-windows-the-other-half-of-ntlmless/&quot; rel=&quot;noopener&quot;&gt;Kerberos AES keys&lt;/a&gt;, and they own the domain. &lt;a href=&quot;https://paragmali.com/blog/the-empty-hash-credential-guard-the-lsaiso-trustlet-and-the-/&quot; rel=&quot;noopener&quot;&gt;Credential Guard&lt;/a&gt;, enabled on every workstation in the building, never saw the credential transit anywhere in its scope.&lt;/p&gt;
&lt;p&gt;That last sentence is the punchline of the entire field. The secret never touches the attacker&apos;s LSASS, so Credential Guard&apos;s design has nothing to say. The keys move from a remote domain controller&apos;s on-disk database, over an RPC channel, into the attacker&apos;s process memory. Credential Guard isolates LSASS on the &lt;em&gt;local&lt;/em&gt; machine. It has no jurisdiction over what a remote DC chooses to send when asked nicely.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Credential Guard isolates LSASS-resident secrets in a separate virtual trust level. DCSync reads secrets from a remote DC&apos;s NTDS.dit over MS-DRSR -- a network protocol attack, not a local-memory attack. Microsoft&apos;s Credential Guard documentation does not claim protection against MS-DRSR-based credential extraction [@credential-guard-considerations].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The 60-second timeline is not a marketing number. One RPC round trip per principal is the protocol&apos;s per-principal cost, fixed by specification. For a single high-value target -- and &lt;code&gt;krbtgt&lt;/code&gt; is the highest-value target in any Active Directory forest, because its long-term key signs every Kerberos ticket -- the attack is over before a typical SOC operator has finished pouring coffee.&lt;/p&gt;
&lt;p&gt;The four people whose names recur in this story are worth meeting now. Benjamin Delpy (gentilkiwi) wrote Mimikatz and authored both the DCSync read primitive (2015) and, with Vincent Le Toux, the DCShadow write primitive (2018). Vincent Le Toux built the original DCShadow implementation and the PingCastle posture-assessment tool. Sean Metcalf (Trimarc / ADSecurity) translated DCSync into operator vocabulary in the weeks after its release. Microsoft&apos;s Defender for Identity team ships the canonical first-party detections that fire on this attack class today.&lt;/p&gt;
&lt;p&gt;This story is what happens when a 23-year-old domain-controller-to-domain-controller protocol&apos;s access check turns out to gate on an ACL, not on whether the caller is a domain controller. Two ACEs on one object should not give an attacker the entire credential trove. The fact that they do is not a bug in the implementation. It is a 1999 design assumption -- that nobody who is not a DC would ever speak this protocol -- that survived twenty-five years as a social convention and was nowhere written down in the access-check code.&lt;/p&gt;
&lt;p&gt;The next section explains why Microsoft built this protocol in the first place, and why its access check forgot to ask the one question that would have closed the loophole.&lt;/p&gt;
&lt;h2&gt;2. Why the Protocol Exists at All&lt;/h2&gt;
&lt;p&gt;Rewind to February 17, 2000. Microsoft ships Windows 2000 Server, and Active Directory replaces the old NT 4.0 PDC/BDC model [@microsoft-windows-2000-launch]. NT 4.0 was single-master: one Primary Domain Controller held the authoritative account database, every Backup Domain Controller held a read-only copy, and a password change on the PDC propagated to BDCs through a hub-and-spoke replication channel. AD inverts the model. Every domain controller in an AD domain holds a writable copy of the directory; a password change on &lt;em&gt;any&lt;/em&gt; DC must propagate to every other DC within minutes; the replication topology is a mesh, not a star.&lt;/p&gt;
&lt;p&gt;Multi-master replication is the design constraint that forces MS-DRSR&apos;s existence. If any DC can accept a write, then every DC must be able to pull every change -- &lt;em&gt;including secret attributes&lt;/em&gt; like &lt;code&gt;unicodePwd&lt;/code&gt;, &lt;code&gt;dBCSPwd&lt;/code&gt;, &lt;code&gt;ntPwdHistory&lt;/code&gt;, &lt;code&gt;lmPwdHistory&lt;/code&gt;, and &lt;code&gt;supplementalCredentials&lt;/code&gt; -- from every other DC. There is no second protocol. Replication of secrets is replication, and replication is MS-DRSR [@ms-drsr-spec].&lt;/p&gt;

The RPC protocol by which any Active Directory domain controller can replicate any object, including secret attributes, from any other DC in the same forest. Layered over DCE/RPC; the current public specification is revision 46.0 dated March 9, 2026 [@ms-drsr-spec]. MS-DRSR is the mechanism that makes AD&apos;s multi-master replication invariant work.

The MS-DRSR method (opnum 3) that returns changed objects within a naming context. A calling DC supplies a target DN and a set of replication flags; the called DC returns the object&apos;s attribute values, with secret attributes encrypted under a session-derived key. This is the protocol method that DCSync invokes.

flowchart LR
    subgraph Domain[&quot;contoso.local domain&quot;]
        DCA[DC-A&lt;br /&gt;NTDS.dit]
        DCB[DC-B&lt;br /&gt;NTDS.dit]
        DCC[DC-C&lt;br /&gt;NTDS.dit]
    end
    DCA -- IDL_DRSGetNCChanges&lt;br /&gt;unicodePwd, ntPwdHistory --&amp;gt; DCB
    DCB -- IDL_DRSGetNCChanges&lt;br /&gt;supplementalCredentials --&amp;gt; DCA
    DCB -- IDL_DRSGetNCChanges&lt;br /&gt;dBCSPwd --&amp;gt; DCC
    DCC -- IDL_DRSGetNCChanges&lt;br /&gt;unicodePwd --&amp;gt; DCB
    DCA -- IDL_DRSGetNCChanges&lt;br /&gt;ntPwdHistory --&amp;gt; DCC
    DCC -- IDL_DRSGetNCChanges&lt;br /&gt;krbtgt keys --&amp;gt; DCA
&lt;p&gt;The access check that gates &lt;code&gt;IDL_DRSGetNCChanges&lt;/code&gt; is defined in [MS-DRSR] §4.1.10. It asks one question: does the calling principal hold the appropriate extended rights on the naming-context root being replicated? Extended rights are AD&apos;s mechanism for control-access permissions that do not fit into the standard ACL bit set [@ms-drsr-spec].&lt;/p&gt;

A schema-defined access-control right identified by a GUID rather than by a standard ACL bit. Extended rights are granted via an access-control entry on a target object and checked at runtime by the operation that requires them. The three replication extended rights are the canonical example: each is identified only by GUID, and each is checked by `IDL_DRSGetNCChanges` against the caller&apos;s effective rights on the naming-context root.

A top-level replication partition of the Active Directory database. Every AD forest has at least three NCs: the Schema NC, the Configuration NC, and one Domain NC per domain (plus optional Application NCs). DCSync operates against the Domain NC root.
&lt;p&gt;Three extended rights together form what practitioners call the rights triad. The two-checkmark version from §1 is the most-common configuration; the third right unlocks a smaller set of attributes flagged confidential. Microsoft&apos;s own AD schema documentation enumerates each one [@ad-schema-get-changes][@ad-schema-get-changes-all][@ad-schema-get-changes-filtered-set]:&lt;/p&gt;

The combination of three replication extended rights on a naming-context root: `DS-Replication-Get-Changes` (the baseline read), `DS-Replication-Get-Changes-All` (unlocks secret attributes), and `DS-Replication-Get-Changes-In-Filtered-Set` (unlocks attributes with the confidential flag). The first two are what most operators mean when they say &quot;DCSync rights&quot;; the third is sometimes required for completeness.
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Right&lt;/th&gt;
&lt;th&gt;Display name&lt;/th&gt;
&lt;th&gt;Rights-GUID&lt;/th&gt;
&lt;th&gt;What it unlocks&lt;/th&gt;
&lt;th&gt;Introduced&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DS-Replication-Get-Changes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replicating Directory Changes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Baseline replication read&lt;/td&gt;
&lt;td&gt;Windows 2000 Server [@ad-schema-get-changes]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DS-Replication-Get-Changes-All&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replicating Directory Changes All&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Secret attribute replication (&lt;code&gt;unicodePwd&lt;/code&gt;, &lt;code&gt;ntPwdHistory&lt;/code&gt;, etc.) [@ad-schema-get-changes-all]&lt;/td&gt;
&lt;td&gt;Windows Server 2003&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DS-Replication-Get-Changes-In-Filtered-Set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replicating Directory Changes In Filtered Set&lt;/td&gt;
&lt;td&gt;&lt;code&gt;89e95b76-444d-4c62-991a-0facbeda640c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Confidential-flag attributes [@ad-schema-get-changes-filtered-set]&lt;/td&gt;
&lt;td&gt;Windows Server 2008&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;In a freshly-installed AD domain, four principal sets hold the rights triad by default on the Domain NC root: the &lt;code&gt;Domain Controllers&lt;/code&gt; group, the &lt;code&gt;Domain Admins&lt;/code&gt; group, the &lt;code&gt;Enterprise Admins&lt;/code&gt; group, and the built-in &lt;code&gt;Administrators&lt;/code&gt; group [@sean-metcalf-dcsync-2015]. Every other ACE on that object is a deliberate delegation choice made by some operator at some point in the domain&apos;s history.&lt;/p&gt;
&lt;p&gt;Now read the access check carefully. It asks whether the caller holds the rights. It does not ask anything about &lt;em&gt;who&lt;/em&gt; the caller is. The protocol does not check whether the caller&apos;s Service Principal Name is &lt;code&gt;GC/...&lt;/code&gt; or &lt;code&gt;ldap/...&lt;/code&gt; or anything else. It does not check whether the caller&apos;s machine account is in the &lt;code&gt;Domain Controllers&lt;/code&gt; group. It does not check whether the caller&apos;s Kerberos service ticket was issued for a DC service. The whole gate is the ACL on one object.&lt;/p&gt;
&lt;p&gt;This is the most consequential design omission in any Microsoft network protocol. Every other replication-style protocol in Windows ships some form of caller-machine-identity assertion. MS-DRSR shipped without one because, in 1999, nobody on the design team imagined an unprivileged workstation would speak the DC-to-DC protocol.&lt;/p&gt;
&lt;p&gt;The 1999 closed-population design assumption is the load-bearing fiction underneath this whole story. The protocol&apos;s designers assumed -- entirely reasonably for the deployment universe of that decade -- that the only software that would ever implement an MS-DRSR client was the DC role itself, which Microsoft shipped, signed, and audited. No defender in 1999 was thinking about a Python script speaking DRSUAPI from a desk. The assumption was a social convention dressed as a security model, and it survived for fifteen years.&lt;/p&gt;
&lt;p&gt;If the protocol exists to replicate every secret in the directory, and the access check is the ACL on one object, what stopped anyone from abusing it for the protocol&apos;s first fifteen years? That accidental safety period is the subject of the next section.&lt;/p&gt;
&lt;h2&gt;3. The Fifteen-Year Accidental Safety Period&lt;/h2&gt;
&lt;p&gt;Between 2000 and 2015, attackers who wanted the entire credential trove of an Active Directory domain had exactly one road open to them: reach a domain controller and steal its database. The reasons are not subtle. The credentials lived in one file -- &lt;code&gt;C:\Windows\NTDS\NTDS.dit&lt;/code&gt;, an Extensible Storage Engine database protected by a Password Encryption Key wrapped under the BootKey scattered across four registry values in the SYSTEM hive [@ntdsxtract-repo]. Without code execution on a DC, there was no obvious way to ask the directory for its secrets in bulk.&lt;/p&gt;

The on-disk Extensible Storage Engine (&quot;Jet Blue&quot;) database that holds every Active Directory object, including secret attributes. Located at `C:\Windows\NTDS\NTDS.dit` on every domain controller. The file is locked while AD DS is running; offline copying requires either Volume Shadow Copy snapshots, `ntdsutil ifm` bundling, or DC downtime [@mitre-t1003-003].
&lt;p&gt;Three sub-techniques bloomed inside this constraint, and MITRE catalogs them collectively as T1003.003 OS Credential Dumping: NTDS [@mitre-t1003-003]. Each is operationally distinct, and each requires the same precondition.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;G1a -- Volume Shadow Copy plus offline parse.&lt;/strong&gt; The attacker runs &lt;code&gt;vssadmin create shadow /for=C:&lt;/code&gt; on a domain controller, creating an instant point-in-time snapshot. They copy &lt;code&gt;NTDS.dit&lt;/code&gt; and &lt;code&gt;SYSTEM&lt;/code&gt; out of the shadow path, exfiltrate both files, and parse them offline with Csaba Barta&apos;s &lt;code&gt;ntdsxtract&lt;/code&gt; toolkit [@ntdsxtract-repo] or Impacket&apos;s &lt;code&gt;secretsdump.py&lt;/code&gt; running in offline mode. The parse walks the &lt;code&gt;datatable&lt;/code&gt; ESE table row by row, decrypts the PEK with the BootKey, and decrypts each principal&apos;s secret attributes with the PEK.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;G1b -- &lt;code&gt;ntdsutil&lt;/code&gt; Install From Media.&lt;/strong&gt; The built-in command &lt;code&gt;ntdsutil &quot;activate instance ntds&quot; &quot;ifm&quot; &quot;create full C:\Temp\IFM&quot;&lt;/code&gt; packages NTDS.dit and the SYSTEM hive into a clean bundle, intended for legitimate seeding of a new replica DC. With local admin on any DC, an attacker invokes it without involving VSS.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;G1c -- LSASS injection of the long-term DC secrets.&lt;/strong&gt; Mimikatz&apos;s pre-DCSync technique reads cached long-term secret material (including the &lt;code&gt;krbtgt&lt;/code&gt; key) directly out of &lt;code&gt;lsass.exe&lt;/code&gt; memory on a domain controller via &lt;code&gt;lsadump::lsa /inject /name:krbtgt&lt;/code&gt;. The variant that turned this surface into a persistence implant was Skeleton Key, disclosed by the Dell SecureWorks Counter Threat Unit on January 12, 2015 [@skeleton-key-wayback]. Skeleton Key patches the &lt;a href=&quot;https://paragmali.com/blog/ntlmless-the-death-of-ntlm-in-windows/&quot; rel=&quot;noopener&quot;&gt;NTLM&lt;/a&gt; and Kerberos validation routines in LSASS on a DC so that a single master password works for any account.&lt;/p&gt;
&lt;p&gt;Skeleton Key sits at the boundary between credential dumping and persistence. It does not produce usable NT hashes or Kerberos keys for offline forging; it gives the attacker a backdoor at the authentication step. The 2015 community debate over Skeleton Key versus the (then-imminent) DCSync technique was settled decisively by DCSync&apos;s August release: dumping the hashes is more useful than backdooring the auth path, because dumped hashes survive a DC reboot and are forge-ready material [@skeleton-key-wayback].&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sub-technique&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Canonical tool&lt;/th&gt;
&lt;th&gt;Emitted artifact&lt;/th&gt;
&lt;th&gt;Host artifact on DC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;G1a VSS + offline parse&lt;/td&gt;
&lt;td&gt;Point-in-time snapshot, file copy, offline ESE parse&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vssadmin&lt;/code&gt; + &lt;code&gt;secretsdump.py&lt;/code&gt; / &lt;code&gt;ntdsxtract&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NT hashes, Kerberos keys, password history&lt;/td&gt;
&lt;td&gt;VSS event in system log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G1b &lt;code&gt;ntdsutil&lt;/code&gt; IFM&lt;/td&gt;
&lt;td&gt;Built-in admin command produces clean bundle&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ntdsutil &quot;ac i ntds&quot; &quot;ifm&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Identical to G1a&lt;/td&gt;
&lt;td&gt;IFM staging directory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G1c LSASS inject&lt;/td&gt;
&lt;td&gt;Read long-term secrets from &lt;code&gt;lsass.exe&lt;/code&gt; memory&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mimikatz lsadump::lsa /inject&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;krbtgt&lt;/code&gt; key and other LSA-cached secrets&lt;/td&gt;
&lt;td&gt;Process access of &lt;code&gt;lsass.exe&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The structural cost across all three is the same: code execution as SYSTEM on a domain controller. Read that requirement slowly. In a mature enterprise with even a basic tiered-administration model -- no interactive logon to DCs from workstations, &lt;a href=&quot;https://paragmali.com/blog/rdp-authentication-26-years/&quot; rel=&quot;noopener&quot;&gt;restricted-admin RDP&lt;/a&gt;, Privileged Access Workstations for Domain Admin sessions, Protected Users group membership for Tier Zero principals -- DCs are the most defended boundary in the network. An attacker who has crossed that boundary has, in operational terms, already won. The credential dump is the trophy lap.&lt;/p&gt;

flowchart TD
    Start[Attacker foothold&lt;br /&gt;on workstation]
    Start --&amp;gt; VSS[G1a: VSS snapshot&lt;br /&gt;+ offline parse]
    Start --&amp;gt; IFM[G1b: ntdsutil IFM&lt;br /&gt;create full]
    Start --&amp;gt; Inject[G1c: LSASS inject&lt;br /&gt;on DC]
    VSS --&amp;gt; Gate[SYSTEM on DC]
    IFM --&amp;gt; Gate
    Inject --&amp;gt; Gate
    Gate --&amp;gt; Creds[All domain credentials&lt;br /&gt;NT hashes, Kerberos keys, krbtgt]
&lt;p&gt;This is what made the closed-population design assumption survive its first fifteen years. The protocol&apos;s design was open to abuse from any joined workstation that held the rights triad, but nobody noticed because the cheaper attack road -- compromise a DC, parse the database offline -- still passed through a defended chokepoint. The ACL on the Domain NC was nowhere on a defender&apos;s risk register, because no offensive tooling existed that treated the ACL as the gate.&lt;/p&gt;
&lt;p&gt;There was a question waiting to be asked, though. &lt;em&gt;What if you could ask the DC to send you the credentials, using a protocol the DC already speaks fluently with its peers?&lt;/em&gt; The protocol&apos;s wire format is published. Microsoft Learn hosts the IDL. The DCs trust each other&apos;s calls by ACL. The only missing piece was a client implementation that any attacker could run from any joined machine.&lt;/p&gt;
&lt;p&gt;In August 2015 the missing piece arrived.&lt;/p&gt;
&lt;h2&gt;4. Generation by Generation&lt;/h2&gt;
&lt;p&gt;August 11, 2015, 01:27 Central European Time. Benjamin Delpy commits 47,132 insertions to the Mimikatz repository in a single push titled &lt;em&gt;&quot;DCSync in mimikatz &amp;amp; for XP/2003.&quot;&lt;/em&gt; The diff introduces four new modules -- &lt;code&gt;kull_m_rpc_drsr.c/.h&lt;/code&gt; and &lt;code&gt;kull_m_rpc_ms-drsr.h/_c.c&lt;/code&gt; -- generated from the [MS-DRSR] IDL. They are an MS-DRSR client surface, slotted into &lt;code&gt;kuhl_m_lsadump.c&lt;/code&gt; as the new &lt;code&gt;lsadump::dcsync&lt;/code&gt; command [@mimikatz-dcsync-commit][@mimikatz-repo]. Vincent Le Toux is credited as co-author.&lt;/p&gt;
&lt;p&gt;Six weeks later Sean Metcalf writes the canonical operator post and presents the technique at DerbyCon V on Friday, September 25, 2015, in the Track 1 (Break Me) slot from 3:00 to 3:50 pm [@sean-metcalf-dcsync-2015][@sean-metcalf-derbycon-2015]. The closed-population assumption shatters in a weekend.&lt;/p&gt;

A commit hash widely cited in operator forums for the DCSync introduction -- `79b3577aed999baac0352cb1ba3a5f86b6d29f34`, dated 2015-07-20 -- does not exist in the Mimikatz repository. A `git --no-pager log --all` against a fresh clone returns `fatal: bad object` for that hash; the GitHub commit URL returns 404; the GitHub API returns 422. The actual introducing commit is `7717b7a7173fa6a6b6566bbbc3e7372b464d988f`, authored by Benjamin DELPY at 2015-08-11 01:27:13 +0200, with the subject line *&quot;DCSync in mimikatz &amp;amp; for XP/2003&quot;* [@mimikatz-dcsync-commit]. Sean Metcalf&apos;s contemporary writeup at ADSecurity confirms August 2015 as the release month [@sean-metcalf-dcsync-2015]. The lesson for verifying any third-party claim about a Mimikatz commit is the same lesson Stage 1 of this article&apos;s research pipeline applied: clone the repo, run `git show`.
&lt;p&gt;What follows is the story of four generations of attacker approaches against this protocol, each motivated by the operational limit of the previous one.&lt;/p&gt;
&lt;h3&gt;Generation 2: DCSync (2015-08-11 onward)&lt;/h3&gt;
&lt;p&gt;Mimikatz becomes a DRSUAPI client. Operator invocation is one line:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lsadump::dcsync /domain:contoso.local /user:krbtgt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Behind that line, six wire steps fire:&lt;/p&gt;

sequenceDiagram
    participant A as Attacker workstation
    participant DC as Target DC (DRSUAPI)
    A-&amp;gt;&amp;gt;DC: 1. resolve target DC by domain
    A-&amp;gt;&amp;gt;DC: 2. IDL_DRSBind (interface UUID DRSUAPI)
    DC--&amp;gt;&amp;gt;A: 3. bind reply (handle, session key)
    A-&amp;gt;&amp;gt;DC: 4. IDL_DRSGetNCChanges (opnum 3, MTX_ADDR DN, EXOP_REPL_OBJ)
    DC--&amp;gt;&amp;gt;A: 5. DRS_MSG_GETCHGREPLY (encrypted secret attributes)
    A-&amp;gt;&amp;gt;A: 6. decrypt with session key, emit NT hash and Kerberos keys
&lt;p&gt;The wire format is one round trip per principal. To dump every account in the domain, Mimikatz&apos;s &lt;code&gt;/all /csv&lt;/code&gt; mode iterates the request server-side via the same opnum with paginating up-to-date vectors. Per-call wire size is a few kilobytes for a single user; full-domain bulk dumps run to tens of megabytes. Wall-clock time is dominated by network latency to the target DC [@sean-metcalf-dcsync-2015][@trellix-silent-domain-hijack].&lt;/p&gt;
&lt;p&gt;The Generation-1 precondition is gone. The attack runs from any joined workstation. The Generation-1 host artifacts -- VSS events, IFM staging directories, multi-megabyte file copies -- all disappear. The only on-the-wire signature is &lt;em&gt;&quot;an &lt;code&gt;IDL_DRSGetNCChanges&lt;/code&gt; call from an IP that is not a domain controller&quot;&lt;/em&gt; -- which has to be detected at the protocol layer, not the host layer.&lt;/p&gt;
&lt;p&gt;Sean Metcalf&apos;s 2015 post named the detection recipe in the same breath as the attack: &lt;em&gt;&quot;Configure IDS to trigger if DsGetNCChange request originates an IP not on the &apos;Replication Allow List&apos; ...&quot;&lt;/em&gt; [@sean-metcalf-dcsync-2015]. Every network detection rule in this space, including Microsoft&apos;s own ATA &quot;Unusual Protocol Implementation&quot; category two years later, descends from that one sentence [@ata-v17-release-notes].&lt;/p&gt;
&lt;p&gt;DCSync is read-only. The ACL pair the attack exploits does not include directory-write rights. An attacker who wants to plant SID-history backdoors, modify &lt;code&gt;userAccountControl&lt;/code&gt; flags, or write to AdminSDHolder needs a different primitive. The same trust loophole that gave them read access turns out to support a symmetric write side.&lt;/p&gt;
&lt;h3&gt;Generation 3: DCShadow (2018-01-24)&lt;/h3&gt;
&lt;p&gt;January 23-24, 2018. Benjamin Delpy and Vincent Le Toux present at BlueHat IL in Tel Aviv. The talk is titled &lt;em&gt;&quot;Active Directory: What can make your million dollar SIEM go blind?&quot;&lt;/em&gt; [@youtube-bluehat-2018-dcshadow]. Four days later, on January 27, 2018, Delpy pushes the mainline merge to Mimikatz with commit &lt;code&gt;ab18bd1&lt;/code&gt;, subject &lt;em&gt;&quot;Pushing @vletoux DCShadow in current branch with some adaptations&quot;&lt;/em&gt; [@mimikatz-dcshadow-commit].&lt;/p&gt;
&lt;p&gt;Vincent Le Toux contributed the original implementation; the BlueHat IL talk shared joint Delpy/Le Toux billing; the Mimikatz mainline merge commit &lt;code&gt;ab18bd103a5cd7e26fb8d475c5ea0157d6633ca9&lt;/code&gt; is dated 2018-01-27 01:37:55 +0100, four days after the conference disclosure. Le Toux is the same author behind PingCastle&apos;s posture-assessment tooling and the canonical &lt;code&gt;dcshadow.com&lt;/code&gt; reference site [@mimikatz-dcshadow-commit][@dcshadow-com].&lt;/p&gt;
&lt;p&gt;DCShadow inverts DCSync. Where DCSync makes the attacker a DRSUAPI client, DCShadow makes the attacker a DRSUAPI &lt;em&gt;server&lt;/em&gt;. The mechanism: temporarily register an &lt;code&gt;nTDSDSA&lt;/code&gt; object plus the associated SPNs in the Configuration NC; signal to a legitimate DC that the new &quot;DC&quot; wants to push changes via &lt;code&gt;IDL_DRSReplicaAdd&lt;/code&gt; followed by &lt;code&gt;IDL_DRSReplicaSync&lt;/code&gt;; the legitimate DC pulls the attacker&apos;s pre-staged writes through the replication channel as if they were legitimate peer replication; deregister to clean up [@mitre-t1207][@dcshadow-com].&lt;/p&gt;

sequenceDiagram
    participant A as Attacker (rogue DC)
    participant Cfg as Configuration NC
    participant T as Target DC
    A-&amp;gt;&amp;gt;Cfg: 1. create nTDSDSA + server object
    A-&amp;gt;&amp;gt;Cfg: 2. add SPNs (GC, DRS UUID) to machine account
    A-&amp;gt;&amp;gt;T: 3. IDL_DRSReplicaAdd (dsaSrc = attacker)
    T-&amp;gt;&amp;gt;A: 4. IDL_DRSGetNCChanges callback (target pulls)
    A--&amp;gt;&amp;gt;T: 5. staged writes returned as replication payload
    A-&amp;gt;&amp;gt;T: 6. IDL_DRSReplicaDel
    A-&amp;gt;&amp;gt;Cfg: 7. delete nTDSDSA, remove SPNs

A directory operation that does not generate the standard Event ID 4662 / 4738 / 5136 object-modification events that the Domain Services Auditing subsystem emits for normal writes. Legitimate DC-to-DC replication is SACL-silent by design -- the audit subsystem intentionally suppresses change events on the replication channel to avoid drowning every DC in audit traffic on every directory change. DCShadow writes are SACL-silent because they ride the same channel.
&lt;p&gt;That last design fact is the entire point of the talk title. A SIEM watching object-modification events sees nothing when a DCShadow write lands, because the modifications arrive on the replication channel that the SIEM intentionally ignores as routine DC chatter. The original 2018 framing -- &quot;your million-dollar SIEM goes blind&quot; -- was correct for SIEMs that monitored only object-modification events. We will see in §6 how that framing has aged.&lt;/p&gt;
&lt;p&gt;DCShadow is &lt;em&gt;write&lt;/em&gt; capability. An attacker who reaches it can plant SID-history backdoors (write &lt;code&gt;S-1-5-21-...-519&lt;/code&gt; for Enterprise Admins into a low-privileged account&apos;s &lt;code&gt;sIDHistory&lt;/code&gt;), modify &lt;code&gt;userAccountControl&lt;/code&gt; to clear &lt;code&gt;ACCOUNTDISABLE&lt;/code&gt; on dormant high-privilege accounts, add ACEs to AdminSDHolder (which then propagate via the SDProp process to every protected admin account every 60 minutes), or set &lt;code&gt;msDS-AllowedToActOnBehalfOfOtherIdentity&lt;/code&gt; on a target computer to enable resource-based constrained delegation chains.&lt;/p&gt;
&lt;h3&gt;Generation 4: Permission-graph attacks (2018-present)&lt;/h3&gt;
&lt;p&gt;By 2018 the attack-side question had shifted. &lt;em&gt;Holding the rights triad directly&lt;/em&gt; was no longer the interesting precondition; the interesting question was how to reach it transitively, through whatever chain of ACL delegations a real-world domain happened to contain. The umbrella term that emerged is permission-graph attack. Its terminal edge is still DCSync; its novelty is the path that gets you to the terminal.&lt;/p&gt;
&lt;p&gt;Three primitives anchor this generation. Elad Shamir&apos;s &lt;em&gt;Wagging the Dog&lt;/em&gt; (January 28, 2019) showed how writing &lt;code&gt;msDS-AllowedToActOnBehalfOfOtherIdentity&lt;/code&gt; on a target computer object enables S4U2Self plus S4U2Proxy impersonation of any user to that computer [@shenaniganslabs-wagging-the-dog]. Shamir&apos;s &lt;em&gt;Shadow Credentials&lt;/em&gt; (June 21, 2021) demonstrated that writing &lt;code&gt;msDS-KeyCredentialLink&lt;/code&gt; on a target account adds an attacker-controlled certificate trust, enabling Kerberos PKINIT authentication as that account without resetting its password [@eladshamir-shadow-credentials]. And Sean Metcalf&apos;s earlier work on AdminSDHolder template abuse showed that writing the AdminSDHolder ACL causes SDProp to propagate the new ACL onto every protected admin account every 60 minutes -- self-healing persistence that survives defender cleanup [@sean-metcalf-adminsdholder].&lt;/p&gt;
&lt;p&gt;The unifying observation: DCSync is a terminal in a permission graph. The graph&apos;s nodes are AD principals; its edges are individual access-control entries (&lt;code&gt;WriteDACL&lt;/code&gt;, &lt;code&gt;WriteOwner&lt;/code&gt;, &lt;code&gt;GenericAll&lt;/code&gt;, &lt;code&gt;WriteProperty&lt;/code&gt;, &lt;code&gt;AddMember&lt;/code&gt;, &lt;code&gt;ForceChangePassword&lt;/code&gt;). Whoever can traverse the graph to the rights triad on the domain root has DCSync transitively. The attacker&apos;s job is no longer to invoke &lt;code&gt;IDL_DRSGetNCChanges&lt;/code&gt;; it is to find the shortest path through the graph from a foothold to the rights triad. The defender&apos;s mirror job is to enumerate all paths into the rights triad and either prune them or monitor them.&lt;/p&gt;

gantt
    title Domain replication attack class evolution
    dateFormat YYYY-MM-DD
    axisFormat %Y
    section Attack
    Gen 1 NTDS.dit theft           :a1, 2000-02-17, 2015-08-11
    Gen 2 DCSync                   :active, a2, 2015-08-11, 2026-12-31
    Gen 3 DCShadow                 :active, a3, 2018-01-24, 2026-12-31
    Gen 4 Permission-graph chains  :active, a4, 2018-06-01, 2026-12-31
    section Defense
    BloodHound 1.0 DEF CON 24     :d1, 2016-08-06, 30d
    ATA 1.7 release                :d2, 2017-04-01, 30d
    Azure ATP DCShadow alerts      :d3, 2018-07-24, 30d
    Azure ATP rebrand to MDI       :d4, 2020-09-22, 30d
    BloodHound v6.0                :d5, 2024-09-30, 30d
    BloodHound v6.3 Butterfly      :d6, 2024-12-09, 30d
    BloodHound v8.0 OpenGraph      :d7, 2025-07-29, 30d
    Trellix NDR Silent Hijack      :d8, 2025-12-08, 30d
&lt;p&gt;To make the read-side access check decision concrete -- and to expose how thin it actually is -- here is the logic of the gate written as a runnable function. This is pseudocode for the &lt;em&gt;decision&lt;/em&gt;, not the protocol bytes:&lt;/p&gt;
&lt;p&gt;{`
// Illustrative model of the MS-DRSR access check on IDL_DRSGetNCChanges.
// Returns &quot;secrets returned&quot; or &quot;ACCESS_DENIED&quot; given a caller&apos;s effective
// rights on the naming-context root.
function dcsyncAccessCheck(callerRights, ncRootDn, requestedAttrs) {
  const GET_CHANGES         = &apos;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2&apos;;
  const GET_CHANGES_ALL     = &apos;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2&apos;;
  const GET_CHANGES_FILTERED = &apos;89e95b76-444d-4c62-991a-0facbeda640c&apos;;&lt;/p&gt;
&lt;p&gt;  const SECRET_ATTRS = new Set([&apos;unicodePwd&apos;, &apos;dBCSPwd&apos;,
    &apos;ntPwdHistory&apos;, &apos;lmPwdHistory&apos;, &apos;supplementalCredentials&apos;]);
  const wantsSecrets = requestedAttrs.some(a =&amp;gt; SECRET_ATTRS.has(a));&lt;/p&gt;
&lt;p&gt;  // Baseline read requires GetChanges.
  if (!callerRights.has(GET_CHANGES)) return &apos;ACCESS_DENIED&apos;;
  // Secret attributes require GetChangesAll.
  if (wantsSecrets &amp;amp;&amp;amp; !callerRights.has(GET_CHANGES_ALL))
    return &apos;ACCESS_DENIED&apos;;&lt;/p&gt;
&lt;p&gt;  // Notice what is NOT checked: caller&apos;s SPN, machine-group membership,
  // ticket service, source IP. The access gate is the ACL, full stop.
  return &apos;secrets returned&apos;;
}&lt;/p&gt;
&lt;p&gt;const attacker = new Set([
  &apos;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2&apos;,
  &apos;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2&apos;,
]);
console.log(dcsyncAccessCheck(attacker, &apos;DC=contoso,DC=local&apos;,
  [&apos;unicodePwd&apos;, &apos;ntPwdHistory&apos;]));
`}&lt;/p&gt;
&lt;p&gt;Four generations, eleven years, no Generation 5 in sight. What is the single insight that lets all of this exist, and why has nobody patched it?&lt;/p&gt;
&lt;h2&gt;5. There Is No DC Check&lt;/h2&gt;
&lt;p&gt;The whole structural error of this protocol is one missing question. Specification [MS-DRSR] §4.1.10 defines the access check on &lt;code&gt;IDL_DRSGetNCChanges&lt;/code&gt; as: does the calling principal hold the rights triad on the naming context being replicated? [@ms-drsr-spec] That is the whole gate. There is no second check that asks &quot;is the caller&apos;s service principal name a domain-controller SPN?&quot; or &quot;is the caller&apos;s machine account in the &lt;code&gt;Domain Controllers&lt;/code&gt; group?&quot; or &quot;was the caller&apos;s Kerberos service ticket issued for a DC service?&quot; The spec is empirically and literally just an ACL check on the NC root.&lt;/p&gt;
&lt;p&gt;The empirical proof that a non-DC client succeeds lives in Mimikatz&apos;s &lt;code&gt;kuhl_m_lsadump.c&lt;/code&gt; and its DRSUAPI client surface in &lt;code&gt;kull_m_rpc_drsr*&lt;/code&gt; [@mimikatz-repo]. Mimikatz is software running on a workstation. It binds to the DRSUAPI interface UUID, calls &lt;code&gt;IDL_DRSGetNCChanges&lt;/code&gt;, and the call returns. The protocol&apos;s behavior is correct by specification. The security model is the bug.&lt;/p&gt;

A major feature added to Mimkatz in August 2015 is &apos;DCSync&apos; which effectively &apos;impersonates&apos; a Domain Controller and requests account password data from the targeted Domain Controller. DCSync was written by Benjamin Delpy and Vincent Le Toux. -- Sean Metcalf, ADSecurity, September 2015 [@sean-metcalf-dcsync-2015]
&lt;p&gt;Read Metcalf&apos;s verb carefully: &lt;em&gt;impersonates&lt;/em&gt;. The scare quotes are doing work. The Mimikatz client is not pretending to be a DC in any cryptographic sense. It is not forging a machine-account ticket. It is not spoofing an SPN. It is not bypassing any signature check.&lt;/p&gt;
&lt;p&gt;It is honestly making an authenticated call as the principal it actually is -- some user or service account that happens to hold the rights triad -- and the protocol honestly responds because its gate is satisfied. The &quot;impersonation&quot; framing is operator vocabulary borrowed from the social model the protocol was written under.&lt;/p&gt;
&lt;p&gt;The 1999 designers assumed only DCs would speak. By that social contract, anyone who speaks must be a DC. The spec encoded the social contract by way of &lt;em&gt;not encoding it at all&lt;/em&gt;. The ACL was the whole gate because, in 1999, the ACL was always satisfied by something that was always a DC.&lt;/p&gt;

flowchart TD
    Start[IDL_DRSGetNCChanges request arrives]
    Start --&amp;gt; Q1{&quot;Caller holds rights triad&lt;br /&gt;on the NC root?&quot;}
    Q1 -- yes --&amp;gt; Return[Return encrypted&lt;br /&gt;secret attributes]
    Q1 -- no --&amp;gt; Deny[ERROR_DS_DRA_ACCESS_DENIED]
    Start -.-&amp;gt; Absent[&quot;What the check does NOT ask:&lt;br /&gt;Is the caller a domain controller?&lt;br /&gt;Is the SPN a DC SPN?&lt;br /&gt;Is the machine in Domain Controllers?&quot;]
    Absent -.-&amp;gt; Nothing[no second gate exists]
&lt;p&gt;This is the article&apos;s intellectual fulcrum. The 1999 closed-population assumption -- only DCs speak this protocol -- survives in 2026 as an open-population reality (anyone with the rights speaks it), and the closed-population assumption is nowhere written down in the access-check code. The fix that would close the model would require a second gate. The spec did not write one. The implementation cannot synthesize one without breaking every legitimate consumer that already operates without one (more on this in §8).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The protocol&apos;s design is correct. The security model is the bug. No patch can fix this without breaking Active Directory replication itself.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There is a temptation, on first encountering this, to assume the missing gate is an oversight that Microsoft will eventually fix. That intuition is wrong, and it is wrong for a deeper reason than &quot;Microsoft has not gotten around to it.&quot; If the protocol is the bug and the protocol cannot be amended, what defenses can possibly work in 2026? That question carries us into the next section.&lt;/p&gt;
&lt;h2&gt;6. What 2026 Actually Ships Against This&lt;/h2&gt;
&lt;p&gt;If the protocol cannot be fixed, the defender&apos;s question is no longer &quot;how do I prevent DCSync?&quot; but &quot;which layer catches which class of attempt?&quot; Four production detection layers ship against this attack class in 2026. None of them is individually sufficient. A fifth layer -- the architectural one that would close the structural error -- does not exist and is not coming.&lt;/p&gt;
&lt;h3&gt;Posture: enumerate who holds the rights&lt;/h3&gt;
&lt;p&gt;The posture layer reads the static ACL on the Domain NC root and surfaces every principal that holds any of the three rights. Microsoft Defender for Identity ships this as an Accounts security posture assessment family, computed continuously from MDI&apos;s per-DC sensors [@mdi-security-posture-accounts]. Vincent Le Toux&apos;s PingCastle exposes it as the &lt;em&gt;C-DCSync&lt;/em&gt; finding in its critical-risks section. Tenable Identity Exposure exposes it as an indicator of exposure. Christopher Keim&apos;s 2025 practitioner guide documents the PowerShell pattern that AD administrators without a posture-tool license can run on demand [@keim-dcsync-rights]:&lt;/p&gt;
&lt;p&gt;{`&lt;/p&gt;
Illustrative model of the posture-layer check.
In production, replace SAMPLE_ACL with output from Get-Acl in PowerShell
or python-ldap against the live Domain NC root.
&lt;p&gt;GET_CHANGES          = &apos;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2&apos;
GET_CHANGES_ALL      = &apos;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2&apos;
GET_CHANGES_FILTERED = &apos;89e95b76-444d-4c62-991a-0facbeda640c&apos;
TRIAD = {GET_CHANGES, GET_CHANGES_ALL, GET_CHANGES_FILTERED}&lt;/p&gt;
&lt;p&gt;DEFAULTS = {
    &apos;BUILTIN\\Administrators&apos;,
    &apos;CONTOSO\\Domain Controllers&apos;,
    &apos;CONTOSO\\Domain Admins&apos;,
    &apos;CONTOSO\\Enterprise Admins&apos;,
    &apos;NT AUTHORITY\\ENTERPRISE DOMAIN CONTROLLERS&apos;,
}&lt;/p&gt;
&lt;p&gt;SAMPLE_ACL = [
    {&apos;principal&apos;: &apos;CONTOSO\\Domain Admins&apos;,     &apos;right&apos;: GET_CHANGES},
    {&apos;principal&apos;: &apos;CONTOSO\\Domain Admins&apos;,     &apos;right&apos;: GET_CHANGES_ALL},
    {&apos;principal&apos;: &apos;CONTOSO\\Enterprise Admins&apos;, &apos;right&apos;: GET_CHANGES},
    {&apos;principal&apos;: &apos;CONTOSO\\Enterprise Admins&apos;, &apos;right&apos;: GET_CHANGES_ALL},
    {&apos;principal&apos;: &apos;CONTOSO\\backup_svc_2017&apos;,   &apos;right&apos;: GET_CHANGES},
    {&apos;principal&apos;: &apos;CONTOSO\\backup_svc_2017&apos;,   &apos;right&apos;: GET_CHANGES_ALL},
    {&apos;principal&apos;: &apos;CONTOSO\\MSOL_a1b2c3&apos;,       &apos;right&apos;: GET_CHANGES},
    {&apos;principal&apos;: &apos;CONTOSO\\MSOL_a1b2c3&apos;,       &apos;right&apos;: GET_CHANGES_ALL},
]&lt;/p&gt;
&lt;p&gt;residual = {}
for ace in SAMPLE_ACL:
    if ace[&apos;right&apos;] in TRIAD and ace[&apos;principal&apos;] not in DEFAULTS:
        residual.setdefault(ace[&apos;principal&apos;], set()).add(ace[&apos;right&apos;])&lt;/p&gt;
&lt;p&gt;for principal, rights in residual.items():
    print(f&quot;{principal}: {len(rights)} of 3 replication rights&quot;)
`}&lt;/p&gt;
&lt;p&gt;The residual set is the operational unit of work. In a freshly-installed domain it is empty. In a ten-year-old forest with a history of mergers and migrations it typically contains five to twenty entries -- a mix of legitimately delegated identity-sync products and forgotten service accounts from projects nobody on the current operations team remembers.&lt;/p&gt;

In Microsoft&apos;s Enhanced Security Administrative Environment and the BloodHound conventions, the set of principals and assets whose compromise yields domain-wide control. The `krbtgt` account, members of `Domain Admins` and `Enterprise Admins`, the Entra ID Connect MSOL_ sync account, and any non-default principal holding the rights triad on the domain root are all Tier Zero by construction.

The Microsoft Entra ID (formerly Azure AD) Connect synchronization service account, created on-premises during Entra Connect installation. The account name uses an `MSOL_` prefix followed by a hex suffix. It legitimately holds the rights triad on the Domain NC root because it must replicate password hashes to the cloud directory; treating it as Tier Zero is the standard hardening recommendation.

Microsoft Defender for Identity&apos;s (and ATA&apos;s earlier) internal baseline of which computers in the domain legitimately speak DRSUAPI to which DCs. Incoming `IDL_DRSGetNCChanges` requests are matched against this baseline; a request from a source outside the list fires the External ID 2006 alert.
&lt;p&gt;An earlier scope document for this article quoted an MDI assessment titled verbatim &lt;em&gt;&quot;Remove non-admin accounts with DCSync permissions.&quot;&lt;/em&gt; That exact title does not appear on the live MDI Accounts security-posture-assessment page in 2026 [@mdi-security-posture-accounts][@mdi-security-posture-hybrid]. The surface exists -- MDI&apos;s Accounts and Hybrid security posture-assessment families together cover non-default principals with replication rights -- but the verbatim title was not reproducible.&lt;/p&gt;
&lt;h3&gt;Behavioral: catch the act after it fires&lt;/h3&gt;
&lt;p&gt;The behavioral layer watches network traffic and event logs for the act of DCSync or DCShadow as it happens. Microsoft&apos;s first-party stack lives in Defender for Identity and ships three canonical alerts [@mdi-alerts-classic]:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;External ID 2006 -- &quot;Suspected DCSync attack (replication of directory services).&quot;&lt;/strong&gt; Credential Access (TA0006), Persistence (TA0003). Technique T1003.006. Severity High. Trigger: a replication request is initiated from a computer that is not a domain controller.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External ID 2028 -- &quot;Suspected DCShadow attack (domain controller promotion).&quot;&lt;/strong&gt; Defense Evasion (TA0005). Technique T1207. Trigger: a machine in the network tries to register as a rogue domain controller.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External ID 2029 -- &quot;Suspected DCShadow attack (domain controller replication request).&quot;&lt;/strong&gt; Defense Evasion (TA0005). Technique T1207. Trigger: a suspicious replication request is generated against a genuine domain controller, indicative of DCShadow.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The product lineage is Microsoft Advanced Threat Analytics 1.7 (April 2017, where DCSync was covered under the umbrella &quot;Unusual Protocol Implementation enhancements&quot; detection category [@ata-v17-release-notes]); Azure Advanced Threat Protection (2018, where Tali Ash&apos;s July 24, 2018 Microsoft Tech Community post announced the two named DCShadow detections that became 2028 and 2029 [@tali-ash-azure-atp-dcshadow]); and Defender for Identity (the Microsoft Ignite 2020 rebrand, week of September 22-24, 2020 [@rcpmag-defender-rebrand]). The detection content carries forward unchanged across product renamings; what changes is the portal it surfaces in [@mdi-whats-new].&lt;/p&gt;
&lt;p&gt;Open-source SIEM equivalents implement the same detection via Event ID 4662 on the domain object. The Sigma rule &lt;em&gt;Mimikatz DC Sync&lt;/em&gt; (id &lt;code&gt;611eab06-a145-4dfa-a295-3ccc5c20f59a&lt;/code&gt;) fires on Event ID 4662 where &lt;code&gt;Properties&lt;/code&gt; contains any of the three rights GUIDs or the literal string &lt;em&gt;Replicating Directory Changes All&lt;/em&gt;, with &lt;code&gt;AccessMask=0x100&lt;/code&gt; [@sigma-rule-dcsync]. Splunk&apos;s parallel detection &lt;em&gt;Windows AD Replication Request Initiated by User Account&lt;/em&gt; (rule &lt;code&gt;51307514-1236-49f6-8686-d46d93cc2821&lt;/code&gt;) implements the equivalent SPL search with the same MITRE T1003.006 annotation and the same known-false-positive list (Azure AD Connect, &lt;code&gt;dcdiag.exe /Test:Replications&lt;/code&gt;) [@splunk-research-dcsync].&lt;/p&gt;
&lt;p&gt;The SACL-event detection layer (Sigma + Splunk + every other SIEM-side implementation) requires that the operator first enable Advanced Security Audit policy &lt;code&gt;Audit Directory Services Access&lt;/code&gt; under &lt;code&gt;DS Access&lt;/code&gt;, with SACLs on the domain root auditing the three rights against &lt;code&gt;Everyone&lt;/code&gt;, &lt;code&gt;Domain Computers&lt;/code&gt;, and &lt;code&gt;Domain Controllers&lt;/code&gt;. A fresh-install AD does not have these SACLs by default [@splunk-research-dcsync]. The detection rule&apos;s documentation calls out the prerequisite explicitly; many operators discover it only after wondering why their Splunk dashboard is silent.&lt;/p&gt;
&lt;h3&gt;Network: NDR on the DRSUAPI interface&lt;/h3&gt;
&lt;p&gt;The network layer parses DCE/RPC traffic on the wire, identifies the DRSUAPI interface by its UUID, and fires when an &lt;code&gt;IDL_DRSGetNCChanges&lt;/code&gt; call originates from a source outside the legitimate-DC baseline. The canonical 2025 reference is Trellix Advanced Research Center&apos;s &lt;em&gt;&quot;Silent Domain Hijack: Uncovering the DCSync Attack and Detecting with Trellix NDR&quot;&lt;/em&gt;, published in week 50 of 2025 [@trellix-silent-domain-hijack]. Equivalent capability ships in Microsoft Defender for Identity&apos;s network analytics layer (the same sensor that powers External ID 2006) [@mdi-alerts-classic].&lt;/p&gt;
&lt;p&gt;The Trellix writeup is explicit about the architecture: &lt;em&gt;&quot;Trellix NDR detects replication protocol abuse by analyzing abnormal DCE/RPC and MS-DRSR traffic. It detects DCSync-like behavior when replication requests are sent from non-DC hosts or unusual users&quot;&lt;/em&gt; [@trellix-silent-domain-hijack]. The detection is independent of host-side telemetry, so it survives EDR tampering and works in environments where a DC is un-sensored from MDI&apos;s perspective.&lt;/p&gt;
&lt;h3&gt;Graph: BloodHound traverses the permission graph&lt;/h3&gt;
&lt;p&gt;The graph layer was built specifically for Generation 4. BloodHound, originally released by Andy Robbins, Rohan Vazarkar, and Will Schroeder at DEF CON 24 on August 6, 2016, models &lt;em&gt;&quot;you have DCSync rights to the domain&quot;&lt;/em&gt; as a directed edge from a principal to the domain node, computed by combining the separately-collected &lt;code&gt;GetChanges&lt;/code&gt; and &lt;code&gt;GetChangesAll&lt;/code&gt; ACEs through nested-group membership [@wald0-bloodhound-intro][@defcon24-bloodhound-pdf][@bloodhound-edge-dcsync]. An operator queries &lt;em&gt;&quot;shortest path from any compromised principal to the DCSync edge into a Domain node&quot;&lt;/em&gt; and gets back every transitive route into the rights triad.&lt;/p&gt;
&lt;p&gt;The 2024-2026 release cadence is dense and matters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BloodHound v6.0 (September 30, 2024)&lt;/strong&gt; improved logic for identifying and creating complex edges requiring multiple permissions, including DCSync, when &lt;code&gt;Authenticated Users&lt;/code&gt; or &lt;code&gt;Everyone&lt;/code&gt; groups are involved [@bloodhound-v6-0-release-notes]. This closed a long-standing blind spot in which rights granted to a wildcard principal (the worst kind of delegation drift, often dating from compatibility settings in pre-2003 forests) were not surfaced as DCSync edges.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BloodHound v6.3 (December 9, 2024)&lt;/strong&gt; introduced the &lt;em&gt;Butterfly&lt;/em&gt; algorithm -- bi-directional risk analysis. Where the historical query was &lt;em&gt;&quot;which principals can attack this target?&quot;&lt;/em&gt;, Butterfly adds &lt;em&gt;&quot;which targets can be attacked from this compromised principal?&quot;&lt;/em&gt; SpecterOps describes the algorithm in their blog post &lt;em&gt;Unwrapping BloodHound v6.3 with Impact Analysis&lt;/em&gt;: &lt;em&gt;&quot;This is a massive upgrade to BloodHound Enterprise&apos;s risk analysis capability with a new algorithm we call &apos;Butterfly&apos;&quot;&lt;/em&gt; [@specterops-butterfly-blog][@bloodhound-v6-3-release-notes].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BloodHound v8.0 (July 29, 2025)&lt;/strong&gt; introduced &lt;em&gt;OpenGraph&lt;/em&gt;, generalizing the graph engine to model attack paths across non-AD systems (GitHub, 1Password, NPM, Snowflake) using the same Cypher front-end. The DCSync edge remains AD-specific; OpenGraph&apos;s relevance is that hybrid-cloud principals can now be modeled end-to-end [@bloodhound-v8-0-release-notes].&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The graph layer is the only one that catches multi-hop chains to the rights triad. A principal A with &lt;code&gt;GenericAll&lt;/code&gt; on group B, where B has the DCSync triad, is invisible to MDI&apos;s posture assessment but stands out as a one-hop path in BloodHound.&lt;/p&gt;

flowchart TD
    subgraph Posture[Posture layer]
        P1[MDI Accounts assessments]
        P2[PingCastle C-DCSync]
        P3[Keim PowerShell pattern]
    end
    subgraph Behavioral[Behavioral layer]
        B1[MDI 2006 / 2028 / 2029]
        B2[Sigma 611eab06]
        B3[Splunk 51307514]
    end
    subgraph Network[Network layer NDR]
        N1[Trellix NDR]
        N2[MDI per-DC sensor]
        N3[CrowdStrike DCE/RPC IoA]
    end
    subgraph Graph[Graph layer]
        G1[BloodHound DCSync edge]
        G2[v6.3 Butterfly Impact]
        G3[v8.0 OpenGraph]
    end
    P1 --&amp;gt; SOC[SOC alert queue]
    P2 --&amp;gt; SOC
    P3 --&amp;gt; SOC
    B1 --&amp;gt; SOC
    B2 --&amp;gt; SOC
    B3 --&amp;gt; SOC
    N1 --&amp;gt; SOC
    N2 --&amp;gt; SOC
    N3 --&amp;gt; SOC
    G1 --&amp;gt; SOC
    G2 --&amp;gt; SOC
    G3 --&amp;gt; SOC
&lt;p&gt;The full layer-by-layer comparison is the load-bearing matrix of the entire 2026 SOTA:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What it inputs&lt;/th&gt;
&lt;th&gt;What it detects&lt;/th&gt;
&lt;th&gt;What it misses&lt;/th&gt;
&lt;th&gt;False-positive class&lt;/th&gt;
&lt;th&gt;Detection latency&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Posture&lt;/td&gt;
&lt;td&gt;NC-root ACL&lt;/td&gt;
&lt;td&gt;Direct grant of the rights triad to a non-default principal&lt;/td&gt;
&lt;td&gt;Multi-hop transitive paths; legitimate-principal abuse&lt;/td&gt;
&lt;td&gt;Identity-sync products (Entra Connect MSOL_, third-party HR-IDM); legitimately delegated backup agents&lt;/td&gt;
&lt;td&gt;24h score recomputation; ACE changes near real time&lt;/td&gt;
&lt;td&gt;Built in to MDI; PowerShell variant free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Behavioral&lt;/td&gt;
&lt;td&gt;Event ID 4662 or sensor-captured DRSUAPI traffic&lt;/td&gt;
&lt;td&gt;The act of DCSync, or the DCShadow registration / replication, after it happens&lt;/td&gt;
&lt;td&gt;Pre-attack staging; compromised-DC speaker; un-sensored DC blind spot&lt;/td&gt;
&lt;td&gt;Azure AD Connect syncs; &lt;code&gt;dcdiag.exe /Test:Replications&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minutes (alert &quot;after the fact&quot;)&lt;/td&gt;
&lt;td&gt;MDI license; Sigma / Splunk free with SACL config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;DCE/RPC packet capture&lt;/td&gt;
&lt;td&gt;Wire signature of &lt;code&gt;IDL_DRSGetNCChanges&lt;/code&gt; from non-DC source&lt;/td&gt;
&lt;td&gt;Encrypted RPC payloads (per-principal granularity); sub-DC replicas misclassified&lt;/td&gt;
&lt;td&gt;Same as behavioral plus Samba-DC peers&lt;/td&gt;
&lt;td&gt;Seconds (passive inspection)&lt;/td&gt;
&lt;td&gt;Commercial NDR appliance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graph&lt;/td&gt;
&lt;td&gt;LDAP-collected ACEs and group nesting&lt;/td&gt;
&lt;td&gt;Multi-hop graph paths that could reach the rights triad&lt;/td&gt;
&lt;td&gt;Net-new ACEs created after the last collection&lt;/td&gt;
&lt;td&gt;Stale data showing principals that no longer hold the right&lt;/td&gt;
&lt;td&gt;Hours to weeks (collection cadence)&lt;/td&gt;
&lt;td&gt;BloodHound CE free; BHE commercial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architectural (un-shipped)&lt;/td&gt;
&lt;td&gt;TPM-attested DC machine identity&lt;/td&gt;
&lt;td&gt;Pre-empts non-DC speaker entirely&lt;/td&gt;
&lt;td&gt;Compromised DC speaker; rogue &lt;code&gt;nTDSDSA&lt;/code&gt; registration&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;N/A (preventive)&lt;/td&gt;
&lt;td&gt;Requires Microsoft protocol amendment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Four layers, dozens of products, no shortage of detections. Why isn&apos;t the problem solved?&lt;/p&gt;
&lt;h2&gt;7. Which Gap Does Each Defense Close?&lt;/h2&gt;
&lt;p&gt;The matrix in §6 has one structural observation that earns its keep. Read down the &lt;em&gt;What it misses&lt;/em&gt; column. Each detection layer has a class of cases it cannot see. The posture layer cannot see transitive paths. The behavioral layer cannot see pre-attack staging or compromised-DC speakers. The network layer cannot see encrypted RPC payloads. The graph layer cannot see net-new ACEs created after the last collection. That is the load-bearing reason a mature defender runs all four in parallel instead of picking one.&lt;/p&gt;
&lt;p&gt;Run a thought experiment. Suppose you ship only the posture layer. Your MDI assessment is green: no non-default principals hold the rights triad. An attacker compromises a workstation belonging to an unrelated business unit, finds that the BU has &lt;code&gt;GenericAll&lt;/code&gt; on a group &lt;code&gt;LegacyApp_Operators&lt;/code&gt; that itself has &lt;code&gt;WriteDACL&lt;/code&gt; on the &lt;code&gt;BackupOperators&lt;/code&gt; group, and adds themselves to &lt;code&gt;BackupOperators&lt;/code&gt;. &lt;code&gt;BackupOperators&lt;/code&gt; inherits a forgotten 2014 delegation of the rights triad through three levels of nesting. Then DCSync runs.&lt;/p&gt;
&lt;p&gt;The posture layer never saw this because the residual list at the NC root is the four defaults. The graph layer would have surfaced the path. The behavioral or network layer would have fired the moment the DCSync call hit the wire. Without those layers, your green dashboard is a single-layer fantasy.&lt;/p&gt;
&lt;p&gt;Now consider the inverse. You ship only the behavioral layer. MDI fires External ID 2006 -- but only after the request hits the DC. Your SOC&apos;s mean response time is twelve minutes. The attacker&apos;s mean &lt;em&gt;complete-the-extraction&lt;/em&gt; time is sixty seconds. The detection is real; the response window is not.&lt;/p&gt;
&lt;p&gt;The same structural observation applies on the attack side. The four attack primitives have very different precondition costs:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attack primitive&lt;/th&gt;
&lt;th&gt;Pre-condition&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Default detection layer&lt;/th&gt;
&lt;th&gt;Tier of damage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;DCSync via Mimikatz&lt;/td&gt;
&lt;td&gt;Rights triad on NC root&lt;/td&gt;
&lt;td&gt;Every secret-attribute value for one or all principals&lt;/td&gt;
&lt;td&gt;MDI 2006; Sigma 611eab06; Splunk 51307514; NDR&lt;/td&gt;
&lt;td&gt;Domain takeover (via Golden Ticket from &lt;code&gt;krbtgt&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DCSync via Impacket&lt;/td&gt;
&lt;td&gt;Same as Mimikatz; no on-target binary footprint&lt;/td&gt;
&lt;td&gt;Same as Mimikatz, plus PtH / PtT auth modes&lt;/td&gt;
&lt;td&gt;Same as Mimikatz (wire signature is identical)&lt;/td&gt;
&lt;td&gt;Domain takeover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DCShadow&lt;/td&gt;
&lt;td&gt;Domain Admin, local administrator on a DC, or &lt;code&gt;KRBTGT&lt;/code&gt; hash (for rogue-DC registration) [@mitre-t1207][@dcshadow-com]&lt;/td&gt;
&lt;td&gt;Arbitrary directory write, SACL-silent&lt;/td&gt;
&lt;td&gt;MDI 2028 + 2029&lt;/td&gt;
&lt;td&gt;Domain persistence (SID-history, AdminSDHolder ACE re-grant)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gen-4 chains&lt;/td&gt;
&lt;td&gt;Any ACE that transitively leads to the triad&lt;/td&gt;
&lt;td&gt;Reach DCSync, then DCSync&apos;s output&lt;/td&gt;
&lt;td&gt;BloodHound DCSync edge inbound paths&lt;/td&gt;
&lt;td&gt;Domain takeover (composite)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;DCSync via Mimikatz and via Impacket [@impacket-secretsdump] share the &lt;em&gt;output&lt;/em&gt; (domain takeover via credential theft) and the &lt;em&gt;default detection&lt;/em&gt; (the wire signature is the same). Their pre-conditions are identical. The Hacker Recipes documents Impacket&apos;s invocation surface for both pass-the-hash and pass-the-ticket modes [@hacker-recipes-dcsync]. This is why Impacket&apos;s &lt;code&gt;secretsdump.py -just-dc&lt;/code&gt; has become the universal red-team and IR tool, while Mimikatz remains the reference implementation that every blue-team detection still names.&lt;/p&gt;
&lt;p&gt;The Generation-4 row is the interesting one. Its pre-condition is dramatically cheaper than the other three (&lt;em&gt;any&lt;/em&gt; ACE that leads to the triad, rather than the triad in hand). Its detection is by graph traversal, not by wire signature. This is why the 2024-2026 frontier in this space has been the graph layer: the attack-side cost asymmetry favors the chain-finding problem, so the defense-side investment has landed there.&lt;/p&gt;

A reader who has internalized the &quot;no DC check&quot; observation will naturally ask: surely the obvious architectural fix is to add a caller-machine-identity check? CVE-2020-1472, popularly known as Zerologon, is the counterexample. Secura&apos;s September 2020 whitepaper, published approximately one month after Microsoft&apos;s August 11, 2020 patch, documented that the Netlogon protocol&apos;s `ComputeNetlogonCredential` AES-CFB8 implementation used an all-zero initialization vector. An attacker who sends an all-zero `ClientChallenge` exploits the IV bug to authenticate with roughly 1-in-256 probability per attempt, then calls `NetrServerPasswordSet2` to reset the DC&apos;s machine account password (commonly to all zeros) and becomes that DC for protocol purposes [@secura-zerologon-whitepaper]. After Zerologon-class compromise, the attacker *is* a DC. Any architectural caller-machine-identity check on `IDL_DRSGetNCChanges` would have been satisfied. Zerologon is the canonical worked proof that promoting the access-check from &quot;rights&quot; to &quot;rights and DC identity&quot; raises the bar but does not change the class.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Every detection layer in this space has a class of cases it cannot see. Mature defenders run all four (posture, behavioral, network, graph) because each layer is the only answer for a specific class of inputs, and accept that the un-shipped fifth layer (architectural) would only narrow the gap, not close it. Single-layer deployments produce confident but incomplete coverage.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If even the hypothetical perfect architectural fix would not close the class, what are the actual theoretical limits of any possible defense?&lt;/p&gt;
&lt;h2&gt;8. Why the Protocol Cannot Be Fixed&lt;/h2&gt;
&lt;p&gt;Two structural ceilings bound any conceivable MS-DRSR amendment. Both are provable from the specification&apos;s own definition of what it must do; both have been corroborated by the post-2018 industry consensus that &lt;em&gt;detection&lt;/em&gt; (not prevention) is the only viable defender posture.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ceiling 1: replicated secret material is the data the protocol exists to carry.&lt;/strong&gt; MS-DRSR is the mechanism by which Active Directory&apos;s multi-master replication invariant holds. If &lt;code&gt;IDL_DRSGetNCChanges&lt;/code&gt; did not return secret attributes -- &lt;code&gt;unicodePwd&lt;/code&gt;, &lt;code&gt;dBCSPwd&lt;/code&gt;, &lt;code&gt;ntPwdHistory&lt;/code&gt;, &lt;code&gt;lmPwdHistory&lt;/code&gt;, &lt;code&gt;supplementalCredentials&lt;/code&gt; -- then a password change on DC-A would not converge to DC-B within the replication interval. AD&apos;s &lt;em&gt;&quot;any DC accepts any write and the others catch up within minutes&quot;&lt;/em&gt; property would collapse [@ms-drsr-spec]. The protocol cannot stop returning secrets without ceasing to be the protocol. This is why &quot;disable MS-DRSR&quot; appears on every list of options that look attractive in a slide deck and break replication in production within minutes of being applied.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ceiling 2: a machine-identity check on the caller would shift the attack class, not close it.&lt;/strong&gt; Suppose Microsoft amended the access check on &lt;code&gt;IDL_DRSGetNCChanges&lt;/code&gt; to require, in addition to the rights triad, a cryptographic proof that the caller&apos;s machine account is in the &lt;code&gt;Domain Controllers&lt;/code&gt; group -- for example, that the call was made under a Kerberos service ticket issued for a DC SPN. This would defeat the Mimikatz-from-workstation case. It would also defeat every legitimate integration that today holds the rights triad on a non-DC service account: the Entra ID Connect MSOL_ account, third-party HR identity-management connectors, every backup and disaster-recovery tool that integrates at the AD level.&lt;/p&gt;
&lt;p&gt;Worse, the new check would shift the attack class to &lt;em&gt;compromising a DC&apos;s machine account&lt;/em&gt; -- CVE-2020-1472 Zerologon being the canonical worked example (see the §7 Aside for the mechanism). After a Zerologon-class compromise the attacker &lt;em&gt;is&lt;/em&gt; a DC, so promoting the speaker check from (rights) to (rights and DC-identity) raises the bar but does not change the class [@secura-zerologon-whitepaper].&lt;/p&gt;

flowchart TD
    Fix[Proposed MS-DRSR fix]
    Fix --&amp;gt; A[Stop returning secrets&lt;br /&gt;in IDL_DRSGetNCChanges]
    Fix --&amp;gt; B[Add machine-identity check&lt;br /&gt;on the caller]
    A --&amp;gt; A1[AD multi-master replication breaks&lt;br /&gt;password changes do not propagate]
    B --&amp;gt; B1[Legitimate integrations break&lt;br /&gt;MSOL_ account, HR IDM, backup tools]
    B --&amp;gt; B2[Attack shifts to compromised DC&lt;br /&gt;machine accounts e.g. Zerologon&lt;br /&gt;CVE-2020-1472]
&lt;p&gt;The honest structural fix would require a different replication architecture: a &lt;a href=&quot;https://paragmali.com/blog/the-tpm-in-windows-one-primitive-twenty-five-years-and-the-c/&quot; rel=&quot;noopener&quot;&gt;TPM&lt;/a&gt;- or HSM-attested DC machine identity, bound to a sealed replication key, with secret attributes encrypted under that key on the wire. No caller without the sealed key (or its hardware-bound equivalent on a different DC) could ever decrypt the response.&lt;/p&gt;
&lt;p&gt;Microsoft has not announced any such architecture. Its closest published precedent in the Windows security stack is the &lt;a href=&quot;https://paragmali.com/blog/vbs-trustlets-what-actually-runs-in-the-secure-kernel/&quot; rel=&quot;noopener&quot;&gt;LSAIso trustlet&lt;/a&gt; that Credential Guard uses for LSASS isolation -- a per-host isolation primitive applied to a per-host secret store. Applying the same idea to a multi-party wire protocol that must interoperate with twenty-five years of installed identity-sync tooling is a different engineering problem at a different scale. Microsoft has not committed to it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The replication attack class is structurally permanent. The honest defender response is detection-and-response, not prevention.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is the article&apos;s humility moment. The reader who arrived at §5 thinking &quot;this is fixable&quot; should now understand why eleven years of attack/defense iteration have produced detection layers, not protocol revisions. The four-layer detection architecture is not a placeholder while we wait for Microsoft to ship the real fix. It is the real fix, conditional on the constraint that the protocol&apos;s job description does not change.&lt;/p&gt;
&lt;p&gt;If the protocol is structurally unfixable, what exactly does 2026 still not solve operationally?&lt;/p&gt;
&lt;h2&gt;9. What 2026 Still Cannot Do&lt;/h2&gt;
&lt;p&gt;Five problems sit on the open-questions register in 2026. Each is documented in the literature. None has a satisfying answer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The DCShadow gap window.&lt;/strong&gt; MDI&apos;s External ID 2029 alert fires on the rogue DC&apos;s replication request, which is structurally &lt;em&gt;after&lt;/em&gt; the rogue &lt;code&gt;nTDSDSA&lt;/code&gt; registration has been committed. The alert documentation describes the detection as firing after the fact [@mdi-alerts-classic]. An attacker who completes the register-replicate-deregister cycle inside the alert&apos;s batch interval commits the persistence write before any SOC responder sees the alert. External ID 2028 (rogue promotion) fires earlier in the kill chain and partially closes the gap, but the gap is structural to the alert-batch model. The directory write that DCShadow lands -- a SID-history injection, an AdminSDHolder ACE re-grant -- survives the alert.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Encrypted-channel DCSync.&lt;/strong&gt; DRSUAPI clients that negotiate &lt;code&gt;AUTH_LEVEL_PKT_PRIVACY&lt;/code&gt; on the RPC binding (the modern hardened-DC default) encrypt the request and response bodies on the wire. Passive NDR sensors that depend on parsing the &lt;code&gt;IDL_DRSGetNCChanges&lt;/code&gt; request to determine which principal is being targeted lose per-principal granularity.&lt;/p&gt;
&lt;p&gt;The interface-bind packet is still in clear, so the existence of a DRSUAPI call is still visible, but the payload is not. The Microsoft channel-binding rollout that began in late 2023 (targeting LDAP rather than DRSUAPI, but cementing the broader trend toward encrypted directory traffic) makes this gap permanent on the wire side [@microsoft-ldap-channel-binding-kb4520412]. Detection moves into the DC itself via the MDI sensor model.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The legitimate-principal-compromise non-detection.&lt;/strong&gt; A hijacked Domain Admin session that uses its rightful DCSync ability triggers no layer. The posture layer sees a default principal. The behavioral layer sees a request from a DC or admin workstation that the Replication Allow List baseline accepts. The network layer sees the same. The graph layer sees the principal as a default Tier Zero member. The MDI alert is explicit: the trigger is &lt;em&gt;&quot;a computer that isn&apos;t a domain controller&quot;&lt;/em&gt; -- a compromised legitimate principal acting from a legitimately-baselined workstation does not fire it [@mdi-alerts-classic].&lt;/p&gt;
&lt;p&gt;This is the failure mode that catches mature SOCs. The attacker who already has Domain Admin does not need to attack DCSync detection because DCSync detection is not designed for legitimate principal abuse. UEBA-style per-principal anomaly detection (&lt;em&gt;&quot;this DA has not run DCSync in 90 days; this DA running DCSync at 03:00 from a workstation it has not used before is anomalous&quot;&lt;/em&gt;) is the partial answer. No production product currently delivers it with low enough false-positive rates to be operationally useful for already-Tier-Zero principals.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cross-forest replication abuse is under-instrumented.&lt;/strong&gt; The interaction of &lt;code&gt;DS-Replication-Get-Changes-In-Filtered-Set&lt;/code&gt; with the SPN-suffix routing matrix in multi-forest environments is poorly covered in public detection guidance. Large enterprises with M&amp;amp;A history hold dozens of trusts; the cross-trust edges are the least-audited surface in their identity architecture. BloodHound&apos;s SharpHound collector can enumerate cross-trust data, and v6.0&apos;s wildcard-principal fix improves the picture, but no fully automated detection pattern exists [@bloodhound-v6-0-release-notes].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Delegation-drift residual long tail.&lt;/strong&gt; Even with the MDI Accounts security posture assessment perfectly tuned, the long tail of forgotten ACE delegations across a twenty-five-year-old forest with mergers, acquisitions, decommissioned products, and migrations remains the canonical entry point. Christopher Keim frames it unambiguously [@keim-dcsync-rights]:&lt;/p&gt;

&quot;The defaults aren&apos;t the problem. The problem is delegation drift, backup agents, identity sync products, and application service accounts accumulate these rights over time, often with no documentation and no review.&quot; -- Christopher Keim, *&quot;DCSync Attack: Finding and Fixing Replication Rights in Active Directory&quot;* (2025) [@keim-dcsync-rights]
&lt;p&gt;The posture-layer detection is necessary but not sufficient; the human-process loop -- documented ownership, periodic review, removal of unjustified ACEs -- is what closes the residual. Most enterprise SOCs are not staffed to run this loop at the cadence the residual requires.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Open problem&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;th&gt;Current best partial result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;DCShadow gap window&lt;/td&gt;
&lt;td&gt;Persistence write commits before SOC sees the alert&lt;/td&gt;
&lt;td&gt;Configure MDI to surface External ID 2028 (rogue promotion) with automated investigation and response to block RPC traffic from the suspected source [@mdi-credential-access-alerts]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encrypted-channel DCSync&lt;/td&gt;
&lt;td&gt;Passive NDR loses per-principal granularity&lt;/td&gt;
&lt;td&gt;Hybrid deployment: NDR for cross-DC visibility, MDI on-DC sensor for per-principal granularity [@trellix-silent-domain-hijack]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legitimate-principal compromise non-detection&lt;/td&gt;
&lt;td&gt;The Tier Zero principal who already has DCSync rights triggers nothing&lt;/td&gt;
&lt;td&gt;Reduce the count of DCSync-capable principals to a number a human can monitor; surface their DCSync activity to a high-severity review queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-forest replication abuse&lt;/td&gt;
&lt;td&gt;Cross-trust DCSync paths are not enumerated by default&lt;/td&gt;
&lt;td&gt;SharpHound trust-collection methods; manual BloodHound inspection of foreign-domain principals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delegation-drift residual long tail&lt;/td&gt;
&lt;td&gt;Posture surfaces the principals; humans still have to decide which are legitimate&lt;/td&gt;
&lt;td&gt;Quarterly posture review with documented justification per non-default principal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;What can a defender actually do on Monday morning, given all of the above?&lt;/p&gt;
&lt;h2&gt;10. What a Defender Does on Monday Morning&lt;/h2&gt;
&lt;p&gt;Three action lanes, in priority order.&lt;/p&gt;
&lt;h3&gt;Lane 1: inventory the rights triad&lt;/h3&gt;
&lt;p&gt;Read the Domain NC root&apos;s ACL. Filter on the three rights GUIDs. Subtract the four default principal sets plus any legitimately delegated identity-sync product (Entra ID Connect&apos;s MSOL_ account is the canonical exclusion). Every entry that remains gets a documented owner and a documented justification, or the ACE gets removed.&lt;/p&gt;
&lt;p&gt;{`&lt;/p&gt;
Operator-facing inventory script. The browser-runnable demo uses a
hardcoded SAMPLE_ACL; in production, replace the SAMPLE_ACL with output
from one of:
PowerShell:  Get-Acl &quot;AD:$( (Get-ADDomain).DistinguishedName )&quot;
python-ldap: ldap_search(ncroot_dn, attr=&apos;nTSecurityDescriptor&apos;)
&lt;p&gt;GET_CHANGES          = &apos;1131f6aa-9c07-11d1-f79f-00c04fc2dcd2&apos;
GET_CHANGES_ALL      = &apos;1131f6ad-9c07-11d1-f79f-00c04fc2dcd2&apos;
GET_CHANGES_FILTERED = &apos;89e95b76-444d-4c62-991a-0facbeda640c&apos;
TRIAD = {GET_CHANGES, GET_CHANGES_ALL, GET_CHANGES_FILTERED}&lt;/p&gt;
&lt;p&gt;DEFAULT_OK = {
    &apos;BUILTIN\\Administrators&apos;,
    &apos;CONTOSO\\Domain Controllers&apos;,
    &apos;CONTOSO\\Domain Admins&apos;,
    &apos;CONTOSO\\Enterprise Admins&apos;,
    &apos;NT AUTHORITY\\ENTERPRISE DOMAIN CONTROLLERS&apos;,
}&lt;/p&gt;
MSOL_ accounts: legitimate Entra ID Connect sync principals.
Exclude by prefix, never by exact name (the suffix is random).
&lt;p&gt;def is_known_legitimate(principal):
    return principal in DEFAULT_OK or &apos;\\MSOL_&apos; in principal&lt;/p&gt;
&lt;p&gt;SAMPLE_ACL = [
    {&apos;principal&apos;: &apos;CONTOSO\\Domain Admins&apos;,     &apos;right&apos;: GET_CHANGES_ALL},
    {&apos;principal&apos;: &apos;CONTOSO\\MSOL_a1b2c3d4&apos;,     &apos;right&apos;: GET_CHANGES_ALL},
    {&apos;principal&apos;: &apos;CONTOSO\\backup_svc_2017&apos;,   &apos;right&apos;: GET_CHANGES_ALL},
    {&apos;principal&apos;: &apos;CONTOSO\\hr_idm_connector&apos;,  &apos;right&apos;: GET_CHANGES},
    {&apos;principal&apos;: &apos;CONTOSO\\fileserver_old$&apos;,   &apos;right&apos;: GET_CHANGES_ALL},
]&lt;/p&gt;
&lt;p&gt;findings = []
for ace in SAMPLE_ACL:
    if ace[&apos;right&apos;] not in TRIAD:
        continue
    if is_known_legitimate(ace[&apos;principal&apos;]):
        continue
    findings.append(ace[&apos;principal&apos;])&lt;/p&gt;
&lt;p&gt;print(&quot;Principals to investigate:&quot;)
for p in sorted(set(findings)):
    print(f&quot;  - {p}  -&amp;gt;  document owner or remove ACE&quot;)
`}&lt;/p&gt;
&lt;p&gt;Anything in the &lt;em&gt;Principals to investigate&lt;/em&gt; output is either a legitimately delegated service (document the owner and add to your exclusions; treat as Tier Zero) or a forgotten ACE from a project nobody remembers (remove it). Christopher Keim&apos;s framing is the operationally useful one: every common culprit is a backup tool, an identity-governance tool, or a service account from a long-dead migration [@keim-dcsync-rights].&lt;/p&gt;

The Microsoft Entra ID Connect synchronization service account, created on-premises during Entra Connect installation with an `MSOL_` prefix and a random hex suffix, legitimately holds the rights triad on the Domain NC root. It must -- it has to replicate password hashes to the cloud directory so that Entra ID can validate cloud logons against on-premises credentials. Removing its ACE breaks Entra ID password hash sync within a single replication interval, and your help desk will know about it.&lt;p&gt;The right answer is not to remove the ACE. It is to treat the MSOL_ account as Tier Zero. Dedicated host (the Entra Connect server itself, hardened as a DC-tier asset). No interactive logon. Multi-factor authentication on any privileged use. Conditional access policies that block sign-in from anything other than the Entra Connect service identity. The MDI Hybrid security posture-assessment family documents the surrounding controls [@mdi-security-posture-hybrid].
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;Lane 2: enable the canonical alerts and audit&lt;/h3&gt;
&lt;p&gt;Three configuration items.&lt;/p&gt;
&lt;p&gt;First, ensure Microsoft Defender for Identity (or an equivalent identity-threat-detection product) is deployed with a sensor on every DC. The un-sensored-DC gap that the MDI alert documentation explicitly warns about creates a structural blind spot that an attacker will preferentially target [@mdi-alerts-classic].&lt;/p&gt;
&lt;p&gt;Second, enable Advanced Security Audit policy &lt;code&gt;Audit Directory Services Access&lt;/code&gt; under &lt;code&gt;DS Access&lt;/code&gt; and apply SACLs on the Domain NC root that audit the three replication rights against &lt;code&gt;Everyone&lt;/code&gt;, &lt;code&gt;Domain Computers&lt;/code&gt;, and &lt;code&gt;Domain Controllers&lt;/code&gt;. This is what makes Event ID 4662 fire on the request, which is what the Sigma 611eab06 and Splunk 51307514 rules consume [@sigma-rule-dcsync][@splunk-research-dcsync]. A fresh-install AD does not have these SACLs by default; the most common reason a SIEM dashboard for DCSync is silent is that the SACL never got applied.&lt;/p&gt;
&lt;p&gt;Third, deploy or configure NDR coverage on the inter-DC subnet, with a rule that fires on DRSUAPI bind requests originating from source IPs outside the legitimate-DC baseline. Trellix NDR, Microsoft&apos;s MDI sensor, CrowdStrike Falcon, and community Zeek/Suricata rulesets all implement this [@trellix-silent-domain-hijack]. Where commercial NDR is out of budget, Sysmon with the SwiftOnSecurity or Olaf Hartong modular configuration surfaces Event ID 3 (NetworkConnect) and Event ID 22 (DnsQuery) outbound from non-DC hosts to DC RPC endpoints; a SIEM correlation rule can combine this endpoint-side signal with Event ID 4662 on the DC to approximate the network-plus-host signature without an appliance budget.&lt;/p&gt;
&lt;h3&gt;Lane 3: run BloodHound on the domain quarterly&lt;/h3&gt;
&lt;p&gt;Collect with SharpHound at minimum quarterly. Continuous collection if BloodHound Enterprise is available. Run the canonical query for the &lt;code&gt;DCSync&lt;/code&gt; edge into the domain node. Trace inbound paths. Close the longest path first -- the longest paths are the ones a human operator is least likely to have noticed and most likely to have been delegated decades ago for a reason nobody remembers.&lt;/p&gt;
&lt;p&gt;The v6.0 wildcard-principal fix is particularly worth a re-run on any forest that has been operated since before 2003: legacy &lt;code&gt;Authenticated Users&lt;/code&gt; or &lt;code&gt;Everyone&lt;/code&gt; ACEs on the domain root are exactly the kind of thing that survived a Server 2003 upgrade silently and never showed up in any subsequent audit [@bloodhound-v6-0-release-notes]. The v6.3 Butterfly algorithm lets you query the inverse view -- &lt;em&gt;which targets fall if this principal is compromised?&lt;/em&gt; -- which is the right question to ask about any newly-discovered non-default DCSync holder [@specterops-butterfly-blog][@bloodhound-v6-3-release-notes].&lt;/p&gt;
&lt;h3&gt;What does not work&lt;/h3&gt;
&lt;p&gt;Four common misbeliefs are worth naming.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; See §1 -- Credential Guard does not stop DCSync. The secret transits remote-to-remote (a DC&apos;s NTDS.dit to the attacker&apos;s process), never local-to-LSASS, so the trustlet&apos;s isolation boundary has no jurisdiction over the call [@credential-guard-considerations]. Credential Guard is the right control for the local-memory attack surface and the wrong control for the network-protocol attack surface.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; After a confirmed DCSync of &lt;code&gt;krbtgt&lt;/code&gt;, rotate the &lt;code&gt;krbtgt&lt;/code&gt; account password twice, with at least ten hours between rotations. The first rotation invalidates the old key after the current Kerberos ticket lifetime expires. The second rotation invalidates the &lt;em&gt;previous&lt;/em&gt; old key, which the directory stores alongside the current key for compatibility during replication convergence. Rotating only once leaves Golden Tickets forged from the dumped key valid for the duration of the second key. Rotating twice ten hours apart is what closes the window [@microsoft-new-krbtgtkeys]. (And neither rotation removes the ACE that allowed the dump in the first place: come back to Lane 1.)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Renaming &lt;code&gt;krbtgt&lt;/code&gt; does nothing. The account&apos;s Relative Identifier (RID 502) is fixed by AD&apos;s design and is what the TGT signing key derives against, not the &lt;code&gt;sAMAccountName&lt;/code&gt; [@microsoft-well-known-sids]. Renaming it to &lt;code&gt;krbtgt-old-do-not-use&lt;/code&gt; confuses operators, not attackers.&lt;/p&gt;
&lt;p&gt;Disabling MS-DRSR is not an option. The protocol is what makes AD replication work. Blocking opnum 3 at the RPC layer or refusing the DRSUAPI bind stops DCSync and stops every DC in the forest from talking to every other DC. Replication grinds. Password changes do not propagate. Domain joins fail. Within hours, the directory is split-brain across DCs, and within days, it is unrecoverable without DR-grade restore from backup: Microsoft&apos;s own AD-replication troubleshooting documentation walks the lingering-object pathology that produces exactly this split-brain when DCs stop replicating for longer than the tombstone lifetime [@microsoft-ad-lingering-objects].&lt;/p&gt;

On any DC, run `auditpol /get /subcategory:&quot;Directory Service Access&quot;` from an elevated prompt. If the output reads `No Auditing`, your Sigma / Splunk SACL-event detection will not fire because Event ID 4662 is not being generated. Enable with `auditpol /set /subcategory:&quot;Directory Service Access&quot; /success:enable /failure:enable`, then apply the SACL on the domain root as described in the Splunk rule&apos;s implementation notes [@splunk-research-dcsync].
&lt;p&gt;The six FAQ items in the next section cover the misconceptions that did not fit into any single lane.&lt;/p&gt;
&lt;h2&gt;11. Frequently Asked Questions&lt;/h2&gt;

No. The access check on `IDL_DRSGetNCChanges` requires both the baseline `DS-Replication-Get-Changes` right *and* `DS-Replication-Get-Changes-All` to read secret attributes [@ad-schema-get-changes][@ad-schema-get-changes-all]. The *All* suffix unlocks the secret attribute set given the baseline right; it is not a self-sufficient gate. Christopher Keim&apos;s PowerShell pattern filters on both GUIDs together [@keim-dcsync-rights]. Confidential-flag attributes additionally require `DS-Replication-Get-Changes-In-Filtered-Set`. Some detection rules (Sigma 611eab06 included) accept either GUID in the SACL event because the operational cost of a false positive on the broader filter is lower than the risk of missing one of the two required ACEs being present without the other [@sigma-rule-dcsync].

No. Credential Guard isolates LSASS-resident secrets in a separate virtual trust level so that local-memory attacks (Mimikatz `sekurlsa::logonpasswords`, comsvcs.dll mini-dumps of `lsass.exe`, and similar) cannot read cached credentials. DCSync does not touch the attacker&apos;s LSASS at all. The secret transits from a remote DC&apos;s NTDS.dit, over an encrypted MS-DRSR session, into the attacker&apos;s process memory. Microsoft&apos;s Credential Guard documentation lists the scenarios Credential Guard does and does not cover; MS-DRSR-based network credential extraction is not in scope [@credential-guard-considerations]. This is one of the clearest examples in the Windows security model of a control that is right for one attack surface and orthogonal to another.

Not anymore. The BlueHat IL 2018 &quot;your million-dollar SIEM goes blind&quot; framing was correct in 2018 against SIEMs that monitored only object-modification events: the replication writes are SACL-silent. By July 24, 2018, Tali Ash announced Azure ATP&apos;s two new preview detections, which fired on the rogue-DC promotion fingerprint (creating an `nTDSDSA` object in the Configuration NC) and the replication request from the rogue, respectively [@tali-ash-azure-atp-dcshadow]. Those detections carried forward as Microsoft Defender for Identity External IDs 2028 and 2029 [@mdi-alerts-classic]. The writes themselves remain SACL-silent; the *registration* fingerprint that has to precede them is not. The honest contemporary statement is &quot;DCShadow&apos;s writes are silent, but the rogue-DC scaffolding is not, and a sensored DC catches the scaffolding.&quot; A skilled attacker who completes the register-replicate-deregister cycle inside the alert batch interval may still commit the persistence write before SOC response.

No. The protocol&apos;s design is the issue. Microsoft has not announced any MS-DRSR amendment that would change the `IDL_DRSGetNCChanges` access check, because amending the access check breaks legitimate non-DC consumers (the Entra ID Connect MSOL_ account is the most prominent) and shifts the attack class to compromised DC machine accounts (Zerologon is the worked example [@secura-zerologon-whitepaper]). The &quot;fixes&quot; that have shipped since 2015 are all detection: ATA 1.7 (April 2017) [@ata-v17-release-notes], Azure ATP (2018) [@tali-ash-azure-atp-dcshadow], Microsoft Defender for Identity (post-Ignite 2020 rebrand) [@rcpmag-defender-rebrand][@mdi-whats-new], and the posture-assessment families that surface non-default rights triad holders [@mdi-security-posture-accounts]. The protocol itself is the same protocol it was in Windows 2000 Server.

The MSOL_ sync account legitimately holds the rights triad on the Domain NC root because it must replicate password hashes to Entra ID. Treat it as Tier Zero with the hardening profile listed in §10&apos;s MSOL_ Aside (dedicated hardened host, no interactive logon, MFA on privileged use, conditional access restricted to the Entra Connect service identity) [@mdi-security-posture-hybrid]. Critically, do *not* remove the ACE from the Domain NC root: doing so breaks Entra ID password hash sync within one replication interval, and your help desk will know about it within hours.

No. DCSync is over DRSUAPI/MS-DRSR, not over LDAP. The directory&apos;s LDAP service refuses to return `unicodePwd` and related secret-attribute values regardless of caller privilege, because the attribute is marked confidential and the LDAP read path does not honor the replication extended rights. There is no &quot;DCSync over LDAP&quot; technique because LDAP simply does not return the data; MITRE T1003.006 names DRSUAPI explicitly as the protocol vector [@mitre-t1003-006]. Operators occasionally confuse this with LDAPS (LDAP over TLS) or with the November 2023 LDAP signing and channel-binding rollout, both of which are channel-protection concerns rather than credential-read concerns.
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;dcsync-dcshadow-and-the-domain-replication-attack-class&quot; keyTerms={[
  { term: &quot;MS-DRSR&quot;, definition: &quot;Directory Replication Service Remote Protocol; the RPC interface by which any AD domain controller can replicate any object including secret attributes from any other DC.&quot; },
  { term: &quot;IDL_DRSGetNCChanges&quot;, definition: &quot;MS-DRSR&apos;s opnum-3 method that returns changed objects within a naming context; the protocol method DCSync invokes.&quot; },
  { term: &quot;Extended Right&quot;, definition: &quot;A schema-defined access-control right keyed by GUID rather than by standard ACL bit. Granted via ACE; checked at runtime by the operation that requires it.&quot; },
  { term: &quot;Naming Context&quot;, definition: &quot;A top-level replication partition of the Active Directory database. DCSync operates against the Domain NC root.&quot; },
  { term: &quot;Rights Triad&quot;, definition: &quot;DS-Replication-Get-Changes, DS-Replication-Get-Changes-All, and DS-Replication-Get-Changes-In-Filtered-Set extended rights on a naming-context root.&quot; },
  { term: &quot;NTDS.dit&quot;, definition: &quot;The on-disk Extensible Storage Engine database holding every AD object including secret attributes.&quot; },
  { term: &quot;SACL-silent&quot;, definition: &quot;A directory operation that does not generate the Event ID 4662/4738/5136 events normally emitted by Domain Services Auditing. Legitimate DC-to-DC replication is SACL-silent by design.&quot; },
  { term: &quot;Tier Zero&quot;, definition: &quot;Principals and assets whose compromise yields domain-wide control. KRBTGT, Domain Admins, the MSOL_ account, and any principal holding the rights triad are all Tier Zero.&quot; },
  { term: &quot;MSOL_ account&quot;, definition: &quot;The Entra ID Connect synchronization service account; legitimately holds the rights triad to replicate password hashes to the cloud directory.&quot; },
  { term: &quot;Replication Allow List&quot;, definition: &quot;MDI&apos;s internal baseline of which computers in the domain legitimately speak DRSUAPI to which DCs.&quot; }
]} flashcards={[
  { front: &quot;What does MS-DRSR §4.1.10 check on IDL_DRSGetNCChanges?&quot;, back: &quot;Only that the calling principal holds the rights triad on the naming-context root. It does not check whether the caller is a domain controller.&quot; },
  { front: &quot;What is the Mimikatz commit hash and date for DCSync&apos;s introduction?&quot;, back: &quot;Commit 7717b7a7173fa6a6b6566bbbc3e7372b464d988f, authored by Benjamin DELPY on 2015-08-11 01:27:13 +0200, subject &apos;DCSync in mimikatz &amp;amp; for XP/2003&apos;.&quot; },
  { front: &quot;What are MDI&apos;s three DCSync/DCShadow alert IDs?&quot;, back: &quot;External ID 2006 (DCSync), 2028 (DCShadow promotion), 2029 (DCShadow replication request).&quot; },
  { front: &quot;Why can&apos;t Microsoft patch DCSync?&quot;, back: &quot;Two structural ceilings: stopping the protocol from returning secrets breaks AD replication; adding a machine-identity check shifts the attack class to compromised DC machine accounts (Zerologon).&quot; },
  { front: &quot;What is the BloodHound v6.3 &apos;Butterfly&apos; algorithm?&quot;, back: &quot;Bi-directional impact analysis: in addition to &apos;which principals can reach this target?&apos;, also computes &apos;which targets fall if this principal is compromised?&apos;.&quot; }
]} questions={[
  { q: &quot;Why does adding a caller-machine-identity check to MS-DRSR not close the attack class?&quot;, a: &quot;Because compromising a DC&apos;s machine account (CVE-2020-1472 Zerologon being the canonical worked example) satisfies the new check while still enabling the original attack.&quot; },
  { q: &quot;Why is Credential Guard the wrong control for DCSync?&quot;, a: &quot;Credential Guard isolates LSASS-resident secrets on the local machine. DCSync reads secrets from a remote DC&apos;s NTDS.dit over MS-DRSR; the secret never transits the attacker&apos;s LSASS.&quot; },
  { q: &quot;Why must the krbtgt password be rotated twice after a confirmed DCSync?&quot;, a: &quot;Each AD account stores both the current and previous password. Rotating once invalidates only the older of the two keys; the most recently dumped key remains valid. Rotating a second time, after the first replication interval has converged, invalidates the dumped key.&quot; },
  { q: &quot;What does each of the four defense layers miss?&quot;, a: &quot;Posture misses transitive paths. Behavioral misses pre-attack staging and compromised-DC speakers. Network misses encrypted RPC payloads. Graph misses net-new ACEs created after the last collection.&quot; },
  { q: &quot;Why is the DCShadow gap window structural?&quot;, a: &quot;MDI External ID 2029 fires on the rogue&apos;s replication request after registration. An attacker who completes register-replicate-deregister inside the alert batch interval commits the persistence write before SOC response.&quot; }
]} /&amp;gt;&lt;/p&gt;
&lt;p&gt;A final observation, since the closing should add something new. The protocol that this article calls structurally unfixable is not unusual. Most Microsoft security primitives that survive long enough enter the same regime -- the LSASS surface, the Kerberos delegation surface, the SMB authentication surface -- where the only honest answer is detection in depth because the protocol&apos;s job description and its abuse surface are the same surface viewed from different chairs.&lt;/p&gt;
&lt;p&gt;The thing that makes MS-DRSR notable is the &lt;em&gt;clarity&lt;/em&gt; with which the structural error is visible. Read §4.1.10 once and you are done. Everything from §6 onward is the industry&apos;s slow accumulation of detection layers around a gate that cannot be moved. Twenty-five years in, the gate is still where it was on February 17, 2000, and the four layers around it are still under active engineering.&lt;/p&gt;
</content:encoded><category>active-directory</category><category>dcsync</category><category>dcshadow</category><category>ms-drsr</category><category>credential-theft</category><category>defender-for-identity</category><category>bloodhound</category><category>kerberos</category><author>noreply@paragmali.com (Parag Mali)</author></item></channel></rss>