General

  • Counts various Active Directory statistics and weaknesses. Change contoso.com to your own domain name or leave it empty (ENDS WITH "") for all domains:
MATCH (u:User)                                        WHERE toLower(u.name) ENDS WITH "contoso.com" RETURN "Users in total" AS what, count(u) AS number UNION ALL
MATCH (u:User {enabled: false})                       WHERE toLower(u.name) ENDS WITH "contoso.com" RETURN "Disabled Users" AS what, count(u) AS number UNION ALL
MATCH (u:User {enabled: true, allowedtodelegate: true}) WHERE toLower(u.name) ENDS WITH "contoso.com" RETURN "Enabled Users with Allowed to Delegate" AS what, count(u) AS number UNION ALL
MATCH (u:User {enabled: true, unconstraineddelegation: true}) WHERE toLower(u.name) ENDS WITH "contoso.com" RETURN "Enabled Users with Unconstrained Delegation" AS what, count(u) AS number UNION ALL
MATCH (u:User {enabled: true, admincount: true})      WHERE toLower(u.name) ENDS WITH "contoso.com" RETURN "Enabled Users with Admin Count = 1" AS what, count(u) AS number UNION ALL
MATCH (u:User {enabled: true,  hasspn: True})         WHERE toLower(u.name) ENDS WITH "contoso.com" AND NOT u.name STARTS WITH 'KRBTGT' RETURN "Kerberoastable & Enabled Users" AS what, count(u) AS number UNION ALL
MATCH (u:User {enabled: false, hasspn: True})         WHERE toLower(u.name) ENDS WITH "contoso.com" AND NOT u.name STARTS WITH 'KRBTGT' RETURN "Kerberoastable & Disabled Users" AS what, count(u) AS number UNION ALL
MATCH (u:User {enabled: true, passwordnotreqd: true}) WHERE toLower(u.name) ENDS WITH "contoso.com" RETURN "Enabled Users with Password Not Required" AS what, count(u) AS number UNION ALL
MATCH (u:User {enabled: true, pwdneverexpires: true}) WHERE toLower(u.name) ENDS WITH "contoso.com" RETURN "Enabled Users with Password Never Expires" AS what, count(u) AS number UNION ALL
MATCH (u:User {enabled: true, dontreqpreauth: true})  WHERE toLower(u.name) ENDS WITH "contoso.com" RETURN "Enabled Users with Dont Require Pre-Authentication (ASREP roastable)" AS what, count(u) AS number UNION ALL
MATCH (u:User {enabled: true})                        WHERE toLower(u.name) ENDS WITH "contoso.com" AND u.pwdlastset > 0 AND u.lastlogon > 0 WITH u.name AS name, u.description AS description, u.enabled AS enabled, datetime({ epochSeconds:toInteger(u.pwdlastset) }) AS pwdlastset, duration.inDays(datetime({ epochSeconds:toInteger(u.pwdlastset) }), date()).days AS days_since_pwdlastset, datetime({ epochSeconds:toInteger(u.lastlogon) }) AS lastlogon, duration.inDays(datetime({ epochSeconds:toInteger(u.lastlogon) }), date()).days AS days_since_lastlogon WHERE days_since_pwdlastset > 90 AND days_since_lastlogon < 7 RETURN "Enabled Users pwdlastset > 90 days and lastlogon < 7 days" AS what, count(name) AS number UNION ALL
MATCH (u:User {enabled: true})                        WHERE toLower(u.name) ENDS WITH "contoso.com" AND u.pwdlastset > 0 AND u.lastlogon > 0 WITH u.name AS name, u.description AS description, u.enabled AS enabled, datetime({ epochSeconds:toInteger(u.pwdlastset) }) AS pwdlastset, duration.inDays(datetime({ epochSeconds:toInteger(u.pwdlastset) }), date()).days AS days_since_pwdlastset WHERE days_since_pwdlastset > 90 RETURN "Enabled Users pwdlastset > 90 days" AS what, count(name) AS number UNION ALL
MATCH (u:User {enabled: true})                        WHERE toLower(u.name) ENDS WITH "contoso.com" AND u.pwdlastset > 0 AND u.lastlogon > 0 WITH u.name AS name, u.description AS description, u.enabled AS enabled, datetime({ epochSeconds:toInteger(u.lastlogon) }) AS lastlogon, duration.inDays(datetime({ epochSeconds:toInteger(u.lastlogon) }), date()).days AS days_since_lastlogon WHERE days_since_lastlogon > 90 RETURN "Enabled Users lastlogon > 180 days" AS what, count(name) AS number UNION ALL
MATCH (u:User {enabled: false})                       WHERE toLower(u.name) ENDS WITH "contoso.com" AND u.pwdlastset > 0 AND u.lastlogon > 0 WITH u.name AS name, u.description AS description, u.enabled AS enabled, datetime({ epochSeconds:toInteger(u.pwdlastset) }) AS pwdlastset, duration.inDays(datetime({ epochSeconds:toInteger(u.pwdlastset) }), date()).days AS days_since_pwdlastset, datetime({ epochSeconds:toInteger(u.lastlogon) }) AS lastlogon, duration.inDays(datetime({ epochSeconds:toInteger(u.lastlogon) }), date()).days AS days_since_lastlogon WHERE days_since_pwdlastset > 90 AND days_since_lastlogon < 7 RETURN "Disabled Users pwdlastset > 90 days and lastlogon < 7 days" AS what, count(name) AS number UNION ALL
MATCH (u:User {enabled: false})                       WHERE toLower(u.name) ENDS WITH "contoso.com" AND u.pwdlastset > 0 AND u.lastlogon > 0 WITH u.name AS name, u.description AS description, u.enabled AS enabled, datetime({ epochSeconds:toInteger(u.pwdlastset) }) AS pwdlastset, duration.inDays(datetime({ epochSeconds:toInteger(u.pwdlastset) }), date()).days AS days_since_pwdlastset WHERE days_since_pwdlastset > 90 RETURN "Disabled Users pwdlastset > 90 days" AS what, count(name) AS number UNION ALL
MATCH (u:User {enabled: false})                       WHERE toLower(u.name) ENDS WITH "contoso.com" AND u.pwdlastset > 0 AND u.lastlogon > 0 WITH u.name AS name, u.description AS description, u.enabled AS enabled, datetime({ epochSeconds:toInteger(u.lastlogon) }) AS lastlogon, duration.inDays(datetime({ epochSeconds:toInteger(u.lastlogon) }), date()).days AS days_since_lastlogon WHERE days_since_lastlogon > 90 RETURN "Disabled Users lastlogon > 180 days" AS what, count(name) AS number UNION ALL
MATCH (u:Computer)                                    WHERE toLower(u.name) ENDS WITH "contoso.com" RETURN "Computers in total" AS what, count(u) AS number UNION ALL
MATCH (u:Group)                                       WHERE toLower(u.name) ENDS WITH "contoso.com" RETURN "Groups in total" AS what, count(u) AS number UNION ALL
MATCH (u:Domain)                                      WHERE toLower(u.name) ENDS WITH "contoso.com" RETURN "Domains in total" AS what, count(u) AS number UNION ALL
MATCH (u:OU)                                          WHERE toLower(u.name) ENDS WITH "contoso.com" RETURN "OUs in total" AS what, count(u) AS number UNION ALL
MATCH (u:GPO)                                         WHERE toLower(u.name) ENDS WITH "contoso.com" RETURN "GPOs in total" AS what, count(u) AS number UNION ALL
MATCH (u {admincount: True})                          WHERE toLower(u.name) ENDS WITH "contoso.com" RETURN "adminCount=1" AS what, count(u) AS number UNION ALL
MATCH (u)                                             WHERE toLower(u.name) ENDS WITH "contoso.com" AND u.userpassword =~ ".+" RETURN "userPassword Not Empty" AS what, count(u) AS number UNION ALL
MATCH (u:Computer {unconstraineddelegation: True})-[:MemberOf]->(g:Group) WHERE toLower(u.name) ENDS WITH "contoso.com" AND (NOT g.name STARTS WITH 'DOMAIN CONTROLLERS') AND (NOT u.distinguishedname CONTAINS "Domain Controllers") RETURN "Unconstrained Delegation Computers" AS what, count(u) AS number UNION ALL
MATCH (u {owned: true})                               WHERE toLower(u.name) ENDS WITH "contoso.com" RETURN "Owned Principals" AS what, count(u) AS number UNION ALL
MATCH (u {highvalue: true})                           WHERE toLower(u.name) ENDS WITH "contoso.com" RETURN "High Value" AS what, count(u) AS number
  • Returns all objects that have SPNs set and checks whether they are allowed to delegate, have admincount set or can be used for unconstrained delegation:
