By making a few additions to the source, we can even integrate these properties into the Node Info tab of BloodHound’s UI:
It’s a useful way to document your compromise as you go, and a convenient visual aide when explaining your critical path of compromise to a client.
Note that modifications to the UI like this are only possible if you tweak BloodHound’s source. I’m compiling all of my UI enhancements into a GitHub repo – if you want to try out these additions yourself then you’ll need to install the customized app from the repo.
For those interested, here’s a sample of the changes made to src/components/SearchContainer/Tabs/UserNodeData.jsx
to make this happen (diff here):var s8 = driver.session()
var s9 = driver.session()
...
s8.run("MATCH (n {name:{name}}) RETURN n.wave", {name:payload})
.then(function(result){
if (result.records[0]._fields[0] != null) {
this.setState({'ownedInWave':result.records[0]._fields[0].low})
}
s8.close()
}.bind(this))
s9.run("MATCH (n {name:{name}}) RETURN n.owned", {name:payload})
.then(function(result){
if (result.records[0]._fields[0] != null) {
this.setState({'ownedMethod':result.records[0]._fields[0]})
}
s9.close()
}.bind(this))
...
<dt>
Owned in Wave
</dt>
<dd>
<NodeALink
ready={this.state.ownedInWave !== -1}
value={this.state.ownedInWave}
click={function(){
emitter.emit('query', "OPTIONAL MATCH (n1:User {wave:{wave}}) WITH collect(distinct n1) as c1 OPTIONAL MATCH (n2:Computer {wave:{wave}}) WITH collect(distinct n2) + c1 as c2 OPTIONAL MATCH (n3:Group {wave:{wave}}) WITH c2, collect(distinct n3) + c2 as c3 UNWIND c2 as n UNWIND c3 as m MATCH (n)-[r]->(m) RETURN n,r,m", {wave:this.state.ownedInWave}
,this.state.label)
}.bind(this)} />
</dd>
<dt>
Owned via Method
</dt>
<dd>
{this.state.ownedMethod}
</dd>
When we designate a node as owned, we want to see the ripple effect across the network. With our wave
property set, we can find those outbound paths like this:MATCH (n)-[r*]->(m) WHERE n.wave=1 RETURN n,r,m
This is similar to the “Find Shortest Paths to Here” idea, but we’re now interested in the paths branching out of a node instead of those coming in. For each new node in the paths we find, we’ll set the wave
property equal to the same wave
value of the source node(s).
Let’s see this idea in action with the example graphdb included in the BloodHound repo. If you’d like to follow along, I recommend working with a copy of the example graphdb to make starting fresh easier.
We’ll start our theoretical penetration test by firing up Responder and grabbing some NTLMv2 hashes. Assume we were able to crack the hashes and obtain cleartext passwords for two accounts, [email protected] and [email protected]. Let’s mark those two accounts as compromised using Cypher:// Adding [email protected] to wave 1 via LLMNR wpad
MATCH (n) WHERE n.name="[email protected]" SET n.owned="LLMNR wpad", n.wave=1
> Set 2 properties, statement completed in 5 ms.
// Adding [email protected] to wave 1 via NBNS wpad
MATCH (n) WHERE n.name="[email protected]" SET n.owned="NBNS wpad", n.wave=1
> Set 2 properties, statement completed in 6 ms.
// Show names of nodes from the first wave
MATCH (n) WHERE n.wave=1 RETURN n.name
+-------------------------+
| n.name |
+-------------------------+
| [email protected] |
| [email protected] |
+-------------------------+
With these two nodes as our source, let’s use BloodHound’s Raw Query feature to find the other nodes collaterally included in this wave of compromise:
We see two additions – both users are a MemberOf “DOMAIN [email protected]”, and one user is AdminTo “SYSTEM38.INTERNAL.LOCAL”. Neat. Go ahead and add both of those nodes to wave
1 as well:MATCH (n)-[r*]->(m) WHERE n.wave=1 SET m.wave=1
> Set 3 properties, statement completed in 5 ms.
// Show updated names of nodes from the first wave
MATCH (n) WHERE n.wave=1 RETURN n.name
+-----------------------------+
| n.name |
+-----------------------------+
| [email protected] |
| DOMAIN [email protected] |
| [email protected] |
| SYSTEM38.INTERNAL.LOCAL |
+-----------------------------+
Simple enough, right? Let’s build on these queries.
We have a way for marking nodes as owned, and we can view the ripple effect of a wave. What happens when we compromise a disjoint set of nodes via some new method? What does the delta in our access look like? It would be terrific if we could see what’s available to us now that wasn’t available to us before.
We can use the same queries as before, but we’ll want to be careful not to overwrite data from previous waves. How do we know which nodes we compromised in previous waves? Each compromised node has a wave
property that already exists. If we detect that this property exists, then we know not to include it in the new wave. This can be done in Cypher by negating the EXISTS
function:MATCH (n)-[r*]->(m) WHERE n.wave=2 AND not(EXISTS(m.wave)) SET m.wave=2
We’re still looking for the paths branching out (except using the second wave
as our starting point), but we don’t want to circle back to nodes we’ve already compromised. The clause AND not(EXISTS(m.wave))
ensures we don’t include any destination nodes with the wave
property set.
Let’s see it in context by building off of the previous example. Imagine that the next step of our penetration test involved a password spraying attack against domain users. We found two users with “Spring2017!” on the INTERNAL.LOCAL domain, ZDEVENS and BPICKEREL. Start by marking these two new nodes as owned in wave 2:// Adding [email protected] to wave 2 via Password spray
MATCH (n) WHERE n.name="[email protected]" SET n.owned="Password spray", n.wave=2
> Set 2 properties, statement completed in 7 ms.
// Adding [email protected] to wave 2 via Password spray
MATCH (n) WHERE n.name="[email protected]" SET n.owned="Password spray", n.wave=2
> Set 2 properties, statement completed in 5 ms.
Find the spread of compromise, then add those nodes to our second wave:MATCH (n)-[r*]->(m) WHERE n.wave=2 AND not(EXISTS(m.wave)) SET m.wave=2
> Set 6 properties, statement completed in 3 ms.
And now the graph showing the second wave. This represents the delta after our password spraying attack:
That’s handy. Now I know which machines I should go plunder for sensitive documents, local hashes, cached passwords, etc.
It’s tedious to manually run these queries each time a node is compromised. Thankfully, Neo4j’s REST API makes automation possible. With a simple Ruby script, we can leverage the same endpoint used by Export-BloodHoundData
to ingest data directly across the network:$ ruby bh-owned.rb
Usage: ruby bh-owned.rb [options]
-u, --username <username> Neo4j database username (default: 'neo4j')
-p, --password <password> Neo4j database password (default: 'BloodHound')
-U, --url <url> URL of Neo4j RESTful host (default: 'http://127.0.0.1:7474/')
-n, --nodes get all node names
-a, --add <file> add 'owned' and 'wave' property to nodes in <file>
-w, --wave <num> value to set 'wave' property (override default behavior)
-e, --examples reference doc of customized Cypher queries for BloodHound
I usually start by dumping all of the nodes from the database with -n
:$ ruby bh-owned.rb -n
[*] Using default username: neo4j
[*] Using default password: BloodHound
[*] Using default URL: http://127.0.0.1:7474/
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
[email protected]
<snipped>
With -e
, you can see some of the useful Cypher queries used throughout this blog post (some are slightly modified to improve query performance):$ ruby bh-owned.rb -e
Find all owned Domain Admins:
MATCH (n:Group) WHERE n.name =~ '.*DOMAIN ADMINS.*' WITH n MATCH p=(n)<-[r:MemberOf*1..]-(m) WHERE exists(m.owned) RETURN nodes(p),relationships(p)
Find Shortest Path from owned node to Domain Admins:
MATCH p=shortestPath((n)-[*1..]->(m)) WHERE exists(n.owned) AND m.name=~ '.*DOMAIN ADMINS.*' RETURN p
List all directly owned nodes:
MATCH (n) WHERE exists(n.owned) RETURN n
Find all nodes in wave $num:
MATCH (n)-[r]->(m) WHERE n.wave=$num AND m.wave=$num RETURN n,r,m
Show all waves up to and including wave $num:
MATCH (n)-[r]->(m) WHERE n.wave<=$num RETURN n,r,m
Set owned and wave properties for a node (named $name, compromised via $method in wave $num):
MATCH (n) WHERE (n.name = '$name') SET n.owned = '$method', n.wave = $num
Find spread of compromise for owned nodes in wave $num:
OPTIONAL MATCH (n1:User {wave:$num}) WITH collect(distinct n1) as c1 OPTIONAL MATCH (n2:Computer {wave:$num}) WITH collect(distinct n2) + c1 as c2 UNWIND c2 as n OPTIONAL MATCH p=shortestPath((n)-[*..20]->(m)) WHERE not(exists(m.wave)) WITH DISTINCT(m) SET m.wave=$num
Continuing with our theoretical penetration test, let’s say that we found a juicy Excel spreadsheet which contained credentials for users [email protected] and [email protected]. We’ll first create a CSV with the node names and method of compromise like so:$ cat 3rd-wave.txt
[email protected],Creds in file on DATABASE5
[email protected],Creds in file on DATABASE5
Then we use the -a
flag to ingest:$ ruby bh-owned.rb -a 3rd-wave.txt
[*] Using default username: neo4j
[*] Using default password: BloodHound
[*] Using default URL: http://127.0.0.1:7474/
[+] Adding [email protected] to wave 3 via Creds in file on DATABASE5
[+] Adding [email protected] to wave 3 via Creds in file on DATABASE5
[+] Querying and updating new owned nodes
The script will first query the database and determine the latest wave added – in this case it was ‘2’. It then increments it by one so that the incoming additions will be in wave ‘3’. You can override this behavior by setting the -w
flag to the preferred wave value.
Once the wave number is determined, the script takes the following steps:
Let’s look at the result for this third wave in BloodHound:
Turns out this wave wasn’t very exciting ¯\_(ツ)_/¯ And we still have to type in our custom query in order to display the graph in BloodHound. Let’s remove that hassle and take it a step further by tweaking the UI and writing some custom queries.
BloodHound added a feature in v1.2 to allow for custom queries (more info on CptJesus’s blog). This has the same effect as adding a pre-built query on the Queries tab, but the configuration file has been decoupled from the project’s source code. I found this file in OS X at ~/Library/Application Support/bloodhound/customqueries.json
.
I’ve added four custom queries (source here):
owned
property.owned
node.If you’re using the customized BloodHound app, these queries will highlight nodes of interest in the graph as well. Let’s see the custom queries and UI enhancements in context of our example penetration test:
Let’s add two more nodes to our compromise:$ cat 4th-wave.txt
[email protected],Mimikatz on MANAGEMENT3
FILESERVER6.INTERNAL.LOCAL,Local Administrator password reuse (dumped from MANAGEMENT3)
$ ruby bh-owned.rb -a 4th-wave.txt
[*] Using default username: neo4j
[*] Using default password: BloodHound
[*] Using default URL: http://127.0.0.1:7474/
[+] Adding [email protected] to wave 4 via Mimikatz on MANAGEMENT3
[+] Adding FILESERVER6.INTERNAL.LOCAL to wave 4 via Local Administrator password reuse (dumped from MANAGEMENT3)
[+] Querying and updating new owned nodes
And now click on the “Find all owned Domain Admins” custom query:
Boom, we got one. Notice that a magenta lightning bolt appears on the top-left of nodes relevant to our custom query. Like the additions to the Node Info tab, this feature is currently only available in the modified version of BloodHound.
This is the same as the “Find Shortest Paths to Domain Admins”, but we’re focusing on nodes we’ve owned. You’ll see in this graph that the two starting nodes, FILESERVER6 and BGRIFFIN, are marked with our owned icon:
This displays a single, isolated wave. It uses query
to present the user with wave values:
It passes the choice to onFinish
to display the result. Here’s wave 2 from our example:
This displays all waves leading up to the selected wave, and then highlights the nodes from that wave. Like ‘Show wave’, it uses the pop-up picker for wave selection.
If wave 2 is selected, the graph shows waves 1 & 2 then highlights the nodes from wave 2:
If wave 3 is selected, the graph shows waves 1-3 then highlights the nodes from wave 3:
I like this graph for visualizing the changes in privilege gains as it pertains to the greater context of the penetration test. Clients like it too for the same reason – it’s an effective visual aide for explaining the collateral risk of each User or Computer you compromise.
Here’s a couple ideas for taking this a little further. Hopefully I’ll have time to tinker with these in the coming weeks. I’ll post updates here and on Twitter via @porterhau5. Please reach out if you want to explore some of these together! I’d really appreciate some help from those of you who are skilled with front-end development :D
Thank you for reading!
porterhau5 BLOG
BloodHound pen-testing