Manual LDAP Querying: Part 2

This post is a follow-up to my previous post on manual LDAP querying. I would highly recommend reading that post prior to reading this one if you are interested in some of the basics of searching LDAP.

A few people asked why I chose dsquery and ldapsearch for the last blog. There are several options for querying LDAP, but dsquery and ldapsearch were the tools I was most comfortable with. Additionally, dsquery being signed binary, it is easy to get on target without being flagged. Similarly, with ldapsearch, it is easily installed and is sometimes already present on hosts and it can be run through SOCKS proxies. For this blog, I will show less examples for conciseness, but remember the focus is how to query with LDAP filters. The important item to focus on is the LDAP filters themselves.

Many tools that query LDAP have a way for you to create custom filters baked in. Therefore, if you understand how to create LDAP filters, you can use any tool that allows you to create your own custom LDAP filter. While this blog focuses on querying in a Windows Active Directory (AD) environment, LDAP queries can work in other forms of directory services.

For this blog, I will focus on items not covered in the previous blog as well as discuss some of the complexities of manually querying. The goal is to try to get a more accurate understanding of an AD environment and recognize some of the common issues that can arise from querying manually.

Combination Filters

One important item not included in the first blog was how to create compound filters with an OR operator. A compound filter with an OR operator will look like this:
“(|(attribute1=value1)(attribute1=value2))”

In this filter, the “|” indicates that it is a combination filter, and any results should have “value1” or “value2” for attribute1. As with other filters, these operators can be combined with others to create more complex queries.

dsquery * -filter “(&(objectClass=computer)(|(name=*WIN-10*)(name=*WIN-11))(!(description=*HIPAA*)))”

The image and query listed show how these different operators can be used in a combination filter. Here, we are using the AND (&), OR (|), NOT (!), and wildcard (*) operators to create the query. The plain language explanation for this filter would read, “I want all of the computer objects that have WIN-10 or WIN-11 in the name but do not have HIPAA in the description.” When you are creating these queries, keep in mind that the parentheses do matter and if you do not close them correctly, your query may fail or give you inaccurate results.

dsquery * -filter “(&(objectClass=computer)(|(name=*WIN-10*)(name=*WIN-11)(!(description=*HIPAA*))))”

This is one example of how the parentheses can affect your results. In this example, the parentheses around the OR statement were moved to include our not parameter. In plain language, the way this is interpreted is, “I want all of the computer objects that have WIN-10, WIN-11, or does not have HIPAA in the description.” The JZOID-WIN-10, which should have been excluded because it contains HIPAA in the description, was included in the results because it met the WIN-10 parameter in the OR statement while others which do not have WIN-10 or WIN-11 were included because they do not have HIPAA in the description.

Nested Groups

Nested groups occur when one group is added as a member of another group. This is a common practice but can create additional complexities and unforeseen results. It can also make it harder to find what you are looking for when you are trying to understand permissions in an environment because permissions inherit down to nested members of a group. Here I will show why it is important to check for nested groups. In this first screenshot, the query is looking for any users who are members of the Domain Admins group.

dsquery * -filter “(&(objectClass=user)(memberOf=CN=Domain Admins,CN=Users,DC=PLANETEXPRESS,DC=LOCAL))”

However, when we look at the same group through the administrator view, we see there is a group nested within the Domain Admins groups.

This group did not show up because our query is looking for users who are members of the Domain Admins group, not groups who are members of the Domain Admins group.

This next block is a query which filters on the memberOf attribute of the Domain Admins group. This returns the Security group, but does not show who the members are who may also be part of the Domain Admins group.

dsquery * -filter “(&(objectClass=group)(name=Domain Admins))” -attr member

From this information, you could further expand and query who the members of the Security group are, as shown below.

dsquery * -filter “(&(objectClass=group)(name=Security))” -attr member

One option for getting a more precise list of group membership would be to ensure that your filter is not being too restrictive. While usually we want to be as specific as possible to reduce the number of results that must be evaluated, this query is an example of how making a query too specific of a filter can give inaccurate results.

dsquery * -filter “(memberOf=CN=Domain Admins,CN=Users,DC=PLANETEXPRESS,DC=LOCAL)”

By not specifying an object class in the query, we will get a list of both the users and groups that are members of the group.

Alternatively, using a combination of dsquery and dsget (which is also a signed binary and available on many servers), we can get the nested members of the group. Here we can now see that the user Turanga Leela is a nested member of the Domain Admins group.

dsquery group -samid “*Domain Admins*” | dsget group -members -expand

While dsget is outside of the scope of LDAP filters, it works here to show an example of how information can be obscured when using LDAP filters. This example shows some of the complexities of manual querying and why it is important to be thorough when you are doing manual investigation. Nested groups are an item you want to look for in an assessment. While it is much easier to visualize with tools such as BloodHound, mapping it out manually can be complex but worth the time.

