14 min read

Reconnaissance

Active Directory Enumeration

This page is the unauthenticated half of AD enumeration: what the domain will tell you before you hold a credential, and how to turn that into a BloodHound graph.

Information

The authenticated queries — every user, group, computer, GPO, trust and delegation attribute, with the queries, filters and output you get from a real account — live on LDAP & Active Directory Enumeration, which owns them. Keep the two phases apart: this one is what an anonymous or newly-foothold user can do, and it is the phase that decides whether the authenticated phase is worth running at all.

The techniques below differ far more in cost than in yield, and the choice between them is usually about what the client can afford to see rather than what you can afford to run. One table, because this is a set of options rather than a flow:

Technique Credential needed What it proves What it leaves behind
Root DSE bind None, if anonymous is allowed Base DN, naming contexts, functional level One query; 2889 insecure-bind if LDAP is audited
kerbrute userenum None The name exists — read from the KDC error code One 4768 per candidate; volume is the only signal
lookupsid / RID cycling None, or any account Account names plus group context from resolved SIDs SAMR volume; no per-name failure event
SYSVOL GPP read Any domain user A cpassword that decrypts to cleartext 5145 with the file name on the DC — the loudest step here
BloodHound DCOnly Any account Directory relationships: ACLs, groups, delegation LDAP reads only, comparable to a directory sync
BloodHound All Any account, admin on targets for sessions The above plus logon-rights and session edges Touches every host: 4624 and 5140 across the estate

Domain Information

Start with the root DSE. An anonymous bind against port 389 returns defaultNamingContext, namingContexts and supportedLDAPVersion without any credential, which gives you the base DN for every later query and reveals whether the forest has more than one naming context (a second domain, or a configuration partition you did not expect). Two results are findings in themselves: an anonymous bind that succeeds is a hardening gap, and domainFunctionality tells you the functional level, which bounds the techniques that will work — a 2008 R2 level estate still permits things a 2016 level one blocks.

If you are on a domain-joined host rather than a Linux box, %USERDOMAIN%, %LOGONSERVER% and nltest /dclist answer the same questions with no LDAP traffic at all, and nltest /domain_trusts gives you the trust list that would otherwise need an authenticated query. Prefer those when the host is in scope and you want to stay quiet.

domain-info.sh
# Anonymous root DSE — base DN, naming contexts, functional level
ldapsearch -x -H ldap://DC_IP -b "" -s base defaultNamingContext namingContexts

# From a domain-joined host (no LDAP traffic)
echo %USERDOMAIN%
echo %LOGONSERVER%
nltest /dclist:%USERDOMAIN%
nltest /domain_trusts /all_trusts

User Enumeration

Username discovery without credentials comes down to asking the KDC a question it must answer differently for a real account than for a fake one. kerbrute userenum sends one AS-REQ per candidate name and reads the error code: KDC_ERR_C_PRINCIPAL_UNKNOWN means the name does not exist, while KDC_ERR_PREAUTH_REQUIRED (or a successful AS-REP) means it does. That is the quietest enumeration available — one packet per name, no failed logons, no lockout counter touched, because no password is ever guessed. The cost is volume: a large wordlist against a busy DC is visible as a burst of 4768 events, so pace it.

lookupsid.py is the fallback when Kerberos pre-auth is patched into silence or you want group context as well: it walks RIDs over the SMB pipe and prints a name for every SID the DC resolves. The output is worth reading as a hierarchy — RID 500 is the built-in Administrator, the 1000-range is the original account block, and gaps suggest deleted or recreated accounts. Add the names you recover to users.txt and keep the file: the AS-REP and Kerberoast steps, and the authenticated page, all consume it.

user-enum.sh
# Kerberos user enumeration (no credentials, no lockout risk)
kerbrute userenum -d corp.local --dc DC_IP users.txt

# RID cycling over SMB (fallback, also works with a null session)
lookupsid.py corp.local/guest@TARGET 20000

# Comprehensive unauthenticated pass
enum4linux-ng -U TARGET

Group Enumeration

Without a credential you are limited to what SAMR and the anonymous RPC interfaces will hand over: the group list, and sometimes membership. What matters is whether a privileged group has a member you can reach. The RIDs are the reliable signal rather than the display names, because groups get renamed and localised — 512 is Domain Admins, 518 is Schema Admins, 519 is Enterprise Admins, 544 is the built-in local Administrators group. A group whose name looks innocuous but whose RID is 512 is still Domain Admins.

Nested membership is the part unauthenticated enumeration usually cannot resolve, and it is where the attack paths hide: a helpdesk user is rarely a direct Domain Admin, but they are often a member of a group that is. Once you have any account, resolve the nesting with the queries on LDAP & Active Directory Enumeration or with BloodHound, below, which does it for you.

group-enum.sh
# Group list over SAMR (null session or any credential)
enum4linux-ng -G TARGET
rpcclient -U '' -N TARGET -c 'enumdomgroups'

