This is a directory-reading lab, not a BloodHound attack path. Target is a fake lab forest LAB.INTERNAL on an isolated VM DC (dc01.lab.internal). Goal: keep one redacted ldapsearch, one PowerView-shaped listing, and the event IDs those reads generate, so a defender can baseline the same queries. I do not follow an ACL edge to DA, I do not Kerberoast, I do not dump NTDS, I do not chain this into an attack playbook.
1Figure 1. Authenticated LDAP is a database read. The map is the product. Alert on who drew it.
2labuser -> ldapsearch (port 389/636) -> dc01.lab.internal
3events: 4624 (logon) 4662 (object access) 1644 (LDAP query, if enabled)
Lab layout
1labs/ad_recon_lab/ # isolated lab DC, not a real tenant
2 ldap-rootdse.txt
3 ldap-users.txt # redacted
4 powerview-spn.txt # redacted, fake names
5 4662.txt
6 1644.txt
Domain: LAB.INTERNAL. DC: dc01.lab.internal (Windows Server 2019, lab). Account: labuser (ordinary user, no extra rights). SIDs below have the unique infix replaced with [REDACTED]. No production names.
Artifact: ldapsearch against the fake DC
RootDSE first — this is what every join and every recon tool reads.
1$ ldapsearch -H ldap://dc01.lab.internal -x -s base -b '' \
2 namingContexts defaultNamingContext dnsHostName
3dn:
4defaultNamingContext: DC=lab,DC=internal
5namingContexts: DC=lab,DC=internal
6namingContexts: CN=Configuration,DC=lab,DC=internal
7namingContexts: CN=Schema,CN=Configuration,DC=lab,DC=internal
8dnsHostName: dc01.lab.internal
Authenticated user listing (password not in the note; bind as labuser). I ask for a short attribute set, not * with nTSecurityDescriptor.
1$ ldapsearch -H ldaps://dc01.lab.internal \
2 -D 'CN=labuser,CN=Users,DC=lab,DC=internal' -W \
3 -b 'DC=lab,DC=internal' -s sub \
4 '(&(objectCategory=person)(objectClass=user))' \
5 sAMAccountName userPrincipalName userAccountControl servicePrincipalName memberOf
6
7dn: CN=labuser,CN=Users,DC=lab,DC=internal
8sAMAccountName: labuser
9userPrincipalName: labuser@lab.internal
10userAccountControl: 512
11memberOf: CN=Helpdesk,OU=T1,DC=lab,DC=internal
12
13dn: CN=alice,OU=T1,DC=lab,DC=internal
14sAMAccountName: alice
15userPrincipalName: alice@lab.internal
16userAccountControl: 512
17memberOf: CN=Helpdesk,OU=T1,DC=lab,DC=internal
18
19dn: CN=svc-web,OU=svc,DC=lab,DC=internal
20sAMAccountName: svc-web
21userPrincipalName: svc-web@lab.internal
22userAccountControl: 512
23servicePrincipalName: HTTP/web01.lab.internal
24servicePrincipalName: HTTP/web01
25
26dn: CN=svc-sql,OU=svc,DC=lab,DC=internal
27sAMAccountName: svc-sql
28userAccountControl: 66048
29servicePrincipalName: MSSQLSvc/sql01.lab.internal:1433
30servicePrincipalName: MSSQLSvc/sql01.lab.internal
31
32# 4 entries (lab is tiny on purpose)
What this listing reveals, as inventory:
| Object | Signal | Why a defender cares |
|---|---|---|
labuser / alice | UAC=512 (NORMAL_ACCOUNT) | people |
svc-web | SPN HTTP/web01 | Kerberos service account; roastable class, not a how-to |
svc-sql | UAC=66048 (NORMAL + DONT_EXPIRE) + MSSQL SPN | long-lived service |
memberOf Helpdesk | nested T1 | role group, not DA |
I do not then request a TGS for those SPNs. Seeing the SPN in LDAP is the recon; roasting is a different ticket.
Group nest (still LDAP, still fake):
1$ ldapsearch ... -b 'CN=Domain Admins,CN=Users,DC=lab,DC=internal' member
2dn: CN=Domain Admins,CN=Users,DC=lab,DC=internal
3member: CN=Administrator,CN=Users,DC=lab,DC=internal
4member: CN=lab-da,OU=T0,DC=lab,DC=internal
5# labuser is NOT here. nested Helpdesk is not DA. file that, do not hunt a nest to make it true.
Artifact: PowerView-style listing (same fake DC)
I keep a PowerShell shape because that is what IR pastes. This is Get-ADUser / Get-ADComputer on the lab; the column layout matches the old PowerView one-liners people grep for. Data is fake.
1PS C:\lab> Get-ADUser -Filter * -Properties servicePrincipalName,userAccountControl |
2 ? { $_.servicePrincipalName } |
3 select SamAccountName, UserAccountControl, servicePrincipalName
4
5SamAccountName UserAccountControl servicePrincipalName
6-------------- ------------------ --------------------
7svc-web 512 {HTTP/web01.lab.internal, HTTP/web01}
8svc-sql 66048 {MSSQLSvc/sql01.lab.internal:1433, MSSQLSvc/sql01.lab.internal}
9
10PS C:\lab> Get-ADComputer -Filter * -Properties userAccountControl,servicePrincipalName |
11 select Name, IPv4Address, userAccountControl
12
13Name IPv4Address userAccountControl
14---- ----------- ------------------
15DC01 10.0.10.10 532480
16WEB01 10.0.10.20 4096
17SQL01 10.0.10.21 4096
18# 532480 = SERVER_TRUST_ACCOUNT | TRUSTED_FOR_DELEGATION (DC default — expected)
19# 4096 = WORKSTATION_TRUST_ACCOUNT
1PS C:\lab> Get-ADGroupMember 'Domain Admins' | select SamAccountName
2Administrator
3lab-da
4
5PS C:\lab> nltest /dclist:LAB
6Get list of DCs in domain 'LAB' from '\\dc01.lab.internal'.
7 dc01.lab.internal [PDC] [DS] Site: Default-First-Site-Name
8The command completed successfully
DNS SRV, same map, still fake:
1$ nslookup -type=SRV _ldap._tcp.dc._msdcs.lab.internal
2_ldap._tcp.dc._msdcs.lab.internal SRV 0 100 389 dc01.lab.internal
Computer UAC 532480 on DC01 is SERVER_TRUST_ACCOUNT | TRUSTED_FOR_DELEGATION. That combination is the default for domain controllers, not a leftover print server. I file it as expected-on-DC, unexpected-on-WEB01. The other note in this series covers the print leftover.
That is the whole recon product for this notebook: users, SPNs, computers, DA members, DC locator. No ACL bomb, no session hunter, no local-admin collector. A tool that also walks nTSecurityDescriptor on every object is the same LDAP class with a louder 4662 stream — I still do not run it here.
Event IDs on the DC (the defender half)
I turned on Directory Service Access auditing for the lab OU and LDAP-query diagnostics (1644) for one hour. Then I ran the ldapsearch above.
4624 — network logon from the workstation that bound:
1Event 4624 An account was successfully logged on.
2Subject: SYSTEM
3Logon Type: 3
4Account Name: labuser
5Account Domain: LAB
6Logon ID: 0x[REDACTED]
7Workstation Name: LABPC01
8Source Network Address: 10.0.10.50
9Source Port: [REDACTED]
10Authentication Package: Kerberos
4662 — object access on a user object (one of many; this is svc-web):
1Event 4662 An operation was performed on an object.
2Subject:
3 Security ID: S-1-5-21-[REDACTED]-1001
4 Account Name: labuser
5 Account Domain: LAB
6 Logon ID: 0x[REDACTED]
7Object:
8 Object Server: DS
9 Object Type: user
10 Object Name: CN=svc-web,OU=svc,DC=lab,DC=internal
11 Handle ID: 0x0
12Access Request Information:
13 Access Mask: 0x10 # Read Property
14 Properties:
15 -- %%{servicePrincipalName}
16 -- %%{userAccountControl}
17 -- %%{memberOf}
A single 4662 is noise. Hundreds of 4662 from labuser on user objects, reading servicePrincipalName and nTSecurityDescriptor, from a workstation that is not an IdM sync engine, is the hunt.
1644 — Field Engineering LDAP query log (noisy; lab only):
1Event 1644 Internal event: ... LDAP search ...
2Starting node: DC=lab,DC=internal
3Filter: (&(objectCategory=person)(objectClass=user))
4Scope: Subtree
5Attribute selection: sAMAccountName, userPrincipalName, userAccountControl,
6 servicePrincipalName, memberOf
7Visited entries: 12
8Returned entries: 4
9Client: 10.0.10.50: [REDACTED]
1644 is how I prove the filter, not something I leave on in production all year. Production equivalent: ETW Microsoft-Windows-ActiveDirectory_DomainService with a tighter subscription, or a DC-adjacent network tap that already records LDAP.
4769 I do not expect from this lab step. A TGS for HTTP/web01 after the listing would be the next class. Absence of 4769 after a bulk 4662 is still recon.
Hunt questions I actually file:
- Which non-server principals read
servicePrincipalName/msDS-AllowedToDelegateTo/nTSecurityDescriptorthis week? - Did those hosts also 4624 from a new workstation the same day?
- Did 4769 for those SPNs follow from the same
Logon ID? (If yes, that is a different ticket — still not a roast walkthrough.)
Mitigation (shrink the map, notice the reader)
- Tiering: DA never logs on to
LABPC01. Helpdesk is T1, not T0. - SPNs on user accounts: inventory (
svc-web,svc-sqlabove). Prefer gMSA. I do not “test roastability”. - Pre-auth stays on (
UACbit 0x400000 absent in the dump — good). - ACL review is a scheduled export, not a surprise from a red team LDAP scrape.
- Detect: baseline LDAP volume per account. Alert on bulk 4662 from interactive users. Do not alert on the DC’s own SAMR/LDAP.
1# defender grep of the lab exports
2findstr /i "servicePrincipalName nTSecurityDescriptor msDS-AllowedToDelegateTo" 4662.txt
What I file after this lab
- Forest:
LAB.INTERNAL/dc01.lab.internal(fake, isolated) ldapsearch4 users; SPNs onsvc-webandsvc-sqlonly- PowerView-style: same two SPN accounts;
DC01UAC532480; DA =Administrator,lab-da - Events: 4624 type 3 from
10.0.10.50; 4662 Read Property onsvc-web; 1644 filterobjectClass=userreturned 4 - No 4769 in this step
- Out of scope: Kerberoast, ACL-to-DA path, BloodHound collect, NTDS dump
Commands appendix
1ldapsearch -H ldap://dc01.lab.internal -x -s base -b '' defaultNamingContext
2ldapsearch -H ldaps://dc01.lab.internal -D 'CN=labuser,CN=Users,DC=lab,DC=internal' -W \
3 -b 'DC=lab,DC=internal' '(&(objectCategory=person)(objectClass=user))' \
4 sAMAccountName servicePrincipalName userAccountControl
5# Windows
6# Get-ADUser -Filter * -Properties servicePrincipalName
7# wevtutil qe Security /q:"*[System[(EventID=4662)]]" /c:5 /f:text