Service Principal Names

Microsoft defines a Service Principal Name (SPN) as, “a unique identifier of a service instance”. Kerberos authentication uses SPNs to associate a service instance with a service logon account.

SPNs can be applied to accounts and are common to find in an Active Directory environment. The reason we want to check for their presence is to discover if there are any accounts that would be good targets for Kerberoasting. We can run a quick query to see if any accounts have SPNs applied to them as shown below.

dsquery * -filter “(servicePrincipalName=*)” -attr name servicePrincipalName

NOTE: This query may be detected by identity protection products as enumeration for Kerberoasting.

In the above example, we are querying for any account that has an SPN applied to it. In the results, we can see there are four accounts. The first one is the computer account belonging to the Domain Controller, which has several SPNs applied. This account, along with the PFRY-WIN-10 computer account would not be good targets for Kerberoasting. This is because computer accounts typically have long randomized passwords set and are difficult to break. Similarly, the krbtgt account would not be a good target for Kerberoasting; if the krbtgt password is easy to crack, the domain is in a very bad position already. The third account in the returned list is a user account, which could potentially be a good candidate for Kerberoasting.

This is not all the information you would need to determine if Kerberoasting is a viable option, but it is information from a quick query that could inform you on potential attack paths. As we have seen with other queries, we can narrow down our results to user accounts only to save a bit of time.

User Account Control

The userAccountControl attribute is a number set depending on the user account setting. These are not as straightforward as some of the other attributes we have discussed so far and the available Microsoft documentation is incomplete on what the numbers indicate. Fortunately other resources contains a more complete list.

Since the domain we are working in here is small, we can list out this attribute for all users as shown below.

dsquery * -filter “(userAccountControl=*)” -attr name userAccountControl

The first value of 512 indicates a NORMAL_ACCOUNT which applied to HFarnsworth and Hermes Conrad. This can be a bit misleading as we know from previous queries that HFarnsworth is a Domain Administrator. So, what does this mean then? If we look at it from the Administrator perspective, we can see this is related to the “Account options” settings as shown below.

What the 512 value indicates is that the user accounts do not have any of the properties enabled. It does not indicate the users’ permissions or privileges in the environment.

The next value is the 514, which means the account is disabled. In addition to the krbtgt account having this value, we can see that the Scruffy Service account is also disabled. This is something we would want to note if the Scruffy Service account were one we wished to target. Additionally, you can use the userAccountControl to filter out disabled accounts like the query below.

dsquery * -filter “(&(objectClass=user)(!(userAccountControl=514)))” -attr name userAccountControl

The next item we will look at is the 66048 value, which indicates a user account whose password does not expire. This could be a good account to go after because it could indicate that the account has an old password, which may be easier to obtain or crack. In the example below, we are printing out which users have a password set to not expire, as well as the pwdLastSet attribute which can help target accounts with old passwords.

dsquery * -filter “(userAccountControl=66048)” -attr name userAccountControl pwdLastSet

There are several more interesting values, but I would suggest reading the sources linked for additional information. The value is shown in decimal, but it is calculated in hexadecimal and is cumulative with an addition calculation. For example, most user accounts are labeled as NORMAL_ACCOUNT, which has a decimal value of 512 and a hexadecimal value of 0x0200. If the password is stored using reversible encryption, it will be assigned a user account control value of 128 in decimal and 0x0080 in hexadecimal. The account setting is displayed as shown in the image below within Active Directory Users & Computers (ADUC).

So if a normal user account is set to store the password with reversible encryption and no other options set, the value for that account would be 640 (512 + 128) in decimal, and 0x0280 (0x0200 + 0x0080) in hexadecimal. This makes querying for a specific user account control setting easy because the value will be unique based on the calculation of the settings for the user. We can query for a user account control value and it will pull all users who have that setting. However, if we were to just query with userAccountControl and the value, we would only get the users with that exact calculated value. In the image below, the query is looking for normal user accounts, but it is only pulling one account.

dsquery * -filter “(userAccountControl=512)” -attr name userAccountControl

Since that is not at all helpful, we have to add a bit filter for bit-wise comparison by specifying the BitFilterRule ID. There is an AND and OR filter:

  • LDAP_MATCHING_RULE_BIT_AND 1.2.840.113556.1.4.803
  • LDAP_MATCHING_RULE_BIT_OR 1.2.840.113556.1.4.804

The way this is added to the LDAP filter is in the following format:

<Attribute name>:<BitFilterRule-ID>:=<decimal comparative value>

With this filter, we can pull all users with a specific setting as well as other settings calculated in the userAccountControl value. As shown below, either filter will work for this particular query, but depending on what you are searching for, you should choose the appropriate filter.

dsquery * -filter “(userAccountControl:1.2.840.113556.1.4.803:=512)” -attr name userAccountControl

