Skip to content

BloodHound Queries

Below is a collection of cypher queries that have been useful on engagements - recorded for posterity. YMMV with the new community edition and some of these may need updated to work:

See the below for other queries - if any of them don't work check if it references an objectsid rather than objectid and update them to use objectid, objectsid was the old format:

  • https://hausec.com/2019/09/09/bloodhound-cypher-cheatsheet/
  • https://hausec.com/2020/11/23/azurehound-cypher-cheatsheet/
  • https://github.com/ZephrFish/Bloodhound-CustomQueries

Most of these queries stiill work with BloodHound CE, but instead of searching for owned = true, you need to search for: WHERE n.system_tags CONTAINS "owned". See:

  • https://arth0s.medium.com/bloodhound-ce-and-neo4j-queries-statistics-adcs-and-more-3a060be0a98c

Graph UI Queries

Find all paths from AZ owned users to Global Admin

1
2
3
4
MATCH p=shortestPath((n:AZUser {owned:true})-[:MemberOf|HasSession|AdminTo|AllExtendedRights|AddMember|ForceChangePassword|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|CanRDP|ExecuteDCOM|AllowedToDelegate|ReadLAPSPassword|Contains|GPLink|AddAllowedToAct|AllowedToAct|SQLAdmin|ReadGMSAPassword|HasSIDHistory|CanPSRemote|SyncLAPSPassword|AZAddMembers|AZAddSecret|AZAvereContributor|AZContains|AZContributor|AZExecuteCommand|AZGetCertificates|AZGetKeys|AZGetSecrets|AZGlobalAdmin|AZGrant|AZGrantSelf|AZHasRole|AZMemberOf|AZOwner|AZOwns|AZPrivilegedRoleAdmin|AZResetPassword|AZUserAccessAdministrator|AZAppAdmin|AZCloudAppAdmin|AZRunsAs|AZKeyVaultContributor|AZVMAdminLogin|AddSelf|WriteSPN|AddKeyCredentialLink|DCSync|AZVMContributor|AZPrivilegedAuthAdmin|AZScopedTo|AZAddOwner|AZManagedIdentity|AZLogicAppContributor|DumpSMSAPassword|AZMGAddSecret|AZMGAddOwner|AZMGAddMember|AZMGGrantAppRoles|AZMGGrantRole|AZNodeResourceGroup|AZWebsiteContributor|AZAutomationContributor|AZAKSContributor|WriteAccountRestrictions*1..]->(m:AZRole)) 
WHERE m.name 
STARTS WITH "GLOBAL ADMINISTRATOR@" 
RETURN p

Find all paths from AZ owned users to Global Admin excluding groups (useful for MFA bypasses based on CA groups)