MATCH (c {hasspn: True}) RETURN c.name as name, c.allowedtodelegate as AllowedToDelegate, c.unconstraineddelegation as UnconstrainedDelegation, c.admincount as AdminCount, c.serviceprincipalnames as SPNs

Principals with most Outbound Controlled objects

  • Returns Top 100 Outbound Control Rights --> First Degree Object Control principals in domain:
MATCH p=(u)-[r1]->(n) WHERE r1.isacl=true 
WITH u.name as name, LABELS(u)[1] as type, 
COUNT(DISTINCT(n)) as controlled 
WHERE name IS NOT NULL 
RETURN type, name, controlled 
ORDER BY controlled DESC 
LIMIT 100
  • Returns Top 100 Outbound Control Rights --> Group Delegated Object Control principals in domain and whether that object is member of high privileged group (such a Domain Admins or Domain Controllers):
MATCH p=(u)-[r1:MemberOf*1..]->(g:Group)-[r2]->(n) WHERE r2.isacl=true
WITH u.name as name, LABELS(u)[1] as type, g.highvalue as highly_privileged,
COUNT(DISTINCT(n)) as controlled 
WHERE name IS NOT NULL 
RETURN type, name, highly_privileged, controlled 
ORDER BY controlled DESC 
LIMIT 100
  • Returns Top 50 Outbound Control Rights --> Transitive Object Control in domain (TAKES ENORMOUS TIME TO COMPUTE! You were warned):
