Source: dirkjanm/BloodHound.py @ 6fa5ba5 (bloodhound-ce branch)

Overview

BloodHound.py CE is a Python-based Active Directory data collector maintained by dirkjanm (Fox-IT / Outflank). The bloodhound-ce branch adapts the original BloodHound.py collector to output data in the format required by SpecterOps’ BloodHound CE backend — replacing the legacy SharpHound/BloodHound 4.x JSON schema. It provides an alternative to the C# SharpHound collector and the Rust-based RustHound, offering advantages in cross-platform operation and easier integration into Python-based offensive pipelines.

The tool queries Active Directory via LDAP for object enumeration and ACLs, then optionally connects to individual computers via SMB/RPC (Impacket) to collect session data, local admin memberships, RDP/DCOM/PSRemote access, and scheduled tasks. All output is written as JSON files with version: 6 metadata, directly ingestable by BloodHound CE.

How It Works

The collection pipeline has three stages:

  1. LDAP enumeration — Connects to a Domain Controller via ldap3, queries for users, groups, computers, GPOs, OUs, containers, and domain trusts. Requested nTSecurityDescriptor attributes are retrieved for ACL collection.

  2. RPC session collection (optional) — For each computer, opens SMB/RPC connections via Impacket’s smbconnection and drsuapi/samr/scmr/atsvc interfaces. Enumerates logged-on sessions (via I_rpc_get_loggedon), net sessions, local admin/RDP/DCOM/PSRemote group members, registry sessions, services, and scheduled tasks.

  3. Output assembly — Results are structured into BloodHound CE’s JSON format and written to files (users.json, groups.json, computers.json, domains.json, gpos.json, ous.json, containers.json). Files auto-rotate at 40,000 entries. ACLs are parsed in parallel using multiprocessing.Pool.

Architecture

bloodhound.py (CLI entry)
    │
    ├── bloodhound/__init__.py (BloodHound — orchestrator)
    │       │
    │       ├── bloodhound/ad/authentication.py
    │       │       (LDAP bind: Simple, NTLM, Kerberos, SSPI)
    │       │
    │       ├── bloodhound/ad/__init__.py (AD — LDAP queries)
    │       │       ├── get_users / get_groups / get_computers
    │       │       ├── get_gpos / get_ous / get_containers
    │       │       ├── get_trusts / get_childobjects
    │       │       └── GC connection + paged search
    │       │
    │       ├── bloodhound/ad/computer.py (ADComputer)
    │       │       └── RPC calls: sessions, group members,
    │       │           loggedon, services, schtasks
    │       │
    │       ├── bloodhound/ad/domain.py (ADDomain)
    │       │       └── DN cache, SID/SAM resolution, trusts
    │       │
    │       └── bloodhound/ad/utils.py
    │               ADUtils, DNSCache, SidCache, SamCache,
    │               AceResolver, ACL parsing helpers
    │
    ├── bloodhound/enumeration/
    │   ├── memberships.py   (MembershipEnumerator)
    │   │       enumerate_users, enumerate_groups,
    │   │       enumerate_gpos, enumerate_ous,
    │   │       enumerate_containers, enumerate_computers_dconly
    │   ├── computers.py     (ComputerEnumerator)
    │   │       Threading pool for RPC collection per host
    │   ├── domains.py       (DomainEnumerator)
    │   │       Domain trust + ACL + GPO link output
    │   ├── acls.py          (parse_binary_acl)
    │   │       Binary SD parser via cstruct
    │   ├── objectresolver.py (ObjectResolver)
    │   │       GC SAM/SID/UPN/DN resolution (thread-safe)
    │   └── outputworker.py  (OutputWorker)
    │           Streaming JSON writers with file rotation
    │
    └── bloodhound/ad/
        ├── trusts.py       (ADDomainTrust — trust type mapping)
        ├── structures.py   (LDAP_SID parser)
        └── dumpntlm.py     (Credentialless NTLM info extraction)

The Spine

The core collection loop runs in bloodhound/__init__.py:

  1. Parse arguments and create an AD connection (LDAP bind with chosen auth method).
  2. Query defaultNamingContext to determine the domain, then discover forest trusts.
  3. For each domain in the forest: instantiate MembershipEnumerator and ComputerEnumerator, then run collection methods (enumerate_users, enumerate_groups, enumerate_computers, dump_domain, etc.).
  4. Each enumerator reads LDAP paged results, resolves memberships via DN cache, optionally parses ACLs in subprocesses, and streams output through OutputWorker.

Authentication is handled by bloodhound/ad/authentication.py, which supports four methods:

  • Simple bind (plaintext password over LDAPS)
  • NTLM password/hash (via impacket.ntlm)
  • Kerberos (requires python3-krb5; uses ccache or password)
  • SSPI (Windows-only, delegates to SspiAuthenticator from impacket.dcerpc.v5.rpcrt)

