ldapsearch

Command-line LDAP query tool. Queries Active Directory directly to pull users, groups, SPNs, delegation settings, GPOs, and every other object AD stores. The raw query tool when you need precision.

AD CPTS Linux
LevelIntermediate. It needs the idea of a directory and a query language, so do Nmap and enum4linux-ng first.
Know firstWhat Active Directory is, that it stores users, groups and computers as objects with attributes, and what a domain name like corp.local looks like.
You will learn tobuild an LDAP query from a base, a filter and attributes, and use it to find users, Kerberoastable accounts and delegation settings.

What is it?

ldapsearch is a command-line tool that sends LDAP queries to a directory service, in pentesting, that's almost always Active Directory. Every object in AD (users, computers, groups, GPOs, OUs) is stored in LDAP, and ldapsearch lets you query it directly with precise filters.

Think of LDAP as AD's database query language. BloodHound collects data automatically, but ldapsearch lets you ask specific questions: find all accounts with SPNs, find accounts trusted for delegation, find computers with unconstrained delegation, find users with "Password Never Expires" set.

Every AD object has attributes. A user object has sAMAccountName, memberOf, servicePrincipalName, userAccountControl, and dozens more. ldapsearch queries filter on these attributes and return them.

Run this first

Start by asking the server about itself. This works without credentials on many domain controllers, and it tells you the domain name that every later query needs.

bash: your first query
$ ldapsearch -x -H ldap://10.10.10.10 -s base "" "*"
  • -x uses simple authentication, so no Kerberos or SASL setup is needed.
  • -H is the LDAP server address.
  • -s base "" searches only the root entry (an empty base), which is the rootDSE.

Look for defaultNamingContext in the output. That value, for example DC=corp,DC=local, is the base DN you pass with -b in every later query.

Understanding LDAP queries

Every ldapsearch query has three parts:

PartWhat it isExample
Base DNWhere to start searching in the directory treeDC=corp,DC=local
FilterWhat to search for (like a SQL WHERE clause)(objectClass=user)
AttributesWhat fields to return (like SQL SELECT)sAMAccountName memberOf

Base DN explained

The Base DN is the root of your search. For the domain corp.local, the base DN is DC=corp,DC=local. For subsidiary.corp.local, it's DC=subsidiary,DC=corp,DC=local. You get this from Nmap's LDAP scripts or the rootDSE.

Filter syntax

ldap filters: basic syntax
# Simple equality
(objectClass=user)                        # all user objects
(sAMAccountName=jsmith)                   # specific user

# Wildcards
(sAMAccountName=svc_*)                    # users starting with svc_
(description=*pass*)                      # descriptions containing "pass"

# AND (ampersand)
(&(objectClass=user)(adminCount=1))       # users with adminCount=1 (privileged)

# OR (pipe)
(|(sAMAccountName=admin)(sAMAccountName=svc_sql))

# NOT (exclamation)
(&(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))
# All enabled user accounts (exclude disabled)

# Presence (attribute exists)
(servicePrincipalName=*)                  # accounts with SPNs set
The userAccountControl bitmask

Many important queries use userAccountControl with the OID 1.2.840.113556.1.4.803 (bitwise AND). The value 2 = ACCOUNTDISABLE, 65536 = DONT_EXPIRE_PASSWORD, 524288 = TRUSTED_FOR_DELEGATION, 4194304 = DONT_REQ_PREAUTH. You'll see these in many queries below.

Anonymous bind

Some Domain Controllers allow anonymous LDAP queries (anonymous bind). This is increasingly rare but worth trying, especially on older environments. Start with the rootDSE, it's often available without authentication:

bash: anonymous rootDSE query
# Query rootDSE (usually works without auth)
$ ldapsearch -x -H ldap://10.10.10.10 -s base "" "*"

# -x = simple authentication (not SASL)
# -H = LDAP URI
# -s base = search scope (just the root)
# "" = empty base DN (rootDSE)
# "*" = all attributes
output: rootDSE gives you the domain info
defaultNamingContext: DC=corp,DC=local                         <-- base DN for queries
rootDomainNamingContext: DC=corp,DC=local
configurationNamingContext: CN=Configuration,DC=corp,DC=local
dnsHostName: DC01.corp.local                                   <-- DC hostname
ldapServiceName: corp.local:dc01$@CORP.LOCAL
domainFunctionality: 7                                         <-- 7 = Windows Server 2016+
forestFunctionality: 7
bash: try anonymous bind for all users
# Attempt anonymous enumeration of all users
$ ldapsearch -x -H ldap://10.10.10.10 -b 'DC=corp,DC=local' "(objectClass=user)" sAMAccountName