MATCH p=shortestPath((u)-[r1:MemberOf|AddMember|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns*1..]->(n))
WHERE u<>n
WITH u.name as name, LABELS(u)[1] as type, 
COUNT(DISTINCT(n)) as controlled 
WHERE name IS NOT NULL
RETURN type, name, controlled 
ORDER BY controlled DESC 
LIMIT 50
  • Returns principals having more than 1000 Outbound Control Rights --> First Degree Object Control controlled:
MATCH p=(u)-[r1]->(n) WHERE r1.isacl=true 
WITH u.name as name, LABELS(u)[1] as type, 
COUNT(DISTINCT(n)) as controlled 
WHERE name IS NOT NULL AND controlled > 1000
RETURN type, name, controlled 
ORDER BY controlled DESC 
  • Returns principals having more than 1000 Outbound Control Rights --> Group Delegated Object Control controlled and whether that object is member of high privileged group (such a Domain Admins or Domain Controllers):
MATCH p=(u)-[r1:MemberOf*1..]->(g:Group)-[r2]->(n) WHERE r2.isacl=true
WITH u.name as name, LABELS(u)[1] as type, g.highvalue as highly_privileged,
COUNT(DISTINCT(n)) as controlled 
WHERE name IS NOT NULL AND controlled > 1000
RETURN type, name, highly_privileged, controlled 
ORDER BY controlled DESC 
  • Returns principals having more than 1000 Outbound Control Rights --> Transitive Object Control controlled (TAKES ENORMOUS TIME TO COMPUTE! You were warned):
MATCH p=shortestPath((u)-[r1:MemberOf|AddMember|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns*1..]->(n))
WHERE u<>n
WITH u.name as name, LABELS(u)[1] as type, 
COUNT(DISTINCT(n)) as controlled 
WHERE name IS NOT NULL AND controlled > 1000
RETURN type, name, controlled 
ORDER BY controlled DESC 

Users

  • Enabled Users with Password Last Set > 90 days and Last Logon < 7 days:
MATCH (u:User {enabled: true}) WHERE u.pwdlastset > 0 AND u.lastlogon > 0
WITH u.name AS name, u.description AS description, u.enabled AS enabled, datetime({ epochSeconds:toInteger(u.pwdlastset) }) AS pwdlastset, duration.inDays(datetime({ epochSeconds:toInteger(u.pwdlastset) }), date()).days AS days_since_pwdlastset, datetime({ epochSeconds:toInteger(u.lastlogon) }) AS lastlogon, duration.inDays(datetime({ epochSeconds:toInteger(u.lastlogon) }), date()).days AS days_since_lastlogon
WHERE days_since_pwdlastset > 90 AND days_since_lastlogon < 7
RETURN name, description, days_since_lastlogon, days_since_pwdlastset, pwdlastset, lastlogon
ORDER BY days_since_pwdlastset DESC
  • Enabled Users with Last Logon earlier than 90 days ago:
MATCH (u:User {enabled: true}) WHERE u.lastlogon > 0 
WITH u.name AS name, u.description AS description, u.enabled AS enabled, datetime({ epochSeconds:toInteger(u.lastlogon) }) AS lastlogon, duration.inDays(datetime({ epochSeconds:toInteger(u.lastlogon) }), date()).days AS days_since_lastlogon
WHERE days_since_lastlogon > 90
RETURN name, description, days_since_lastlogon, lastlogon 
ORDER BY days_since_lastlogon DESC 
  • Enabled Users with Password Last Set earlier than 90 days ago:
MATCH (u:User {enabled: true}) WHERE u.pwdlastset > 0 
WITH u.name AS name, u.description AS description, u.enabled AS enabled, datetime({ epochSeconds:toInteger(u.pwdlastset) }) AS pwdlastset, duration.inDays(datetime({ epochSeconds:toInteger(u.pwdlastset) }), date()).days AS days_since_pwdlastset
WHERE days_since_pwdlastset > 90
RETURN name, description, days_since_pwdlastset, pwdlastset 
ORDER BY days_since_pwdlastset DESC 
  • Pulls kerberoastable Users belonging to critical groups such as Domain Admins, Schema Admins, Domain Controllers, Enterprise Admins:
