|
Research
|
Blog
|
Newsletter
|
Websession
|
EN

A short Introduction to BloodHound Custom Queries

28.01.2025

In my last blog post I already showed how to interact with BloodHound CE and some Cypher queries to find common misconfigurations. In this post, we’ll present custom BloodHound queries to find real-world vulnerabilities and misconfigurations in Active Directory networks.

Introduction

Active Directory plays a very important role in our Corporate Network penetration tests. In many of our tests we manage to compromise the target domain in a short time. Often the abused attack paths are relatively simple, such as escalating privileges via ESC8. However, these very simple attack paths are becoming increasingly rare. The tool which is unavoidable in pentesting Active Directory is BloodHound. During those slightly tougher pentests BloodHound helped us to identify other attack paths which are a little more complex and require more steps. By utilizing BloodHounds Cypher queries and modifying them yielded good results for us (to get domain admin) and for our clients (to get detailed information about misconfigurations). This blog post will discuss some of the custom queries that we use and how some connections can be abused.

One thing we like to do during our engagements is, if possible, to run SharpHound before and after getting domain admin privileges. This allows us to see the domain from two different perspectives. First the view from a normal user account and then a more white box view over the Acitve Directory. With domain admin privileges we can see much more of the possible information that BloodHound can collect, such as all local admin privileges on all systems, all RDP privileges and all active sessions. With this information we can find more potential attack paths that could be exploited. This can be very useful as we can identify potential attack paths from specific users, e.g. from a lower tier admin to a domain admin.

Practical Examples

Although BloodHound has some useful queries built in and writing simple queries is not that difficult, it is very useful to have custom queries at hand. Not only does it speed up the process, but it also reduces the risk of forgetting to look at certain aspects of the Active Directory during an engagement. In every one of our recent engagements we have discovered something new and unique. Sometimes this was just a unique configuration that wasn’t exploitable on its own, but there have also been some cases where a domain compromises has been achieved by finding specific configurations through BloodHound.

Will discuss the following topics:

  • Inactive objects
  • Cross domain group memberships
  • Local admin rights
  • Protected users
  • Path to untagged Tier Zero

All the examples are done in an lab based on GOAD with some modifications.

Inactive objects

The Active Directory is quite historically grown in many environments we see. Old Active Directory objects may get disabled and/or put in an separate OU but remain in the Active Directory for many years. We often find test users or retired service accounts with very weak passwords, that have not been used in a long time but are still active. These forgotten accounts can be abused by attackers as they often have some sort of privilege that is of interest for attackers. Additionally, in certain circumstances, the only way to compromise other users is to reset their password, so these inactive objects can be used as the risk of impacting the environment is alot smaller. The following two conditions can be added to exsisting queries to filter for active/inactive objects:

Inactive objects

u.lastlogon < (datetime().epochseconds - (90 * 86400)) AND 
u.lastlogontimestamp < (datetime().epochseconds - (90 * 86400))

Active objects

(c.lastlogon > (datetime().epochseconds - (30 * 86400)) OR 
c.lastlogontimestamp > (datetime().epochseconds - (30 * 86400)))

Both attributes lastlogon and lastlogontimestamp must be used to obtain an accurate timestamp. They represent two different times: the last logon on the queried DC and the last logon on another DC (through replication). These two conditions can be added to any cypher query in order to further filter the results. For example: If we were able to dump the NTDS.DIT from the DC and crack some of the passwords. To further increase the value of this finding for our client, we can use BloodHound to generate lists of items filtered by specific conditions, such as inactive users (no login in the last n days). This can be useful for the client to see directly how many active users have weak passwords or how many unused accounts could easily be compromised. To generate such a list, that can be used to grep users from the cracked NTDS.DIT or can be given to the client as a list of affected resources, the BloodHound API or neo4j (directly or via web interface) must be used.

Inactive users (no logon in last 90 days)

MATCH (u:User) 
  WHERE u.lastlogon < (datetime().epochseconds - (90 * 86400)) AND 
      u.lastlogontimestamp < (datetime().epochseconds - (90 * 86400)) AND 
      u.samaccountname <> 'krbtgt' AND 
      u.enabled = true
RETURN u

Active computers with unsupported OS (logon in last 30 days)

MATCH (c:Computer) 
  WHERE c.operatingsystem =~ '(?i).*(2000|2003|2008|2012|xp|vista|7|8|me).*' AND 
    (c.lastlogon > (datetime().epochseconds - (30 * 86400)) OR 
    c.lastlogontimestamp > (datetime().epochseconds - (30 * 86400))) AND 
    c.enabled = true 
RETURN c

An example script to retrieve the names of all inactive and enabled users from BloodHound using the API.

Cross-domain group memberships