# If this returns "Size limit exceeded" you're getting data but there's a lot
# Add -E pr=1000/noprompt for paged results

Authenticated queries

Once you have credentials, you can query everything in AD. The standard authenticated query format:

bash: authenticated ldapsearch template
$ ldapsearch -x -H ldap://10.10.10.10 \
  -D 'jsmith@corp.local' \
  -w 'Password123!' \
  -b 'DC=corp,DC=local' \
  "(FILTER)" attribute1 attribute2

# -D = bind DN (your user, using UPN format user@domain)
# -w = password (use -W to prompt instead of putting it on command line)
# -b = base DN (search root)

Essential queries for CPTS

All domain users

bash: enumerate all users
$ ldapsearch -x -H ldap://10.10.10.10 -D 'jsmith@corp.local' -w 'Password123!' \
  -b 'DC=corp,DC=local' \
  '(&(objectCategory=person)(objectClass=user))' \
  sAMAccountName description memberOf

Kerberoastable accounts (SPNs set)

bash: find accounts with SPNs
# Accounts with servicePrincipalName = Kerberoastable
$ ldapsearch -x -H ldap://10.10.10.10 -D 'jsmith@corp.local' -w 'Password123!' \
  -b 'DC=corp,DC=local' \
  '(&(objectClass=user)(servicePrincipalName=*)(!(cn=krbtgt))(!(userAccountControl:1.2.840.113556.1.4.803:=2)))' \
  sAMAccountName servicePrincipalName memberOf
Why this query matters

Any user account with an SPN can be Kerberoasted, you request a service ticket encrypted with their password hash, then crack it offline with Hashcat. Service accounts frequently have weak or reused passwords. Feed results to Rubeus Kerberoast.

AS-REP Roastable accounts

bash: accounts that don't require pre-auth
# DONT_REQ_PREAUTH = 4194304: AS-REP Roastable
$ ldapsearch -x -H ldap://10.10.10.10 -D 'jsmith@corp.local' -w 'Password123!' \
  -b 'DC=corp,DC=local' \
  '(&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=4194304))' \
  sAMAccountName

Unconstrained delegation

bash: trusted for delegation
# TRUSTED_FOR_DELEGATION = 524288
$ ldapsearch -x -H ldap://10.10.10.10 -D 'jsmith@corp.local' -w 'Password123!' \
  -b 'DC=corp,DC=local' \
  '(&(userAccountControl:1.2.840.113556.1.4.803:=524288)(!(cn=krbtgt)))' \
  sAMAccountName dNSHostName

Constrained delegation (RBCD targets)

bash: constrained delegation and RBCD
# Accounts with constrained delegation configured
$ ldapsearch -x -H ldap://10.10.10.10 -D 'jsmith@corp.local' -w 'Password123!' \
  -b 'DC=corp,DC=local' \
  '(msDS-AllowedToDelegateTo=*)' \
  sAMAccountName msDS-AllowedToDelegateTo

# Accounts with RBCD (Resource-Based Constrained Delegation)
$ ldapsearch -x -H ldap://10.10.10.10 -D 'jsmith@corp.local' -w 'Password123!' \
  -b 'DC=corp,DC=local' \
  '(msDS-AllowedToActOnBehalfOfOtherIdentity=*)' \
  sAMAccountName msDS-AllowedToActOnBehalfOfOtherIdentity

Domain Admins group members

bash: who is Domain Admin?
$ ldapsearch -x -H ldap://10.10.10.10 -D 'jsmith@corp.local' -w 'Password123!' \
  -b 'CN=Domain Admins,CN=Users,DC=corp,DC=local' \
  member

Computers in the domain

bash: list all computers
$ ldapsearch -x -H ldap://10.10.10.10 -D 'jsmith@corp.local' -w 'Password123!' \
  -b 'DC=corp,DC=local' \
  '(objectClass=computer)' \
  dNSHostName operatingSystem operatingSystemVersion

