Skip to main content

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.

When to Use This Reference
  • 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:

  • objectCategory is indexed and faster than objectClass for broad queries
  • Use LDAP filters for server-side filtering (Get-ADUser -LDAPFilter or ldapsearch)
  • 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)
Quick Tip

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)
Server-Side Processing

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

AttributeReplicatedAccuracyUse Case
lastLogonNoHighRequires querying all DCs
lastLogonTimestampYesApproximateGood 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()
Replication Lag

lastLogonTimestamp replicates with coarse granularity (usually 9-14 days). For absolute accuracy, query lastLogon on all writable DCs and take the highest value.

Technical Details

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
note

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)}}
note

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)}}
Accuracy Trade-off

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
note

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
note

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
What is adminCount?

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))'
In-Chain Matching

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).

note

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])
}
}
Faster Than Recursion

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
Security Risk

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.

note

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-1499
  • member;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
PowerShell Handles This

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)}}
note

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
note

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
note

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
note

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:

FlagDecimalHexDescription
ACCOUNTDISABLE20x0002Account is disabled
LOCKOUT160x0010Account is locked out
PASSWD_NOTREQD320x0020No password required
NORMAL_ACCOUNT5120x0200Normal user account
DONT_EXPIRE_PASSWORD655360x10000Password never expires
SMARTCARD_REQUIRED2621440x40000Smart card required
DONT_REQ_PREAUTH41943040x400000Kerberos 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:

CharacterEscape AsExample
*\2a(cn=Test\2a)
(\28(cn=John\28Test\29)
)\29(cn=John\28Test\29)
\\5c(cn=Path\5cName)
NUL\00Rare in practice

PowerShell:

  • Wrap -LDAPFilter strings with single quotes
  • Escape single quotes inside the string if needed

Quick Troubleshooting

When your filters don't work:

  1. DC Type: Did you query a writable DC or Global Catalog? GC uses partial attribute sets.
  2. Replication: Did you account for replication lag?
  3. Page Size: Are you hitting MaxPageSize or range retrieval limits?
  4. Syntax: Is your LDAP filter correct? Test with a simpler filter first.
  5. 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


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.