MATCH (u:User {hasspn: True})-[r:MemberOf*1..]->(n:Group) 
WHERE (n.objectid =~ "(?i)S-1-5-.*-512") OR (n.objectid =~ "(?i)S-1-5-.*-516") OR (n.objectid =~ "(?i)S-1-5-.*-518") OR (n.objectid =~ "(?i)S-1-5-.*-519") OR (n.objectid =~ "(?i)S-1-5-.*-520") OR (n.objectid =~ "(?i)S-1-5-.*-544") OR (n.objectid =~ "(?i)S-1-5-.*-548") OR (n.objectid =~ "(?i)S-1-5-.*-549") OR (n.objectid =~ "(?i)S-1-5-.*-551")
RETURN u.name AS UserName, n.name AS GroupName, u.displayname As DisplayName, u.description As Descrition
ORDER BY GroupName
  • Pulls users eligible for ASREP roasting
MATCH (u:User {dontreqpreauth: true}) RETURN u.name, u.displayname, u.description, u.objectid
  • Shortest path from ASREP roastable users to Domain Admins
MATCH (A:User {dontreqpreauth: true}), (B:Group), x=shortestPath((A)-[*1..]->(B)) WHERE B.name STARTS WITH 'DOMAIN ADMINS' RETURN x
  • Pulls users with adminCount=1
MATCH (u:User {admincount: True}) WHERE NOT u.name starts with 'KRBTGT' RETURN u.name, u.displayname, u.description, u.objectid
  • Pulls users with PasswordNeverExpires set.
MATCH (u:User {pwdneverexpires: True}) WHERE NOT u.name starts with 'KRBTGT' RETURN u.name, u.displayname, u.description, u.objectid
  • Pulls kerberoastable users with adminCount=1
MATCH (u:User {admincount: True, hasspn: True}) WHERE NOT u.name starts with 'KRBTGT' RETURN u.name, u.displayname, u.hasspn as Kerberoastable, u.description, u.objectid
  • Pulls users with adminCount=1 and displays whether they're Kerberoastable, ASREPRoastable or Owned
MATCH (u:User {admincount: True}) WHERE NOT u.name starts with 'KRBTGT' RETURN u.name, u.displayname, u.owned as owned, u.hasspn as Kerberoastable, u.dontreqpreauth as ASREPRoastable, u.description, u.objectid
  • Pulls users eligible for Kerberoasting
MATCH (u:User {hasspn: True}) WHERE NOT u.name starts with 'KRBTGT' RETURN u.name, u.displayname, u.description, u.objectid
  • Return Kerberoastable users with a path to High Value groups:
MATCH p=shortestPath((u:User {hasspn: true})-[r:MemberOf*1..]->(g:Group {highvalue: true})) RETURN u.name AS kerberoastable_user, g.name AS high_value_group, u.displayname AS user_displayname
  • Shortest path from Kerberoastable users to Domain Admins
MATCH (A:User),(B:Group),p=shortestPath((A)-[*1..]->(B)) WHERE A.hasspn=true AND B.name STARTS WITH 'DOMAIN ADMINS' RETURN p
  • Shortest path from any user that has PASSWORD_NOT_REQUIRED set to any computer
MATCH (m:User {enabled: True, passwordnotreqd: True}), (n:Computer), p = shortestPath((m)-[*1..]->(n)) RETURN p
  • Find all users that have userPassword attribute not empty
MATCH (u:User) WHERE u.userpassword =~ ".+" RETURN u.name, u.userpassword, u.displayname, u.description, u.objectid
  • Return enabled users that have PASSWORD_NOT_REQUIRED flag set in their UserAccountControl field (thus they have an empty password set)
MATCH (u:User {enabled: True, passwordnotreqd: True}) RETURN u.name, u.displayname, u.description, u.objectid
  • Find enabled users not requiring Pre-Authentication (their passwords will be a lot easier to crack):
MATCH (u:User {enabled: True, dontreqpreauth: true}) RETURN u.name, u.displayname, u.description, u.objectid
  • Find a shortest path from any user that has PASSWORD_NOT_REQUIRED set to Domain Admins group:
MATCH (m:User {enabled: True, passwordnotreqd: True}), (n:Group), p = shortestPath((m)-[*1..]->(n)) WHERE n.name STARTS WITH 'DOMAIN ADMINS' RETURN p
  • Find all users that have direct or indirect admin privileges over a computer:
MATCH (u:User)-[r:AdminTo|MemberOf*1..]->(c:Computer) RETURN u.name
  • Find all the users that can RDP into a machine where they have special privileges:
MATCH (u:User)-[:CanRDP]->(c:Computer) WITH u,c
OPTIONAL MATCH (u)-[:MemberOf*1..]->(g:Group)-[:CanRDP]->(c) WITH u,c
MATCH (u)-[:CanPrivesc]->(c) RETURN u.name, c.name
  • Pulls Kerberoastable users and returns their Outbound Control Rights --> First Degree Object Control in domain:
MATCH (u:User {hasspn: True}), p=(u)-[r1]->(n)
WHERE NOT u.name starts with 'KRBTGT' AND r1.isacl=true
WITH u.name as name, LABELS(u)[1] as type, 
COUNT(DISTINCT(n)) as controlled 
WHERE name IS NOT NULL 
RETURN type, name, controlled 
ORDER BY controlled DESC 
  • Pulls Kerberoastable users and returns their Outbound Control Rights --> Group Delegated Object Control in domain and whether that object is member of high privileged group (such a Domain Admins or Domain Controllers):
