Skip to content

Instantly share code, notes, and snippets.

@joeminicucci
Created February 13, 2020 05:54
Show Gist options
  • Save joeminicucci/d9fb42f03186f6aaa556cc5f961f537b to your computer and use it in GitHub Desktop.
Save joeminicucci/d9fb42f03186f6aaa556cc5f961f537b to your computer and use it in GitHub Desktop.
DogWhisperer - BloodHound Cypher Cheat Sheet (v2)

DogWhisperer - BloodHound Cypher Cheat Sheet (v2)

Collection of BloodHound Cypher Query Examples

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

HostBusters




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: '[email protected]'}),
p=(A)-[r:MemberOf*1..1]->(B) 
RETURN p

Group Membership - Degree 4

MATCH 
(A:User), 
(B:Group {name: '[email protected]'}), 
p=(A)-[r:MemberOf*1..4]->(B) 
RETURN p

Group Membership - Any degree

MATCH 
(A:User), 
(B:Group {name: '[email protected]'}), 
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: '[email protected]'}),
(B:Group {name: 'DOMAIN [email protected]'}),
x=shortestPath((A)-[*1..]->(B))
RETURN x

Shortest Path from A to B - specific Edge types

MATCH
(A:User {name: '[email protected]'}),
(B:Group {name: 'DOMAIN [email protected]'}),
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: '[email protected]'}),
(B:Group {name: 'DOMAIN [email protected]'}),
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: '[email protected]'}), 
(C:User {name: '[email protected]'}), 
x=shortestPath((A)-[*1..]->(C)) 
RETURN x 
UNION ALL 
MATCH 
(C:User {name: '[email protected]'}),
(B:Group {name: 'DOMAIN [email protected]'}),
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 [email protected]'}),
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 $Body to 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" : "[email protected]" }
}'

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" : "[email protected]", "ParamB" : "DOMAIN [email protected]" }
}'



Putting it all together...

Post to server. Get reply. Parse data. Automate other stuff with that data... Fantastic!

GreatestDogInTheWorld

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 [email protected]              MemberOf   ->        [email protected]
   1 [email protected] MemberOf   ->        DOMAIN [email protected]         
   2 DOMAIN [email protected]          AdminTo    ->        DESKTOP11.EXTERNAL.LOCAL             
   3 DESKTOP11.EXTERNAL.LOCAL              HasSession ->        [email protected]              
   4 [email protected]               MemberOf   ->        [email protected]          
   5 [email protected]           MemberOf   ->        [email protected]          
   6 [email protected]           MemberOf   ->        [email protected]          
   7 [email protected]           MemberOf   ->        [email protected]          
   8 [email protected]           AdminTo    ->        MANAGEMENT7.INTERNAL.LOCAL           
   9 MANAGEMENT7.INTERNAL.LOCAL            HasSession ->        [email protected]        
  10 [email protected]         MemberOf   ->        DOMAIN [email protected]




Moaaar Stuff

Useful links

Links to more info on/around the topic

Github

Twitter

Slack

Blog

Video

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.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)




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...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment