Active Directory Domain escalation is an important part of most penetration tests and red team engagements. While gaining domain/enterprise administrator rights is not the end goal of an assessment, it often makes achieving test objectives much easier.
A typical domain escalation process revolves around the ability to gather plain text credentials or tokens of users logged onto systems you have or can gain gain elevated privileges on – an ability most famously made possible by Mimikatz. Find a domain admin logged onto a system you have admin rights on, pivot to that system, and collect the admin’s credentials.
But what if you’re in a more complex situation where you don’t immediately have admin rights on a system where a domain admin is located? You may be one, two, three, or more hops away from being able to compromise a domain admin, and will need to do quite a bit of analysis (or just trial-and-error) to find your path.
Let’s take a look at a hypothetical situation: we gained Domain User level rights in an environment with hundreds of thousands of workstations and servers joined to an Active Directory forest with multiple domains with varying trusts. Our objective was to escalate our rights to Enterprise Administrator, if possible. Luckily, the network topology was effectively flat; however, the client enforced extremely strict least privilege practices and effectively had zero low hanging fruit. After fighting to find a means to elevate our rights, we were finally able to compromise a server administrator account which we’ll call ‘Steve-Admin’.
‘Steve-Admin’ was a local administrator on the servers he needed to be an admin on – and nowhere else. We’d take that list of servers and find out which users were logged onto those servers. At that point, though, we needed to decide which users we were going to go after. None of the logged on users we could compromise were Domain Admins, nor were they admins on boxes where Domain Admins were logged on. We had to choose an account, find the systems it had admin rights on, enumerate logged on users on those machines, and continue until we eventually found a path that worked. In an environment with hundreds of thousands of computers and users, that process can take days or even weeks.
In this post, I will explain and demonstrate a proof of concept for automating this process.
This proof of concept relies heavily upon existing tools and concepts graciously shared publicly by some very smart, very hardworking folks:
- PowerView by Will Schroeder (@harmj0y) – https://github.com/PowerShellMafia/PowerSploit/blob/master/Recon/PowerView.ps1
- Derivative Local Admin by Justin Warner (@sixdub) – http://www.sixdub.net/?p=591
- A PowerShell implementation of Dijkstra’s Algorithm by Jim Truher (@jwtruher) – https://jtruher3.wordpress.com/2006/10/16/dijkstra/
- Active Directory Control Paths by Emmanuel Gras and Lucas Bouillot – https://github.com/ANSSI-FR/AD-control-paths
- Nodal analysis of Domain Trusts by Justin Warner (@sixdub)- http://www.sixdub.net/?p=285
Imagine if instead of trying to find a path from ‘Steve-Admin’ to an Enterprise Admin, we were trying to find a path from Seattle, Washington to Portland, Oregon. As a human being, you can take a look at a map and quite easily determine that Interstate 5 will get you there. A computer can find the path between Seattle and Portland (and between ‘Steve-Admin’ and an Enterprise Admin, if one exists) with mathematics.
PowerView can give us most of the data we need to automate the process of finding a path from ‘Steve-Admin’ to an Enterprise Admin. The rest comes from a branch of mathematics created by Leonhard Euler in the 18th century, now known as graph theory. Euler was able to use the now-standard fundamentals of graph theory to prove there was no solution to the Seven Bridges of Königsberg problem. Those fundamentals include:
- Vertices – A vertex (or node) is a point used to represent an individual element of the represented system. You can think of the cities on a map as being vertices.
- Edges – An edge is used to connect vertices. Edges can be directed (i.e.: one way), or undirected (i.e.: bidirectional). Edges generically represent a relationship. If Seattle and Portland can be thought of as vertices, I-5 can be thought of as a bidirectional edge connecting those cities.
- Path – A path is a set of edges and nodes connecting one node to another, whether those nodes are adjacent or not.
- Adjacency – Vertices that share an edge are said to be adjacent.
The proof of concept I’ve come up with for this problem was designed with one goal in mind – automate the process of finding the shortest path to the compromise of a domain administrator, without writing to disk or requiring offline analysis. As such, the design of this graph may not be suitable for other problems.
In designing this graph, I wanted to focus primarily on simplicity. After numerous false starts, I finally landed on a design that works:
- Each user and computer is a vertex.
- All edges are directed and unweighted.
- A directed edge from a user to a computer indicates local admin rights.
- A directed edge from a computer to a user indicates a logged on user.
Imagine a very basic network composed of two computers and two users. The “Administrator” account has admin rights on both systems. One of these systems has a user ‘mnelson’ logged on. The visual representation of this system, using the above design, would look like this:
Each user and computer is a vertex. The orange-colored edges tell us that the Administrator account has admin rights on the two systems. The blue-colored edge tells us that ‘mnelson’ is logged onto HR-WS-002. In this design, an edge always means that the source vertex can compromise the target vertex – Administrator can compromise HR-WS-002, and HR-WS-002 (i.e.: the SYSTEM account on this machine) can compromise mnelson.
Building the Graph
Finding the vertices for our graph couldn’t be easier. Because we’re treating each user and computer as a node, it’s as simple as using two PowerView cmdlets – Get-NetUser and Get-NetComputer:
A visual representation of the graph at this point could look like this:
In preparation for running Dijkstra’s Algorithm, we give each vertex the following properties:
- Name – The name of the vertex. Example: ‘mnelson’ or ‘HR-WS-002’
- Edges – An array of vertices this vertex has an edge to. Initially set to $Null.
- Distance – The number of hops needed to get from the source vertex to this vertex. Initially set to Infinity. Note this is an unweighted graph.
- Visited – Whether or not the shortest distance to this node has been determined. Initially set to $False.
- Predecessor – The name of the preceding vertex in the path from the source vertex to this vertex. Initially set to $Null.
Identifying each vertex’s edges is slightly more involved. Again, we can take advantage of a PowerView cmdlet – this time it’s Get-NetSession. This cmdlet returns session information for each computer we run it against, allowing us to see what users have sessions on that computer and where that session comes from, effectively allowing us to determine what users are logged on where – all without elevated rights. Using this information, we can populate computer vertices with edges to the logged on users. Next, for each computer we have user logon information for, we recursively enumerate the local administrator users on that machine. This information allows us to populate the corresponding user vertices with edges, indicating local admin rights, to that computer.
In my test lab, the completed graph with all edges can be visually represented like this:
Recall that a user -> computer edge denotes admin rights, and a computer -> user edge denotes a logged on user.
Obviously, the “Administrator” account is an admin on each of the three computers. The ‘mnelson’ user is an admin on OPS-WS-002. The ‘jwarner’ user is an admin on IT-SRV-002.
HR-WS-002 has one user logged on: mnelson. OPS-WS-002, one user: jwarner. Finally, IT-SRV-002 with three logged on users: rwinchester, jfrank, and Administrator. The jdimmock user is neither an admin nor logged on anywhere (he’s probably on PTO).
We now have everything we need to find the shortest path from any vertex to any other .
Think back to the scenario I outlined earlier. From ‘Steve-Admin’, we have dozens of computers and users to target, none of which give us immediate access to a Domain Admin account. Instead of spending hours, days, or even weeks analyzing each option (or worse: going through our options in a trial-and-error methodology), we can use an algorithm to find that path for us in minutes.
The more time I spend studying Dijkstra’s Algorithm, the more I recognize and appreciate the genius that went into creating this elegant, efficient approach. Dijkstra’s algorithm allows us to specify a source vertex and find the shortest path to every other vertex in the graph – and it only requires n loops where n is the number of vertices in the graph. Here is how Dijkstra’s algorithm works:
- Identify a source vertex. Set its distance to 0. Set the distance of every other vertex to infinity.
- Identify the unvisited vertex with the lowest distance value and mark it as the Current vertex.
- Consider the edges of the Current vertex. For each vertex adjacent to the Current vertex, compare its distance to the value of the Current vertex’s distance plus one – update the adjacent vertex’s distance if this calculated distance is lower than the current value, and update that adjacent vertex’s predecessor value to the name of the Current vertex.
- Go to step 2 until all vertices have been visited.
Once the algorithm completes, each vertex’s distance value will tell us whether that vertex can be reached by the source vertex, and in how many hops. Additionally, we have a bread crumb trail back to our source thanks to the predecessor property in each vertex:
- Take the name of the target vertex and add it to our path array.
- Take the most recently added vertex in the path array and find its predecessor. Add the predecessor to the path array.
- Repeat until there are no predecessors. At that point we have reached our source vertex.
Conclusion and Proof of Concept
We’ve barely scratched the surface of what’s possible here. There are some very exciting possibilities of applying graph theory (and other fields of mathematics) to Active Directory offense and defense (check out this great post by Brandon Helms aka cr0n1c: https://cr0n1c.wordpress.com/2016/01/27/using-sccm-to-violate-best-practices/ ). For example, by reversing the direction of each edge in this graph and using admin rights to get more logged on user data, we could specify the ‘Administrator’ user as the source and determine, with one iteration of Dijkstra’s Algorithm, all the accounts in AD that could compromise the ‘Administrator’ account.
You can find the proof of concept script here: