BPL Logo Banner
CareersContact

In penetration tests and red team engagements, we frequently exploit privilege escalation paths in Active Directory (AD). When discussing remediation, clients often think, “I’ll just disable that account to eliminate the risk.” However, in some instances, simply disabling the account does not remove the risk. When we explain this to customers, they tend to look confused because the account is indeed disabled. Still, the residual risk is tied to specific escalations involving permissions over a higher-privilege account from alternative access. For example, privileges such as “WriteDACL” or “GenericAll” can enable an attacker to exploit a disabled account by re-enabling it and leveraging the escalation path as if it were never disabled.

Basic Example:

During an engagement, the team gains access to several users through password guessing or by discovering cleartext passwords on shared drives and begins evaluating the access and privilege escalation paths of these compromised users within Active Directory. The team identifies one user (User A) who has (GenericAll) rights over User (B).

This is especially interesting because appears that User A’s access to User B is somewhat outside the normal permissions, as the rest of the permission paths reside with high-value targets.

Further investigation shows that User B can do a lot of interesting things (effectively has control over all the domain objects):

In this instance, User B is disabled. However, since the attackers have User A, who has “GenericAll” over User B, they can enable the user and leverage it to escalate their privileges.

This perfectly demonstrates how simply disabling the account of User B does not reduce the risk for this attack path; it just adds an extra step for the attacker to enable the account, which is trivial.

So What Should You Do?

Now that you understand simply disabling accounts within AD doesn’t always eliminate the attack surface, you should consider the following:

  1. Removing the account if it’s no longer needed
  2. Removing the access from the account if you are unable to remove it entirely
  3. Evaluate attack paths to this disabled account to see if paths exist that expose you to risk

Tips for Investigation:

We find it useful to assess the outbound rights of all disabled accounts (we do this for every account in the domain, but to focus on the topic of this blog, we will concentrate on disabled users) to identify those with significant access. From this point, examine who has rights to reach them (inbound permissions). This can assist in pinpointing potential escalation paths from lower targets to disabled accounts that possess considerable access. Below are some helpful Cypher Queries you can leverage within Neo4j against the Bloodhound database:

List all Disabled Users:

MATCH (m:User {enabled: false}) return m.name, m.objectid, m.admincount

Using the SID for each user (m.objectid), you can list out transitive permissions for each user based on a count:

MATCH (n) WHERE NOT n.objectid='"+objectid+"' MATCH p=shortestPath((u:User 
{objectid: '"+objectid+"'})-
[r1:MemberOf|AddSelf|WriteSPN|AddKeyCredentialLink|AddMember|AllExtendedRights
|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns*1..]->(n)) RETURN count(p)

In practice, we find querying is best done in Python. Below is a more complete snippet of code:

def get_disabledUserOutboundRights_transitive():
    with GraphDatabase.driver(URI, auth=AUTH) as driver:
        result = do_query(driver, "MATCH (m:User {enabled: false}) return m.name, m.objectid, m.admincount")
        disabled_outbound_file=open("disabled_user_outbound_transitive_rights.txt", "w")
        for record in result:
          if record["m.name"]:
              username = record["m.name"]
              objectid = record["m.objectid"]
              admincount= record["m.admincount"]
              result1 = do_query(driver, "MATCH (n) WHERE NOT n.objectid='"+objectid+"' MATCH 
p=shortestPath((u:User {objectid: '"+objectid+"'})-[r1:MemberOf|AddSelf|WriteSPN|AddKeyCredentialLink|AddMember|
AllExtendedRights|ForceChangePassword|GenericAll|GenericWrite|WriteDacl|WriteOwner|Owns*1..]->(n)) RETURN count(p)")
              for record1 in result1:
                  if record1["count(p)"]:
                      trans_rights=str(record1["count(p)"])
                      disabled_outbound_file.write("[-] User: "+username+" Transitive Outbound Rights: "+trans_rights+" 
Admincount: "+str(admincount)+"\n")
        disabled_outbound_file.close()

    with open("disabled_user_outbound_transitive_rights.txt", "r") as fp:
        entries = str(len(fp.readlines()))
    print("[+] Generating a List of Disabled Users with Transitive Outbound Rights: disabled_user_outbound_trans_rights.txt 
("+entries+") lines")

The result will make a text file of all the disabled users and their transitive outbound rights, one per line. This can help us analyze the outbound rights of all disabled users quickly to find interesting targets for additional analysis:

The output will look like this:

[-] User: userb@BREAKPOINT.LABS First Degree Outbound Rights: 20771 Admincount: True

So to sort the data based on "who has the most rights", first we will use the Linux sort utility based on a number in reverse 
order on the 7th element in the line (which is the outbound right count).

└─$ cat disabled_users_outbound_transitive_rights.txt | sort -nr -k 7,7 | less -S
[-] User: userb@BREAKPOINT.LABS Transitive Outbound Rights: 20771 Admincount: True
[-] User: userc@BREAKPOINT.LABS Transitive Outbound Rights: 6165 Admincount: True
[-] User:userd@BREAKPOINT.LABS Transitive Outbound Rights: 5098 Admincount: True
[-] User: usere@BREAKPOINT.LABS Transitive Outbound Rights: 4821 Admincount: True
[-] User: userf@BREAKPOINT.LABS Transitive Outbound Rights: 4821 Admincount: True

From here, review the inbound rights for these accounts in Bloodhound, and you might discover an interesting way to control one of these disabled users who have extensive rights in the domain.

Key Takeaways

Disabling an Active Directory account does not always equate to eliminating risk. Attackers can potentially re-enable disabled accounts if they possess the necessary permissions, such as “GenericAll” or “WriteDACL,” on those accounts. To truly mitigate risks, organizations should go beyond simply disabling accounts. Consider removing the account entirely if it’s no longer needed, revoking its access rights, and thoroughly evaluating potential attack paths leading to these disabled accounts.

By proactively assessing outbound and inbound rights, especially for disabled accounts with significant access, and utilizing tools like Bloodhound and Cypher queries, you can uncover hidden vulnerabilities and strengthen your Active Directory security posture. Remember, a seemingly disabled account can still pose a threat if the underlying permissions are not properly addressed.

Author Image

About the Author

Andrew McNicol is the Chief Technology Officer and a co-founder of BreakPoint Labs, where he oversees the company’s technical strategy and manages the Cybersecurity Assessments line of business. He is a recognized expert in adversarial penetration testing, with more than 16 years of experience leading numerous technical teams on red team operations, vulnerability assessments, and penetration testing engagements to achieve each client’s specific objectives. Andrew holds numerous professional certifications and is a frequent speaker at government and industry security forums.

chevron-down