During one of our more recent engagements we were faced with two domains: let’s call them A and B. Domain A was newly built and had security in mind. Since it was the primary used domain, we focused on it. But the customer did a pretty good job and we weren’t getting far regarding the privilege escalation to domain admin. So we decided to take a look at domain B. Domain B was the previous domain in use and was still being used for some services and systems, but the plan was to retire this domain soon. This also meant that a trust relationship between these two domains was present. Getting domain admin in domain B was quickly achieved. After collecting BloodHound data from both domains, the combined information provided some interesting information. The following query was written to identify cross-domain group memberships:

MATCH p=((u1:Group)-[r:MemberOf]->(u2:Group))
  WHERE toLower(u1.domain) <> toLower(u2.domain) 
RETURN p

This query is sub-optimal but it got the job done. We were able to identify a group in domain B which is a member of a high privileged group in domain A. In the end we were able to compromise domain A by abusing this cross-domain connection and some further exploits. Something similar can be found in GOAD.

The bottom two clusters are due to the parent-child trust relationship of the sevenkingdoms.local and north.sevenkingdoms.local domains. The cluster on the top on the other hand is not default. It’s possible to fully unwrap all nested group memberships, which end in crossing a domain. This can be useful, for example, to identify users to target. The following query returns all objects that cross their domain via nested group memberships:

MATCH p=((n)-[r:MemberOf*1..]->(m:Group))
  WHERE n.domainsid <> m.domainsid
return p

This query is also improved by using the domainsid to compare instead of the domain name. By using the domainsid, the query also returns objects that are not collected by their coresponding domain. Our hacky query using the domain name worked because we collected both domains. Depending on the size of the domains, this query may take some time, but can benefit the client by providing a detailed list of all objects that cross domain boundaries.

An example script to retrieve the names of all objects in groups which cross the domain boundary from BloodHound using the neo4j endpoint.

Local admin rights

Another interesting connection to visualize in BloodHound is local administrative privileges. In order to collect the nessessary data, we need to have administrative privileges on the systems we want to collect the data from. Therefore, it makes the most sense to run as DA to collect all the data, show potential misconfigurations and reveal new attack paths. If we can run SharpHound as our normal user, we can only see this information for systems where we have local administrative privileges, or only from a very few systems. This is enough if we are just try to escalate to DA, but with DA we can go further and present more valuable information to the client.

In a recent engagement we were able to get DA by abusing computer accounts that had local administrative privileges on other computer accounts. In combination with other misconfigurations such as missing SMB Signing, we were able to coerce an authentication from a system with local admin privileges to that system and dump the SAM. Using the local administrator’s password hash, we were able to dump the LSASS process and continue on our path to DA.

The following query can be used to identify these connections between two computer objects:

MATCH p=(m:Computer)-[:AdminTo]->(n:Computer) 
RETURN p

This may seem like a strange configuration to look at but some software solutions require it, e.g. SCCM. In SCCM, the site server has local administrative privileges on the site systems (ref. Misconfiguration-Manager: ELEVATE-1). This can also be modified to include group memberships in cases where the computer object doesn’t have the admin privileges directly but through a group. However, we will focus on the direct connection for this attack. The final result could look like this (not included in GOAD by default):

This means we can coerce an authentication from meereen and relay it to braavos in order to gain local administrative access on braavos. The only requirement is that SMB signing is disabled on braavos.

The following systems are used: meereen (xx.xx.xx.62), braavos (xx.xx.xx.64), attacker (xx.xx.xx.65)

We can use many coercion methods to perform this attack. Here we use printerbug.

The coerced authentication can be relayed to braavos using ntlmrelayx in order to read the SAM database.

The dumped local admin hash can be used to access the system and dump the LSASS process in order to get hashes from domain users.

Protected users

One missing hardening measure we often find is that highly privileged users are not in the protected users group. Users in this group have additional protections against credential theft, such as disabling NTLM authentication for these users or limiting the validity of Kerberos tickets. The following snippet returns all users that are tagged as Tier Zero and are not in the protected users group:

MATCH (u1:User)-[:MemberOf*0..]->(g1:Group) 
  WHERE g1.objectid ENDS WITH '-525'
WITH COLLECT(u1) AS exclude1 
MATCH (u:User)
  WHERE u.system_tags = 'admin_tier_0' AND
    NOT u IN exclude1
RETURN u

The returned users can be provided to the client in order to add missing administrative users to this group. Additionally, this query can be integrated into other queries for further filtering. A good use case would be to search for all active sessions and filter out all protected users. This way we can find all active sessions where we could dump the hash and pass-the-hash for authentication. The following query uses the unprotected tier zero users in the targets variable and uses it to filter on all active sessions:

