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
Can be entered in the Raw Query input box at the bottom of the BloodHound UI
MATCH (n) RETURN n
MATCH (n:User) RETURN n
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)
MATCH (n:User) WHERE exists(n.test) RETURN n
Return all nodes that have a property 'test' (value or not)
MATCH (n:User) WHERE NOT exists(n.test) RETURN n
Return all user that dont have a property called 'test'
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
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
TIP: It's possible to paste multi-lines in the query box
MATCH
(A:User),
(B:Group {name: 'CONTRACTINGH@INTERNAL.LOCAL'}),
p=(A)-[r:MemberOf*1..1]->(B)
RETURN p
MATCH
(A:User),
(B:Group {name: 'CONTRACTINGH@INTERNAL.LOCAL'}),
p=(A)-[r:MemberOf*1..4]->(B)
RETURN p
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
MATCH
(A:User {name: 'ACHAVARIN@EXTERNAL.LOCAL'}),
(B:Group {name: 'DOMAIN ADMINS@INTERNAL.LOCAL'}),
x=shortestPath((A)-[*1..]->(B))
RETURN x
MATCH
(A:User {name: 'ACHAVARIN@EXTERNAL.LOCAL'}),
(B:Group {name: 'DOMAIN ADMINS@INTERNAL.LOCAL'}),
x=shortestPath((A)-[:HasSession|:AdminTo|:MemberOf*1..]->(B))
RETURN x
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
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
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
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.
MATCH (n:Group) WHERE n.name =~ "(?i).*DOMAIN ADMINS.*"
WITH n
MATCH (n)<-[r:MemberOf*1..]-(m)
RETURN n,r,m
MATCH
(n:User),
(m:Group {name: 'DOMAIN ADMINS@INTERNAL.LOCAL'}),
p=shortestPath((n)-[*1..]->(m))
RETURN p
MATCH
p=(a:Computer)-[r:HasSession]->(b:User)
WITH a,b,r
MATCH
p=shortestPath((b)-[:AdminTo|MemberOf*1..]->(a))
RETURN b,a,r
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
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
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
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
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
MATCH (n:Domain) MATCH p=(n)-[r]-() RETURN p
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
Add/Delete Nodes/Properties/Edges to/from DB. (The world is yours...)
MERGE (n:User {name: 'bob'})
Creates Node if doesn't already exist
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
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)
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)
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
MATCH (n:User {name: 'bob'}) DETACH DELETE n
/!\ 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
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)
MATCH (x) DETACH DELETE x
/!\ Simple and efficient. Try at your own (data) expense
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
# 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
$Body
to build query. The rest stays the same. See examples below...
$Body = '{
"query" : "MATCH (A:Computer {name: {ParamA}}) RETURN A",
"params" : { "ParamA" : "APOLLO.EXTERNAL.LOCAL" }
}'
$Body = '{
"query" : "MERGE (n:User {name: {P1}}) RETURN n",
"params" : { "P1" : "bob" }
}'
$Body = '{
"query" : "MATCH (n) WHERE n.name={Usr} SET n.number={Val}",
"params" : { "Usr" : "bob", "Val" : 8 }
}'
$Body = '{
"query" : "MATCH (n) WHERE n.name={input} REMOVE n.number",
"params": {"input": "Alice"}
}'
$Body = '{
"query" : "MATCH (n:User {name: {thisname}}) DETACH DELETE n",
"params": { "thisname" : "bob" }
}'
$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" }
}'
$Body = '{
"query" : "MERGE (n:User {name: {U1}}) MERGE (m:User {name: {U2}}) MERGE (m)-[r:IsSister]->(n)",
"params": { "U1" : "bob", "U2" : "alice"}
}'
$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" }
}'
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
curl
on 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
Links to more info on/around the topic
-
@harmj0y Click on Follow...
-
@_wald0 Click on Follow...
-
@CptJesus Click on Follow...
-
@Porterhau5 Click on Follow
- Channel Here get invite here
-
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
-
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)
-
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
Want to play with BloodHound but don't have an AD at hand? Install the supplied sample DB.
With bloodhound/neo4j stopped:
-
Copy
BloodHoundExampleDB.graphdb
folder to[...]/neo4j/data/database/
-
Open
[...]/neo4j/conf/ne4j
in 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=true
and Save change -
Done
For automated BloodHound install script check here (windows64)
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)
:( 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...