# Membership of a specific group
rpcclient -U '' -N TARGET -c 'querygroupmem 0x200'

Computer Enumeration

Computer enumeration tells you what to attack next, and the useful fields are the operating system and its build number: the build is a patch-level proxy, so a cluster of machines still on an old build is a cluster that has missed several years of updates. Sort the list by build rather than by name and look for the outliers — the one Server 2008 file server in an otherwise 2022 estate is the finding. Unauthenticated, the sources are the DNS records from the discovery phase, the SMB sweep's OS field and the SRV records that identify DCs and global catalogues. Authenticated LDAP gives you the full list with the OS version, last logon and the delegation attributes, and that query set is on the LDAP page.

computer-enum.sh
# OS and build from an unauthenticated SMB sweep
nxc smb 10.0.0.0/24

# Computers in DNS
nslookup -type=SRV _ldap._tcp.dc._msdcs.corp.local

GPO Enumeration

There is exactly one unauthenticated GPO finding worth chasing: SYSVOL is readable by any authenticated user, and on older estates it is readable more broadly still. The preferences files under Policies can contain a cpassword — a value encrypted with a key Microsoft published — which decrypts instantly with gpp-decrypt. Finding it needs no GPO parsing: search SYSVOL for Groups.xml and look at what comes back.

Anything beyond that — which GPO applies to which computer, who can edit it, and whether a writable GPO gives you a path to a privileged group — needs an authenticated query, and LDAP & Active Directory Enumeration covers it. Treat the GPP password as the finding and the GPO inventory as the follow-up.

gpp-cpassword.sh
# Look for GPP cpassword in SYSVOL
smbclient //DC_IP/SYSVOL -U 'user%pass' -c 'recurse;ls' | grep -i groups.xml

# Decrypt once downloaded
gpp-decrypt "encrypted_password_here"

Trust Enumeration

Be precise about what a trust gives you, because it is the most over-reported finding in AD work. The direction is stated from this domain's point of view: an outbound trust means this domain trusts the other one, so users from the other domain can authenticate here; an inbound trust means the other domain trusts this one, so your accounts can reach it; bidirectional is both. Only an inbound or bidirectional trust is a path outward, and only if the trust is not filtered — SID filtering (quarantine) blocks the classic extra-SID attack across a forest trust, and selective authentication blocks it entirely.

Unauthenticated, trust discovery is mostly inference: the domain names that appear in SMB sweep output, the DNS suffixes in DHCP, and the _msdcs zone boundaries tell you a second domain exists. The authoritative list — trust type, direction, transitivity and SID filtering state — comes from the trustedDomain objects, which need a credential; the query is on the LDAP page, and the exploitation of an unfiltered trust is on Trust Attacks.

BloodHound Collection

Tip

BloodHound provides visual attack path analysis. Collect data with SharpHound, then import into BloodHound for analysis.

Danger

Collection method is an authorisation decision, not a technical one. -c All authenticates to every host in the domain and cannot be limited to a subnet, so it reaches excluded systems and end-user workstations by design. Agree the method in writing before the run, prefer -c DCOnly on estates with a monitored SOC, and treat the collected JSON as a copy of the client's directory — it contains staff names, group membership and ACLs, so it lives on the engagement host and is destroyed on the agreed date.

BloodHound is the bridge between this phase and the authenticated one: it takes whatever context you have and answers the only question that matters — what is the shortest path from where I am to Domain Admin, and which edge do I have to abuse first. Choose the collection method by what you can afford to generate. -c DCOnly reads only the directory and is the quietest useful collection; -c All adds session, logged-on-user, share and registry collection, which is what produces the logon-rights and session edges, and which is also the noisiest thing you can run against a large estate. On Linux, bloodhound-python produces the same JSON from a single credential.

Read the graph in this order: shortest path from your owned principal to Domain Admins, then the Kerberoastable and AS-REP-roastable users, then the computers with unconstrained delegation, then the principals with GenericAll or WriteDacl on something interesting. Mark the accounts you actually control as owned before you run the query, or the path it returns will start from an account you do not have.

bloodhound-collect.sh
# SharpHound collection
# Full collection (noisy)
.\SharpHound.exe -c All

# Stealthier - skip session collection
.\SharpHound.exe -c DCOnly

# Session collection (requires admin on targets)
.\SharpHound.exe -c Session

# From Linux with bloodhound-python
bloodhound-python -u user -p password -d corp.local -dc DC01.corp.local -c All

# Import resulting JSON files into BloodHound
# Look for:
# - Shortest path to Domain Admin
# - Kerberoastable users
# - AS-REP roastable users
# - Unconstrained delegation

Impacket Enumeration

Impacket's enumeration scripts are authenticated tools, so their coverage belongs on the LDAP page rather than here. The one you may need during this phase is GetNPUsers.py with -no-pass and a user list: it asks for an AS-REP for each name and returns a crackable hash for any account with pre-authentication disabled, which is a credential-free finding if the hashes are in scope. GetADUsers.py, GetUserSPNs.py and ldapdomaindump all require a valid account.

