Source: SpecterOps/SharpHound @ 4dca071
How It Works
SharpHound is the C# data collector for BloodHound CE, the Active Directory attack path mapping tool. It runs as a console executable targeting .NET Framework 4.7.2, connects to a domain via LDAP, enumerates AD objects (users, computers, groups, GPOs, OUs, containers, certificate infrastructure), collects ACLs, sessions, local group memberships, trust relationships, and registry data, then writes the results as structured JSON files zipped for ingestion by the BloodHound CE backend.
The tool supports ~25 collection methods that can be combined as bitmask flags — Group, ACL, Session, LocalGroup, DCOnly, CertServices, and so on. A stealth mode replaces direct computer enumeration with LDAP-property-based target inference. A loop mode runs periodic collection over a configurable duration, producing timestamped output files.
The actual enumeration logic lives in a separate NuGet package — SharpHoundCommon (v4.6.x) — which provides LDAP utilities, result type definitions (User, Computer, Domain, Group, etc.), and specialized processors (ACLProcessor, CertAbuseProcessor, DomainTrustProcessor, etc.). The SharpHound repo itself is the orchestration layer.
Architecture
The architecture follows a producer-consumer pipeline coordinated by a chain-of-responsibility initialization sequence.
Pipeline Overview
CLI Args → SharpLinks (chain) → CollectionTask
├── Producer (LdapProducer / StealthProducer / ComputerFileProducer)
│ └── writes IDirectoryObject to _ldapChannel (bounded, 1000)
├── N Consumer threads (default 50)
│ └── reads _ldapChannel → ObjectProcessors → writes OutputBase to _outputChannel
└── OutputWriter
└── reads _outputChannel → type-switch → JsonDataWriter<T> per entity type → zip
Three channels connect the stages:
_ldapChannel: boundedChannel<IDirectoryObject>(capacity 1000, backpressure on write)_outputChannel: unboundedChannel<OutputBase>(single reader)_compStatusChannel: optional unboundedChannel<CSVComputerStatus>for debug tracking
Directory Structure
| Directory | Role |
|---|---|
src/ | Entry point, options, context, chain links |
src/Client/ | IContext interface, Flags, Links<T> interface, enums |
src/Producers/ | BaseProducer, LdapProducer, StealthProducer, ComputerFileProducer |
src/Runtime/ | CollectionTask, ObjectProcessors, OutputWriter, LoopManager |
src/Writers/ | BaseWriter<T>, JsonDataWriter<T>, CompStatusWriter |
The Spine
The execution spine is the chain of responsibility defined in SharpLinks, invoked sequentially in Program.StartCollection():
context = links.Initialize(context, ldapOptions);
context = await links.TestConnection(context);
context = links.SetSessionUserName(options.OverrideUserName, context);
context = links.InitCommonLib(context);
context = await links.GetDomainsForEnumeration(context);
context = links.StartBaseCollectionTask(context);
context = await links.AwaitBaseRunCompletion(context);
context = links.StartLoopTimer(context);
context = links.StartLoop(context);
context = await links.AwaitLoopCompletion(context);
context = links.SaveCacheFile(context);
links.Finish(context);Each link mutates the shared IContext object. Fault checking (IsFaulted) gates early exit at critical points. The Links<T> interface is not a traditional chain-of-responsibility with linked handlers — it’s a flat method list on one concrete class, called manually in order. The pattern name in the code comments is aspirational rather than implemented with chaining.
The actual work happens in StartBaseCollectionTask, which creates a CollectionTask and calls StartCollection(). This method wires up the producer-consumer pipeline, awaits completion, writes well-known principals, then flushes all writers and zips the output.
Key Patterns
1. Bitmask Collection Methods
Collection methods are combined using [Flags] enums. The Options.ResolveCollectionMethods method parses CLI strings into a bitmask via CollectionMethodOptions switch expressions, then bitwise-ORs them. Stealth mode surgically removes computer-touching methods and substitutes GPOLocalGroup:
resolved |= option switch
{
CollectionMethodOptions.Group => CollectionMethod.Group,
CollectionMethodOptions.ACL => CollectionMethod.ACL,
// ... 20+ mappings
};2. Object Processor Dispatch
ObjectProcessors.ProcessObject() uses a Label enum (from SharpHoundCommonLib) to dispatch to 14 typed processing methods. Each method builds the appropriate output type, conditionally invoking processors based on the active collection method flags:
switch (resolvedSearchResult.ObjectType)
{
case Label.User: return await ProcessUserObject(entry, resolvedSearchResult);
case Label.Computer: return await ProcessComputerObject(entry, resolvedSearchResult);
case Label.Domain: return await ProcessDomainObject(entry, resolvedSearchResult);
// ... 11 more cases
}The computer processor is by far the most complex — it performs availability checks, then conditionally runs session enumeration, local group enumeration, registry reads, SMB scans, and user rights assignments, each gated by DoDelay() for throttle/jitter.
3. Type-Switched Output Writing
OutputWriter.StartWriter() reads from the output channel and pattern-matches on 13 concrete types, routing each to a dedicated JsonDataWriter<T>. Each writer buffers 30 items before flushing to disk, and appends a meta tag with count, data version (6), collection method flags, and collector version on close.
4. AdminSDHolder Hashing
During LDAP production, the producer queries cn=adminsdholder,cn=system for each domain, computes an implicit ACL hash via ACLProcessor.CalculateImplicitACLHash(), and stores it in BaseContext.AdminSDHolderHash. Consumer threads later use this hash to detect if objects are AdminSDHolder-protected, without re-querying.
5. Lazy Registry Processor per Domain
The NTLMRegistry collection creates RegistryProcessor instances lazily per domain SID, cached in a ConcurrentDictionary<string, Lazy<RegistryProcessor>>. This avoids redundant initialization while keeping thread safety:
var processor = _registryProcessorMap.GetOrAdd(
resolvedSearchResult.DomainSid,
_ => new Lazy<RegistryProcessor>(() => {
var newProcessor = new RegistryProcessor(null, new StrategyExecutor(), resolvedSearchResult.Domain);
newProcessor.ComputerStatusEvent += HandleCompStatusEvent;
return newProcessor;
})).Value;Non-Obvious Details
Bounded vs. unbounded channels. The LDAP channel is bounded at 1000 with Wait semantics — producers block when consumers fall behind, preventing uncontrolled memory growth during large domain enumeration. The output channel is unbounded, relying on fast JSON serialization to keep up.
Enterprise DC suffix filtering. After the pipeline completes, well-known principals are written to the output channel, but Enterprise DC entries (SID ending in S-1-5-9) with empty members are silently dropped to avoid polluting the graph.
Cache identity via registry MachineGuid. The cache filename defaults to a Base64-encoded MachineGuid from HKLM\SOFTWARE\Microsoft\Cryptography, making each machine’s cache unique without user configuration.
Configuration NC separate pass. Certificate services data lives in the Configuration naming context, not the Default NC. The producer runs Produce() then ProduceConfigNC() as separate phases, deduplicating Configuration NCs across domains in a HashSet.
LDAPConsumer is dead code. The static LDAPConsumer.ConsumeSearchResults() method duplicates CollectionTask.ConsumeSearchResults() almost line-for-line but is never called anywhere in the codebase.
Costura.Fody for single-binary deployment. The csproj includes Costura.Fody, which embeds all NuGet dependencies into the output assembly — producing a single self-contained EXE suitable for pentest droppers.
Commented-out CancelKeyPress handler. The console cancellation handler is commented out in StartCollection(), meaning Ctrl+C does not gracefully cancel enumeration — it terminates the process.
Assessment
SharpHound is a well-structured data collection tool with clean separation between orchestration (this repo) and domain-specific logic (SharpHoundCommon NuGet). The producer-consumer channel pipeline with configurable parallelism is the right approach for IO-bound enumeration of large AD environments.
Strengths: The bitmask collection method system is flexible and composable. The stealth mode is thoughtful — inferring targets from user home-directory paths rather than scanning all computers. Throttle and jitter support is built into every computer-touching call. The output format is versioned (data version 6) with metadata for backend compatibility.
Weaknesses: The IContext interface is a god-object carrying 30+ properties — any change ripples everywhere. The chain-of-responsibility pattern is nominally declared but not actually implemented as a chain; SharpLinks is just a regular class with methods called in sequence. LDAPConsumer.cs is dead code. The commented-out CancelKeyPress handler and the TODO markers in BaseContext.Dispose() suggest incomplete cleanup from a refactor.
Notable: The growing emphasis on certificate services infrastructure (CertAbuseProcessor, CARegistry, NTLMRegistry, Enterprise CA NTLM endpoint scanning, IssuancePolicy mapping) reflects the current state of AD offensive research — ESC attacks are now a first-class collection target alongside traditional ACL/session/group enumeration.
Related
- analyzing-scubagear — AD offensive security tool, complementary to SharpHound’s enumeration
- analyzing-pacu — AWS exploitation framework; similar producer-consumer enumeration pattern