BloodHound Deep Dive Part 1: History, Community Edition, and Where It Sits in an Engagement

Every series on this site about attacking Active Directory so far has skipped a step. Impacket Deep Dive went from a phished foothold straight to samrdump.py and GetADUsers.py, enumerating CONTOSO.LOCAL by hand. Password Cracking and Wordlist Engineering picked up the hashes those tools produced and cracked them. Neither post asked the question a real engagement asks first: out of every user, group, and computer in the domain, which ones actually matter? BloodHound is the tool that answers that question, and this series covers it the same way the site has covered Impacket and Sn1per: real source, real syntax, and a real graph database seeded with this site’s own lab and queried for real.

From a DEF CON talk to a graph database

BloodHound started as a research project by Andy Robbins, Rohan Vazarkar, and Will Schroeder, presented at DEF CON 24 in 2016. The idea was simple and, at the time, not how anyone thought about Active Directory security: model the domain as a graph. Users, computers, and groups become nodes. Group memberships, session data, and abusable permissions become edges. Once the whole thing is in a graph database, questions that are painful to answer by hand, like “what is the shortest path from any user I control to Domain Admins,” become a single query.

The original tool paired a PowerShell, and later C#, collector called SharpHound with a Neo4j graph database and an Electron desktop app for visualization. It worked, and it became close to standard equipment on red team engagements, but the architecture showed its age: a single Neo4j instance with no access control, a desktop app instead of a shared service, and no way for a team to collect once and query together.

Community Edition’s actual architecture

In August 2023, SpecterOps, the company Robbins, Vazarkar, and Schroeder went on to found, shipped BloodHound Community Edition, a full rearchitecture rather than a patch. The project’s own docker-compose.yml in the SpecterOps/BloodHound repository lays out exactly what changed:

services:
  app-db:
    image: docker.io/library/postgres:18
    environment:
      - POSTGRES_USER=${POSTGRES_USER:-bloodhound}
      - POSTGRES_DB=${POSTGRES_DB:-bloodhound}
  graph-db:
    image: docker.io/library/neo4j:4.4.42
    environment:
      - NEO4J_AUTH=${NEO4J_USER:-neo4j}/${NEO4J_SECRET:-bloodhoundcommunityedition}
  bloodhound:
    image: docker.io/specterops/bloodhound:${BLOODHOUND_TAG:-latest}
    environment:
      - bhe_disable_cypher_complexity_limit=${bhe_disable_cypher_complexity_limit:-false}
      - bhe_enable_cypher_mutations=${bhe_enable_cypher_mutations:-false}
      - bhe_graph_query_memory_limit=${bhe_graph_query_memory_limit:-2}
      - bhe_database_connection=user=${POSTGRES_USER:-bloodhound} password=${POSTGRES_PASSWORD} dbname=${POSTGRES_DB:-bloodhound} host=app-db
      - bhe_neo4j_connection=neo4j://${NEO4J_USER:-neo4j}:${NEO4J_SECRET}@graph-db:7687/

Three containers, three jobs. app-db is Postgres, holding application state: user accounts, roles, saved queries, asset group definitions, uploaded-file metadata. graph-db is Neo4j, holding the actual attack graph. bloodhound is a single Go binary that serves both the HTTP API and the React web UI, and is the only piece that talks to either database directly.

A few of the environment variables are worth noting before the rest of this series leans on them. bhe_enable_cypher_mutations defaults to false, meaning the ad-hoc query box in the UI is read-only against the graph by default: a raw Cypher CREATE or DELETE typed into the UI isn’t possible unless that flag is deliberately flipped. bhe_disable_cypher_complexity_limit also defaults to false, capping how expensive a query the UI will run. Both are safety rails that matter once BloodHound is running as a shared team service rather than a personal tool, and both point at where Part 3 of this series is headed: the ingestion pipeline and what BHCE will and won’t let a user do to the graph directly.

BloodHound Community Edition is free, self-hosted, and licensed under Apache 2.0, per the repository’s own license header. BloodHound Enterprise, SpecterOps’ commercial product, adds continuous collection and attack path management workflows aimed at defenders rather than operators, plus cloud hosting. This series covers Community Edition only.

Where BloodHound sits in an engagement

The site’s existing offensive series line up into a rough order of operations, and BloodHound has always been the missing link between two of them.

Sn1per finds hosts and services. Once there’s a foothold, like CONTOSO\j.reyes in the Impacket series, the attacker knows one account and one workstation. What they don’t know, without BloodHound, is whether j.reyes is three hops from Domain Admins or three hundred. Running Kerberoasting against every SPN-bearing account in a large domain, or trying GetUserSPNs.py and GetNPUsers.py against a list built by hand with GetADUsers.py, works fine on a three-user lab domain like CONTOSO.LOCAL. It does not scale to a real enterprise domain with thousands of accounts, most of which lead nowhere useful. BloodHound’s job is to rank the domain before any of that effort gets spent: which accounts are Kerberoastable, which are AS-REP roastable, and, more importantly, which of those actually sit on a path to something worth having.

Password Cracking and Wordlist Engineering and Pivoting and Tunneling both pick up after a credential or a foothold already exists. BloodHound is what decides which credential is worth the effort of cracking, and which pivot target is worth reaching, before either series’ tools get involved. That ordering, recon, then attack-path analysis, then targeted exploitation, then lateral movement, is what the rest of this series follows.

The lab, extended

This series reuses the exact CONTOSO.LOCAL domain from Impacket Deep Dive: DC01 as the domain controller, FS01 as a member server carrying svc-backup’s SPN, WKS01 as the workstation where j.reyes has local admin, and the four accounts j.reyes, svc-backup, svc-legacy-scan, and a.oyelaran. One addition was made specifically for this series: a security group called IT-HELPDESK, of which j.reyes is a member, was granted GenericAll over the Backup Operators group during a 2024 ticketing-tool rollout in the lab’s fictional timeline and the permission was never revoked. That single leftover ACE is what Parts 4 through 6 of this series trace from a canned query to a working attack path.

Verification framing

CONTOSO.LOCAL has never been a live Active Directory build. It’s documented and simulated, the same framing Impacket Deep Dive used from the start, because the Cowork sandbox this site is authored from has no Windows systems and no domain to collect from. SharpHound itself is a Windows-only C# binary and genuinely can’t run here.

What changes for this series is the graph layer. BloodHound’s own Community Edition stack needs Docker, and this sandbox has neither Docker nor root, so the full three-container application wasn’t stood up. But Neo4j itself is a portable JVM application, and a Neo4j 4.4.30 Community server, chosen for compatibility with the sandbox’s Java 11 runtime and close to BHCE’s own pinned neo4j:4.4.42, was downloaded, extracted, and run directly with no Docker and no root involved. It was seeded with a Cypher-native version of CONTOSO.LOCAL’s principals and the IT-HELPDESK ACE described above, and driven with real cypher-shell queries. Every query result shown later in this series, the Kerberoastable and AS-REP roastable lookups in Part 5, the GenericAll and ForceChangePassword chain in Parts 4 and 6, is a genuine result from that instance, not a transcription of documentation. Where SharpHound’s own collection behavior is described, in Part 2, it’s taken directly from SpecterOps’ published flag reference rather than guessed.

Next: Part 2, where SharpHound’s real collection methods determine exactly what ends up in that graph before a single Cypher query gets written.