MATCH (u1:User)-[:MemberOf*0..]->(g1:Group)
  WHERE g1.objectid ENDS WITH '-525'
WITH COLLECT(u1) AS exclude1 
MATCH (u:User) 
  WHERE u.system_tags = 'admin_tier_0' AND
    NOT u IN exclude1
WITH COLLECT(u) AS targets 
MATCH p=(n:User)<-[:HasSession]-(c:Computer)
  WHERE n IN targets
RETURN p

However, it should be noted that it is still possible to abuse active sessions of protected users.

Path to untagged Tier Zero

In an Active Directory environment, there are often services with many permissions that can be used to escalate privileges, e.g. Exchange, WSUS, SCCM or CA. All of these services and systems could be used to compromise high value targets and are therefore of interest to adversaries. For example, if an adversary can successfully compromise the Certificate Authority (CA), they can forge any certificate and authenticate as a domain administrator. The following query shows the shortest paths to compromise the CA from non Tier Zero objects. It may be necessary to add a limit for larger domains.

MATCH p=shortestPath((n)-[:Owns|GenericAll|GenericWrite|WriteOwner|WriteDacl|
  MemberOf|ForceChangePassword|AllExtendedRights|AddMember|Contains|GPLink|
  AllowedToDelegate|AllowedToAct|AdminTo|CanPSRemote|CanRDP|ExecuteDCOM|
  HasSIDHistory|AddSelf|DCSync|ReadLAPSPassword|ReadGMSAPassword|DumpSMSAPassword|
  SQLAdmin|AddAllowedToAct|WriteSPN|AddKeyCredentialLink|SyncLAPSPassword|
  WriteAccountRestrictions*1..]->(ca:EnterpriseCA)) 
    WHERE (n.system_tags <> 'admin_tier_0' or n.system_tags IS NULL) and 
          n.objectid <> 'S-1-5-32' and 
          not n:OU and 
          not n:Container and 
          n <> ca
RETURN p

In the where clause, the results are filtered and cleaned to return only starting notes which are not Tier Zero and filter out some starting points like OUs or Containers which by themself can not really be leveraged as a starting point in an attack.

This figure shows that there are quite a few ways to compromise the CA. Let’s assume we are on an engagement and have compromised the user lord.varys@sevenkingoms.local. We look for a path from our compromised objects to the CA by adding {system_tags: 'owned'} as a starting condition in the previous query.

MATCH p=shortestPath((n {system_tags:'owned'})-[:Owns|GenericAll|GenericWrite|WriteOwner|WriteDacl|
  MemberOf|ForceChangePassword|AllExtendedRights|AddMember|Contains|GPLink|
  AllowedToDelegate|AllowedToAct|AdminTo|CanPSRemote|CanRDP|ExecuteDCOM|
  HasSIDHistory|AddSelf|DCSync|ReadLAPSPassword|ReadGMSAPassword|DumpSMSAPassword|
  SQLAdmin|AddAllowedToAct|WriteSPN|AddKeyCredentialLink|SyncLAPSPassword|
  WriteAccountRestrictions*1..]->(ca:EnterpriseCA)) 
    WHERE (n.system_tags <> 'admin_tier_0' or n.system_tags IS NULL) and 
          not n:OU and 
          not n:Container and 
          n <> ca
RETURN p

This query returned the following path. Our compromised user is a member of a group that is a member of a group in another domain. This group has high privileges in the other domain that allows us to change Active Directory attributes of the computer object braavos, which is the CA. This can be used to add shadow credentials to take over the CA. Once an attacker has compromised the CA, the domain can also be compromised as well.

Adding shadow credentials to the computer account braavos and requesting a TGT.

The first step in our scenario is to add shadow credentials to the braavos computer object using pywhisker. Afterwards the TGT and hash of the computer account can be requested from the DC.

Requesting the NT hash for braavos.
Retrieving the private key from the CA and forging a user certificate for the domain admin.

The last step is to use the Kerberos S4U2self functionality to gain local administrative privileges and create a backup of the CA. The CA’s private key can then be used to create arbitrary certificates, including for domain admins, that can be used for authentication.

Conclusion

To sum it all up: Custom queries not only speed up the process of analyzing BloodHound data, but also allow us to filter the data to get exactly what we need. For us attackers, this can help us be more efficient and find unique attack paths more easily. For our clients, the data can be presented in a way that answers their specific in a straightforward way, e.g. which users are affected by a particular attack/misconfiguration.

I hope this blog post has given you a short overview of how custom queries can be used to get the most out of BloodHound and that some of the shown queries shown are useful for your use case. If you came up with custom queries yourself, feel free to reach out and share them!

Cheers,
Robin Meier

Zurück zur Blog-übersicht
Zurück zuM Research-Blog
Alle Mitarbeiter-Interviews