Common LDAP Queries Every AD Admin Should Know
If you are responsible for maintaining an environment that uses Active Directory, LDAP queries could provide you with some useful insight. Finding disabled accounts, stale computers, service accounts with SPNs, and nested group members without the right queries means clicking through ADUC for hours. Nobody has time for that.
This is a practical reference of LDAP filters you can use directly with ldapsearch or as LDAPFilter strings in PowerShell's ActiveDirectory module (Get-ADUser, Get-ADComputer, Get-ADObject). Each example includes notes on gotchas and faster alternatives.
Not Convinced About Using LDAP Queries yet?
Group membership problems, stale accounts, service accounts, and misconfigured user accounts are the top causes of outages and security incidents I see. Good LDAP queries let you find these problems fast without relying on GUIs and manual processes.
- Finding disabled, locked, or stale accounts
- Auditing service accounts and privileged users
- Troubleshooting group membership issues
- Automating AD queries with PowerShell or ldapsearch
LDAP Basics
Performance:
objectCategoryis indexed and faster thanobjectClassfor broad queries- Use LDAP filters for server-side filtering (
Get-ADUser -LDAPFilterorldapsearch) - For large result sets, use paging (PowerShell
-ResultPageSize, ldapsearch-E pr=1000)
Timestamps:
- AD stores some timestamps as Windows FILETIME (100-nanosecond intervals since 1601-01-01)
- You'll need to convert these before building filters (see the timestamp section below)
Stick to indexed attributes like sAMAccountName and objectCategory for the best performance.
Important LDAP Matching Rules
Bitwise AND (for userAccountControl flags)
OID: 1.2.840.113556.1.4.803
Use this to test individual bits in userAccountControl:
(userAccountControl:1.2.840.113556.1.4.803:=2)
Recursive Group Membership (in-chain)
OID: 1.2.840.113556.1.4.1941
This returns all members including nested groups:
(memberOf:1.2.840.113556.1.4.1941:=CN=MyGroup,OU=Groups,DC=contoso,DC=local)
Both of these matching rules run on the server, which is way faster than doing recursion on the client side.
Timestamp Handling: lastLogon vs lastLogonTimestamp
| Attribute | Replicated | Accuracy | Use Case |
|---|---|---|---|
lastLogon | No | High | Requires querying all DCs |
lastLogonTimestamp | Yes | Approximate | Good for stale account detection |
FILETIME Conversion:
# Convert from FILETIME to DateTime
[DateTime]::FromFileTimeUTC([int64]$filetime)
# Create FILETIME for LDAP filter (90 days ago)
$limit = (Get-Date).AddDays(-90)
$filetime = $limit.ToFileTimeUtc()
lastLogonTimestamp replicates with coarse granularity (usually 9-14 days). For absolute accuracy, query lastLogon on all writable DCs and take the highest value.
The replication interval is controlled by the msDS-LogonTimeSyncInterval attribute (default: 14 days minus a random 0-5 days to prevent replication storms). You can adjust this value between 5-100,000 days, or set it to 0 to disable the feature. See Microsoft's LastLogonTimestamp documentation for more details.
Common Queries
1) Disabled Users
LDAP:
(&(objectCategory=person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=2))
PowerShell:
Get-ADUser -LDAPFilter '(&(objectCategory=person)(userAccountControl:1.2.840.113556.1.4.803:=2))' `
-Properties SamAccountName,Enabled | Select SamAccountName
Using the bitwise matching rule is reliable even when multiple flags are set. Flag value 2 = ACCOUNTDISABLE.
2) Locked Accounts
LDAP:
(&(objectCategory=person)(objectClass=user)(lockoutTime>=1))
PowerShell:
Get-ADUser -LDAPFilter '(&(objectCategory=person)(lockoutTime>=1))' `
-Properties lockoutTime |
Select SamAccountName,@{n='LockedAt';e={[DateTime]::FromFileTimeUTC($_.lockoutTime)}}
lockoutTime is 0 when the account isn't locked.
3) Inactive Users (Stale Accounts)
Use lastLogonTimestamp for a quick site-wide answer. For 100% accuracy, query lastLogon on each DC.
LDAP:
(&(objectCategory=person)(objectClass=user)(lastLogonTimestamp<=<filetime>))
PowerShell (90 days inactive):
$limit = (Get-Date).AddDays(-90)
$filetime = $limit.ToFileTimeUtc()
Get-ADUser -LDAPFilter "(&(objectCategory=person)(objectClass=user)(lastLogonTimestamp<=$filetime))" `
-Properties lastLogonTimestamp |
Select Name,@{n='LastLogon';e={[DateTime]::FromFileTimeUTC($_.lastLogonTimestamp)}}
lastLogonTimestamp might under-report recent activity. If you need absolute accuracy, query lastLogon across all DCs.
4) Stale Computer Accounts
Same pattern as users, just targeting computer objects.
LDAP:
(&(objectCategory=computer)(lastLogonTimestamp<=<filetime>))
PowerShell (120 days inactive):
$limit = (Get-Date).AddDays(-120)
$filetime = $limit.ToFileTimeUtc()
Get-ADComputer -LDAPFilter "(&(objectCategory=computer)(lastLogonTimestamp<=$filetime))" `
-Properties lastLogonTimestamp |
Select Name,@{n='LastLogon';e={[DateTime]::FromFileTimeUTC($_.lastLogonTimestamp)}}
5) Service Accounts (SPNs)
LDAP:
(&(objectCategory=person)(objectClass=user)(servicePrincipalName=*))
PowerShell:
Get-ADUser -LDAPFilter '(&(objectCategory=person)(servicePrincipalName=*))' `
-Properties servicePrincipalName |
Select SamAccountName,servicePrincipalName
Check computer objects too (they can have SPNs). Watch for duplicate SPNs, which cause authentication failures.
6) Password Never Expires
LDAP:
(&(objectCategory=person)(userAccountControl:1.2.840.113556.1.4.803:=65536))
PowerShell:
Get-ADUser -LDAPFilter '(&(objectCategory=person)(userAccountControl:1.2.840.113556.1.4.803:=65536))' `
-Properties userAccountControl |
Select SamAccountName
65536 = USER_DONT_EXPIRE_PASSWORD flag.
7) AdminSDHolder Protected Accounts (adminCount=1)
LDAP:
(&(objectCategory=person)(objectClass=user)(adminCount=1))
PowerShell:
Get-ADUser -LDAPFilter '(&(objectCategory=person)(adminCount=1))' `
-Properties adminCount | Select SamAccountName
adminCount=1 marks objects that have had their ACLs hardened by AdminSDHolder (usually privileged accounts).
8) Recursive Group Membership
Find all users who are members of a group (including nested groups).
LDAP:
(&(objectCategory=person)(memberOf:1.2.840.113556.1.4.1941:=CN=MyGroup,OU=Groups,DC=contoso,DC=local))
PowerShell (native cmdlet):
Get-ADGroupMember -Identity "MyGroup" -Recursive
PowerShell (LDAPFilter for server-side recursion):
Get-ADUser -LDAPFilter '(&(objectCategory=person)(memberOf:1.2.840.113556.1.4.1941:=CN=MyGroup,OU=Groups,DC=contoso,DC=local))'
The in-chain matching rule (1.2.840.113556.1.4.1941) runs on the server and avoids client-side recursion. Much faster.
9) Primary Group Membership (primaryGroupID)
Primary group membership doesn't show up in memberOf. You need to search for objects whose primaryGroupID equals the group's RID.
LDAP:
(&(objectCategory=person)(primaryGroupID=<RID>))
PowerShell:
$group = Get-ADGroup "MyGroup"
$rid = $group.SID.Value.Split('-')[-1]
Get-ADUser -Filter "primaryGroupID -eq $rid"
10) Effective Group Membership (tokenGroups)
Get all effective group SIDs for a user (includes nested groups and primary group).
The tokenGroups attribute is a constructed attribute that can only be retrieved via LDAP/PowerShell queries, not through an LDAP filter. Use the PowerShell method below.
PowerShell:
Get-ADUser -Identity jsmith -Properties tokenGroups |
ForEach-Object {
$_.tokenGroups | ForEach-Object {
(New-Object System.Security.Principal.SecurityIdentifier($_,0)).Translate([System.Security.Principal.NTAccount])
}
}
tokenGroups is faster than recursive membership checks because it returns the effective security token.
11) Kerberos Preauth Disabled
LDAP (DONT_REQUIRE_PREAUTH = 0x400000 = 4194304):
(&(objectCategory=person)(userAccountControl:1.2.840.113556.1.4.803:=4194304))
PowerShell:
Get-ADUser -LDAPFilter '(&(objectCategory=person)(userAccountControl:1.2.840.113556.1.4.803:=4194304))' `
-Properties userAccountControl | Select SamAccountName
Accounts with Kerberos preauth disabled are vulnerable to offline password cracking (AS-REP roasting).
12) Orphaned SIDs in Groups
Find unresolvable SIDs in a group's member attribute.
Orphaned SIDs require programmatic checking since they need resolution logic. Use the PowerShell method below.
PowerShell:
$group = Get-ADGroup "MyGroup" -Properties member
foreach ($m in $group.member) {
try {
Get-ADObject -Identity $m -ErrorAction Stop
} catch {
Write-Output "Orphaned: $m"
}
}
13) Range Retrieval (Large Multi-Valued Attributes)
When a multi-valued attribute like member has thousands of values, AD returns them in ranges:
member;range=0-1499member;range=1500-2999- Continue until you get
member;range=1500-*(asterisk = final range)
LDAP:
(objectClass=group)
ldapsearch command example:
ldapsearch -x -b "CN=MyGroup,DC=contoso,DC=local" \
"(objectClass=group)" member;range=0-1499
Get-ADGroupMember does range retrieval automatically.
14) Accounts with Expired Passwords
Find users whose passwords have expired (excludes accounts with password never expires).
LDAP:
(&(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=65536)(pwdLastSet<=<filetime>))
PowerShell:
# Get domain password policy
$domain = Get-ADDomain
$maxPwdAge = (Get-ADObject $domain.DistinguishedName -Properties maxPwdAge).maxPwdAge
# Calculate expiration threshold
$expirationDate = (Get-Date).AddDays(-($maxPwdAge.Days))
$filetime = $expirationDate.ToFileTimeUtc()
# Find users with expired passwords
Get-ADUser -LDAPFilter "(&(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=65536)(pwdLastSet<=$filetime))" `
-Properties pwdLastSet |
Select SamAccountName,@{n='PwdLastSet';e={[DateTime]::FromFileTimeUTC($_.pwdLastSet)}}
Requires checking domain's maxPwdAge policy. Excludes accounts with DONT_EXPIRE_PASSWORD flag. pwdLastSet=0 means "user must change password at next logon".
15) Smartcard Required Accounts
LDAP (SMARTCARD_REQUIRED = 0x40000 = 262144):
(&(objectCategory=person)(userAccountControl:1.2.840.113556.1.4.803:=262144))
PowerShell:
Get-ADUser -LDAPFilter '(&(objectCategory=person)(userAccountControl:1.2.840.113556.1.4.803:=262144))' `
-Properties userAccountControl | Select SamAccountName
These accounts cannot authenticate with passwords. Useful for compliance auditing.
16) Computers by Operating System
LDAP (Windows Server 2022):
(&(objectCategory=computer)(operatingSystem=Windows Server 2022*))
PowerShell (find all Server 2022 computers):
Get-ADComputer -LDAPFilter '(&(objectCategory=computer)(operatingSystem=Windows Server 2022*))' `
-Properties operatingSystem,operatingSystemVersion |
Select Name,operatingSystem,operatingSystemVersion
PowerShell (group by OS version):
Get-ADComputer -Filter * -Properties operatingSystem |
Group-Object operatingSystem |
Select Name,Count |
Sort Count -Descending
Use wildcards for version variations. Useful for upgrade planning and inventory.
17) Recently Created Accounts
Find accounts created in the last 30 days.
LDAP:
(&(objectCategory=person)(whenCreated>=<timestamp>))
PowerShell:
$limit = (Get-Date).AddDays(-30)
$filetime = $limit.ToFileTimeUtc()
Get-ADUser -LDAPFilter "(&(objectCategory=person)(whenCreated>=$filetime))" `
-Properties whenCreated |
Select SamAccountName,@{n='Created';e={$_.whenCreated}} |
Sort Created -Descending
whenCreated is automatically populated by AD. Useful for auditing new account creation. Can also use whenChanged to find recently modified accounts.
UserAccountControl Flag Reference
Common userAccountControl flags for bitwise matching:
| Flag | Decimal | Hex | Description |
|---|---|---|---|
| ACCOUNTDISABLE | 2 | 0x0002 | Account is disabled |
| LOCKOUT | 16 | 0x0010 | Account is locked out |
| PASSWD_NOTREQD | 32 | 0x0020 | No password required |
| NORMAL_ACCOUNT | 512 | 0x0200 | Normal user account |
| DONT_EXPIRE_PASSWORD | 65536 | 0x10000 | Password never expires |
| SMARTCARD_REQUIRED | 262144 | 0x40000 | Smart card required |
| DONT_REQ_PREAUTH | 4194304 | 0x400000 | Kerberos preauth not required |
Performance Tips
✅ Use indexed attributes (sAMAccountName, objectCategory)
✅ Use server-side filtering (-LDAPFilter instead of -Filter)
✅ Page large results with -ResultPageSize for queries returning >1000 objects
✅ Query from a DC in the same site to avoid cross-site traffic
✅ For absolute lastLogon accuracy, query all writable DCs and pick the max
❌ Avoid leading wildcards in queries ((cn=*mith) is inefficient)
❌ Don't use Global Catalog for full attribute sets (GC returns partial attributes)
Escaping Special Characters
Escape these characters per RFC 4515:
| Character | Escape As | Example |
|---|---|---|
* | \2a | (cn=Test\2a) |
( | \28 | (cn=John\28Test\29) |
) | \29 | (cn=John\28Test\29) |
\ | \5c | (cn=Path\5cName) |
| NUL | \00 | Rare in practice |
PowerShell:
- Wrap
-LDAPFilterstrings with single quotes - Escape single quotes inside the string if needed
Quick Troubleshooting
When your filters don't work:
- DC Type: Did you query a writable DC or Global Catalog? GC uses partial attribute sets.
- Replication: Did you account for replication lag?
- Page Size: Are you hitting
MaxPageSizeor range retrieval limits? - Syntax: Is your LDAP filter correct? Test with a simpler filter first.
- Permissions: Does your account have read access to the attributes?
Sample Commands
ldapsearch:
ldapsearch -x -b "DC=contoso,DC=local" \
"(&(objectCategory=person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=2))" \
sAMAccountName displayName
PowerShell:
Get-ADUser -LDAPFilter '(&(objectCategory=person)(userAccountControl:1.2.840.113556.1.4.803:=2))' `
-Properties displayName |
Select SamAccountName,displayName
Additional Resources
- RFC 4515 (LDAP Search Filters)
- Microsoft: Active Directory Schema Attributes
- Microsoft: LDAP Matching Rules
- UserAccountControl Flags
These queries cover most of the daily AD admin tasks that you can expect to run into: stale accounts, locked users, service accounts, privileged accounts, and group membership problems. Bookmark this, adapt the examples to your domain, and always test in a lab before you use these in production automation.