Skip to content

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

-- Mark custom high value targets
MATCH (n)
WHERE n.name IN ['[email protected]', '[email protected]']
SET n.highvalue = true
RETURN n.name, labels(n)[0] as Type

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

Performance Optimization Tips

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