MATCH (u:User {hasspn: True}), p=(u)-[r1:MemberOf*1..]->(g:Group)-[r2]->(n) 
WHERE NOT u.name starts with 'KRBTGT' AND r2.isacl=true
WITH u.name as name, LABELS(u)[1] as type, g.highvalue as highly_privileged,
COUNT(DISTINCT(n)) as controlled 
WHERE name IS NOT NULL 
RETURN type, name, highly_privileged, controlled 
ORDER BY controlled DESC 
  • Pulls Kerberoastable users and returns their Outbound Control Rights --> Transitive Object Control in domain (TAKES ENORMOUS TIME TO COMPUTE! You were warned):
MATCH (u:User {hasspn: True}), p=shortestPath((u)-[r1:MemberOf|AddMember|AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns*1..]->(n))
WHERE NOT u.name starts with 'KRBTGT' AND u<>n
WITH u.name as name, LABELS(u)[1] as type, 
COUNT(DISTINCT(n)) as controlled 
WHERE name IS NOT NULL
RETURN type, name, controlled 
ORDER BY controlled DESC 
  • Returns username and number of computers where it has admin rights to for top 10 users (author: jeffmcjunkin ):
MATCH 
(U:User)-[r:MemberOf|AdminTo*1..]->(C:Computer)
WITH
U.name as n,
COUNT(DISTINCT(C)) as c 
RETURN n,c
ORDER BY c DESC
LIMIT 10
  • Returns group and number of computers that group has admin rights to - for top 10 groups (author: jeffmcjunkin ):
MATCH 
(G:Group)-[r:MemberOf|AdminTo*1..]->(C:Computer)
WITH
G.name as n,
COUNT(DISTINCT(C)) as c 
RETURN n,c
ORDER BY c DESC
LIMIT 10
  • Show all users that are administrators on more than one machine (author: jeffmcjunkin ):
MATCH 
(U:User)-[r:MemberOf|AdminTo*1..]->(C:Computer)
WITH
U.name as n,
COUNT(DISTINCT(C)) as c 
WHERE c>1
RETURN n
ORDER BY c DESC
  • Show all users that are administrative on at least one machine, ranked by the number of machines they are admin on. (author: jeffmcjunkin ):
MATCH (u:User)
WITH u
OPTIONAL MATCH (u)-[r:AdminTo]->(c:Computer)
WITH u,COUNT(c) as expAdmin
OPTIONAL MATCH (u)-[r:MemberOf*1..]->(g:Group)-[r2:AdminTo]->(c:Computer)
WHERE NOT (u)-[:AdminTo]->(c)
WITH u,expAdmin,COUNT(DISTINCT(c)) as unrolledAdmin
RETURN u.name,expAdmin,unrolledAdmin,expAdmin + unrolledAdmin as totalAdmin
ORDER BY totalAdmin ASC
  • Returns shortest path from any of owned nodes to any of highvalue nodes:
RETURN shortestPath((O:{owned:True})-[*1..]->(H {highvalue: True}))

Groups

  • Find the most privileged groups on the domain (author: hausec ):
MATCH (g:Group) OPTIONAL MATCH (g)-[:AdminTo]->(c1:Computer) OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c2:Computer) WITH g, COLLECT(c1) + COLLECT(c2) AS tempVar UNWIND tempVar AS computers RETURN g.name AS GroupName,COUNT(DISTINCT(computers)) AS AdminRightCount ORDER BY AdminRightCount DESC
  • Find groups with most local admins (either explicit admins or derivative/unrolled) (modified version of query taken from almighty hausec ):
MATCH (g:Group) WITH g OPTIONAL MATCH (g)-[r:AdminTo]->(c1:Computer) WITH g,COUNT(c1) as explicitAdmins OPTIONAL MATCH (g)-[r:MemberOf*1..]->(a:Group)-[r2:AdminTo]->(c2:Computer) WITH g,explicitAdmins,COUNT(DISTINCT(c2)) as unrolledAdmins where g.name IS NOT NULL AND (explicitAdmins + unrolledAdmins) > 0 RETURN g.name,explicitAdmins,unrolledAdmins, explicitAdmins + unrolledAdmins as totalAdmins ORDER BY totalAdmins DESC
  • Counts unrolled members of Tier-0 privileged AD groups (copy all query lines, as they are UNION ALL joined):
