DogWhisperer - BloodHound Cypher Cheat Sheet (v2)
May 8, 2018 ยท View on GitHub
Collection of BloodHound Cypher Query Examples
- I- Raw
- II- Built-In
- III- Custom
- IV- DB Manipulation
- V- REST API (PowerShell)
see also Neo4j Syntax Reference for more Cypher madness
This is a quick guide and is not ment to be exhaustive or anything. Just a collection of bits and pieces I found here and there. Enough to start scratching the surface by looking at some examples, but surely not enough to master the full power of BloodHound Cypher Queries.
For advance BloodHound Cypher, check the pros...
Note: All examples in this guide can be run against the Bloodhound sample database for testing

I- Raw
Can be entered in the Raw Query input box at the bottom of the BloodHound UI
A- Nodes
All Nodes
MATCH (n) RETURN n
All User Nodes (Computer/Group/Domain)
MATCH (n:User) RETURN n
Node by Name
MATCH (x:Computer {name: 'APOLLO.EXTERNAL.LOCAL'}) RETURN x
Return Computer node name 'APOLLO.EXTERNAL.LOCAL'
MATCH (x:Computer) WHERE x.name='APOLLO.EXTERNAL.LOCAL' RETURN x
Same as above, different syntax
MATCH (x) WHERE x.name='APOLLO.EXTERNAL.LOCAL' RETURN x
Same without specifying node type (probably less eco-friendly)
Node by Property - Property Exists
MATCH (n:User) WHERE exists(n.test) RETURN n
Return all nodes that have a property 'test' (value or not)
Node by Property - Does Not Exists
MATCH (n:User) WHERE NOT exists(n.test) RETURN n
Return all user that dont have a property called 'test'
Node by Property - Property Value
MATCH (n:User) WHERE n.test='helloWorld' RETURN n
Return all user that have a property 'test' with value 'helloworld'
MATCH (X:Group) WHERE X.name CONTAINS 'ADMIN' RETURN X
Return All Groups with 'ADMIN' in name (case sensitive)
MATCH (X:Group) WHERE X.name =~ '(?i).*aDMiN.*' RETURN X
Same as above, using (case insensitive) regex
Comparaison Operators
List of operators that can be used with the WHERE clause
| OPERATOR | SYNTAX |
|---|---|
| Is Equal To | = |
| Is Not Equal To | <> |
| Is Less Than | < |
| Is Greater Than | > |
| Is Less or Equal | <= |
| Is Greater or Equal | >= |
| Is Null | IS NULL |
| Us Not Null | IS NOT NULL |
| Prefix Search * | STARTS WITH |
| Suffix Search * | ENDS WITH |
| Inclusion Search * | CONTAINS |
* String specific
B- Edges
TIP: It's possible to paste multi-lines in the query box
Group Membership - Direct
MATCH
(A:User),
(B:Group {name: 'CONTRACTINGH@INTERNAL.LOCAL'}),
p=(A)-[r:MemberOf*1..1]->(B)
RETURN p
Group Membership - Degree 4
MATCH
(A:User),
(B:Group {name: 'CONTRACTINGH@INTERNAL.LOCAL'}),
p=(A)-[r:MemberOf*1..4]->(B)
RETURN p
Group Membership - Any degree
MATCH
(A:User),
(B:Group {name: 'CONTRACTINGH@INTERNAL.LOCAL'}),
p=(A)-[r:MemberOf*1..]->(B)
RETURN p
List of available Edges types (ACL since 1.3)
| Source Node Type | Edge Type | Target Node Type |
|---|---|---|
| User/Group | :MemberOf | Group |
| User/Group | :AdminTo | Computer |
| Computer | :HasSession | User |
| Domain | :TrustedBy | Domain |
| User/Group | :ForceChangePassword * | User |
| User/Group | :AddMembers * | Group |
| User/Group | :GenericAll * | User/Computer/Group |
| User/Group | :GenericWrite * | User/Computer/Group |
| User/Group | :WriteOwner * | User/Computer/Group |
| User/Group | :WriteDACL * | User/Computer/Group |
| User/Group | :AllExtendedRights * | User/Computer/Group |
* More info on ACLs
C- Paths
Shortest Path from A to B - any Edge type
MATCH
(A:User {name: 'ACHAVARIN@EXTERNAL.LOCAL'}),
(B:Group {name: 'DOMAIN ADMINS@INTERNAL.LOCAL'}),
x=shortestPath((A)-[*1..]->(B))
RETURN x
Shortest Path from A to B - specific Edge types
MATCH
(A:User {name: 'ACHAVARIN@EXTERNAL.LOCAL'}),
(B:Group {name: 'DOMAIN ADMINS@INTERNAL.LOCAL'}),
x=shortestPath((A)-[:HasSession|:AdminTo|:MemberOf*1..]->(B))
RETURN x
Advanced Path
MATCH
(A:User),
(B:Computer {name: 'WEBSERVER3.INTERNAL.LOCAL'}),
p=(A)-[r:MemberOf|:AdminTo*1..3]->(B)
RETURN p
All admin user max 3 hops away by group membership from specified target computer
All Sortest Paths
MATCH
(A:User {name: 'ACHAVARIN@EXTERNAL.LOCAL'}),
(B:Group {name: 'DOMAIN ADMINS@INTERNAL.LOCAL'}),
x=allShortestPaths((A)-[*1..]->(B))
RETURN x
The allShortestPaths() function works the same way as shortestPath() but returns all possible shortest path
(= more ways to get to target with same amount of hops)
/!\ Restrict Edge type / max hops for heavy queries
Union
Multiple returned results can be combined into a single output/graph using UNION or UNION ALL
In this Example a Path from A to B via C
MATCH
(A:User {name: 'ACHAVARIN@EXTERNAL.LOCAL'}),
(C:User {name: 'CBARCLAY@INTERNAL.LOCAL'}),
x=shortestPath((A)-[*1..]->(C))
RETURN x
UNION ALL
MATCH
(C:User {name: 'CBARCLAY@INTERNAL.LOCAL'}),
(B:Group {name: 'DOMAIN USERS@INTERNAL.LOCAL'}),
x=shortestPath((C)-[*0..]->(B))
RETURN x
II- Built-In
Commonly used queries. Found under the Query Tab.
Avoids having to come up with syntax every time.
A lot of cool example in there.
source code can be found here
Below is there equivalent syntax if you were to insert them in the Query Box.
All Domain Admin
MATCH (n:Group) WHERE n.name =~ "(?i).*DOMAIN ADMINS.*"
WITH n
MATCH (n)<-[r:MemberOf*1..]-(m)
RETURN n,r,m
Shortest Path to Domain Admin
MATCH
(n:User),
(m:Group {name: 'DOMAIN ADMINS@INTERNAL.LOCAL'}),
p=shortestPath((n)-[*1..]->(m))
RETURN p
All Logged in Admins
MATCH
p=(a:Computer)-[r:HasSession]->(b:User)
WITH a,b,r
MATCH
p=shortestPath((b)-[:AdminTo|MemberOf*1..]->(a))
RETURN b,a,r
Top 10 Users with Most Sessions
MATCH
(n:User),(m:Computer),
(n)<-[r:HasSession]-(m)
WHERE NOT n.name STARTS WITH 'ANONYMOUS LOGON'
AND NOT n.name='' WITH n,
count(r) as rel_count
order by rel_count desc
LIMIT 10
MATCH
(m)-[r:HasSession]->(n)
RETURN n,r,m
Top 10 Users with Most Local Admin Rights
MATCH
(n:User),
(m:Computer),
(n)-[r:AdminTo]->(m)
WHERE NOT n.name STARTS WITH 'ANONYMOUS LOGON'
AND NOT n.name='' WITH n,
count(r) as rel_count
order by rel_count desc
LIMIT 10
MATCH
(m)<-[r:AdminTo]-(n)
RETURN n,r,m
Top 10 Computers with Most Admins
MATCH
(n:User),
(m:Computer),
(n)-[r:AdminTo]->(m)
WHERE NOT n.name STARTS WITH 'ANONYMOUS LOGON'
AND NOT n.name='' WITH m,
count(r) as rel_count
order by rel_count desc
LIMIT 10
MATCH
(m)<-[r:AdminTo]-(n)
RETURN n,r,m
Users with Foreign Domain Group Membership
MATCH
(n:User)
WHERE n.name ENDS WITH ('@' + 'INTERNAL.LOCAL')
WITH n
MATCH (n)-[r:MemberOf]->(m:Group)
WHERE NOT m.name ENDS WITH ('@' + 'INTERNAL.LOCAL')
RETURN n,r,m
Groups with Foreign Group Membership
MATCH
(n:Group)
WHERE n.name ENDS WITH '@EXTERNAL.LOCAL'
WITH n
MATCH
(n)-[r:MemberOf*1..]->(m:Group)
WHERE NOT m.name ENDS WITH '@EXTERNAL.LOCAL'
RETURN n,r,m
Map Domain Trusts
MATCH (n:Domain) MATCH p=(n)-[r]-() RETURN p
III- Custom
Add homemade queries to the interface (= ease of use).
Looks & feels exactly like built-in queries once added.
To add custom queries, click on the pen icon all the way at the bottom of the query tab.
Open in Notepad. Paste Query.
/!\ Don't forget to save changes.
Will be saved to C:\Users\<username>\AppData\Roaming\bloodhound\customqueries.
Click on refresh icon next to pen.
Voila.
Check Built-In query source code for syntax examples
Check @cptjesus intro to Cypher for more info
IV- DB Manipulation
Add/Delete Nodes/Properties/Edges to/from DB. (The world is yours...)
Create Node
MERGE (n:User {name: 'bob'})
Creates Node if doesn't already exist
Add/Update Node property
MATCH (n) WHERE n.name='bob' SET n.age=23
MATCH (n) WHERE n.name='bob' SET n.age=27, n.hair='black', n.sport='Chess-Boxing'
Both Create missing properties, overwrites existing property values
Remove Node property
MATCH (n) WHERE n.name='Bob' REMOVE n.sport
MATCH (U:User) WHERE EXISTS(U.age) REMOVE U.age
MATCH (U:User) WHERE EXISTS(U.hair) REMOVE U.age, U.hair RETURN U
Removes property from node (Single Node / multiple Nodes / multiple props)
Create Edge between Nodes (/!\ direction)
MATCH (A:User {name: 'alice'})
MATCH (B:User {name: 'bob'})
CREATE (A)-[r:IsSister]->(B)
MATCH (A:User {name: 'alice'})
MATCH (B:User {name: 'bob'})
CREATE (A)<-[r:IsBrother]-(B)
Delete Edge
MATCH (n:User {name: 'alice'})-[r:IsSister]->(m:User {name: 'bob'})
DELETE r
/!\ not specifying any Edge type will remove all Edges between specified Nodes
Delete Node (and all connected edges)
MATCH (n:User {name: 'bob'}) DETACH DELETE n
Create Node & Properties
/!\ DANGER ZONE /!\
MERGE (n:User {name: 'alice', age:23, hair:'black'}) RETURN n
/!\ Use only if Node name doesn't already exist. Prefer safer MERGE/SET command
Create nodes & Properties & Edges
MERGE (A:User {name:bob})-[r:IsBrother]->(B:User {name:'Paul'})
MERGE (A:User {name:'Jack', age:14, hair:'black'})-[r:IsBrother]->(B:User {name:'Jimmy'})
/!\ Use only if Nodes don't already exist. otherwise MERGE or MERGE/SET each block sperately
Recommended syntax:
MERGE (A:User {name:'bob'})
MERGE (B:User {name: 'Paul'})
MERGE (A)-[r:IsBrother]->(B)
MERGE(X:User {name:'Jack'}) SET X.age=14, X.hair='black'
MERGE(Y:User {name:'Jimmy'}) SET Y.age=21, X.hair='black'
MERGE (X)-[r:IsBrother]->(Y)
Nuke DB
MATCH (x) DETACH DELETE x
/!\ Simple and efficient. Try at your own (data) expense
V- REST API
Access/Manipulate BloodHound data via REST API.
Example here is with PowerShell, but you can apply same method with language of your choosing.
Note: To Access Bloodhound (on localhost) via API, uncomment #dbms.security.auth_enabled=false in neo4j config file
API Call - Basic
# Prep Vars
$Server = 'localhost'
$Port = '7474'
$Uri = "http://$Server:$Port/db/data/cypher"
$Header = @{'Accept'='application/json; charset=UTF-8';'Content-Type'='application/json'}
$Method = 'POST'
$Body = '----- tbd -----'
# Make Call
$Reply = Invoke-RestMethod -Uri $Uri -Method $Method -Headers $Header -Body $Body
# Node Data
$NodeData = $Reply.data.data
Only need to add
$Bodyto build query. The rest stays the same. See examples below...
A- Node
Node View
$Body = '{
"query" : "MATCH (A:Computer {name: {ParamA}}) RETURN A",
"params" : { "ParamA" : "APOLLO.EXTERNAL.LOCAL" }
}'
Node New
$Body = '{
"query" : "MERGE (n:User {name: {P1}}) RETURN n",
"params" : { "P1" : "bob" }
}'
Node Add Property / Update Value
$Body = '{
"query" : "MATCH (n) WHERE n.name={Usr} SET n.number={Val}",
"params" : { "Usr" : "bob", "Val" : 8 }
}'
Node remove property
$Body = '{
"query" : "MATCH (n) WHERE n.name={input} REMOVE n.number",
"params": {"input": "Alice"}
}'
Node Delete
$Body = '{
"query" : "MATCH (n:User {name: {thisname}}) DETACH DELETE n",
"params": { "thisname" : "bob" }
}'
B- Edge
Edge View
$Body = '{
"query" : "MATCH (A:User),(B:User {name: {ParamB}}) MATCH p=(A)-[r:MemberOf*1..1]->(B) RETURN A",
"params" : { "ParamB" : "AUDIT_B@EXTERNAL.LOCAL" }
}'
Edge Create
$Body = '{
"query" : "MERGE (n:User {name: {U1}}) MERGE (m:User {name: {U2}}) MERGE (m)-[r:IsSister]->(n)",
"params": { "U1" : "bob", "U2" : "alice"}
}'
C- Path
Shortest Path
$Body = '{
"query" : "MATCH (A:User {name: {ParamA}}), (B:Group {name: {ParamB}}), x=shortestPath((A)-[*1..]->(B)) RETURN x",
"params" : { "ParamA" : "ACHAVARIN@EXTERNAL.LOCAL", "ParamB" : "DOMAIN ADMINS@EXTERNAL.LOCAL" }
}'
Putting it all together...
Post to server. Get reply. Parse data. Automate other stuff with that data... Fantastic!

A basic PowerShell function to call the API could look like this...
## Function
function Invoke-DogPost{
[CmdletBinding()]
[Alias('DogPost')]
Param(
[Parameter(Mandatory=1)][string]$Body,
[Parameter()][String]$Server='localhost',
[Parameter()][int]$Port=7474,
[Parameter()][Switch]$RawData
)
$Uri = "http://${Server}:${Port}/db/data/cypher"
$Header=@{'Accept'='application/json; charset=UTF-8';'Content-Type'='application/json'}
$Result = Try{Invoke-RestMethod -Uri $Uri -Method Post -Headers $Header -Body $Body}Catch{$Error[0].Exception}
if($RawData){Return $result}
else{Return $Result.data.data}
}
## TestCall
$Body='
{
"query" : "MATCH (A:Computer {name: {ParamA}}) RETURN A",
"params" : { "ParamA" : "APOLLO.EXTERNAL.LOCAL" }
}
'
DogPost $Body
Works exact same way with a
curlon linux
Attackers Think in Graph... Automations Don't.
Returning Graphs is not suited for all command line tools (ba dum tsss!), but computers love data...
Return Nodes, or parse Paths into Objects
Example: (just an idea)
Step StartNode Edge Direction EndNode
---- --------- ---- --------- -------
0 ACHAVARIN@EXTERNAL.LOCAL MemberOf -> INFORMATIONTECHNOLOGY7@EXTERNAL.LOCAL
1 INFORMATIONTECHNOLOGY7@EXTERNAL.LOCAL MemberOf -> DOMAIN ADMINS@EXTERNAL.LOCAL
2 DOMAIN ADMINS@EXTERNAL.LOCAL AdminTo -> DESKTOP11.EXTERNAL.LOCAL
3 DESKTOP11.EXTERNAL.LOCAL HasSession -> AMEADORS@EXTERNAL.LOCAL
4 AMEADORS@EXTERNAL.LOCAL MemberOf -> CONTRACTINGF@INTERNAL.LOCAL
5 CONTRACTINGF@INTERNAL.LOCAL MemberOf -> CONTRACTINGG@INTERNAL.LOCAL
6 CONTRACTINGG@INTERNAL.LOCAL MemberOf -> CONTRACTINGH@INTERNAL.LOCAL
7 CONTRACTINGH@INTERNAL.LOCAL MemberOf -> CONTRACTINGI@INTERNAL.LOCAL
8 CONTRACTINGI@INTERNAL.LOCAL AdminTo -> MANAGEMENT7.INTERNAL.LOCAL
9 MANAGEMENT7.INTERNAL.LOCAL HasSession -> ASANDERS.ADMIN@INTERNAL.LOCAL
10 ASANDERS.ADMIN@INTERNAL.LOCAL MemberOf -> DOMAIN ADMINS@INTERNAL.LOCAL
Moaaar Stuff
Useful links
Links to more info on/around the topic
Github
-
@harmj0y Click on Follow...
-
@_wald0 Click on Follow...
-
@CptJesus Click on Follow...
-
@Porterhau5 Click on Follow
Slack
- Channel Here get invite here
Blog
-
Introducing BloodHound by @_wald0
-
Intro to Cypher by @CptJesus
-
ACL Attack Paths by @_wald0
-
Extending Bloodhound... by @Porterhau5
-
Representing Password Reuse in BloodHound by @Porterhau5
Video
-
Six degrees of Domain Admin by @_wald0 & Co - BSides LV 2016
-
Here Be Dragons... by @_wald0 & Co - DerbyCon 2017
-
GoFetch by @TaltheMaor @TalBerySec - BlackHat 2017
-
Extending Bloodhound for RedTeamers by @Porterhau5 - WWHF 2017
-
Requiem for an Admin by @SadProcessor - BSides Amsterdam 2017 (Shameless Plug)
Noe4j
More Cool Tools
-
CypherDog/DogStrike PowerShell Module to interact with BloodHound (& Empire) API (1.4 soon...)
-
GoFetch Automation of lateral movement with BloodHound & Empire
-
AngryPuppy BH & CS automation by @Vysec and @Spartan
Sample DB
Want to play with BloodHound but don't have an AD at hand? Install the supplied sample DB.
With bloodhound/neo4j stopped:
-
Copy
BloodHoundExampleDB.graphdbfolder to[...]/neo4j/data/database/ -
Open
[...]/neo4j/conf/ne4jin text editor -
Uncomment and set db name to mount to
dbms.active_database=BloodHoundExampleDB.graphdb -
Uncomment
#dbms.allow_upgrade=true -
Save changes
-
start neo4j/bloodhound
(you should see a graph from sample data)
-
Re-comment
dbms.allow_upgrade=trueand Save change -
Done
For automated BloodHound install script check here (windows64)
KeyBoard Shortcuts
| KEY | ACTION |
|---|---|
CTRL | Node labels ON/OFF |
CTRL+SPACE | Node Search Dialog Box |
CRTL+R | Restart BloodHound |
CTRL+SHIFT+I | Console Debug |
Can use CTRL+Z and CTRL+Y in Query Box as kind of history function
Note: Debuging queries is easier via neo4j browser (http://localhost:7474/Browser)
UI Tweaks
:( Made some cool ones (Dark Theme). Didn't document process. Deleted VM. Will have to try that again later...
:) Check out @porterhau5 in links for some awesome stuff
That's all I got for now. Like I said, this is just scratching the surface of Cypher queries. You can get quite funky with it (try googling for non-bloodhound cypher stuff... quite cool). I'll keep digging.
Hope this will be useful to someone somewhere. Now you can take your Dog for a walk.
Hack the Planet...