This post serves for those who want to know the basics of LDAP reconnaissance and doing this in an OPSEC-safe manner. It is also a nice cheat sheet for future me. Stay tuned for the bonus at the end!
LDAP syntax
LDAP syntax follow this format in general:
(attribute operator value)
- Attribute is a field name, e.g., sAMAccountName.
- Operator is a comparison type, e.g., equal to (=). A list of all operators can be found here.
- Value: the pattern to match. Allows for wildcards. For example, (sAMAccountName=j*)searches for all usernames starting with “j”.
You can chain more search filters together. For example,
(|(attribute1=value1)(attribute2=value2))
will search for objects where either value1 is the value of attribute1 or value2 is the value of attribute2. For the full syntax documentation, see Microsoft.
Queries
We now list some queries for LDAP reconnaissance. Placeholders are in <>.
The following examples use the
ldapsearchBOF from TrustedSec’s Situational Awareness repo. In my lab, I use Havoc as C2 which uses a pretty outdatedldapsearchBOF by default. I have written a new “agressor” script, which supports the latest version of the BOF. The script can be found here.
Important: add
--attributes *,ntsecuritydescriptorto get the ACLs field on the objects you are querying. This is helpful in BloodHound. Also, make sure there are no spaces afterntsecuritydescriptorbecause otherwise the BOF will not display this field.
View domain info
This query allows you to obtain the domain SID among other fields, which can be useful when e.g., crafting diamond tickets.
ldapsearch "(objectClass=domain)" --attributes *,ntsecuritydescriptor
Query single SID
ldapsearch "(objectSid=<SID>)" --attributes *,ntsecuritydescriptor
Query user or group
ldapsearch "(sAMAccountName=<user or group>)" --attributes *,ntsecuritydescriptor
OU Walking
Search for an Organizational Unit (OU) with a specific keyword in the name. After that, you can do as Dominic Chell likes to call OU walking:
“You can consider LDAP to take a hierarchical structure, much like a tree. This structure is composed of various entries, each representing objects like users, groups, or organisational units. Each entry is identified by a unique Distinguished Name (DN). The search base determines the starting point in the directory from where the search for entries begin. The search base, in combination with the search scope, determines which entries are considered during the search. The scope can be set to different levels such as:”
| --scope | Name | Description | 
|---|---|---|
| 1 | Base | Searches only the entry defined as the search base | 
| 2 | One level | Search all entries in the first level below the base-entry, excluding the base-entry. | 
| 3 | Subtree | Searches the base entry and all entries under it, traversing the entire subtree | 
“Essentially, OU walking involves slowly mapping out the organisational units in the directory. This hopefully provides sufficient information on identifying where the objects of interest might reside, assuming a tidily structured directory.”
In the following example, we search for all organizational units in OU “Domain Controllers”, but we do not traverse sub-OUs.
ldapsearch "(objectClass=organizationalunit)"
--dn "OU=Domain Controllers,DC=<domain>,DC=<local>"
--scope 1 --attributes *,ntsecuritydescriptor
Unrolling group membership
The query below finds all objects that are members, directly or indirectly of CN=<GroupName>,DC=<domain>,DC=<local>.
ldapsearch
"(memberOf:1.2.840.113556.1.4.1941:=CN=<GroupName>,DC=<domain>,DC=<local>)"
--attributes *,ntsecuritydescriptor
1.2.840.113556.1.4.1941 is a matching rule Object Identifier (OID), which in this case represents LDAP_MATCHING_RULE_IN_CHAIN. From Microsoft docs:
“This rule is limited to filters that apply to the DN. This is a special “extended” match operator that walks the chain of ancestry in objects all the way to the root until it finds a match.”
In other words, it traverses the entire chain of membership, essentially unrolling it.
Note: OID
LDAP_MATCHING_RULE_IN_CHAINis also known asLDAP_MATCHING_RULE_TRANSITIVE_EVAL, according to MS docs. Just Microsoft things I guess.
User Account Control
The userAccountControl is a bitmask that stores various properties and states of a user or computer account. Some examples:
| Property | Hex value | Decimal value | Description | 
|---|---|---|---|
| NORMAL_ACCOUNT | 0x200 | 512 | Regular user account | 
| DONT_REQ_PREAUTH | 0x400000 | 4194304 | ASREProastable | 
| TRUSTED_FOR_DELEGATION | 0x80000 | 524288 | Unconstrained delegation | 
| TRUSTED_TO_AUTH_FOR_DELEGATION | 0x1000000 | 16777216 | Constrained delegation | 
All possible userAccountControl values can be found here.
Because userAccountControl is a bitmask, we need to apply specific matching rule OIDs, such as LDAP_MATCHING_RULE_BIT_AND aka 1.2.840.113556.1.4.803. This rule is equivalent to a bitwise AND operation.
For example, here we query for a regular user account that does not require preauthentication. The userAccountControl value 4194816 is the OR of 512 and 4194304.
ldapsearch "(userAccountControl:1.2.840.113556.1.4.803:=4194816)"
--attributes *,ntsecuritydescriptor
It is therefore logically equivalent to:
ldapsearch "(&(userAccountControl:1.2.840.113556.1.4.803:=512)
(userAccountControl:1.2.840.113556.1.4.803:=4194304))"
--attributes *,ntsecuritydescriptor
Note: in the
ldapsearchBOF, you cannot query by hex value, just decimal.
SAM Account Type
It is also possible to search by account type. For example, a group object is 0x10000000, and a machine account is 0x30000001. Note that unlike userAccountControl, this is not a bitmask! All possible values can be found here.
For example, here we search for all machine accounts:
ldapsearch "(sAMAccountType=805306369)" --attributes *,ntsecuritydescriptor
Query LAPS information
This query searches for the ms-Mcs-AdmPwd attribute, which contains the plaintext LAPS password of computer objects.
ldapsearch "(name=ms-Mcs-AdmPwd)"
Please note that by default, only Domain Admins are able to read this property. For more techniques to find passwords, check Hope Walker’s post.
Practical example: Game of Active Directory (GOAD)
Now let’s put the above into practice. We will try to elevate our privileges in domain north.sevenkingkingdoms.local, part of Game of Active Directory (GOAD). We start as user robb.stark. Firstly, we query the Domain Admins group:
Again, make sure that
ntsecuritydescriptordoes not end with a space, otherwise this field will not be included in the output.
ldapsearch "(sAMAccountname=Domain Admins)" --attributes *,ntsecuritydescriptor
--snip--
objectClass: top, group
cn: Domain Admins
description: Designated administrators of the domain
member: CN=robb.stark,CN=Users,DC=north,DC=sevenkingdoms,DC=local, CN=eddard.stark,CN=Users,DC=north,DC=sevenkingdoms,DC=local, CN=Administrator,CN=Users,DC=north,DC=sevenkingdoms,DC=local
distinguishedName: CN=Domain Admins,CN=Users,DC=north,DC=sevenkingdoms,DC=local
instanceType: 4
whenCreated: 20250617175604.0Z
whenChanged: 20251003201639.0Z
--snip--
We use the BOFHound tool to convert this LDAP data into a BloodHound-compatible zip.
Havoc/data $ bofhound -i loot --parser Havoc --zip
 _____________________________ __    __    ______    __    __   __   __   _______
