BloodHound CE Cypher Queries Cheatsheet (PostgreSQL)
Basic Query Structure & Operators
Pattern Matching Operators
-- Case-insensitive regex matching
WHERE n.name =~ '(?i).*admin.*'
-- Exact match
WHERE n.name = '[email protected]'
-- Contains substring
WHERE n.name CONTAINS 'SVC'
-- Starts with
WHERE n.name STARTS WITH 'SRV'
-- Ends with
WHERE n.name ENDS WITH '[email protected]'
-- IN operator for multiple values
WHERE n.name IN ['[email protected]', '[email protected]']
-- NOT operators
WHERE NOT n.name CONTAINS 'test'
User Hunting Queries
Find Specific Users by Pattern
-- Find all service accounts
MATCH (u:User)
WHERE u.name =~ '(?i).*svc.*' OR u.name =~ '(?i).*service.*'
RETURN u.name, u.enabled, u.pwdlastset
ORDER BY u.pwdlastset DESC
-- Find users with specific keywords in description
MATCH (u:User)
WHERE u.description =~ '(?i).*(password|pwd|pass|cred).*'
RETURN u.name, u.description
-- Find admin accounts not in Admin groups
MATCH (u:User)
WHERE u.name =~ '(?i).*adm.*'
AND NOT (u)-[:MemberOf*1..]->(:Group {name: 'DOMAIN [email protected]'})
RETURN u.name, u.enabled
Find Privileged Users
-- Users with DCSync rights
MATCH p=(u:User)-[:DCSync|GenericAll|AllExtendedRights]->(d:Domain)
RETURN u.name, labels(u)[0] as Type
ORDER BY u.name
-- Find users with most privileges (top 10)
MATCH (u:User)
OPTIONAL MATCH (u)-[r:AdminTo|CanRDP|CanPSRemote|ExecuteDCOM|HasSession]->(c:Computer)
WITH u, COUNT(DISTINCT c) as computerAccess
OPTIONAL MATCH (u)-[r2]->(g:Group)
WHERE type(r2) IN ['MemberOf', 'GenericAll', 'GenericWrite', 'WriteOwner', 'WriteDacl']
WITH u, computerAccess, COUNT(DISTINCT g) as groupPrivs
WHERE computerAccess > 0 OR groupPrivs > 0
RETURN u.name, computerAccess, groupPrivs, (computerAccess + groupPrivs) as totalPrivs
ORDER BY totalPrivs DESC
LIMIT 10
Kerberoastable Users
-- All Kerberoastable users with details
MATCH (u:User)
WHERE u.hasspn = true AND u.enabled = true
RETURN u.name, u.serviceprincipalnames, u.pwdlastset, u.lastlogon
ORDER BY u.pwdlastset ASC
-- Kerberoastable users in privileged groups
MATCH (u:User)-[:MemberOf*1..]->(g:Group)
WHERE u.hasspn = true
AND g.name =~ '(?i).*(admin|operator|backup|replicat).*'
RETURN DISTINCT u.name, u.serviceprincipalnames, COLLECT(DISTINCT g.name) as Groups
AS-REP Roastable Users
-- AS-REP roastable users
MATCH (u:User)
WHERE u.dontreqpreauth = true AND u.enabled = true
RETURN u.name, u.pwdlastset, u.lastlogon
ORDER BY u.pwdlastset ASC
-- AS-REP roastable with admin access
MATCH (u:User)-[:AdminTo|CanRDP|CanPSRemote]->(c:Computer)
WHERE u.dontreqpreauth = true
RETURN u.name, COLLECT(c.name) as Computers
Attack Path Queries
Shortest Paths from Owned Users
-- All paths from owned users to Domain Admin
MATCH p = shortestPath((u:User {owned: true})-[*1..]->(g:Group {name: 'DOMAIN [email protected]'}))
RETURN p
-- Shortest path from specific user pattern to any high value target
MATCH p = shortestPath((n)-[*1..]->(c))
WHERE n.name =~ '(?i).*svc.*'
AND c.highvalue = true
AND NOT c = n
RETURN p
-- All paths from owned to high value (max 5 hops)
MATCH p = (u {owned: true})-[*1..5]->(h {highvalue: true})
RETURN p
Find Dangerous Paths
-- Users with indirect DCSync rights
MATCH p = (u:User)-[*1..3]->(g:Group)-[:DCSync|GetChangesAll|GetChanges]->(d:Domain)
WHERE NOT (u)-[:DCSync|GetChangesAll|GetChanges]->(d)
RETURN u.name, [node in nodes(p) | node.name] as Path
ORDER BY length(p) ASC
-- Find paths through computer local admin
MATCH p = shortestPath((u:User {owned: true})-[*1..]->(c:Computer)-[*1..]->(g:Group {name: 'DOMAIN [email protected]'}))
WHERE ANY(r IN relationships(p) WHERE type(r) IN ['AdminTo', 'HasSession'])
RETURN p
Delegation Abuse Paths
-- Unconstrained delegation computers with sessions
MATCH (c:Computer {unconstraineddelegation: true})-[:HasSession]-(u:User)
WHERE NOT c.name CONTAINS 'DC'
RETURN c.name, COLLECT(u.name) as Users
ORDER BY SIZE(Users) DESC
-- Constrained delegation abuse paths
MATCH (u:User {owned: true})-[:MemberOf*1..]->(g:Group)-[:AdminTo]->(c:Computer)
WHERE c.allowedtodelegate IS NOT NULL
RETURN u.name, g.name, c.name, c.allowedtodelegate
Computer Queries
Find Vulnerable Computers
-- Computers with unsupported OS
MATCH (c:Computer)
WHERE c.operatingsystem =~ '(?i).*(2008|2003|2000|xp|vista|7).*'
AND c.enabled = true
RETURN c.name, c.operatingsystem, c.lastlogontimestamp
ORDER BY c.lastlogontimestamp DESC
-- Find computers without LAPS
MATCH (c:Computer)
WHERE c.haslaps = false
AND c.enabled = true
AND NOT c.name CONTAINS 'DC'
RETURN c.name, c.operatingsystem
ORDER BY c.name
-- Computers with sessions from privileged users
MATCH (c:Computer)-[:HasSession]-(u:User)-[:MemberOf*1..]->(g:Group)
WHERE g.name =~ '(?i).*(admin|operator).*'
RETURN c.name, COLLECT(DISTINCT u.name) as PrivilegedUsers
ORDER BY SIZE(PrivilegedUsers) DESC
Domain Controllers
-- All domain controllers with sessions
MATCH (c:Computer)-[:HasSession]-(u:User)
WHERE c.name =~ '(?i).*DC.*'
RETURN c.name, COUNT(DISTINCT u) as SessionCount, COLLECT(u.name) as Users
ORDER BY SessionCount DESC
-- Find non-DC computers with DC-like privileges
MATCH (c:Computer)-[r:DCSync|GetChangesAll|GetChanges]->(d:Domain)
WHERE NOT c.name CONTAINS 'DC'
RETURN c.name, type(r) as Privilege
Group Analysis
Find Interesting Groups
-- Groups with most members
MATCH (g:Group)<-[:MemberOf]-(m)
WITH g, COUNT(m) as memberCount
WHERE memberCount > 10
RETURN g.name, memberCount
ORDER BY memberCount DESC
LIMIT 20
-- Nested group memberships
MATCH p = (u:User)-[:MemberOf*1..]->(g:Group {name: 'DOMAIN [email protected]'})
WHERE length(p) > 1
RETURN u.name, [group in nodes(p) WHERE group:Group | group.name] as GroupPath
ORDER BY length(p) DESC
-- Empty high-privilege groups
MATCH (g:Group)
WHERE g.admincount = true
AND NOT ()-[:MemberOf]->(g)
RETURN g.name
Cross-Domain Group Memberships
-- Foreign group memberships
MATCH (u:User)-[:MemberOf]->(g:Group)
WHERE NOT u.domain = g.domain
RETURN u.name, u.domain, g.name, g.domain
ORDER BY u.domain, g.domain
ACL Abuse Queries
WriteDACL Rights
-- Users with WriteDacl on groups
MATCH p = (u:User)-[:WriteDacl]->(g:Group)
WHERE g.admincount = true
RETURN u.name, g.name
ORDER BY g.name
-- WriteDacl paths to Domain Admin
MATCH p = (u:User {owned: true})-[:WriteDacl*1..3]->(g:Group {name: 'DOMAIN [email protected]'})
RETURN p
GenericAll/GenericWrite
-- GenericAll rights on computers
MATCH (u:User)-[:GenericAll]->(c:Computer)
WHERE u.owned = true
RETURN u.name, COLLECT(c.name) as Computers
-- GenericWrite on users
MATCH (u1:User {owned: true})-[:GenericWrite]->(u2:User)
WHERE u2.admincount = true
RETURN u1.name, u2.name
AddMember Rights
-- Who can add members to privileged groups
MATCH (n)-[:AddMember]->(g:Group)
WHERE g.admincount = true
RETURN n.name, labels(n)[0] as Type, g.name
ORDER BY g.name
GPO Queries
GPO Control
-- Users who can modify GPOs
MATCH (u:User)-[r:GenericAll|GenericWrite|WriteProperty|WriteDacl]->(g:GPO)
RETURN u.name, type(r) as Permission, g.name
ORDER BY g.name
-- GPOs affecting Domain Controllers
MATCH (g:GPO)-[:GpLink]->(ou:OU)-[:Contains*1..]->(c:Computer)
WHERE c.name CONTAINS 'DC'
RETURN g.name, COLLECT(DISTINCT c.name) as DCs
Certificate Template Queries
ADCS Vulnerabilities
-- Users who can enroll in certificate templates
MATCH (u:User {owned: true})-[:Enroll|GenericAll|GenericWrite]->(ct:CertTemplate)
WHERE ct.enrolleesuppliessubject = true
OR ct.subjectaltrequireupn = true
OR ct.subjectaltrequiredns = true
RETURN u.name, ct.name, ct.enrolleesuppliessubject, ct.subjectaltrequireupn
Session Analysis
Active Sessions
-- Find where owned users have sessions
MATCH (u:User {owned: true})-[:HasSession]->(c:Computer)
RETURN u.name, COLLECT(c.name) as Computers
ORDER BY SIZE(Computers) DESC
-- Computers with most privileged sessions
MATCH (c:Computer)-[:HasSession]-(u:User)-[:MemberOf*0..]->(g:Group)
WHERE g.admincount = true
WITH c, COUNT(DISTINCT u) as privUserCount
WHERE privUserCount > 0
RETURN c.name, privUserCount
ORDER BY privUserCount DESC
Session Paths
-- Session-based attack paths
MATCH p = (u1:User {owned: true})-[:HasSession]->(c:Computer)<-[:AdminTo]-(u2:User)-[:MemberOf*1..]->(g:Group {highvalue: true})
RETURN p
LAPS Queries
LAPS Coverage
-- Computers with LAPS and who can read it
MATCH (c:Computer {haslaps: true})
OPTIONAL MATCH (n)-[:ReadLAPSPassword]->(c)
RETURN c.name, COLLECT(n.name) as CanReadLAPS
ORDER BY SIZE(CanReadLAPS) DESC
-- Users who can read LAPS passwords
MATCH (u:User)-[:ReadLAPSPassword]->(c:Computer)
RETURN u.name, COUNT(c) as ComputerCount, COLLECT(c.name) as Computers
ORDER BY ComputerCount DESC
Statistics Queries
Domain Statistics
-- Quick domain stats
MATCH (u:User {enabled: true})
WITH COUNT(u) as UserCount
MATCH (c:Computer {enabled: true})
WITH UserCount, COUNT(c) as ComputerCount
MATCH (g:Group)
WITH UserCount, ComputerCount, COUNT(g) as GroupCount
MATCH (u2:User {owned: true})
WITH UserCount, ComputerCount, GroupCount, COUNT(u2) as OwnedUsers
MATCH (c2:Computer {owned: true})
WITH UserCount, ComputerCount, GroupCount, OwnedUsers, COUNT(c2) as OwnedComputers
RETURN UserCount, ComputerCount, GroupCount, OwnedUsers, OwnedComputers
-- Users with most ACL rights
MATCH (u:User)-[r]->(n)
WHERE type(r) IN ['GenericAll', 'GenericWrite', 'WriteOwner', 'WriteDacl', 'ForceChangePassword', 'AddMember']
WITH u, COUNT(r) as AclCount
WHERE AclCount > 5
RETURN u.name, AclCount
ORDER BY AclCount DESC
Password Statistics
-- Password age analysis
MATCH (u:User {enabled: true})
WHERE u.pwdlastset > 0
WITH u, datetime() as now, datetime({epochSeconds: u.pwdlastset}) as pwdDate
WITH u, duration.between(pwdDate, now).days as daysSinceChange
RETURN
CASE
WHEN daysSinceChange < 30 THEN '0-30 days'
WHEN daysSinceChange < 90 THEN '30-90 days'
WHEN daysSinceChange < 180 THEN '90-180 days'
WHEN daysSinceChange < 365 THEN '180-365 days'
ELSE '365+ days'
END as PasswordAge,
COUNT(u) as UserCount
ORDER BY PasswordAge
Cleanup & Utility Queries
Mark Nodes as Owned
-- Mark user as owned
MATCH (u:User {name: '[email protected]'})
SET u.owned = true
RETURN u.name, u.owned
-- Mark multiple users as owned
MATCH (u:User)
WHERE u.name IN ['[email protected]', '[email protected]']
SET u.owned = true
RETURN u.name
-- Mark computers as owned
MATCH (c:Computer)
WHERE c.name =~ '(?i).*WS.*'
SET c.owned = true
RETURN c.name
Mark High Value Targets
Find Anomalies
-- Users never logged in
MATCH (u:User {enabled: true})
WHERE u.lastlogon = -1 OR u.lastlogon IS NULL
RETURN u.name, u.created
ORDER BY u.created DESC
-- Computers never logged in
MATCH (c:Computer {enabled: true})
WHERE c.lastlogontimestamp = -1 OR c.lastlogontimestamp IS NULL
RETURN c.name, c.created
-- Find duplicate SPNs
MATCH (u:User)
WHERE u.serviceprincipalnames IS NOT NULL
WITH u.serviceprincipalnames as spn, COLLECT(u.name) as users
WHERE SIZE(users) > 1
RETURN spn, users
Advanced Pattern Queries
Complex Attack Chains
-- Find all users who can reach DA in exactly 3 hops
MATCH p = (u:User)-[*3]->(g:Group {name: 'DOMAIN [email protected]'})
WHERE ALL(r IN relationships(p) WHERE type(r) IN ['MemberOf', 'GenericAll', 'GenericWrite', 'ForceChangePassword', 'AddMember'])
RETURN DISTINCT u.name, [node in nodes(p) | node.name] as Path
-- Find privilege escalation through computer compromise
MATCH (u1:User {owned: true})-[:AdminTo]->(c:Computer)-[:HasSession]-(u2:User)-[:MemberOf*1..]->(g:Group {highvalue: true})
WHERE NOT u1 = u2
RETURN u1.name as OwnedUser, c.name as Computer, u2.name as TargetUser, g.name as TargetGroup
Lateral Movement Opportunities
-- Find computers where owned users can move laterally
MATCH (u:User {owned: true})-[r:AdminTo|CanRDP|CanPSRemote|ExecuteDCOM]->(c:Computer)
WHERE c.owned = false
RETURN u.name, type(r) as AccessType, c.name
ORDER BY c.name
-- RDP access chains
MATCH p = (u:User {owned: true})-[:CanRDP*1..3]->(c:Computer {highvalue: true})
RETURN p
Use LIMIT for Testing
-- Always test queries with LIMIT first
MATCH p = (u:User)-[*1..]->(g:Group {highvalue: true})
RETURN p
LIMIT 10
Use Indexes
-- Check if properties are indexed (run in neo4j console)
:schema
-- Count before complex operations
MATCH (u:User {owned: true})
RETURN COUNT(u)
Avoid Cartesian Products
-- Bad: Creates cartesian product
MATCH (u:User), (c:Computer)
WHERE u.owned = true AND c.highvalue = true
-- Good: Use directed relationships
MATCH (u:User {owned: true})
MATCH (c:Computer {highvalue: true})
MATCH p = shortestPath((u)-[*1..]->(c))
RETURN p
Common Filters & Conditions
Time-based Filters
-- Recent activity (last 30 days)
MATCH (u:User)
WHERE u.lastlogon > (datetime().epochSeconds - (30 * 24 * 60 * 60))
RETURN u.name, datetime({epochSeconds: u.lastlogon}) as LastLogon
-- Never expires passwords
MATCH (u:User)
WHERE u.pwdneverexpires = true AND u.enabled = true
RETURN u.name, u.pwdlastset
Excluding System Accounts
-- Exclude computer accounts and krbtgt
MATCH (u:User)
WHERE NOT u.name ENDS WITH '[email protected]'
AND NOT u.name STARTS WITH 'KRBTGT'
RETURN u.name
Combining Multiple Conditions
-- Complex user search
MATCH (u:User)
WHERE u.enabled = true
AND u.pwdneverexpires = false
AND (u.admincount = true OR u.name =~ '(?i).*admin.*')
AND u.lastlogon > 0
RETURN u.name, u.description
ORDER BY u.lastlogon DESC
Quick Reference - Common Patterns
-- Check if path exists
MATCH p = (a)-[*1..5]->(b)
WHERE a.name = '[email protected]'
AND b.name = '[email protected]'
RETURN COUNT(p) > 0 as PathExists
-- Get all relationship types
MATCH ()-[r]->()
RETURN DISTINCT type(r)
-- Find isolated users (no group membership)
MATCH (u:User)
WHERE NOT (u)-[:MemberOf]->(:Group)
RETURN u.name
-- Find circular group memberships
MATCH p = (g:Group)-[:MemberOf*1..]->(g)
RETURN p
-- Export specific paths to JSON (for reporting)
MATCH p = shortestPath((u:User {owned: true})-[*1..]->(g:Group {highvalue: true}))
RETURN p
Notes
- Case Sensitivity: Use
(?i) for case-insensitive regex matching
- Performance: Always test with
LIMIT first on large datasets
- Relationships: Use
[*1..5] to limit relationship depth
- Null Checks: Use
IS NULL or IS NOT NULL for null checking
- Collections: Use
COLLECT(), SIZE(), COUNT() for aggregations
- Paths:
shortestPath() is more efficient than allShortestPaths()
- Labels: Access with
labels(n)[0] to get primary label
- Properties: Check existence with
EXISTS(n.property)
BloodHound CE Specific Notes
- PostgreSQL backend in CE vs Neo4j in Legacy
- New node types:
CertTemplate, AIACA, RootCA, EnterpriseCA, NTAuthStore
- New edges:
Enroll, PublishedTo, TrustedForNTAuth
- Datetime handling: Use
datetime() for current time
- Owned/High Value: Set directly with
SET n.owned = true