18 min read

Enumeration

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

This page is the reference for the queries themselves and for what their output means. The credential-free half of the same phase — root DSE, Kerberos user enumeration, RID cycling, GPP passwords and BloodHound — is on AD Enumeration. If you are working from Linux without RSAT, use the 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.

domain-info.sh
# 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)" objectSid

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

password-policy.sh
# 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-policy

User 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-users.sh
# 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.

privileged-users.sh
# 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" member

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

service-accounts.sh
# 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.

asrep-roastable.sh
# 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.txt

Computer 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-computers.sh
# 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 operatingSystem

Delegation Settings

Warning

Computers with delegation settings are high-value targets for Kerberos attacks.

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.

delegation.sh
# 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=*)" \
  sAMAccountName

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

groups.sh
# 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.

ad-module.ps1
# 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

powerview.ps1
# 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-InterestingDomainAcl

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

gpo-enum.sh
# 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-DomainGPOUserLocalGroupMapping

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

trust-enum.sh
# 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_trusts

Automated Tools

Danger

A full directory dump is a copy of the client's staff data. 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

ldapdomaindump.sh
# 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/json

windapsearch

windapsearch.sh
# 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 --unconstrained

nxc LDAP

nxc-ldap.sh
# 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-delegation

What 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-AllowedToActOnBehalfOfOtherIdentity and msDS-KeyCredentialLink reads 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 -x on 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=1 then 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/fs01 is 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_PREAUTH and servicePrincipalName filters 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-users or nxc ldap --admin-count agrees 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