|   _   /  /  __   / |   ____/|  |  |  |  /  __  \  |  |  |  | |  \ |  | |       \
|  |_)  | |  |  |  | |  |__   |  |__|  | |  |  |  | |  |  |  | |   \|  | |  .--.  |
|   _  <  |  |  |  | |   __|  |   __   | |  |  |  | |  |  |  | |  . `  | |  |  |  |
|  |_)  | |  `--'  | |  |     |  |  |  | |  `--'  | |  `--'  | |  |\   | |  '--'  |
|______/   \______/  |__|     |__|  |___\_\________\_\________\|__| \___\|_________\
                            << @coffeegist | @Tw1sm >>
[17:57:44] INFO     Located 4 beacon log files
--snip--
[17:57:44] INFO     JSON files written to current directory
[17:57:44] INFO     Files compressed into bloodhound_20250926_175744.zip
Then, throw the zip in BloodHound.

We can see two SIDs that have WriteOwner privilege on the Domain Admins group. Let’s resolve them:
ldapsearch "(objectSid=S-1-5-32-544)" --attributes *,ntsecuritydescriptor
--snip--
objectClass: top, group
cn: Administrators
description: Administrators have complete and unrestricted access to the computer/domain
member: CN=robb.stark,CN=Users,DC=north,DC=sevenkingdoms,DC=local, CN=catelyn.stark,CN=Users,DC=north,DC=sevenkingdoms,DC=local, CN=eddard.stark,CN=Users,DC=north,DC=sevenkingdoms,DC=local, CN=Enterprise Admins,CN=Users,DC=sevenkingdoms,DC=local, CN=Domain Admins,CN=Users,DC=north,DC=sevenkingdoms,DC=local, CN=localuser,CN=Users,DC=north,DC=sevenkingdoms,DC=local, CN=Administrator,CN=Users,DC=north,DC=sevenkingdoms,DC=local
distinguishedName: CN=Administrators,CN=Builtin,DC=north,DC=sevenkingdoms,DC=local
instanceType: 4
whenCreated: 20250617175530.0Z
whenChanged: 20250617184809.0Z
--snip--
We were not able to resolve the other SID, as it is not part of our current domain. More on this later.

As seen in the image above and the LDAP output, we have discovered a way to escalate privileges: robb.stark is able to add itself to the Domain Admins group through the WriteOwner privilege.
OPSEC considerations
I really recommend checking out Manually Enumerating AD Attack Paths with BOFHound (YouTube) where it’s authors discuss the BOFHound tool, as well as how to perform LDAP reconnaissance with OPSEC in mind. These are the takeaways from the video:
- Do not use too large queries, reduce results by setting scope (--scope), maximum number of results (--count) and search base (--dn).
- Do not use too insufficient queries. For example, searching for sAMAccountName=*arequires much more server-side filtering than searching forsAMAccountName=a*.
- Do not search for specific attributes which could be an IOC, for example, users with an SPN (Kerberoasting):
ldapsearch "(servicePrincipalName=*)"
Instead, get all search results and filter for such attributes offline.
Local group membership and sessions
Merely utilizing LDAP information in BloodHound does not allow you to populate edges such as HasSession, AdminTo, CanRDP, CanPSRemote, and ExecuteDCOM. To this end, one of the authors of BOFHound created/updated BOFs in the Situational Awareness BOF collection. They are explained in his blog post which I recommend checking out.
BONUS: reverse engineering SharpHound for more LDAP queries
I always wanted to reverse SharpHound to find out what queries it uses to build its data. Why not learn from the best, right?
To this end, I added a print statement to SharpHound/src/Producers/LdapProducer.cs @ public override async Task Produce()
foreach (var filter in ldapData.Filter.GetFilterList()) {
    foreach (var partitionedFilter in GetPartitionedFilter(filter)) {
+       Console.WriteLine($"About to send LDAP query: Filter={partitionedFilter} Attributes={string.Join(",", ldapData.Attributes ?? Array.Empty<string>())} Domain={domain.Name} SearchBase={Context.SearchBase}");
        await foreach (var result in Context.LDAPUtils.PagedQuery(new LdapQueryParameters() {
And to public override async Task ProduceConfigNC():
foreach (var filter in configNcData.Filter.GetFilterList()) {
+   Console.WriteLine($"About to send LDAP query: Filter={filter} Attributes={string.Join(",", configNcData.Attributes ?? Array.Empty<string>())} Domain={domain.Name} SearchBase={path}");
    await foreach (var result in Context.LDAPUtils.PagedQuery(new LdapQueryParameters() {
Finally, I let it loose in my GOAD lab.
While most of the queries it employs found have been covered above, there are a couple interesting object class (filter objectClass=) queries:
- groupPolicyContainerdefines group policies, or GPOs.
For the following object classes, SharpHound explicitly sets the search base (
--dn) to:CN=Configuration,DC=<domain>,DC=<local>
- configurationholds configuration data of a domain.
- pKICertificateTemplateShows certificate templates.
- certificationAuthorityand- pKIEnrollmentServiceShow CAs.
- msPKI-Enterprise-Oidshows name, enhanced key usage, application policy, and issuance policy for a certificate template.
Note: the full SharpHound output can be found here.
But wait, there’s more!
I had a hunch that adding those two print statements was not enough to find all queries SharpHound makes. For example, how does it enumerate domain trusts?. To this end, we grep the SharpHoundCommon repo for trustedDomain:
LDAPFilter = new LdapFilter().AddFilter("(objectClass=trustedDomain)", true)
Bingo. Now that we see how LDAP searches are constructed, we can also grep for AddFilter and "(. This should give some pretty good coverage.
- trustedDomainallows us to search the trusted domains. For example, we can use it to resolve the SID we we weren’t able to resolve in the GOAD example earlier.
ldapsearch "(objectClass=trustedDomain)"
--snip--
distinguishedName: CN=sevenkingdoms.local,CN=System,DC=north,DC=sevenkingdoms,DC=local
--snip--
As we are currently in north.sevenkingdoms.local, we need to change our distinguished name to DC=sevenkingdoms,DC=local:
ldapsearch "(objectsid=S-1-5-21-2971658996-2234651943-1348665489-519)"
--dn "DC=sevenkingdoms,DC=local"
--snip--
sAMAccountName: Enterprise Admins
--snip--
You could also query across a trust using --hostname:
ldapsearch (objectClass=domain) --attributes *,ntsecuritydescriptor
--hostname dc.trusted.<domain>.<local> --dn "DC=trusted,DC=<domain>,DC=<local>"
- schemaIDGUIDgets all schema info. Warning: querying for- (schemaIDGUID=*)gives huge output when not scoped properly!
- primaryGroupIDis an attribute that contains the relative identifier (RID) for the primary group of the user.
- objectGUIDis an attribute that contains the unique identifier for an object.
Easter egg
While exploring AD LDAP attributes, I found drink: “The drink (Favorite Drink) attribute type specifies the favorite drink of an object (or person).”. Do with it what you will 😆
Sources
These are some sources I based this blog post on, I really recommend checking them out:
- https://posts.specterops.io/an-introduction-to-manual-active-directory-querying-with-dsquery-and-ldapsearch-84943c13d7eb
- https://posts.specterops.io/manual-ldap-querying-part-2-8a65099e12e3
- https://www.youtube.com/watch?v=Xxm4YktSKVY
- https://blog.tw1sm.io/p/bofhound-session-integration
- https://www.mdsec.co.uk/2024/02/active-directory-enumeration-for-red-teams/