The dumpntlm.py module is a standalone credentialless recon tool. It connects to SMB1/SMB2/RPC endpoints on port 445/139/135 and extracts NTLM challenge information (native OS, dialect, signing requirements, DNS names) without any authentication. This is based on a modified Impacket PR (#1404 by NtAlexio2) that was not upstreamed.

Key Patterns

Paged LDAP queries. All enumeration uses ldap3’s SUBTREE scope with paged results. The AD.search() method wraps connection.extend.standard.paged_search() with configurable page size (default 1000). This avoids memory issues in large environments.

Parallel ACL parsing. Security descriptors are binary blobs fetched from nTSecurityDescriptor. Parsing is offloaded to multiprocessing.Pool.apply_async() with a callback that resolves ACE SIDs and writes results. This is necessary because cstruct-based binary SD parsing is CPU-bound and doesn’t benefit from threading (GIL).

Caching layers. Four caches avoid redundant lookups:

  • DNSCache — bidirectional hostname ↔ IP resolution
  • SidCache — SID → entry mapping
  • SamCache — SAM account name → SID list (handles ambiguous names across domains)
  • dncache — distinguished name → ObjectIdentifier mapping (used for membership resolution)

Threaded computer enumeration. RPC-heavy collection (sessions, local admins, etc.) spawns N worker threads (default 10) that pull from a queue. Results are pushed to a separate writer thread via queue.Queue.

Binary ACL parsing with cstruct. The acls.py module implements a complete Security Descriptor parser using bloodhound.lib.cstruct. It handles ACCESS_ALLOWED_ACE (type 0x00) and ACCESS_ALLOWED_OBJECT_ACE (type 0x05), extracting BloodHound-relevant rights: GenericAll, GenericWrite, WriteDacl, WriteOwner, AllExtendedRights, GetChanges/GetChangesAll/GetChangesInFilteredSet, ForceChangePassword, AddMember, AddAllowedToAct, WriteAccountRestrictions, WriteGPLink, AddKeyCredentialLink, WriteSPN, ReadLAPSPassword, AddSelf, and Owns.

CE output format. All output files use version: 6 metadata and wrap data in {"data": [...], "meta": {"type": "...", "count": N, "version": 6}}. The build_relation() function in acls.py explicitly rejects the old BloodHound 4.0 acetype field, raising ValueError if called. ACL relations now use rightname + sid + inherited fields.

Non-Obvious Details

LAPS dual-property support. The ACL parser checks three different LAPS password attributes: ms-mcs-admpwd (legacy LAPS), ms-laps-password (Windows LAPS), and ms-laps-encryptedpassword (encrypted Windows LAPS). This covers both the legacy Microsoft LAPS solution and the newer Windows LAPS.

GMSA password ACL extraction. Group Managed Service Accounts store their password readers in a separate msDS-GroupMSAMembership attribute (a DACL). BloodHound.py parses this independently of the main ACL and maps the resulting ACEs to ReadGMSAPassword rights.

SID history resolution. When objectprops collection is enabled, each user’s sIDHistory attribute is parsed into canonical SIDs and resolved via the AceResolver (which queries the Global Catalog for cross-forest SIDs). These become HasSIDHistory entries in the output.

Domain trust classification. The ADDomainTrust class in trusts.py maps raw trust attributes to BloodHound’s trust types:

  • WITHIN_FOREST → ParentChild
  • FOREST_TRANSITIVE → Forest
  • TREAT_AS_EXTERNAL / CROSS_ORGANIZATION → External
  • SID filtering is enabled by default except for Forest trusts with TREAT_AS_EXTERNAL

Container filtering. The is_filtered_container() and is_filtered_container_child() methods in ADUtils skip built-in containers like LostAndFound, Infrastructure, Program Data, and NTDS Quotas — these clutter the graph without adding attack path value.

Forest cache mechanism. createforestcache.py runs collection against all domains in a forest, producing a complete set of JSON files prefixed with the domain DNS name. This enables BloodHound CE to ingest multi-domain environments. It auto-discovers domains via the trusts collection method.

The build_relation() function raises ValueError("BH 4.0 incompatible output called") if an acetype argument is passed. This is a guard rail — the CE branch has fully migrated away from the legacy format. Any code path that tries to emit the old format will crash rather than silently producing incompatible output.

BloodHound.py requires no code execution on targets — all remote interaction is via standard LDAP/SMB/RPC protocols. This makes it suitable for environments where PowerShell execution or .NET assembly loading is blocked or monitored.

Assessment

Strengths:

  • Cross-platform (Linux/macOS/Windows) — unlike SharpHound which requires .NET
  • Flexible authentication: password, NTLM hash, Kerberos ccache, SSPI
  • Thorough ACL parsing covering all BloodHound 4.x+ right types including LAPS, GMSA, KeyCredentialLink
  • Credentialless recon via dumpntlm.py for initial enumeration without valid credentials
  • Well-structured caching avoids redundant LDAP queries and DNS lookups
  • Multiprocess ACL parsing prevents the GIL from bottlenecking on large domains

Weaknesses:

  • No API-based ingestion to BloodHound CE — only file-based output (SharpHound CE supports direct API upload)
  • RPC session collection can be slow in large environments (default 10 workers; scaling requires manual tuning)
  • No certificate-based authentication (LDAPS only, no TLS client certs)
  • The dumpntlm.py module depends on unmerged Impacket changes, requiring a patched Impacket install
  • Error handling in computer enumeration catches broad Exception types, which can mask underlying issues
  • The outputworker.py uses bare except: clauses in file open operations

Compared to SharpHound: BloodHound.py trades SharpHound’s .NET native performance and API integration for cross-platform portability and Python ecosystem compatibility. SharpHound supports direct BloodHound CE API uploads; BloodHound.py outputs files that must be manually uploaded. SharpHound’s C# LDAP and RPC implementations may be faster for very large environments, but BloodHound.py’s multiprocessing ACL parsing partially compensates.

Compared to RustHound CE: Both target the CE ingestion format. RustHound is a newer Rust implementation with potentially better performance for pure LDAP collection, but lacks the RPC-based session/local admin enumeration that BloodHound.py provides via Impacket.