LDAP & Active Directory Enumeration
LDAP on 389/636 is a read interface to the entire directory: one valid domain account can enumerate
every user, group, computer, GPO, trust and delegation setting in the forest, and most of it is
readable by the default Authenticated Users group.
Five object classes carry almost everything a domain will tell you, and each one is a set of attributes rather than a branch of a flow. The table below is the map; the sections after it are the queries.
| Object class | Attributes that matter | What you do with them |
|---|---|---|
| domainDNS | msDS-Behavior-Version, password policy, objectSid |
Bound the techniques that will work, size the spray, get the domain SID for SID history work |
| user | sAMAccountName, description, memberOf, pwdLastSet, userAccountControl, servicePrincipalName |
Build the spray list, find Kerberoastable and AS-REP-roastable accounts, spot passwords in descriptions |
| computer | operatingSystemVersion, msDS-AllowedToDelegateTo, msDS-AllowedToActOnBehalfOfOtherIdentity, userAccountControl |
Rank hosts by patch level and find delegation and RBCD paths |
| group | member, sAMAccountName, description |
Resolve privileged membership, including the nesting that hides the real paths |
| groupPolicyContainer | gPCFileSysPath, displayName, ACL on the object |
Find preference files with cpassword and GPOs a controlled principal can edit |
| trustedDomain | trustDirection, trustType, trustAttributes |
Decide whether a trust is a path outward, a diagram line, or blocked by SID filtering |
Information
ldapsearch and Impacket forms; the
PowerView and AD-module commands are for a Windows foothold.
Domain Information
Query the root DSE before anything else. It gives you namingContexts — the base DN for
every later search — plus the functional level and, if the schema is readable, the forest and domain
version. msDS-Behavior-Version is the number that matters: 7 is Server 2016, 6 is 2012 R2,
5 is 2008 R2, and the level bounds which hardening features the client can rely on. It also tells you
whether an anonymous bind is still permitted, which is a finding on its own.
# Get naming contexts (find base DN)
ldapsearch -x -H ldap://DC_IP -b '' -s base '(objectClass=*)' namingContexts
# Domain information
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W -b "DC=corp,DC=local" "(objectClass=domain)"
# Domain functional level
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W -b "DC=corp,DC=local" "(objectClass=domainDNS)" msDS-Behavior-Version
# Domain SID
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W -b "DC=corp,DC=local" "(objectClass=domain)" objectSidPassword Policy
Read the password policy before you read the user list, because it decides whether credential
attacks are viable and how much noise you are allowed to make. lockoutThreshold is the
safety limit: if it is 5, four failed attempts per account per observation window is the ceiling, and
the spray must be designed around it rather than around the wordlist. minPwdLength plus
pwdHistoryLength tell you how much the estate cares; a policy with no lockout and a
minimum of 7 is the finding that makes a spray worth attempting at all. Fine-grained PSOs override the
domain default for specific groups, so query them too — service accounts frequently sit under a PSO
with a longer minimum and no lockout.
# Default domain password policy
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W -b "DC=corp,DC=local" "(objectClass=domainDNS)" \
minPwdLength maxPwdAge minPwdAge pwdHistoryLength lockoutThreshold lockoutDuration
# Fine-grained password policies (PSOs)
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "CN=Password Settings Container,CN=System,DC=corp,DC=local" "(objectClass=msDS-PasswordSettings)"
# nxc
nxc ldap DC_IP -u user -p pass --password-policyUser Enumeration
The full user list is the spine of the rest of the engagement: it is the spray list, the Kerberoast
candidate list and the set of names you will search for in shares and logs. Pull
sAMAccountName, displayName, description and
memberOf in one query and keep the output as a file, because you will re-read it more often
than you expect. Two attributes are worth knowing before you query: pwdLastSet is a
FILETIME, so 0 means the password has never been set and a very old value means it has not
rotated since the account was created, and userAccountControl is a bit field where the
interesting bits are the ones you filter on below.
All Users
# All domain users
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "DC=corp,DC=local" "(&(objectClass=user)(objectCategory=person))" \
sAMAccountName displayName description memberOf
# Users with useful attributes
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "DC=corp,DC=local" "(&(objectClass=user)(objectCategory=person))" \
sAMAccountName displayName description mail title department manager
The description field is worth its own pass. Helpdesk tickets get recorded there, and it
is common to find "temp password: Welcome2024" or "contractor — expires March" in a description that
was never intended to be readable by every domain user. The department and manager fields matter for
a different reason: they identify the finance and IT staff whose accounts are worth prioritising, and
the manager chain is often how a phish is aimed at a named person rather than a role.
Privileged Users
adminCount=1 is the shortcut to "this account was at some point inside a protected group".
The attribute is stamped by the AdminSDHolder process and is not cleared when the account is removed
from the group, so it produces false positives — but a false positive here means "was privileged at
some point", which is still a better target than a generic user. Confirm actual membership from the
group side rather than trusting the flag alone.
# Users with adminCount=1 (protected users)
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "DC=corp,DC=local" "(adminCount=1)" sAMAccountName
# Domain Admins members
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "CN=Domain Admins,CN=Users,DC=corp,DC=local" member
# Enterprise Admins
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "CN=Enterprise Admins,CN=Users,DC=corp,DC=local" memberService Accounts
Any user object with a servicePrincipalName is a Kerberoasting candidate, because any
authenticated user may request a service ticket for it and the ticket is encrypted with that account's
password hash. The service name in the SPN also tells you what the account is for and therefore how
privileged it is likely to be: MSSQLSvc accounts are frequently sysadmin on their
instance, and an SPN on a domain admin account is a direct path. Managed service accounts
(msDS-ManagedServiceAccount) and group managed service accounts are the good case — their
passwords are 240 random bytes rotated by AD, so a ticket you obtain from one is not crackable.
# Users with SPNs (Kerberoastable)
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "DC=corp,DC=local" "(&(objectClass=user)(servicePrincipalName=*))" \
sAMAccountName servicePrincipalName
# Managed Service Accounts
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "DC=corp,DC=local" "(objectClass=msDS-ManagedServiceAccount)"
# Group Managed Service Accounts
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "DC=corp,DC=local" "(objectClass=msDS-GroupManagedServiceAccount)"AS-REP Roastable Users
DONT_REQ_PREAUTH (bit 4194304) means the KDC will hand out an AS-REP encrypted with the
user's key without asking the client to prove it knows the password — so anyone who can send an AS-REQ
gets material to crack offline, with no credential and no failed logon. The output of this query is
short on most estates and each line is a near-certain crack if the password is in a wordlist. Hashcat
mode 18200 takes the format these tools emit.
# Users without Kerberos preauth (AS-REP Roastable)
# userAccountControl flag 4194304 = DONT_REQ_PREAUTH
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "DC=corp,DC=local" "(&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=4194304))" \
sAMAccountName
# Using nxc
nxc ldap DC_IP -u user -p pass --asreproast output.txtComputer Enumeration
The computer list is where you find the hosts worth attacking, and the sort key is the operating
system build rather than the name. A machine still reporting Server 2008 or Windows 7 is out of support
and is usually the easiest foothold on the estate; the domain controller OU tells you which hosts hold
the directory, and the operatingSystemVersion field gives you the patch-level proxy you
need for correlating with known vulnerabilities. Two attributes on this object class drive most of the
later attacks: msDS-AllowedToDelegateTo and
msDS-AllowedToActOnBehalfOfOtherIdentity, both queried below.
All Computers
# All domain computers
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "DC=corp,DC=local" "(objectClass=computer)" \
name dNSHostName operatingSystem operatingSystemVersion
# Domain Controllers
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "OU=Domain Controllers,DC=corp,DC=local" "(objectClass=computer)" name
# Servers only
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "DC=corp,DC=local" "(&(objectClass=computer)(operatingSystem=*Server*))" \
name operatingSystemDelegation Settings
Warning
The three delegation queries below answer three different questions and each maps to a different
attack. Unconstrained delegation (TRUSTED_FOR_DELEGATION, bit 524288) means the host caches
TGTs of everyone who authenticates to it — compromise the host and you can impersonate whoever has
logged on. msDS-AllowedToDelegateTo means the object may impersonate users to those SPNs,
and the SPN can be altered (see
Delegation Attacks).
msDS-AllowedToActOnBehalfOfOtherIdentity is RBCD: the target names who may delegate to it,
which means write access to that attribute is enough to take the machine. Record which objects have
each setting — a host with more than one is a compound path.
# Unconstrained Delegation (TRUSTED_FOR_DELEGATION)
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "DC=corp,DC=local" "(userAccountControl:1.2.840.113556.1.4.803:=524288)" \
sAMAccountName
# Constrained Delegation
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "DC=corp,DC=local" "(msDS-AllowedToDelegateTo=*)" \
sAMAccountName msDS-AllowedToDelegateTo
# Resource-Based Constrained Delegation
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "DC=corp,DC=local" "(msDS-AllowedToActOnBehalfOfOtherIdentity=*)" \
sAMAccountNameGroup Enumeration
The member attribute holds distinguished names, not usernames, and it is not transitive:
querying Domain Admins returns its direct members only, which on most estates is a short list of a few
people and several nested groups. That nesting is exactly where the paths hide, so either expand it
recursively — LDAP matching rule 1.2.840.113556.1.4.1941 does it in one query — or let
BloodHound do it. Also query the groups that look unremarkable: Account Operators can
create and modify accounts, Backup Operators can read the registry and NTDS.dit off a DC,
and a group with WriteDacl on a privileged object is worth more than its name suggests.
# All groups
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "DC=corp,DC=local" "(objectClass=group)" \
sAMAccountName description member
# High-value groups
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "DC=corp,DC=local" "(|(cn=Domain Admins)(cn=Enterprise Admins)(cn=Administrators)(cn=Account Operators)(cn=Backup Operators))" \
member
# Groups with specific user
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "DC=corp,DC=local" "(&(objectClass=group)(member=CN=targetuser,CN=Users,DC=corp,DC=local))"PowerShell AD Enumeration
Two toolkits, two trade-offs. The built-in ActiveDirectory module needs RSAT and generates
the same LDAP traffic as any other client, so it blends in; PowerView is a script that must be dropped
on the host and whose function names and query shapes are widely signatured. Prefer the AD module when
you are allowed to install RSAT on a management host, and PowerView when you need the composite
commands (Find-LocalAdminAccess, Find-InterestingDomainAcl) that would take
a dozen queries to reproduce.
Active Directory Module
-Properties * is convenient and expensive: it pulls every attribute for every object and
is the query that most often trips a "large LDAP response" alert. Ask for the properties you need, and
use the filter server-side — -Filter "adminCount -eq 1" is one query, whereas piping
Get-ADUser -Filter * into Where-Object pulls the whole directory first.
# Import module (RSAT required)
Import-Module ActiveDirectory
# Domain info
Get-ADDomain
Get-ADForest
# Users
Get-ADUser -Filter * -Properties *
Get-ADUser -Filter "adminCount -eq 1" -Properties adminCount,memberOf
# Service accounts with SPNs
Get-ADUser -Filter "ServicePrincipalName -ne '$null'" -Properties ServicePrincipalName
# Computers
Get-ADComputer -Filter * -Properties operatingSystem,operatingSystemVersion
# Groups
Get-ADGroup -Filter * | Get-ADGroupMember
# Domain Controllers
Get-ADDomainController -Filter *PowerView
# Import PowerView
Import-Module .\PowerView.ps1
. .\PowerView.ps1
# Domain enumeration
Get-Domain
Get-DomainController
Get-DomainPolicy
# User enumeration
Get-DomainUser
Get-DomainUser -AdminCount
Get-DomainUser -SPN
# Find interesting users
Find-DomainUserLocation
Find-LocalAdminAccess
# Computers
Get-DomainComputer
Get-DomainComputer -Unconstrained
Get-DomainComputer -TrustedToAuth
# Shares
Find-DomainShare
Find-InterestingDomainShareFile
# ACLs
Get-DomainObjectAcl -Identity "Domain Admins"
Find-InterestingDomainAclGPO Enumeration
GPOs matter for two reasons. First, gPCFileSysPath points at the policy's folder in
SYSVOL, and that folder is where preference files with cpassword values live — the GPP
finding from the previous page. Second, a GPO that is writable by a principal you control is a
persistence and privilege-escalation path, because the next time it applies it runs your scheduled task
or adds your account to a local group. Get-DomainGPOLocalGroup and
Get-DomainGPOUserLocalGroupMapping answer the question that matters operationally: which
GPO decides local administrators on which machines, and therefore who can become admin where.
# List all GPOs
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "CN=Policies,CN=System,DC=corp,DC=local" "(objectClass=groupPolicyContainer)" \
displayName gPCFileSysPath
# PowerShell
Get-GPO -All
Get-GPOReport -All -ReportType HTML -Path "gpo_report.html"
# PowerView
Get-DomainGPO
Get-DomainGPOLocalGroup
Get-DomainGPOUserLocalGroupMappingTrust Enumeration
Read the trust objects for direction, transitivity and filtering, not just existence. On the
trustedDomain object, trustDirection 1 is inbound, 2 is outbound and 3 is
bidirectional from this domain's point of view, trustType separates an AD forest or
domain trust from an external or MIT trust, and trustAttributes carries the flags that
decide whether the classic extra-SID attack works — quarantine (SID filtering) blocks it, and
selective authentication blocks access to resources entirely until someone is explicitly allowed. A
trust that exists but is both filtered and selective is a diagram line, not an attack path.
# Domain trusts via LDAP
ldapsearch -x -H ldap://DC_IP -D "user@corp.local" -W \
-b "CN=System,DC=corp,DC=local" "(objectClass=trustedDomain)"
# PowerShell
Get-ADTrust -Filter *
Get-DomainTrust
# nltest
nltest /domain_trusts /all_trustsAutomated Tools
Danger
ldapdomaindump writes every user, group, computer and policy — names, job titles,
descriptions, group membership — to plain HTML and JSON. That output is personal data under most
privacy regimes: keep it on the engagement host, never in a shared folder or a personal laptop, and
delete it on the date agreed in the contract. Use one tool per question rather than all three against
the same DC, and treat any LDAP attribute that stores a secret (cpassword,
unixUserPassword) as material for the report rather than for reuse.
Automated tools trade noise for completeness, and the choice is about which output you will actually
read. ldapdomaindump writes HTML and JSON per object class, which is the best format for
handing a colleague the directory without handing them your shell. windapsearch is the
one-liner for a specific question — privileged users, unconstrained delegation — and
nxc ldap is best for confirming a credential and pulling the two roastable sets in a single
pass. All three make the same queries repeatedly; running all three against one DC is how you turn a
quiet enumeration phase into a logged one.
ldapdomaindump
# Full domain dump to HTML/JSON
ldapdomaindump -u 'corp.local\user' -p 'password' ldap://DC_IP
# Output files:
# domain_users.html/json
# domain_groups.html/json
# domain_computers.html/json
# domain_policy.html/json
# domain_trusts.html/jsonwindapsearch
# User enumeration
windapsearch -d corp.local --dc DC_IP -u user@corp.local -p password -U
# Computer enumeration
windapsearch -d corp.local --dc DC_IP -u user@corp.local -p password -C
# Privileged users
windapsearch -d corp.local --dc DC_IP -u user@corp.local -p password --da
# Unconstrained delegation
windapsearch -d corp.local --dc DC_IP -u user@corp.local -p password --unconstrainednxc LDAP
# LDAP enumeration
nxc ldap DC_IP -u user -p pass --users
nxc ldap DC_IP -u user -p pass --groups
nxc ldap DC_IP -u user -p pass --password-policy
# Kerberoasting
nxc ldap DC_IP -u user -p pass --kerberoasting output.txt
# AS-REP Roasting
nxc ldap DC_IP -u user -p pass --asreproast output.txt
# Find delegation
nxc ldap DC_IP -u user -p pass --find-delegationWhat the Defender Sees
LDAP enumeration is the quietest phase of an internal test and the one most often missed by detection, which is worth saying plainly in the report. What exists to be seen:
- 4662 (directory service access): only recorded when the object or attribute has a SACL. Most estates do not audit read access, which is why enumeration usually leaves nothing here — audit
msDS-AllowedToActOnBehalfOfOtherIdentityandmsDS-KeyCredentialLinkreads specifically if you care. - 1644 (expensive search): logged by the DC when a query exceeds the configured limits — the signature of an unfiltered
-Filter *or a full-directory dump. - 2889 / 2887: insecure or clear-text LDAP binds. An
ldapsearch -xon port 389 without TLS is exactly this event, so your own enumeration will light it up. - Query shape: the reliable signal is behavioural — one account reading every object class in minutes, or a burst of
adminCount=1then delegation-attribute queries. That is the analytic to ask the blue team whether they have.
Evidence Collection
Every query on this page is reproducible, so the evidence is the filter and the attribute list, not a screenshot of the result.
Base DN and functional level:
The root DSE response with namingContexts and msDS-Behavior-Version. Every later query is relative to it, and the level decides which hardening the client can rely on.
Password policy:
The domain policy plus any PSOs, with lockoutThreshold, lockoutDuration and minPwdLength. This is the safety limit for every credential attack in the report.
User and privileged-group queries:
The exact ldapsearch filter or PowerView command, plus the count returned. A list of 40 Domain Admins without the query is not auditable.
Roastable sets:
The users with an SPN and the users with DONT_REQ_PREAUTH, exported with the script that produced them. Those two lists are the phase's most actionable output.
Delegation and RBCD: Per object: which of the three delegation attributes is set and to what. These map directly onto the attack pages, so record the values verbatim rather than a count.
False Positive Identification
- <code>adminCount=1</code> is historical: AdminSDHolder stamps the attribute and never clears it, so an account removed from Domain Admins years ago still carries it. Confirm membership from the group object before reporting the account as privileged.
- An SPN on a managed account: gMSA and MSA accounts hold SPNs and are Kerberoastable in name only — their passwords are 240 random bytes rotated by AD. Exclude them from the roastable set rather than inflating it.
-
A delegated SPN pointing at a file server:
Constrained delegation to
cifs/fs01is not domain compromise. Rank by the target service and say what the delegation actually reaches. - A trust with quarantine: A forest trust with SID filtering and selective authentication is a diagram line, not a path. Report direction and filtering state together or the finding reads as an open door.
Testing Checklist
LDAP answers everything, so the checklist is about keeping every answer attached to the filter and the base DN that produced it.
Confirm the queries return what they claim
- ☐ Every query carries its base DN and filter, with the record count written next to it.
- ☐ The password policy is read (
lockoutThreshold,lockoutDuration,minPwdLength) plus any PSO, before any credential attack is planned. - ☐ The
DONT_REQ_PREAUTHandservicePrincipalNamefilters each return a list you can export and re-run. - ☐ Privileged users come from group membership, not from the historical adminCount attribute.
Confirm the reading of the result
- ☐ gMSA and MSA accounts holding SPNs are excluded from the roastable set before it is counted.
- ☐ Each delegation attribute is recorded verbatim with its target SPN, so the target is judged rather than the flag.
- ☐ A trust is reported with direction and SID-filtering state in the same sentence.
- ☐
windapsearch -m privileged-usersornxc ldap --admin-countagrees with the raw filter; a disagreement is investigated, not averaged.
Remediation & Defense
LDAP is a read interface that will keep answering; the fix is to make the answers less valuable.
Reduce what a domain user can read
- • Remove credentials and hints from <code>description</code>, <code>info</code> and <code>comment</code> attributes
- • Clear <code>adminCount</code> where it is stale, so privileged-group membership means something
- • Restrict who can read delegation attributes and <code>msDS-KeyCredentialLink</code>
Close the escalation paths
- • Move service accounts to gMSA where the application supports it
- • Require Kerberos pre-authentication on every account
- • Set <code>ms-DS-MachineAccountQuota</code> to 0 and audit write access on computer objects
Enforce encryption and detect the dump
- • Require LDAP signing and channel binding, and disable plain LDAP binds (2889/2887 events)
- • Baseline LDAP query volume per account and alert on a full-directory read
- • Audit read access to the attributes that matter, and watch for 1644 expensive-search events