dsquery * -filter “(userAccountControl:1.2.840.113556.1.4.804:=512)” -attr name userAccountControl

With the BitFilterRule, you can query for other values stored in a hexadecimal value, such as grouptype.

Passwords in LDAP Attributes

Although most organizations make efforts to secure passwords, it is possible that passwords or password hashes are stored in LDAP attributes. The most common way I have seen this occur is when an organization adds software to the environment that can use AD for authentication. Sometimes this software will update the schema to add additional attributes in AD which store passwords or hashes. To find this, you can search if these fields exist and are populated in an environment with the following filter:

“(|(UserPassword=*)(unicodePwd=*)(UnixUserPassword=*) (msSFU30Password=*)(orclCommonAttribute=*)(defender-tokenData=*)(ms-Mcs-AdmPwd=*))”

This filter includes common attributes which store credentials in AD. These are the attributes this filter looks at:

Another area where I have found passwords stored is in the description for accounts, so it is a good idea to check there as well.

Organizational Units

Organizational units (OUs) are logical groups for objects in AD. OUs differ from containers because they allow policies in the form of Group Policy Objects (GPOs) to be applied to them whereas containers do not. OUs can be created and organized at administrators’ discretion though, by default, one OU is automatically created for Domain Controllers when a domain is created. The OUs can be hierarchical, where one OU is added as a member to another OU and can be nested. Many organizations will create the OU structure to reflect the organization’s business structure.

To get a quick list of the OUs in a domain, you can run the dsquery command for the object class “organizationalUnit” to get the OUs.

dsquery * -filter “(objectClass=organizationalUnit)”

This will give you the distinguished names of the OUs. In this domain, the Device OU is nested under the Domain Computers OU, which is reflected in the full name of the OU. If we are querying an object, the OU is reflected in the distinguished name of the object.

dsquery * -filter “(name=BROD-SELF)” -attr name distinguishedName

This will reflect where in the AD the structure object resides. If the object is not in any OUs, the distinguished name will reflect where in the container structure the object resides as shown below.

dsquery * -filter “(name=FS-CREW)” -attr name distinguishedName

The difference between these two is the computer object that is not in an OU would not have GPOs applied to it. If you were looking for objects to target, this could be an indication of a misconfiguration in an AD environment where an object was created in the wrong container or OU and does not have the same policies applied.

Searching for objects in an OU is not as straightforward as some other queries. OUs for an object make up the distinguished name, but this does not form a separate attribute stored in the object information. LDAP does not have a way for you to partially match a distinguished name easily and wildcards will not return results. Instead, you can specify the distinguished name as the start node for your query. This will specify the OU where you want the query to start searching for the objects specified in the LDAP filter.

dsquery * -filter “(objectCategory=computer)” -attr name distinguishedName

dsquery * “OU=Domain Computers,DC=PLANETEXPRESS,DC=LOCAL” -filter “(objectCategory=computer)” -attr name distinguishedName

dsquery * “OU=Devices,OU=Domain Computers,DC=PLANETEXPRESS,DC=LOCAL” -filter “(objectCategory=computer)” -attr name distinguishedName

In the first query, the search will encompass the entire domain. In the second query, we are specifying the start node which will begin searching in the “OU=Domain Computers,DC=PLANETEXPRESS,DC=LOCAL” for objects. In the third query, we are specifying the nested OU, “OU=Devices,OU=Domain Computers,DC=PLANETEXPRESS,DC=LOCAL”, as where we want to start our search. As we can see, specifying the start node will give us more specific results depending on what OU we are starting our search in.

Lastly, if we wanted to see the objects in an OU, an alternate way we could view this information quickly is by providing the start name in our query and simply specify the OU as the start node.

dsquery * “OU=Domain Computers,DC=PLANETEXPRESS,DC=LOCAL”

Typically, when querying for OUs from an operational perspective, the goal is to correlate the objects with what group policies are applied to it or vice versa.

Domain Trusts

For this section, I will assume you are already familiar with the concepts and terms associated with domain trusts. If you are not, this resource from Microsoft can help to get you familiar with what you need to know. The important thing to know is that when a trust relationship is established between domains, the relationship is represented in AD as a trusted domain object. The easiest way to start looking at the trust relationships is by querying for that object type, as shown below:

dsquery * -filter “(objectCategory=trustedDomain)”

The information provided here is limited but it will give us a quick list of what relationships, if any, exist in a domain. This alone is not enough information to understand the trust relationship between the domains, so we will need to get additional information. If you are looking for a specific type of trust relationship, you can filter the trustDirection attribute in your LDAP filter to specify the type of relationship.

dsquery * -filter “(trustDirection=3)”