1
2
3
4
5
6
7
MATCH (n:AZUser {owned: true})-[:AZMemberOf]->(g:AZGroup)
WHERE g.name IN ["Group1", "Group2", "Group3"]
WITH collect(n) AS excludedUsers
MATCH p=shortestPath((n:AZUser {owned: true})-[:MemberOf|HasSession|AdminTo|AllExtendedRights|AddMember|ForceChangePassword|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|CanRDP|ExecuteDCOM|AllowedToDelegate|ReadLAPSPassword|Contains|GPLink|AddAllowedToAct|AllowedToAct|SQLAdmin|ReadGMSAPassword|HasSIDHistory|CanPSRemote|SyncLAPSPassword|AZAddMembers|AZAddSecret|AZAvereContributor|AZContains|AZContributor|AZExecuteCommand|AZGetCertificates|AZGetKeys|AZGetSecrets|AZGlobalAdmin|AZGrant|AZGrantSelf|AZHasRole|AZMemberOf|AZOwner|AZOwns|AZPrivilegedRoleAdmin|AZResetPassword|AZUserAccessAdministrator|AZAppAdmin|AZCloudAppAdmin|AZRunsAs|AZKeyVaultContributor|AZVMAdminLogin|AddSelf|WriteSPN|AddKeyCredentialLink|DCSync|AZVMContributor|AZPrivilegedAuthAdmin|AZScopedTo|AZAddOwner|AZManagedIdentity|AZLogicAppContributor|DumpSMSAPassword|AZMGAddSecret|AZMGAddOwner|AZMGAddMember|AZMGGrantAppRoles|AZMGGrantRole|AZNodeResourceGroup|AZWebsiteContributor|AZAutomationContributor|AZAKSContributor|WriteAccountRestrictions*1..]->(m:AZRole))
WHERE NOT n IN excludedUsers 
AND m.name STARTS WITH "GLOBAL ADMINISTRATOR@"
RETURN p
Pay attention to casing and group FQDN here - you can also do some cypher transforms to take the list straight from roadrecon's caps.html, although pay attention to groups with duplicate prefixes if you do this. This may be more intensive on large domains and group lists:
// Define the set of groups to exclude and convert names to uppercase
WITH ["Group1", "Group2", "Group3"] AS excludeGroups
UNWIND excludeGroups AS groupPrefix
WITH collect(toUpper(groupPrefix)) AS excludeGroupsUpper
// Match AZUsers who are members of groups with names starting with the specified prefixes
MATCH (n:AZUser {owned: true})-[:AZMemberOf]->(g:AZGroup)
WHERE any(prefix IN excludeGroupsUpper WHERE toUpper(g.name) STARTS WITH prefix)
WITH collect(n) AS excludedUsers
// Find the shortest path excluding users in the specified groups
MATCH p=shortestPath((n:AZUser {owned: true})-[:MemberOf|HasSession|AdminTo|AllExtendedRights|AddMember|ForceChangePassword|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|CanRDP|ExecuteDCOM|AllowedToDelegate|ReadLAPSPassword|Contains|GPLink|AddAllowedToAct|AllowedToAct|SQLAdmin|ReadGMSAPassword|HasSIDHistory|CanPSRemote|SyncLAPSPassword|AZAddMembers|AZAddSecret|AZAvereContributor|AZContains|AZContributor|AZExecuteCommand|AZGetCertificates|AZGetKeys|AZGetSecrets|AZGlobalAdmin|AZGrant|AZGrantSelf|AZHasRole|AZMemberOf|AZOwner|AZOwns|AZPrivilegedRoleAdmin|AZResetPassword|AZUserAccessAdministrator|AZAppAdmin|AZCloudAppAdmin|AZRunsAs|AZKeyVaultContributor|AZVMAdminLogin|AddSelf|WriteSPN|AddKeyCredentialLink|DCSync|AZVMContributor|AZPrivilegedAuthAdmin|AZScopedTo|AZAddOwner|AZManagedIdentity|AZLogicAppContributor|DumpSMSAPassword|AZMGAddSecret|AZMGAddOwner|AZMGAddMember|AZMGGrantAppRoles|AZMGGrantRole|AZNodeResourceGroup|AZWebsiteContributor|AZAutomationContributor|AZAKSContributor|WriteAccountRestrictions*1..]->(m:AZRole))
WHERE NOT n IN excludedUsers AND m.name STARTS WITH "GLOBAL ADMINISTRATOR@"
RETURN p

All non-highvalue groups permissions in domain:

1
2
3
MATCH p = (g:AZServicePrincipal)-[r]->(n) 
WHERE g.ServicePrincipalType = Application 
RETURN p

Find all DA session not on DCs

1
2
3
4
5
MATCH (c1:Computer)-[:MemberOf*1..]->(g1:Group) 
WHERE g1.objectid ENDS WITH '-516' WITH COLLECT(c1.name) AS domainControllers 
MATCH (u1:User)-[:MemberOf]->(g2:Group) WHERE g2.objectid ENDS WITH '-512' MATCH p = (c2:Computer)-[:HasSession]->(u1) 
WHERE NOT c2.name IN domainControllers 
RETURN p

Console Queries

Mark all Azure users as owned when their on-prem equivalent is

Matches based on username and email - works well with Max DPAT: https://github.com/knavesec/Max

1
2
3
4
5
6
MATCH (u:User {owned: true})
WITH u, TOUPPER(split(u.name, '@')[0]) AS userPart, TOUPPER(u.email) AS userEmail
MATCH (az:AZUser)
WHERE TOUPPER(split(az.name, '@')[0]) = userPart OR TOUPPER(az.name) = userEmail
SET az.owned = true
RETURN az.name, u.name, u.email

Mark all Azure users as owned when their password is cracked

Note: Requires you to have used DPAT to load AD passwords into BloodHound - copies the password field to the AZ object

1
2
3
4
5
6
MATCH (u:User {cracked: true})
WITH u, TOUPPER(u.email) AS userEmail
MATCH (az:AZUser)
WHERE TOUPPER(az.name) = userEmail
SET az.owned = true, az.password = u.password
RETURN az.name, u.name, u.email, u.password

All enabled users with SPNs in a group containing the word admin, returning the user and group names and descriptions (might produce overlap on purpose if an account is in more than one admin group)

