Internal Network & Active Directory · intermediate · ~10 min
**What you will learn** - Explain how Active Directory (AD) is queried over LDAP on ports 389/636 and why any authenticated domain user can read most of the directory. - Build safe, read-only `ldapsearch` filters to enumerate users, groups, computers, and account flags in an authorized lab. - Recognise the high-value findings enumeration exposes: Service Principal Names (SPNs), disabled Kerberos pre-authentication, nested-group effective admins, and secrets leaked into attributes like `description`. - Draw a threat model of AD's read-access trust boundary and decide where enumeration data actually comes from. - Detect enumeration defensively: which Windows events and LDAP logs reveal bulk directory reads, and what to log versus never log. - Remediate the leaks enumeration finds and verify the fix rejects the bad state and accepts the good state.
Security objective. The asset you are protecting is the Active Directory directory itself — the authoritative inventory of every user, group, computer, and policy in an organization. The threat is reconnaissance: an attacker (or a red-teamer with written authorization) who holds one ordinary domain account and uses it to read the whole directory, then picks their next attack from that map. In this lesson you will learn to perform that read-only enumeration in a lab, and — just as importantly — to detect it and to close the specific leaks it finds.
What LDAP is. Active Directory is, underneath, an LDAP directory. LDAP (Lightweight Directory Access Protocol) is the query language and wire protocol clients use to search and read AD. Domain controllers listen on 389/tcp (plain or STARTTLS) and 636/tcp (LDAPS, TLS-wrapped). The Global Catalog answers on 3268/3269.
The uncomfortable default. In a standard AD deployment, any authenticated domain user — not just admins — can read most objects and most attributes. That is by design: applications, logon scripts, address books, and the OS itself all depend on broad directory reads. The consequence is that a single low-privilege login yields a complete, authoritative map of the environment: who the admins really are (after group nesting is resolved), which accounts are Kerberoastable, which have pre-auth disabled, which are stale, and which have passwords sitting in a comment field.
How this builds on your prereq. You already met AD objects: users, groups, computers, and OUs. This lesson is the query layer on top of those objects — it teaches how those same objects are read over the wire, and which of their attributes turn into attack surface. The next lesson, credential attacks (spraying and roasting), consumes exactly the target list you produce here.
In authorized internal penetration tests and red-team engagements, LDAP enumeration is almost always the first substantive step after gaining any foothold credential. It is the highest-yield, lowest-noise reconnaissance available: one ordinary account, no exploit, no privilege escalation, reveals the entire attack surface.
For defenders — the larger and more durable career path — the same knowledge is what lets you answer "could someone map our whole domain from a single helpdesk account?" The answer is usually yes, so the professional work is reducing what that map reveals (attribute hygiene, tiering, least privilege) and detecting the reconnaissance itself (LDAP query logging, honey objects, baselines). A blue-teamer who has never watched enumeration happen cannot recognise it in the logs; a pentester who cannot explain the remediation writes a useless report.
Everything downstream depends on this step. Kerberoasting picks its targets from the SPN list you enumerate. Password spraying picks accounts and respects lockout thresholds you read from the domain policy. Attack-path tools like BloodHound are, at heart, industrial-scale LDAP enumerators plus a graph engine. Get enumeration right and the rest of the engagement is informed; skip it and you are guessing.
Definition. LDAP is a protocol for querying a hierarchical directory of entries, each identified by a Distinguished Name (DN) such as CN=Alice,OU=Staff,DC=lab,DC=local.
Plain explanation. Think of AD as a giant read-mostly database of objects arranged in a tree. LDAP is the query language and network protocol you use to search that tree and pull back attributes.
How it works. A client binds (authenticates) to a domain controller on 389 or 636, then issues a search specifying three things: a base DN (where in the tree to start), a scope (this object / one level / the whole subtree), and a filter (which objects match). The DC returns matching entries and the attributes you asked for.
When / when-not. Enumeration over LDAP is appropriate on any engagement where you have a valid credential and written authorization. It is not appropriate against systems you do not own or lack explicit permission to test — that is unauthorized access, full stop.
Pitfall. Beginners think they need admin rights. They do not. The whole point is that a normal user account is enough, which is exactly why this is a defensive priority.
Definition. By default, the built-in Authenticated Users / Pre-Windows 2000 Compatible Access configuration grants read on most of the directory to any domain member.
Plain explanation. The trust boundary in AD is not "admin vs. non-admin" for reads — it is "authenticated vs. anonymous." Once you are any authenticated user, you are inside the read boundary.
How it works. Applications legitimately need to resolve group membership, find printers, look up email addresses, and run logon scripts, so Microsoft ships broad read ACLs on directory objects. Tightening them breaks applications, which is why you cannot simply "turn reads off."
When / when-not. Some sensitive attributes (like ms-Mcs-AdmPwd, the LAPS local-admin password) are not world-readable and are protected by their own ACLs — enumerating those requires delegated rights. Do not assume every attribute is readable.
Pitfall. Assuming "low privilege" means "low visibility." In AD, low privilege still means near-total read visibility.
Definition. The set of high-value facts a directory read exposes.
Plain explanation. You are not attacking yet — you are reading an authoritative map. From it you can pick targets without touching them.
How it works — the findings that matter:
| Finding | LDAP signal | Why it matters |
|---|---|---|
| Kerberoastable account | servicePrincipalName is set on a user object |
Its service ticket can be requested and cracked offline |
| AS-REP-roastable account | userAccountControl has the DONT_REQ_PREAUTH bit (0x400000) |
Its AS-REP can be requested with no password and cracked offline |
| Effective admin | Nested memberOf chains resolving into Domain Admins / privileged groups |
Reveals who is really an admin, not just direct members |
| Stale / risky account | pwdLastSet, lastLogonTimestamp, userAccountControl flags (never-expires, disabled) |
Old, forgotten, over-privileged accounts are easy wins |
| Leaked secret | Cleartext password in description, info, or comment fields |
Instant credential with zero cracking |
When / when-not. You enumerate broadly first, then narrow. Do not fixate on one object type; the value is in cross-referencing (e.g., an SPN account that is also a member of a privileged group).
Pitfall. Treating enumeration output as an attack. Reading is not exploitation — but acting on what you read (roasting, spraying) is, and requires the same authorization scope.
Definition. The clients that speak LDAP to AD.
Plain explanation. ldapsearch (the classic OpenLDAP CLI) is the transparent, teachable one — you write the filter yourself. PowerShell's Get-ADUser/Get-ADObject cmdlets wrap the same queries on Windows. Purpose-built tools and BloodHound ingest the same data and compute attack paths automatically.
How it works. All of them do the same thing under the hood: bind, search, read attributes. Only the ergonomics differ.
Pitfall. Believing a fancy tool sees something ldapsearch cannot. It generally does not — it just correlates faster. Learning the raw filter makes you tool-independent and a better defender, because you understand what the logs will show.
Definition. The two realistic controls: attribute hygiene and query monitoring.
Plain explanation. Because reads are load-bearing for the whole environment, defense is not "deny" — it is "reduce what a read reveals" and "notice when someone reads everything."
How it works. Attribute hygiene means keeping secrets out of readable fields, minimising SPNs on user accounts, disabling DONT_REQ_PREAUTH, and cleaning up stale privileged accounts. Monitoring means enabling directory-service and LDAP query logging, baselining normal query volume, and alerting on bulk or unusual searches — optionally seeding honey objects that no legitimate app should ever query.
Pitfall. Trusting a vulnerability scanner's "pass" as proof of security. A clean scan does not mean the directory is safe to enumerate — scanners rarely model "one normal user can map everything." Nothing here is ever "completely secure"; you reduce and detect.
AD LDAP ENUMERATION — THREAT MODEL (authorized lab)
ASSET: the AD directory (users, groups, computers, policy, secrets-in-attributes)
+-------------------+ 389/636 LDAP bind + search +----------------------+
| Any AUTHENTICATED| --------------------------------------> | Domain Controller |
| domain user | (base DN, scope, filter, attrs) | (LDAP service) |
| (foothold cred) | <-------------------------------------- | Directory database |
+-------------------+ matching entries + attributes +----------------------+
| |
| ENTRY POINT: valid credential (phished, sprayed, reused) |
v v
=================== TRUST BOUNDARY ==============================================
Outside: anonymous / unauthenticated | Inside: authenticated read (near-total)
==================================================================================
INSECURE ASSUMPTION: "low privilege = low visibility" (FALSE in default AD)
DETECTION SURFACE: DC Directory-Service log, LDAP query events (1644),
logon events (4624), volume/pattern baselines, honey objects
Knowledge check
The core operation is an LDAP search: bind to a DC, then specify base DN, scope, filter, and the attributes to return. With ldapsearch:
ldapsearch -x \ # simple bind (use -Y GSSAPI for Kerberos)
-H ldap://dc01.lab.local \ # the domain controller (lab host only)
-D 'LAB\svc-reader' \ # bind DN / user (a normal low-priv account)
-w '<lab-placeholder-password>' \ # never a real secret; lab placeholder
-b 'DC=lab,DC=local' \ # BASE DN: where in the tree to start
-s sub \ # SCOPE: sub = whole subtree
'(objectClass=user)' \ # FILTER: which objects match
sAMAccountName memberOf # ATTRIBUTES to return (omit = all readable)
Key filter building blocks (LDAP filters are prefix/Polish notation, parenthesised):
| Goal | Filter |
|---|---|
| All person users | (&(objectCategory=person)(objectClass=user)) |
| Accounts with an SPN (Kerberoastable) | (&(objectClass=user)(servicePrincipalName=*)) |
| Pre-auth disabled (AS-REP-roastable) | (userAccountControl:1.2.840.113556.1.4.803:=4194304) |
| Password-never-expires | (userAccountControl:1.2.840.113556.1.4.803:=65536) |
| Disabled accounts | (userAccountControl:1.2.840.113556.1.4.803:=2) |
1.2.840.113556.1.4.803 is the bitwise-AND matching rule OID — it tests a single flag inside the userAccountControl bitmask. 4194304 = 0x400000 = DONT_REQ_PREAUTH.
LDAP (Lightweight Directory Access Protocol, ports 389/636) is how clients query Active Directory.
The key point: any authenticated domain user can read most of the directory. So a single low-privilege account exposes an enormous amount of information.
description field.You are not attacking yet — you are reading a complete, authoritative map of the organization with one normal account.
From that map you can identify:
ldapsearch, the PowerShell AD cmdlets, and purpose-built tools all enumerate this data.
BloodHound (covered a couple of lessons from now) ingests it and computes attack paths automatically.
Read access is broad by default, so you cannot simply forbid directory reads. The realistic defenses are:
description field.The shape below is INSECURE (what enumeration finds) -> SECURE (the remediation) -> VERIFY (proof the fix rejects the bad state and accepts the good state). Run every command only against a domain you own or are authorized to test — localhost, a container, or an intentionally vulnerable lab such as GOAD (Game of Active Directory).
WARNING: intentionally vulnerable — use only in a local, isolated, authorized lab. Do not deploy.
A classic leak: an admin stored a service password in the description field, thinking it was harmless. Any authenticated user can read it.
# LAB ONLY. Read the description attribute of every user in one search.
ldapsearch -x -H ldap://dc01.lab.local \
-D 'LAB\svc-reader' -w '<lab-placeholder-password>' \
-b 'DC=lab,DC=local' -s sub \
'(&(objectCategory=person)(objectClass=user)(description=*))' \
sAMAccountName description
# Example (fabricated lab) output — note the leaked secret:
# sAMAccountName: svc-backup
# description: backup service - pw Winter2026! do not change
The same one-account read also exposes roastable targets:
# LAB ONLY. Enumerate SPN (Kerberoastable) and pre-auth-disabled accounts.
ldapsearch -x -H ldap://dc01.lab.local \
-D 'LAB\svc-reader' -w '<lab-placeholder-password>' \
-b 'DC=lab,DC=local' -s sub \
'(&(objectClass=user)(servicePrincipalName=*))' sAMAccountName servicePrincipalName
ldapsearch -x -H ldap://dc01.lab.local \
-D 'LAB\svc-reader' -w '<lab-placeholder-password>' \
-b 'DC=lab,DC=local' -s sub \
'(userAccountControl:1.2.840.113556.1.4.803:=4194304)' sAMAccountName
Defense is not blocking the read; it is removing what the read reveals. Run these as an authorized admin in the lab.
# 1. Purge the secret from the readable attribute.
Set-ADUser -Identity svc-backup -Clear description
# 2. Rotate the exposed credential (it is now compromised) and store it in a vault,
# never in a directory attribute.
# (Password rotation is done through your vault / IdM, not shown here.)
# 3. Re-enable Kerberos pre-authentication (clear the DONT_REQ_PREAUTH bit).
Set-ADAccountControl -Identity someUser -DoesNotRequirePreAuth $false
# 4. Remove SPNs from ordinary user accounts that do not need them;
# prefer Group Managed Service Accounts (gMSA) with long random secrets.
Set-ADUser -Identity svc-legacy -ServicePrincipalNames @{Remove='HTTP/app.lab.local'}
And turn on detection so the next enumeration is seen:
# Enable expensive/inefficient LDAP query logging on the DC (writes event 1644).
# 15-minute threshold, log searches that are slow or return many objects.
reg add "HKLM\SYSTEM\CurrentControlSet\Services\NTDS\Diagnostics" `
/v "15 Field Engineering" /t REG_DWORD /d 5 /f
# REJECTS bad state: the leaked-secret search must now return NO entries.
ldapsearch -x -H ldap://dc01.lab.local \
-D 'LAB\svc-reader' -w '<lab-placeholder-password>' \
-b 'DC=lab,DC=local' -s sub \
'(&(objectClass=user)(description=*Winter2026*))' sAMAccountName
# Expected: '# numEntries: 0' (or no result line) -> secret is gone.
# REJECTS bad state: no user should still be AS-REP-roastable.
ldapsearch -x -H ldap://dc01.lab.local \
-D 'LAB\svc-reader' -w '<lab-placeholder-password>' \
-b 'DC=lab,DC=local' -s sub \
'(userAccountControl:1.2.840.113556.1.4.803:=4194304)' sAMAccountName
# Expected: zero entries -> pre-auth is required again.
# ACCEPTS good state: legitimate lookups still work (no functionality broken).
ldapsearch -x -H ldap://dc01.lab.local \
-D 'LAB\svc-reader' -w '<lab-placeholder-password>' \
-b 'DC=lab,DC=local' -s sub \
'(sAMAccountName=svc-backup)' sAMAccountName memberOf
# Expected: the account still resolves; only the leaked description is gone.
Then confirm detection fired: on the DC, check the Directory Service event log for event ID 1644 entries recording the bulk searches you just ran. If they are absent, logging is not configured and enumeration would go unseen.
Expected outcome. After remediation, the two "bad state" searches return zero entries while the normal lookup still succeeds — that is the definition of a verified fix: it rejects the insecure condition and accepts legitimate use.
Walking the core ldapsearch invocation and what each part does:
| Step | Token | What happens |
|---|---|---|
| 1 | -x |
Use a simple bind (username + password). In a real lab prefer -Y GSSAPI for Kerberos so no password crosses the wire. |
| 2 | -H ldap://dc01.lab.local |
Open a connection to the domain controller's LDAP service on 389. Use ldaps://...:636 for TLS. |
| 3 | -D 'LAB\svc-reader' |
Bind as a normal, low-privilege account — the whole point is that this is enough to read. |
| 4 | -w '<lab-placeholder-password>' |
The bind password. Never a real secret in teaching material; a placeholder here. |
| 5 | -b 'DC=lab,DC=local' |
Base DN: start the search at the domain root so the whole tree is in scope. |
| 6 | -s sub |
Scope sub = search the entire subtree beneath the base. (base = just that object, one = one level.) |
| 7 | '(&(objectCategory=person)(objectClass=user))' |
The filter. & is logical AND; this matches objects that are both person-category and user-class, excluding computer accounts. |
| 8 | sAMAccountName description |
The attribute list — return only these fields. Omitting it returns every readable attribute (noisier, but that is how leaks in odd fields get found). |
How values change as you tighten the filter. Start with (objectClass=user) and you get every user. Add (servicePrincipalName=*) and the result set shrinks to only accounts that carry an SPN — your Kerberoast candidate list. Swap in the bitwise rule (...803:=4194304) and the DC evaluates userAccountControl AND 0x400000: any account whose flag bit is set matches, giving the AS-REP-roastable list. Each refinement is a different slice of the same one authenticated read — the DC does the filtering, so the network cost is small and the privilege cost is zero.
Why the bitwise OID matters. userAccountControl is a single integer whose bits each mean something (disabled, password-never-expires, pre-auth-not-required, ...). The matching-rule OID 1.2.840.113556.1.4.803 tells the DC "AND my constant against that integer and match if non-zero," which is the only way to test one flag without pulling and decoding every account client-side.
Mistake 1 — assuming you need Domain Admin to enumerate.
Mistake 2 — treating enumeration output as an attack you can run anywhere.
ldapsearch at a domain you found on the internet or a client system outside the agreed scope.Mistake 3 — confusing reading with verifying.
Mistake 4 — believing a clean scanner or a tightened firewall means the domain is safe.
Common failures and how to work through them:
ldap_bind: Invalid credentials (49) — the bind DN or password is wrong, or the account is locked. Confirm the DOMAIN\user form (or full UPN user@lab.local), and check the account is enabled. In a lab, a wrong password can lock the account — mind the lockout policy you enumerated.Operations error (1) on a plain bind — some DCs require signing or channel binding and reject simple binds over 389. Switch to ldaps://host:636 or Kerberos (-Y GSSAPI).DC=lab,DC=local must match the actual domain; a typo returns nothing. Also confirm scope is sub, not base.(&(a=1)(b=2)) is correct; (a=1 & b=2) is not. Balance your parentheses.4194304 = 0x400000 (DONT_REQ_PREAUTH), 65536 = password-never-expires, 2 = disabled. A wrong constant tests the wrong bit.LDAPTLS_REQCERT=never ldapsearch ... bypasses validation; never do this against production.Questions to ask when it fails: Am I authenticated (bind succeeded) or just connected? Is my base DN the real domain root? Is my scope subtree? Is my filter valid prefix notation with balanced parens? Is the attribute I want actually world-readable, or is it ACL-protected (e.g., LAPS)?
Security & safety — detection and logging for enumeration.
Because you cannot block directory reads, detection is the primary control. Configure the domain controller to record LDAP activity and build a baseline of normal query volume so bulk enumeration stands out.
What to log (for each notable directory query / bind):
| Field | Example | Why |
|---|---|---|
| Timestamp (UTC) | 2026-07-07T14:02:11Z |
Order events, correlate across hosts |
| Source identity | LAB\svc-reader + source IP/host |
Who and from where |
| Resource / scope | base DN, filter, attributes requested | Distinguishes a targeted lookup from a whole-tree sweep |
| Result | entries returned, duration | High counts / slow expensive searches signal enumeration |
| Security decision | bind allowed/denied, signing enforced | Shows the control that fired |
| Correlation ID | session/request GUID | Stitch a burst of searches into one campaign |
Concrete signals in Windows: enable Field Engineering diagnostic level 5 on the DC (writes event 1644 for expensive/inefficient searches), correlate with logon event 4624 for the source account, and watch for a single account issuing whole-subtree searches for servicePrincipalName=*, userAccountControl bit filters, or reading description/memberOf across the entire tree in a short window.
What to NEVER log: the passwords or secrets you find in attributes (log "secret present in description of
Which events signal abuse: a low-privilege or service account suddenly performing broad, high-count subtree searches; queries filtering specifically on roastable attributes; reads of honey objects (decoy accounts no real app should ever touch); enumeration bursts immediately after a new/first-seen logon.
How false positives arise: legitimate tools also enumerate broadly — vulnerability scanners, asset-inventory agents, identity-governance sync jobs, email address-book lookups, and even BloodHound run by your own blue team. Baseline these known sources and source IPs first, or you will drown real alerts in normal directory traffic. Tune on volume, timing, and which attributes, not on the mere fact that a read happened.
Authorized real-world use case. During a scoped internal penetration test, a consultant is given a single standard domain user (simulating a phished employee). Their first move is read-only LDAP enumeration: user and group inventory, effective admins via nested groups, SPN and pre-auth-disabled lists, stale privileged accounts, and a sweep of description/info fields for leaked secrets. The deliverable is not "we got in" but a prioritised map that drives the rest of the engagement and, more importantly, a remediation list for the client. On the blue-team side, the exact same technique is used to audit one's own domain — "what can a helpdesk account see?" — and to validate that LDAP query logging actually captures the sweep.
Professional best-practice habits
| Habit | Beginner | Advanced |
|---|---|---|
| Authorization | Keep the signed scope open; never touch a host it doesn't cover | Track scope per-subnet/OU; pause and re-confirm before touching new trusts |
| Least privilege | Use the lowest-priv account that works | Model tiered admin; verify reads that should be blocked (LAPS) truly are |
| Validation vs. reading | Never call a decoded value "verified" | Cryptographically verify tokens/signatures separately from reads |
| Secure defaults | Remove secrets from readable attributes | Enforce gMSA for services, drop unneeded SPNs, require LDAP signing/channel binding |
| Logging | Enable event 1644 in the lab and watch your own queries appear | Baseline query volume, deploy honey objects, alert on roastable-attribute sweeps |
| Error handling | Read ldapsearch result codes instead of guessing |
Handle referrals, paging (>1000 results), and TLS/signing requirements cleanly |
Authorization checklist (before any lab or engagement enumeration):
All tasks are lab-only: run them against a domain you own or an intentionally vulnerable AD lab (e.g., GOAD) on localhost/containers/an isolated VM. Confirm the authorization checklist first. Do not attack third-party systems.
Beginner 1 — Inventory users read-only.
ldapsearch returning sAMAccountName and memberOf for (&(objectCategory=person)(objectClass=user)).sub.Beginner 2 — Read the domain password policy.
lockoutThreshold, minPwdLength, maxPwdAge.-s base on the domain DN reads the domain object itself.base.Intermediate 1 — Build the roastable target lists.
(&(objectClass=user)(servicePrincipalName=*)) and the ...803:=4194304 bitwise filter.sAMAccountName lists; note any account on both.4194304 = 0x400000.userAccountControl filters.Intermediate 2 — Hunt secrets in attributes, then remediate.
description/info, then close the leak.(description=*); for any hit containing a credential, clear the attribute (Set-ADUser -Clear description) and note that the secret must be rotated in a vault.Challenge — Prove detection works end-to-end.
Lab cleanup / reset: restore any attributes or flags you changed to their intended secure state (not the vulnerable one), revert the 15 Field Engineering registry value if your lab baseline expects it off, re-enable/rotate any test credential you touched, and snapshot-revert the VM if the lab supports it.
Main concepts. Active Directory is an LDAP directory queried on 389/636; the defining fact is that any authenticated domain user can read most of it. That single low-privilege read yields the full attack surface: Kerberoastable (SPN) accounts, AS-REP-roastable (pre-auth-disabled) accounts, effective admins revealed by nested groups, stale privileged accounts, and secrets leaked into readable attributes like description.
Key syntax/commands. An LDAP search = base DN + scope + filter + attributes. Filters are parenthesised prefix notation: (&(objectCategory=person)(objectClass=user)), (servicePrincipalName=*), and the bitwise rule (userAccountControl:1.2.840.113556.1.4.803:=4194304) for pre-auth-disabled. ldapsearch -x -H ldap://dc -D user -w pass -b base -s sub 'filter' attrs.
Common mistakes. Thinking you need admin rights (you don't); enumerating outside your written authorization; confusing reading a value with verifying it (decoding a JWT is not verifying its signature); and trusting a clean scanner as proof of security (it never is — nothing is completely secure).
What to remember. Defense cannot block reads, so it is trim + watch: keep secrets out of readable attributes, minimise SPNs, require pre-auth, clean up stale admins — and log LDAP queries (event 1644), baseline them, and seed honey objects so bulk enumeration is detected. Never log the secrets you find. Every finding gets a remediation and a verification that the fix rejects the bad state and accepts the good one. All of this happens only on systems you own or are explicitly authorized to test.