Since this attribute is unique to this object type, we can filter for just that attribute for a less complicated filter. In this example, the value of 3 means that it is a bidirectional trust relationship. For additional information, you can review the Microsoft documentation here. For quick reference, the following is the mapping of numbers to relationships:

  • 0 — trust relationship is disabled
  • 1 — trust relationship is inbound
  • 2 — trust relationship is outbound
  • 3 — trust relationship is bidirectional

Trust type is another attribute that can help us to understand trust relationships. The trust type, represented with the trustType attribute, will indicate the type of trust the current domain has with the foreign domain.

dsquery * -filter “(trustType=2)”

With this query, we are specifically looking for domains which are Windows domains and are running Windows AD. The type of domain is relevant when you are looking to cross domains and want to know what technology the other trusted domain is running. LDAP works with environments not running Windows AD, so LDAP queries and filters will often work as well. To read more about the trust types, Windows has provided this documentation. The quick meaning of the trust type values is as follows:

  • 1 — Windows domain not running AD
  • 2 — Windows domain running AD
  • 3 — non-Windows with Kerberos
  • 4 — not used
  • 5 — Entra ID (formerly Azure AD)

Trust partner is a mandatory attribute which holds the fully qualified domain name (FQDN) of the trusted domain. When you are enumerating the trust relationships, the FQDN is necessary for activities such as querying into the trusted domain or interacting with some resources in the other domain.

dsquery * -filter “(objectClass=trustedDomain)” -attr name trustPartner

Instead of searching by this attribute, this query will retrieve the trusted domain objects and list the name and trustPartner attributes. This is a simple attribute, however additional information can be found in the Microsoft documentation. Keep in mind that when you are querying, limiting your output can be very beneficial for getting exactly the information you need to work with.

The last part of the discussion of trust relationships is the trust attributes. This is another attribute which describes the trust relationship the current domain has with other trusted domains.

dsquery * -filter “(trustAttributes=8)”

In this example, the filter will return trust relationships which have a trustAttributes value of 8, which means the trust is established between forests. These trust attributes can help inform you on how you can interact with another domain due to the attribute value containing information on restrictions. There is further detail in the documentation from Microsoft on the trust attributes. The quick reference for these attributes is as follows:

  • 1 — Trust is not transitive
  • 2 — Only Windows 2000 and newer operating systems can use the trust
  • 4 — Domain is quarantined and subject to SID filtering
  • 8 — Cross forest trust between forests
  • 16 — Domain or forest is not part of the organization
  • 32 — Trusted domain is in the same forest
  • 64 — Trust is treated as an external trust for SID filtering
  • 128 — Set when trustType is TRUST_TYPE_MIT, which can use RC4 keys
  • 512 — Tickets under this trust are not trusted for delegation
  • 1024 — Cross-forest trust to a domain is treated as Privileged Identity Management (PIM) trust for the purposes of SID filtering
  • 2048 — Tickets under this trust are trusted for delegation

Foreign Security Principal

The foreign security principal is an object which originates from an external source. The easiest way to identify a foreign security principal is to query for the object class.

dsquery * -filter “(objectClass=foreignSecurityPrincipal)”

The first four items can be ignored or filtered out because they are standard in every domain. Unlike the other returned objects, the principal is not stored with an easily readable name but uses the object SID to represent the object. When retrieving further information on the object, we can see that it is still not entirely clear what the object is.

dsquery * -filter “(distinguishedName=CN=S-1–5–21–3542491776–619976232–3744802786–1605,CN=ForeignSecurityPrincipals,DC=PLANETEXPRESS,DC=LOCAL)” -attr *

Since the setup within this environment is simple, with only one other connected domain, the object is easy to find in the trusted domain by using the trust relationship that is established between the domains. Depending on the tool you are using, the flags will be different, but in dsquery the /domain flag will query another domain for information.

dsquery * -filter “(objectSid=S-1–5–21–3542491776–619976232–3744802786–1605)” /domain DOOP-HQ.LOCAL

Expanding the attributes will provide additional information about the object.

dsquery * -filter “(objectSid=S-1–5–21–3542491776–619976232–3744802786–1605)” /domain DOOP-HQ.LOCAL -attr *

With this, we can see the object is a group which references a user from the foreign domain. This information is helpful when understanding how trust relationships are used in an environment. These sorts of relationships can be potential paths for traversal in an environment when looking to move to a different domain.

Conclusion

That’s it for another round of LDAP filter discussions. The topics in this blog were chosen based on what I was not able to cover in the last blog and feedback I received from the first blog. Unfortunately, there again was not the space or time to explain everything, but hopefully this helps create a bit more understanding about manual LDAP querying.

Special thanks to Adam and Sarah for proofreading and editing!

Manual LDAP Querying: Part 2 was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.

Article Link: Manual LDAP Querying: Part 2. This post is a follow-up to my previous… | by Hope Walker | May, 2024 | Posts By SpecterOps Team Members