Overview
Three open-source collectors produce data for SpecterOps’ BloodHound CE graph analysis platform: SharpHound (C#, SpecterOps), RustHound-CE (Rust, g0h4n), and BloodHound.py CE (Python, dirkjanm). All three query Active Directory via LDAP, parse security descriptors, and output JSON files ingestible by BloodHound CE, but they differ significantly in collection depth, platform support, and operational characteristics. This report compares them based on source code analysis of SharpHound (2.X branch, 4dca071), RustHound-CE (main branch, b6f809f), and BloodHound.py (bloodhound-ce branch, 6fa5ba5).
BloodHound.py is the oldest of the three — its ACL parsing logic served as the reference implementation that RustHound-CE's parser was derived from. The CE branch adds BloodHound CE output format support (v6 metadata) to the existing Python collector.
Key Findings
Architecture Comparison
SharpHound uses a producer-consumer pipeline built on System.Threading.Channels. CollectionTask.cs creates three channels (LDAP objects, output objects, computer status), spawns N consumer tasks (default 50 threads) that call ObjectProcessors.ProcessObject(), and coordinates a single producer writing paged LDAP results. The chain-of-responsibility pattern in SharpLinks.cs orchestrates initialization, connection testing, collection, looping, cache saving, and output finalization.
RustHound-CE uses a linear single-pass pipeline: ldap_search() streams all entries from each naming context into a storage backend (in-memory Vec or disk-backed DiskStorage), then prepare_results_from_source() iterates over all entries sequentially to classify and parse them by type. There is no concurrent processing — the async runtime (tokio) handles the LDAP I/O, but object parsing is single-threaded.
BloodHound.py CE uses a per-domain enumeration pipeline with layered caching. For each domain in the forest, it instantiates MembershipEnumerator and ComputerEnumerator classes. LDAP paged searches (ldap3 with configurable page size, default 1000) feed into resolution caches (DNS, SID, SAM, DN), then optional ACL parsing is offloaded to multiprocessing.Pool (CPU-bound cstruct binary parsing bypasses the GIL). Computer enumeration uses threaded worker pools (default 10 threads) for RPC calls, with results pushed to a writer thread via queue.Queue.
| Aspect | SharpHound | RustHound-CE | BloodHound.py CE |
|---|---|---|---|
| Concurrency model | Multi-threaded channels (configurable, default 50) | Single-threaded parse, async LDAP I/O | Multiprocess ACL parse, threaded RPC, sequential LDAP |
| Pipeline style | Producer → bounded channel → N consumers → output | LDAP stream → storage → sequential parse → JSON | LDAP → cache → parallel ACL + threaded RPC → JSON |
| Naming contexts | Default NC + Config NC, separate passes | All NCs from root DSE | Per-domain with GC cross-ref |
| Filter generation | Method-aware LDAP filter builder | Fixed (objectClass=*) | Per-method attribute selection |
| Output streaming | In-memory assembly → single write | Sequential parse → file write | OutputWorker with file rotation at 40K entries |
SharpHound's filter generation is collection-method-aware — it only requests the object classes and attributes needed for the selected methods. RustHound-CE always pulls all attributes (
["*", "nTSecurityDescriptor"]), increasing bandwidth and server load. BloodHound.py uses per-method attribute lists, similar to SharpHound's approach.
Collection Method Parity
SharpHound exposes 25+ collection methods as a bitmask enum. RustHound-CE offers All and DCOnly. BloodHound.py CE supports individual method flags (--collect, --excludedcs) with defaults covering all LDAP methods plus optional computer enumeration.
| Method | SharpHound | RustHound-CE | BloodHound.py CE | Notes |
|---|---|---|---|---|
| Group (membership) | ✅ | ✅ | ✅ | All parse member attribute |
| ACL/ACE parsing | ✅ | ✅ | ✅ | All parse nTSecurityDescriptor |
| ObjectProps | ✅ | ✅ (subset) | ✅ | BH.py includes sIDHistory resolution |
| Container | ✅ | ✅ | ✅ | GPOLinks, containment hierarchy |
| Trusts | ✅ | ✅ | ✅ | BH.py classifies trust types (ParentChild/Forest/External) |
| Session (NetSessionEnum) | ✅ SMB | ❌ | ✅ SMB | Both SharpHound and BH.py via RPC |
| LoggedOn (privileged) | ✅ SMB | ❌ | ✅ SMB | Requires remote registry/SAM access |
| Registry sessions | ✅ SMB | ❌ | ✅ SMB | Remote registry reads |
| LocalAdmin / RDP / DCOM / PSRemote | ✅ SMB | ❌ | ✅ SMB | Remote RPC/SMB enumeration |
| LocalGroup | ✅ SMB | ❌ | ✅ SMB | SAMR protocol calls |
| GPOLocalGroup | ✅ LDAP | ✅ | ✅ | GPO filtered group membership |
| SPNTargets | ✅ LDAP | ❌ | ✅ | Resolves SPN hostnames to SIDs |
| UserRights | ✅ SMB | ❌ | ❌ | LSA policy queries |
| CertServices | ✅ LDAP+SMB | ✅ LDAP only | ✅ LDAP only | No CA registry collection |
| CARegistry | ✅ SMB | ❌ | ❌ | Remote registry on CA servers |
| DCRegistry | ✅ SMB | ❌ | ❌ | Remote registry on DCs |
| WebClientService | ✅ SMB | ❌ | ❌ | Web Client service status |
| LdapServices | ✅ SMB | ❌ | ❌ | LDAP signing/channel binding checks |
| SmbInfo | ✅ SMB | ❌ | ❌ | SMB signing enforcement checks |
| NTLMRegistry | ✅ SMB | ❌ | ❌ | NTLM audit settings from registry |
| Stealth mode | ✅ | ❌ | ❌ | Target inference from user homedir/scriptpath |
| Loop mode | ✅ | ❌ | ❌ | Periodic re-collection |
| Throttle / Jitter | ✅ | ❌ | ❌ | Delay between computer connections |
| Forest / RecurseDomains | ✅ | ❌ | ✅ | Cross-domain trust traversal |
| Credentialless recon | ❌ | ❌ | ✅ | dumpntlm.py — NTLM info without auth |
| Services / Scheduled tasks | ❌ | ❌ | ✅ | via SAMR/SCMR/ATSVC RPC calls |
RustHound-CE's
Allmode name is misleading. It performs LDAP-only collection — equivalent to SharpHound'sDCOnly. BloodHound.py CE fills the middle ground: it supports remote computer enumeration via Impacket's RPC stack, but cannot match SharpHound's full set of registry-based collectors.
ACL/ACE Parsing
All three tools parse raw nTSecurityDescriptor byte arrays into BloodHound-compatible ACE relations, but their implementations differ significantly.
SharpHound delegates ACL processing to ACLProcessor (from SharpHoundCommonLib), which uses .NET’s System.DirectoryServices.Protocols built-in SD parsing. It tracks AdminSDHolder implicit ACL hashes per-domain and detects AdminSDHolder protection status — a BloodHound 5.0 feature. It computes IsACLProtected, InheritanceHashes, owner-rights ACE detection, and GMSA reader ACEs.
RustHound-CE implements a hand-rolled binary parser in enums/secdesc.rs using nom7 for the security descriptor structure, and enums/acl.rs for the ACE-to-BloodHound-rights mapping (~2700 lines). The logic closely follows BloodHound.py’s original implementation, handling Access Allowed (0x00) and Access Allowed Object (0x05) ACE types with mask flag checks.
BloodHound.py CE implements a binary SD parser in enumeration/acls.py using its own bloodhound.lib.cstruct library. It handles the same two ACE types (0x00 and 0x05) and maps them to BloodHound right names. ACL parsing runs in multiprocessing.Pool to bypass the GIL — a practical choice since cstruct binary parsing is CPU-bound.
Key differences in ACL handling:
| Feature | SharpHound | RustHound-CE | BloodHound.py CE |
|---|---|---|---|
| SD parsing | .NET built-in | Hand-rolled nom parser | Hand-rolled cstruct parser |
| AdminSDHolder protection | ✅ Hash-based detection | ❌ | ❌ |
| Inheritance hashes | ✅ Per-object | ❌ | ❌ |
| Owner-rights ACE detection | ✅ | ❌ | ❌ |
| GMSA reader ACEs | ✅ | ❌ | ✅ (separate msDS-GroupMSAMembership parsing) |
| LAPS read detection | ✅ | ✅ | ✅ (ms-mcs-admpwd, ms-laps-password, ms-laps-encryptedpassword) |
| SID history | ✅ | ✅ | ✅ (with cross-forest GC resolution) |
| Property-specific GUIDs | Comprehensive | Good coverage | Comprehensive |
| Concurrency | Thread pool | Single-threaded | Multiprocess pool |
BloodHound.py CE is the progenitor of RustHound-CE's ACL logic — RustHound's
enums/acl.rsis a direct port of BloodHound.py's approach. BloodHound.py additionally parses GMSA DACLs frommsDS-GroupMSAMembershipas a separate attribute, which RustHound-CE does not handle.
Output Format Compatibility
All three produce JSON files structured for BloodHound CE ingestion with the same data/meta envelope:
| Aspect | SharpHound | RustHound-CE | BloodHound.py CE |
|---|---|---|---|
| Meta version | version: 6 | version: 6 | version: 6 |
| Collector version | Assembly version | RustHound-CE v{version} | BloodHound.py CE |
| Object types | Full (users, groups, computers, domains, OUs, GPOs, containers, AD CS) | Full | Full |
| ZIP packaging | Default (zipped), password-protected, randomized filenames | Opt-in (--zip) | ❌ (individual JSON files) |
| File rotation | N/A (single archive) | N/A | ✅ (auto-rotate at 40K entries) |
| BH 4.0 guard | N/A (CE-only) | N/A (CE-only) | ✅ (ValueError if legacy acetype used) |
| API upload | ✅ (direct to BH CE) | ❌ | ❌ |
SharpHound is the only collector that supports direct API upload to BloodHound CE. RustHound-CE and BloodHound.py CE output files that must be manually uploaded via the BH CE web interface or API.
Remote Enumeration
This is the largest differentiator. SharpHound and BloodHound.py both support authenticated SMB/RPC connections to remote computers. RustHound-CE is a pure LDAP client.
SharpHound maintains separate processor classes for every remote enumeration technique: ComputerSessionProcessor, LocalGroupProcessor, UserRightsAssignmentProcessor, DCRegistryProcessor, CertAbuseProcessor, WebClientServiceProcessor, SmbProcessor, RegistryProcessor.
BloodHound.py CE covers a subset via Impacket’s RPC stack:
- Sessions:
I_rpc_get_loggedon(logged-on users),NetWkstaUserEnum,DsRoleGetPrimaryDomainInfo - Local groups: SAMR
QueryLocalGroupMembershipfor RDP/DCOM/PSRemote/LocalAdmin - Services/Scheduled tasks: SCMR
EnumerateServicesW, ATSVRNetJobEnum - Registry sessions:
OpenBaseRegKeyvia Impacket drsuapi
BloodHound.py lacks SharpHound’s UserRights (LSA), DCRegistry, CARegistry, WebClientService, LdapServices, and SmbInfo collectors.
For environments where only LDAP-based collection is feasible (e.g., from a non-domain-joined Linux host without SMB access), RustHound-CE is the lightest option. BloodHound.py CE adds remote computer enumeration when SMB access is available.
Deployment and OPSEC Considerations
SharpHound requires .NET Framework 4.7.2+ (Windows only). It uses Costura.Fody to embed dependencies into a single DLL (~2 MB), typically executed via Invoke-SharpHound PowerShell wrapper. As a .NET assembly, it is detectable by AMSI and well-known to endpoint security products.
RustHound-CE compiles to a static native binary (~1-2 MB) with aggressive optimization (opt-level = "z", LTO, strip, panic = "abort"). Cross-compiles to Linux (x86_64, armv7 musl), macOS, and Windows. Uses obfstr for compile-time string obfuscation. No runtime dependency.
BloodHound.py CE requires Python 3 with ldap3, impacket, and optionally python3-krb5. It runs on any platform supporting Python without compilation. There is no binary distribution — deployment requires installing dependencies (often via pip). The dumpntlm.py module depends on unmerged Impacket changes (PR #1404), requiring a patched Impacket install.
| Factor | SharpHound | RustHound-CE | BloodHound.py CE |
|---|---|---|---|
| Platform | Windows only (.NET 4.7.2+) | Linux, macOS, Windows | Linux, macOS, Windows |
| Runtime dependency | .NET Framework | None (static binary) | Python 3 + ldap3 + impacket |
| Distribution | Pre-built DLL/EXE | Pre-built binary | Source (pip install) |
| AMSI detection | High (well-known .NET assembly) | Lower (native binary) | Low (interpreted Python) |
| Certificate verification | Configurable | Always disabled | Configurable (-d for disable) |
| Kerberos auth | Negotiate (native SSPI) | GSSAPI via ldap3 | python3-krb5 (ccache or password) |
| NTLM hash auth | ❌ (password only) | ✅ (via NTLM hash) | ✅ (via NTLM hash) |
| Stealth features | Target inference, loop, throttle, jitter, random filenames | None | None |
| Python ecosystem integration | ❌ | ❌ | ✅ (native Python, scriptable) |
RustHound-CE hardcodes
set_no_tls_verify(true), making it impossible to use where LDAPS certificate validation is required. BloodHound.py and SharpHound both support configurable certificate verification.
Authentication Methods
All three support standard LDAP authentication. Notable differences:
| Method | SharpHound | RustHound-CE | BloodHound.py CE |
|---|---|---|---|
| Simple bind (plaintext) | ✅ | ✅ | ✅ |
| NTLM password | ✅ (Negotiate) | ✅ | ✅ |
| NTLM hash (pass-the-hash) | ❌ | ✅ | ✅ |
| Kerberos | ✅ (SSPI, native) | ✅ (GSSAPI ccache) | ✅ (python3-krb5 ccache or password) |
| SSPI (Windows native) | ✅ | ❌ | ✅ (via Impacket) |
BloodHound.py CE and RustHound-CE both support pass-the-hash authentication via NTLM, which SharpHound does not. This matters in scenarios where only NTLM hashes are available (e.g., from a credential dump).
Assessment
SharpHound is the reference implementation. It offers the most complete collection coverage (25+ methods including all registry-based collectors), fine-grained method selection, OPSEC controls (stealth, throttle, jitter, loop), and direct API upload to BloodHound CE. Its limitation is Windows-only execution and lack of pass-the-hash support.
RustHound-CE fills a niche for lightweight, LDAP-only collection from any platform. Its native binary is the most portable and least detectable option. However, it lacks remote computer enumeration entirely, has no stealth features, and always disables TLS verification. The All mode name overstates capabilities.
BloodHound.py CE occupies the middle ground. It provides the broadest cross-platform remote enumeration (sessions, local groups, services, scheduled tasks via Impacket RPC), supports pass-the-hash authentication, offers per-method collection control, and integrates natively with the Python offensive ecosystem. Its ACL parser is the reference implementation that RustHound-CE was derived from, and it uniquely supports GMSA DACL parsing and credentialless NTLM reconnaissance. The trade-off is requiring Python dependencies and lacking SharpHound’s registry-based collectors and OPSEC features.
When to use SharpHound:
- Full-fidelity collection with all registry-based methods
- Production engagements requiring stealth/throttle/jitter
- Environments where Windows execution is available
- AdminSDHolder protection detection and inheritance hashing needed
- Direct API upload to BloodHound CE desired
When to use RustHound-CE:
- LDAP-only collection from any platform
- Minimal footprint required (single static binary)
- OPSEC priority (native binary, compile-time obfuscation)
- No Python runtime available on collection host
When to use BloodHound.py CE:
- Cross-platform collection with remote computer enumeration
- Pass-the-hash authentication required
- Python offensive pipeline integration (e.g., C2 scripting)
- Credentialless initial reconnaissance (
dumpntlm.py) - Multi-domain forest collection with GC-based resolution
- GMSA password ACL extraction needed
Gaps common to RustHound-CE and BloodHound.py CE:
- No AdminSDHolder protection or inheritance hash detection
- No UserRights (LSA) enumeration
- No DC/CA registry collectors
- No stealth mode, throttling, or loop collection
- No direct API upload to BloodHound CE
Related
- analyzing-sharphound — detailed codebase analysis of SharpHound
- analyzing-rusthound-ce — detailed codebase analysis of RustHound-CE
- analyzing-bloodhound-py-ce — detailed codebase analysis of BloodHound.py CE
- analyzing-bloodhound — analysis of the BloodHound CE graph platform
- analyzing-scubagear — OPSEC tool that removes SharpHound artifacts
- analyzing-mimikatz — credential extraction tool often used alongside BloodHound
- analyzing-netext — network extension tool for BloodHound data collection