Users with descriptions (often contain passwords)

bash: check description fields for passwords
# Descriptions containing "pass": admins often put temp passwords here
$ ldapsearch -x -H ldap://10.10.10.10 -D 'jsmith@corp.local' -w 'Password123!' \
  -b 'DC=corp,DC=local' \
  '(&(objectClass=user)(description=*pass*))' \
  sAMAccountName description
Description fields often hold passwords

Lazy admins put passwords in the description field during account creation and forget to remove them. Also search for *pwd*, *cred*, *temp*. This is one of the easiest wins in AD pentesting.

Reading the output

ldapsearch output is in LDIF format, each attribute on its own line. Here's how to parse what you see:

output: typical user entry
dn: CN=SQL Service,CN=Users,DC=corp,DC=local
objectClass: user
sAMAccountName: svc_sql                                        <-- the username
servicePrincipalName: MSSQLSvc/DB01.corp.local:1433            <-- Kerberoastable!
servicePrincipalName: MSSQLSvc/DB01.corp.local                 <-- can have multiple SPNs
memberOf: CN=Domain Admins,CN=Users,DC=corp,DC=local           <-- DA member!
description: SQL service - Temp pass Summer2023!               <-- PASSWORD IN DESCRIPTION!
userAccountControl: 66048
Pipe output for better handling

LDAP output is verbose. Use grep and awk to filter: ldapsearch ... | grep sAMAccountName | awk '{print $2}' gives you a clean user list. Or save raw output with > ldap_users.txt and search through it.

Filter syntax deep dive

OperatorSyntaxExample
Equals(attr=value)(sAMAccountName=admin)
Wildcard(attr=*value*)(description=*password*)
Presence(attr=*)(servicePrincipalName=*)
AND(&(filter1)(filter2))(&(objectClass=user)(adminCount=1))
OR(|(filter1)(filter2))(|(cn=admin)(cn=svc_*))
NOT(!(filter))(!(userAccountControl:1.2.840.113556.1.4.803:=2))
Bitwise AND(attr:1.2.840.113556.1.4.803:=value)Used for userAccountControl flags
Greater/equal(attr>=value)(badPwdCount>=3)

Common mistakes

Things that go wrong
  • Wrong base DN: DC=corp,DC=local not corp.local. Get it from the rootDSE query first
  • Forgetting -x: Without -x, ldapsearch tries SASL authentication which usually fails. Always include -x for simple bind
  • Putting password on command line: Use -W to prompt for password instead of -w if you're worried about bash history. Or use -w "$(cat pass.txt)"
  • Not paging results: AD limits results to 1000 by default. Add -E pr=1000/noprompt for paged results on large domains
  • Missing the ampersand in AND filters: It's (&(filter1)(filter2)) with the ampersand FIRST, not between filters. Shell escaping also trips people up, single-quote the whole filter
  • Ignoring base64-encoded attributes: Some attributes (like objectSid) are returned base64-encoded. Use -E pr=1000/noprompt and pipe through decode tools
  • Only querying users: Don't forget computers, groups, GPOs, and OUs. Computer accounts with unconstrained delegation are high-value targets

Check yourself

Answer each question out loud or on paper first, then open it. Retrieving the answer teaches you more than rereading the page.

Name the three parts of an ldapsearch query and give an example of each.
The base DN, where to start searching (DC=corp,DC=local). The filter, what to match ((objectClass=user)). The attributes, which fields to return (sAMAccountName memberOf).
What does the filter (userAccountControl:1.2.840.113556.1.4.803:=4194304) find?
Accounts with the DONT_REQ_PREAUTH flag set, which are AS-REP roastable. The OID is the bitwise AND matching rule, and 4194304 is the bit for that flag.
You see "Size limit exceeded". Is the query failing?
No. You are getting data, but the server caps the result size. Ask for paged results with -E pr=1000/noprompt.
Which attribute marks an account as Kerberoastable, and what does the filter exclude?
servicePrincipalName being set. The page filter excludes krbtgt, which always has an SPN, and disabled accounts.
Why is the description attribute worth querying?
Administrators sometimes put temporary passwords in it, and a normal domain user can usually read it.