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
| 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)
| 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:
| MATCH p = (g:AZServicePrincipal)-[r]->(n)
WHERE g.ServicePrincipalType = Application
RETURN p
|
Find all DA session not on DCs
| 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
| 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
| 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)
| 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
| 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
| 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
| MATCH (u:User)
WHERE u.dontreqpreauth=true
AND u.enabled=true
RETURN u.name
|
Look up username from email
| 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
| 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
| MATCH (o:OU)-[:Contains]->(c:Computer)
WHERE o.name =~'(?i).*DOMAIN CONTROLLERS.*'
RETURN o.name, c.name, c.operatingsystem
|
Unsupported OS
| 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:
| 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
|
| 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:
| 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
| 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
| 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:
| 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:
| 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:
| 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:
| 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:
| 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
| 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
| 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
| 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
| 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;
|