MATCH (u)-[:MemberOf*1..]->(g:Group) WHERE g.name starts with "ENTERPRISE ADMINS" RETURN g.name AS GroupName, count(u) AS MembersCounted UNION ALL
MATCH (u)-[:MemberOf*1..]->(g:Group) WHERE g.name starts with "DOMAIN ADMINS" RETURN g.name AS GroupName, count(u) AS MembersCounted UNION ALL
MATCH (u)-[:MemberOf*1..]->(g:Group) WHERE g.name starts with "SCHEMA ADMIN" RETURN g.name AS GroupName, count(u) AS MembersCounted UNION ALL
MATCH (u)-[:MemberOf*1..]->(g:Group) WHERE g.name starts with "ACCOUNT OPERATORS" RETURN g.name AS GroupName, count(u) AS MembersCounted UNION ALL
MATCH (u)-[:MemberOf*1..]->(g:Group) WHERE g.name starts with "BACKUP OPERATORS" RETURN g.name AS GroupName, count(u) AS MembersCounted UNION ALL
MATCH (u)-[:MemberOf*1..]->(g:Group) WHERE g.name starts with "PRINT OPERATORS" RETURN g.name AS GroupName, count(u) AS MembersCounted UNION ALL
MATCH (u)-[:MemberOf*1..]->(g:Group) WHERE g.name starts with "SERVER OPERATORS" RETURN g.name AS GroupName, count(u) AS MembersCounted UNION ALL
MATCH (u)-[:MemberOf*1..]->(g:Group) WHERE g.name starts with "DOMAIN CONTROLLERS" RETURN g.name AS GroupName, count(u) AS MembersCounted UNION ALL
MATCH (u)-[:MemberOf*1..]->(g:Group) WHERE g.name starts with "READ-ONLY DOMAIN CONTROLLERS" RETURN g.name AS GroupName, count(u) AS MembersCounted UNION ALL
MATCH (u)-[:MemberOf*1..]->(g:Group) WHERE g.name starts with "GROUP POLICY CREATOR OWNERS" RETURN g.name AS GroupName, count(u) AS MembersCounted UNION ALL
MATCH (u)-[:MemberOf*1..]->(g:Group) WHERE g.name starts with "CRYPTOGRAPHIC OPERATORS" RETURN g.name AS GroupName, count(u) AS MembersCounted UNION ALL
MATCH (u)-[:MemberOf*1..]->(g:Group) WHERE g.name starts with "DISTRIBUTED COM USERS" RETURN g.name AS GroupName, count(u) AS MembersCounted

Computers

  • Returns enabled computers with PwdLastSet > 30 days and LastLogon < 30 days:
MATCH (u:Computer {enabled: true}) WHERE u.pwdlastset > 0 AND u.lastlogon > 0
WITH u.name AS name, u.description AS description, u.enabled AS enabled, datetime({ epochSeconds:toInteger(u.pwdlastset) }) AS pwdlastset, duration.inDays(datetime({ epochSeconds:toInteger(u.pwdlastset) }), date()).days AS days_since_pwdlastset, datetime({ epochSeconds:toInteger(u.lastlogon) }) AS lastlogon, duration.inDays(datetime({ epochSeconds:toInteger(u.lastlogon) }), date()).days AS days_since_lastlogon
WHERE days_since_pwdlastset > 30 AND days_since_lastlogon < 30
RETURN name, description, days_since_lastlogon, days_since_pwdlastset, pwdlastset, lastlogon
ORDER BY days_since_pwdlastset DESC
  • Returns computer names and their operating system for statistics purposes
MATCH (c:Computer) WHERE c.operatingsystem is not null RETURN c.name as Name, c.operatingsystem as OS
  • Returns a summary report of machines grouped by their operating systems versions, along with number of machines running specific OS version:
MATCH (c:Computer) WHERE c.operatingsystem is not null MATCH (n:Computer {operatingsystem: c.operatingsystem}) RETURN c.operatingsystem as OS, count(distinct n) AS Number ORDER BY Number DESC
  • Returns non-DC computers that enable unconstrained delegation along with their LDAP DN paths and operating systems.:
MATCH (c:Computer {unconstraineddelegation: True})-[:MemberOf]->(g:Group) WHERE (NOT g.name STARTS WITH 'DOMAIN CONTROLLERS') AND (NOT c.distinguishedname CONTAINS "Domain Controllers") RETURN c.name, c.distinguishedname, c.operatingsystem
  • Riccardo Ancarani's cypher queries (src: GPOPowerParser) useful for any lateral movement insights:
  • Find all the NTLM relay opportunities for computer accounts:
MATCH (u1:Computer)-[:AdminTo]->(c1:Computer {signing: false}) RETURN u1.name, c1.name
MATCH (u2)-[:MemberOf*1..]->(g:Group)-[:AdminTo]->(c2 {signing: false}) RETURN u2.name, c2.name
  • PrivExchange audit: Finds computers that are members of "Exchange Trusted Subsystem" group, which has admin rights over all its members. This way, we could execute authentication coercion attack against one exchange server and relay it to another, thus obtaining SYSTEM over that another Exchange server:
MATCH p=(c:Computer)-[r1:MemberOf*1..]->(g:Group)-[r2:AdminTo]->(n:Computer) RETURN p