1
2
3
MATCH (u:User)-[:MemberOf*1..]->(g:Group) 
WHERE g.name =~ '(?i).*admin.*' AND u.hasspn=true AND u.enabled=true 
RETURN u.name AS Username, u.description AS User_Description, g.name AS Group, g.description AS Group_Description

Kerberoastable

1
2
3
4
MATCH (u:User) 
WHERE u.hasspn=true 
AND u.enabled=true 
RETURN u.name

Return all users with SPNs that are enabled and in high value targets

1
2
3
4
5
MATCH (u:User)-[:MemberOf*1..]->(g:Group) 
WHERE g.highvalue=true 
AND u.hasspn=true 
AND u.enabled=true 
RETURN u.name AS USER, u.description AS Description

Return all as-rep roastable users that are enabled

1
2
3
4
MATCH (u:User) 
WHERE u.dontreqpreauth=true 
AND u.enabled=true 
RETURN u.name

Look up username from email

1
2
3
4
MATCH (u:User) 
WHERE u.email =~ '(?i).*simon.*' 
AND u.email =~ '(?i).*preller.*' 
RETURN u.name, u.email

Lookup all members of the BACS group

1
2
3
4
MATCH (u:User)-[:MemberOf*1..]->(g:Group) 
WHERE g.name =~ '(?i).*BACS.*' 
AND u.enabled=true 
RETURN u.name AS Username, u.description AS User_Description, g.name AS Group, g.description AS Group_Description

All domain controllers based on OU

1
2
3
MATCH (o:OU)-[:Contains]->(c:Computer) 
WHERE o.name =~'(?i).*DOMAIN CONTROLLERS.*' 
RETURN o.name, c.name, c.operatingsystem

Unsupported OS

1
2
3
MATCH (c:Computer) 
WHERE c.operatingsystem =~ '(?i).*(2000|2003|2008|xp|vista|7|me).*' 
RETURN c.name

Unsupported OS used in the last six months:

1
2
3
4
MATCH (c:Computer) 
WHERE c.operatingsystem =~ '(?i).*(2000|2003|2008|xp|vista|7|me).*' 
AND c.lastlogontimestamp > (datetime().epochseconds - (180 * 86400)) 
RETURN c.name, c.operatingsystem, c.lastlogontimestamp ORDER BY c.lastlogontimestamp
1
2
3
4
5
MATCH (c:Computer) 
WHERE c.operatingsystem =~ '(?i).*(2000|2003|2008|xp|vista|7|me).*' 
AND c.lastlogontimestamp > (datetime().epochseconds - (180 * 86400)) 
RETURN c.name, c.operatingsystem, datetime({epochSeconds: toInteger(c.lastlogontimestamp)}) 
ORDER BY c.operatingsystem

Unsupported OS used in last six months, showing number of days since last logon:

1
2
3
4
5
MATCH (c:Computer) 
WHERE c.operatingsystem =~ '(?i).*(2000|2003|2008|xp|vista|7|me).*' 
AND c.lastlogontimestamp > (datetime().epochseconds - (180 * 86400)) 
RETURN c.name, c.operatingsystem, round((datetime().epochseconds - c.lastlogontimestamp) / 86400) 
ORDER BY c.lastlogontimestamp DESC

Find all DA session not on DCs

1
2
3
4
5
6
7
MATCH (c1:Computer)-[:MemberOf*1..]->(g1:Group) 
WHERE g1.objectid ENDS WITH '-516' WITH COLLECT(c1.name) AS domainControllers 
MATCH (u1:User)-[:MemberOf]->(g2:Group) 
WHERE g2.objectid ENDS WITH '-512' 
MATCH (c2:Computer)-[:HasSession]->(u1) 
WHERE NOT c2.name IN domainControllers 
RETURN u1.name, c2.name

DAs with sessions outside of a DC, with a count of how many sessions sorted by the largest first

1
2
3
4
5
6
7
MATCH (c1:Computer)-[:MemberOf*1..]->(g1:Group) 
WHERE g1.objectid ENDS WITH '-516' WITH COLLECT(c1.name) AS domainControllers 
MATCH (u1:User)-[:MemberOf]->(g2:Group) 
WHERE g2.objectid ENDS WITH '-512' 
MATCH (c2:Computer)-[:HasSession]->(u1) 
WHERE NOT c2.name IN domainControllers 
RETURN u1.name AS Username, COUNT(c2) AS Sessions ORDER BY COUNT(c2) DESC

All DA/EA/BA:

