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.
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.
$ ldapsearch -x -H ldap://10.10.10.10 -s base "" "*"
-xuses simple authentication, so no Kerberos or SASL setup is needed.-His 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:
| Part | What it is | Example |
|---|---|---|
| Base DN | Where to start searching in the directory tree | DC=corp,DC=local |
| Filter | What to search for (like a SQL WHERE clause) | (objectClass=user) |
| Attributes | What 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
# 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
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:
# 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
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
# 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:
$ 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
$ 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)
# 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
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
# 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
# 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)
# 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
$ 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
$ 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)
# 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
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:
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
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
| Operator | Syntax | Example |
|---|---|---|
| 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
- Wrong base DN:
DC=corp,DC=localnotcorp.local. Get it from the rootDSE query first - Forgetting
-x: Without-x, ldapsearch tries SASL authentication which usually fails. Always include-xfor simple bind - Putting password on command line: Use
-Wto prompt for password instead of-wif 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/nopromptfor 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/nopromptand 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.
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?
You see "Size limit exceeded". Is the query failing?
-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.