What the Defender Sees

The techniques on this page differ enormously in how much they leave behind, and that difference is worth stating in the report because it is the client's detection gap:

  • Anonymous root DSE: an insecure bind event (2889) on the DC if plain LDAP is audited. One query, easy to miss, and the fix is to disable anonymous binds.
  • kerbrute userenum: a burst of 4768 with 0x6 (principal unknown) failures from one source. The volume is the signal — a handful of names looks like a mistyped logon, a thousand does not.
  • RID cycling: SAMR traffic over SMB, logged as anonymous or guest network logons (4624 type 3) plus RPC volume. No per-name failure event, which is why it survives most SIEM rules.
  • SYSVOL GPP read: 5145 with the file name on the DC's file share, and a successful logon by the account that read it. This is the most detectable thing on the page and the one most worth checking for in the client's telemetry.
  • BloodHound: DCOnly is LDAP reads only; All adds session collection, which touches every host and produces 4624 and 5140 events across the estate. That is the collection method to agree in advance.

Quick Reference

Target Tool Value
Base DN and functional level ldapsearch rootDSE Anonymous bind status, query base
Valid usernames kerbrute Account list without a lockout risk
Account RIDs lookupsid.py Names plus group context
GPP password grep SYSVOL Cleartext credential from Groups.xml
Attack paths BloodHound Shortest path from owned to DA

Evidence Collection

Unauthenticated enumeration is judged on two things: what it proved and what it cost to prove. Capture both.

Anonymous-bind result: The root DSE response, including whether the bind succeeded anonymously, plus domainFunctionality. An accepted anonymous bind is a finding on its own.

Validated account list: The names kerbrute or RID cycling confirmed, with the method per name. That file is the input to every later credential step, so keep it deduplicated and dated.

GPP password: The full SYSVOL path, the GPO that owns it, the decrypted value and whether the GPO is still linked. A password from an unlinked GPO is a historical artefact, not a live credential.

Privileged-group membership: Direct membership of the groups that matter, keyed by RID (512, 518, 519, 544) rather than display name, plus the note that nesting needs a credential to resolve.

BloodHound dataset: The collection method, the account used, the file set and the Cypher query behind each attack path you report. A path screenshot without the query is not reproducible.

False Positive Identification

  • A null session that lists default shares: IPC$, NETLOGON and SYSVOL exist on every domain member. The finding is a non-default share you can read or a name that resolves, not the presence of the defaults.
  • A localised group name: Groups get renamed and translated. A friendly name with RID 512 is still Domain Admins, and a group called "Domain Admins" that does not carry 512 is not.
  • A trust that cannot be crossed: An outbound trust lets the other domain authenticate here; it is not a path outward. Report direction and SID-filtering state, or the finding overstates what is reachable.
  • BloodHound paths through objects you do not own: The shortest path query will happily start from an account you do not control unless you mark owned principals first. Check the path starts where you actually are.

Testing Checklist

This phase has no exploit, so success is measured by what the directory answered without a credential and what a BloodHound path proved.

Confirm what answers unauthenticated

  • ☐ The anonymous LDAP bind either succeeds or returns a specific error; record which, with the DC that answered.
  • ☐ kerbrute userenum returns valid names from AS-REQ errors without a password attempt; count them and keep the file.
  • ☐ RID cycling (nxc smb --rid-brute) resolves names for the ranges it may read and stops where it is refused.
  • ☐ SYSVOL is readable for the account in scope, and any cpassword found is checked against its GPO's link state.

Confirm the reachable facts

  • ☐ Privileged groups are keyed by RID (512, 518, 519, 544) so a renamed or localised group is still counted.
  • ☐ Each trust is recorded with direction and SID-filtering state together; an outbound trust is not a path outward.
  • ☐ BloodHound collection completes with the account and method named, and at least one path query returns a route to a privileged group.
  • ☐ Every name in the validated account list names the method that confirmed it, so nothing rests on a guessed username format.

Remediation & Defense

Everything this phase recovers is a hardening gap rather than a vulnerability, which is why the fixes are configuration changes.

Close the credential-free paths

  • • Disable anonymous LDAP binds and restrict anonymous SYSVOL access
  • • Remove <code>cpassword</code> values from GPP preferences; Microsoft published the key in 2012, so any remaining value is a plaintext password
  • • Block unauthenticated SAMR and RID enumeration where the estate permits it, and audit anonymous reads of SYSVOL

Reduce the directory surface

  • • Tier privileged accounts so a standard user cannot read the group structure that matters
  • • Audit who can read <code>AdminSDHolder</code>, delegation attributes and <code>msDS-KeyCredentialLink</code>
  • • Retire legacy functional levels that permit techniques current hardening blocks

Detect the enumeration

  • • Alert on bursts of 4768 with status 0x6 and on high-volume SAMR traffic from one source
  • • Audit read access to SYSVOL preference files and to delegation attributes
  • • Baseline LDAP query volume per account so a full-directory dump stands out