1
2
3
MATCH (u:User)-[:MemberOf*1..]->(g:Group) 
WHERE g.objectid ENDS WITH '-512' OR g.objectid ENDS WITH '-544' OR g.objectid ENDS WITH '-519' 
RETURN u.name AS Username, g.name as Group

All other high value:

1
2
3
4
MATCH (u:User)-[:MemberOf*1..]->(g:Group)
WHERE g.highvalue = true 
AND NOT (g.objectid ENDS WITH '-512' OR g.objectid ENDS WITH '-544' OR g.objectid ENDS WITH '-519') 
RETURN u.name AS Username, g.name as Group

All user object permissions in domain:

1
2
3
MATCH p=(m:Group)-[r:AddMember|AdminTo|AllExtendedRights|AllowedToDelegate|CanRDP|Contains|ExecuteDCOM|ForceChangePassword|GenericAll|GenericWrite|GetChanges|GetChangesAll|HasSession|Owns|ReadLAPSPassword|SQLAdmin|TrustedBy|WriteDacl|WriteOwner|AddAllowedToAct|AllowedToAct]->(t) 
WHERE m.objectid ENDS WITH '-513' OR m.objectid ENDS WITH '-515' OR m.objectid ENDS WITH 'S-1-5-11' OR m.objectid ENDS WITH 'S-1-1-0' 
RETURN m.name,TYPE(r),t.name,t.enabled

All non-highvalue groups permissions in domain:

1
2
3
MATCH p=(m:Group)-[r:AddMember|AdminTo|AllExtendedRights|AllowedToDelegate|CanRDP|Contains|ExecuteDCOM|ForceChangePassword|GenericAll|GenericWrite|GetChanges|GetChangesAll|HasSession|Owns|ReadLAPSPassword|SQLAdmin|TrustedBy|WriteDacl|WriteOwner|AddAllowedToAct|AllowedToAct]->(t) 
WHERE m.highvalue = false
RETURN m.name,TYPE(r),COUNT(t)

Domain Admins with a password last changed over a year ago:

1
2
3
4
5
MATCH (u:User)-[:MemberOf*1..]->(g:Group) 
WHERE g.name =~ '(?i).*domain admin.*' 
AND u.pwdlastset < (datetime().epochseconds - (360 * 86400)) 
AND NOT u.pwdlastset IN [-1.0, 0.0] 
RETURN u.name, u.pwdlastset

Get all Domains and their Functional Level

1
2
3
MATCH (d:Domain)
RETURN d.name AS DomainName, d.functionallevel AS FunctionalLevel
ORDER BY d.name

Get all users and hosts they can RDP to. Show if it is direct asignment or via a group

1
2
3
4
5
MATCH (u:User)-[r:CanRDP]->(c:Computer)
RETURN u.name AS User, c.name AS Hostname, "Direct Assignment" AS AssignmentType, "" AS Group
UNION
MATCH (u:User)-[:MemberOf*1..]->(g:Group)-[r:CanRDP]->(c:Computer)
RETURN u.name AS User, c.name AS Hostname, "Group Membership" AS AssignmentType, g.name AS Group

Get all users and hosts they are Local Admin to. Show if it is direct asignment or via a group

1
2
3
4
5
MATCH (u:User)-[r:AdminTo]->(c:Computer)
RETURN u.name AS User, c.name AS Hostname, "Direct Assignment" AS AssignmentType, "" AS Group
UNION
MATCH (u:User)-[:MemberOf*1..]->(g:Group)-[:AdminTo]->(c:Computer)
RETURN u.name AS User, c.name AS Hostname, "Group Membership" AS AssignmentType, g.name AS Group

Get all EA/DA/Admins with their cracked and enabled status

1
2
3
4
5
6
MATCH (g:Group)
WHERE g.objectid ENDS WITH '-519'  // EA
   OR g.objectid ENDS WITH '-512'  // DA
   OR g.objectid ENDS WITH '-544'  // Administrators
MATCH (u:User)-[:MemberOf*1..]->(g)
RETURN DISTINCT u.name AS UserName, u.cracked as Cracked, u.password as Password, u.enabled as Enabled

Find all objects which support legacy DES encryption types

MATCH (n)
WHERE EXISTS(n.supportedencryptiontypes) AND
     ("DES-CBC-CRC" IN n.supportedencryptiontypes OR
      "DES-CBC-MD5" IN n.supportedencryptiontypes)

WITH n, [l IN labels(n) WHERE l <> 'Base'] AS realLabels
RETURN n.name                                          AS Principal,
       realLabels[0]                                   AS Type,
       n.supportedencryptiontypes
ORDER BY Type, Principal;