Not All Paths are Created Equal

Nov 8, 2019 // Attackers' Economy (Part 1) // Linkedin // Twitter

Introduction

BloodHound was a revolution for evaluating Active Directory (AD) security and identifying unintended paths that could lead to the compromise of sensitive groups such as Domain Admins. The community has been using it successfully in many engagements and against highly secure environments.

For those who’ve been living in a cave for the past couple of years, in a nutshell BloodHound is a tool that can be used by both defenders and attackers to identify hidden and unintended relationships between AD principals. It’s heavily based on the work published by Microsoft’s researchers: Heat-ray: Combating Identity Snowball Attacks Using Machine Learning, Combinatorial Optimization and Attack Graphs . Although BloodHound does not rely on machine learning yet, the data model is pretty similar to the proposed one.

An example of the relationships created using BloodHound:

This article is mainly based on some research that I’ve been doing recently on attack paths and graph theory (although this specific article will not be focused on graph theory).

Let’s start with a consideration about BloodHound:

Not all the attack paths have the same easiness of exploitability; some of them are easier to exploit or abuse and some other are, by nature, harder.

Let’s make a couple of examples and edge cases to clarify the concept:

But let’s think about it for a second, what’s easier to exploit? If we follow the “shortest path” algorithm, clearly User B is closer. However, if you’ve ever done an AD assessment in the real live you’ll quickly realise that:

In the other case, User A is already a DA and doesn’t need to do anything at all (except from DCSync’ing maybe?). I do realise that the proposed example is quite forced and in a real scenario you would realise that the user you compromise is a DA. But you get the idea right?

We could look at this problem from an opsec perspective as well, what are the most noisy actions you could do? Probably resetting a DA password should be the last resort if detection is something you care about.

Hopefully I convinced you that not all the paths that are identified by BloodHound are equal, both from a practical and an opsec perspective. The approach I propose involve the use of weighted connections between the different nodes, the weight of a connection or cost for those who are familiar with graph theory can be interpreted as follows:

In the tables below I will propose some of the costs associated with the relationship types within BloodHound:

RelationshipOpsecEasiness
CanRDP70100
CanPSRemote60100
ExecuteDCOM60100
HasSession4040
Write/GenericAll/ForceChangePwd3030
AdminTo2020
MemberOf55

These values are purely arbitrary and come from my experience, don’t take those for gold and don’t kill me if you don’t agree. It’s just an example and those values may change depending on your team capabilities, tooling, experience and so on! Feel free to give me feedbacks and I will update the table accordingly.

Implementation

Now, neo4j does not support weighted connections out of the box. Luckily for us, there are several modules we can import that would allow us to calculate shortest paths using link costs. The plugin we’re looking for in this case is Neo4j Graph Algorithms .

Since I’m cool and I have a MacBook, I always use the Desktop version of Neo4j. It’s quite easy to install the aforementioned plugin:

Let’s now set the costs accordingly to our table (in this example just from easiness to exploit, not opsec):

MATCH ()-[r]->()
SET r.cost = 30

MATCH ()-[r:CanRDP|CanPSRemote|ExecuteDCOM]->()
SET r.cost = 100

MATCH ()-[r:HasSession]->()
SET r.cost = 40

MATCH ()-[r:AdminTo]->()
SET r.cost = 20

MATCH ()-[r:MemberOf]->()
SET r.cost = 5

Let’s suppose we now want to find the shortest path to Domain Admins from a normal user, we could use a query like this one:

MATCH (start:User), (end:Group{name: "DOMAIN ADMINS@HACKERS.LAB"})
CALL algo.shortestPath.stream(start, end, 'cost',{write:true,writeProperty:'sssp', direction:'OUTGOING'})
YIELD nodeId, cost
RETURN algo.asNode(nodeId).name AS name, cost

Let’s briefly explain what the query does:

An example of output is shown below:

In the figure, it is possible to see all the steps and their relative cost, note that the output may contain different paths with the same cost!

If we repeat the same cypher query but from User A and User B that we introduced in the example, we’ll see that the path from User B is much more expensive, although it requires less steps.

For User A:

and for User B:

We just demonstrated that it is possible to set costs to various links between nodes and identify the shortest paths with the smallest cost! Hopefully this initial research will be continued and will help both red teamers and defenders to identify the path with the least resistance that could help us reaching our objectives!