GPOs

  • Print GPO names and their container paths:
MATCH (n:GPO) return n.name,n.gpcpath
  • Pull GPOs linked to users being member of a specified group:
MATCH p = (:GPO)-[:GpLink]->(d)-[:Contains*1..]->(u:User)-[:MemberOf*1..]->(g:Group {name:'GROUP_NAME@CONTOSO.LOCAL'}) RETURN p
  • Print GPOs with interesting words in their names along with their container paths:
unwind ["360totalsecurity", "access", "acronis", "adaware", "admin", "admin", "aegislab", "ahnlab", "alienvault", "altavista", "amsi", "anti-virus", "antivirus", "antiy", "apexone", "applock", "arcabit", "arcsight", "atm", "atp", "av", "avast", "avg", "avira", "baidu", "baiduspider", "bank", "barracuda", "bingbot", "bitdefender", "bluvector", "canary", "carbon", "carbonblack", "certificate", "check", "checkpoint", "citrix", "clamav", "code42", "comodo", "countercept", "countertack", "credential", "crowdstrike", "custom", "cyberark", "cybereason", "cylance", "cynet360", "cyren", "darktrace", "datadog", "defender", "druva", "drweb", "duckduckbot", "edr", "egambit", "emsisoft", "encase", "endgame", "ensilo", "escan", "eset", "exabot", "exception", "f-secure", "f5", "falcon", "fidelis", "fireeye", "firewall", "fix", "forcepoint", "forti", "fortigate", "fortil", "fortinet", "gdata", "gravityzone", "guard", "honey", "huntress", "identity", "ikarussecurity", "insight", "ivanti", "juniper", "k7antivirus", "k7computing", "kaspersky", "kingsoft", "kiosk", "laps", "lightcyber", "logging", "logrhythm", "lynx", "malwarebytes", "manageengine", "mass", "mcafee", "microsoft", "mj12bot", "msnbot", "nanoav", "nessus", "netwitness", "office365", "onedrive", "orion", "palo", "paloalto", "paloaltonetworks", "panda", "pass", "powershell", "proofpoint", "proxy", "qradar", "rdp", "rsa", "runasppl", "sandboxe", "sap", "scanner", "scanning", "sccm", "script", "secret", "secureage", "secureworks", "security", "sensitive", "sentinel", "sentinelone", "slurp", "smartcard", "sogou", "solarwinds", "sonicwall", "sophos", "splunk", "superantispyware", "symantec", "tachyon", "temporary", "tencent", "totaldefense", "transfer", "trapmine", "trend micro", "trendmicro", "trusteer", "trustlook", "uac", "vdi", "virusblokada", "virustotal", "virustotalcloud", "vpn", "vuln", "webroot", "whitelist", "wifi", "winrm", "workaround", "yubikey", "zillya", "zonealarm", "zscaler"] as word match (n:GPO) where toLower(n.name) CONTAINS toLower(word) RETURN word, n.name, n.description, n.gpcpath ORDER BY n.name

OUs

  • Returns a list of OUs along with their members count (source: hausec.com )
MATCH (o:OU)-[:Contains]->(c) RETURN o.name,o.guid, COUNT(c) ORDER BY COUNT(c) DESC

Other

  • Retrieves nodes having particular juicy keywords in their name or description properties:
UNWIND ["admin", "amministratore", "contrase", "empfindlich", "geheim", "hasło", "important", "azure", "MSOL", "Kennwort", "parol", "parola", "pass", "passe", "secret", "secreto", "segreto", "sekret", "sensibil", "sensibile", "sensible", "sensitive", "wrażliw"] AS word MATCH (n) WHERE (toLower(n.name) CONTAINS toLower(word)) OR (toLower(n.description) CONTAINS toLower(word)) RETURN word, n.name, n.description ORDER BY n.name
  • Retrieves nodes that contain UNC paths to SMB shares in their description fields:
MATCH (n) WHERE n.description CONTAINS '\\\\' RETURN n.name, n.description
  • Print Security Solutions (think SIEM, EDRs, AVs, Anomaly detection systems, etc) deployed in the company by searching for keywords in name, description, distinguishedname of all objects (User, Group, Computer, OU, ...)
UNWIND ["360totalsecurity", "acronis", "adaware", "aegislab", "ahnlab", "alienvault", "altavista", "anti-virus", "antivirus", "antiy", "apexone", "arcabit", "arcsight", "attivo", "avast", "avg", "avira", "baidu", "baiduspider", "barracuda", "bingbot", "bitdefender", "bitdefender", "bluecoat", "bluvector", "canary", "carbon", "carbonblack", "carbonblack", "check", "checkpoint", "clamav", "code42", "comodo", "cortex", "countercept", "countertack", "crowdstrike", "cyberark", "cybereason", "cylance", "cynet360", "cyren", "darktrace", "datadog", "defender", "druva", "drweb", "duckduckbot", "edr", "egambit", "emsisoft", "encase", "endgame", "ensilo", "escan", "eset", "exabot", "f-secure", "f5", "falcon", "fidelis", "fireeye", "forcepoint", "fortigate", "fortil", "fortinet", "gdata", "gdata", "gravityzone", "honey", "huntress", "ia_archiver", "ikarussecurity", "ivanti", "juniper", "k7antivirus", "k7computing", "kaspersky", "kingsoft", "lightcyber", "lynx", "malwarebytes", "mcafee", "microsoft", "mj12bot", "morphisec", "msnbot", "nanoav", "nessus", "netwitness", "office365", "palo", "paloalto", "paloaltonetworks", "panda", "proofpoint", "qradar", "sandboxe", "scanner", "scanning", "secureage", "secureworks", "security", "sentinelone", "simplepie", "slurp", "sogou", "solarwinds", "sonicwall", "sophos", "splunk", "superantispyware", "symantec", "tachyon", "tencent", "totaldefense", "trapmine", "trend", "trendmicro", "trusteer", "trustlook", "virus", "virustotal", "virustotalcloud", "webroot", "zillya", "zonealarm", "zscaler"] AS word MATCH (n) WHERE toLower(n.name) CONTAINS toLower(word) OR toLower(n.description) CONTAINS toLower(word) OR toLower(n.distinguishedname) CONTAINS toLower(word) RETURN word as keyword, LABELS(n)[1] as type, n.name, n.description, n.distinguishedname ORDER BY n.name
  • Find all other Rights Domain Users shouldn't have (author: jeffmcjunkin ):
MATCH p=(m:Group)-[r:Owns|WriteDacl|GenericAll|WriteOwner|ExecuteDCOM|GenericWrite|AllowedToDelegate|ForceChangePassword]->(n:Computer) WHERE m.name STARTS WITH 'DOMAIN USERS' RETURN p

CREATE Nodes and Edges

  • Mark nodes as Owned:
MATCH (u) WHERE toLower(u.name) = "user1@contoso.com" SET u.owned=True RETURN 1 UNION
MATCH (u) WHERE toLower(u.name) = "group2@contoso.com" SET u.owned=True RETURN 1 UNION
MATCH (u) WHERE toLower(u.name) = "computer3@contoso.com" SET u.owned=True RETURN 1
  • Mark users with non-empty UserPassword field as Owned:
MATCH (u:User) WHERE u.userpassword =~ ".+" SET u.owned = true RETURN u.name
  • Mark High Value all members of High Value groups:
MATCH (u)-[:MemberOf]->(n {highvalue: true}) SET u.highvalue = true
  • Add HasSession edge for user ALICE@DOMAIN being logged onto COMPUTER@DOMAIN :
MATCH (A:Computer {name: "COMPUTER@DOMAIN"}) 
MATCH (B:User {name: "ALICE@DOMAIN"})
CREATE (A)-[:HasSession]->(B)
  • Adds HasSession relationship on all domain controllers to Domain Admins group:
MATCH (u:Computer)-[:MemberOf*1..]->(g:Group) WHERE g.name starts with "DOMAIN CONTROLLERS" 
MATCH (h:Group) WHERE h.name starts with "DOMAIN ADMINS" 
CREATE (u)-[:HasSession]->(h)
  • Adds AdminTo relationship from User to Computer:
MATCH (A:User {name: "ALICE@DOMAIN"})
MATCH (B:Computer {name: "COMPUTER.DOMAIN"})
CREATE (A)-[:AdminTo]->(B)
  • Authored by Knavesec on a #cypher_queries Bloodhound slack: Prints graph paths of the returns yielded by query in p variable. Modify the first line to determine paths you would like to be printed (for later grepping, searching). Example:
match p=shortestPath((g:Group)-[*1..]->(n {highvalue:true})) where g.objectid ends with "-513"

WITH [node in nodes(p) | coalesce(node.name, '')] as nodeLabels,
     [rel in relationships(p) | type(rel)] as relationshipLabels,
     length(p) as path_len
WITH reduce(path='', x in range(0,path_len-1) | path + nodeLabels[x] + ' - ' + relationshipLabels[x] + ' -> ') as path,
     nodeLabels[path_len] as final_node
return distinct path + final_node as full_path
limit 3

Example output:

DOMAIN USERS@WINDOMAIN.LOCAL - AdminTo -> SECWWKS1000000.WINDOMAIN.LOCAL - GenericAll -> ELMER_GUERRERO@WINDOMAIN.LOCAL - MemberOf -> DOMAIN CONTROLLERS@WINDOMAIN.LOCAL
DOMAIN USERS@WINDOMAIN.LOCAL - AdminTo -> SECWWKS1000000.WINDOMAIN.LOCAL - GenericAll -> GENARO_PARKER@WINDOMAIN.LOCAL - MemberOf -> GROUP POLICY CREATOR OWNERS@WINDOMAIN.LOCAL

Other sources of great Cypher Queries