passive-external-recon.md (24276B)
1 --- 2 title: "Stage 00 — Passive External Recon" 3 description: "CPTS attack-flow reference for stage 00 — passive external recon in an authorised engagement." 4 category: pentest-workflow 5 subcategory: "CPTS Attack Flow" 6 order: 1 7 tags: ["htb", "cpts", "htb-attack-flow", "htb-attack-flow-stage-00", "pentest-workflow"] 8 tools: ["crt.sh", "dig", "Shodan", "Gitleaks", "TruffleHog"] 9 difficulty: intermediate 10 updated: "2026-08-29" 11 source: "vault:Pentest Attack Flow/01 - Stage 00 - Passive External Recon.md" 12 --- 13 > [!dashboard] Attack-flow navigation 14 > **Dashboard:** [HTB Pentest Attack Flow](/sheets/pentest-workflow/attack-flow-dashboard) 15 > 16 > **Section:** 01 of 17 · **Focus:** Stage 00 — Passive External Recon 17 > 18 > **Previous:** [HTB Pentest Attack Flow](/sheets/pentest-workflow/attack-flow-dashboard) · **Next:** [Stage 01 — Recon and Host Discovery](/sheets/pentest-workflow/recon-and-host-discovery) 19 20 --- 21 # 🌐 STAGE 0 — Passive & External Recon (OSINT) 22 23 Before a single packet hits the target. On HTB boxes you usually get an IP and jump to the scan below — but the **CPTS exam and any real engagement open here**, with the external attack surface: subdomains, cloud buckets, leaked secrets, and staff. All passive — nothing touches the client's infrastructure. Deep dive: 2.0 - Cheatsheet - Infrastructure Enumeration Tools · secret-scanning 2.4 - Cheatsheet - Gitleaks · 2.5 - Cheatsheet - TruffleHog. 24 25 **What to look for** → forgotten/expired subdomains, real origin IP behind Cloudflare, public cloud buckets, `.env`/`.sql`/key files indexed by Google, secrets in git history, and the org's username convention. 26 27 > [!abstract] MITRE ATT&CK — Reconnaissance (TA0043) 28 > | Technique | Where it lands here | 29 > |---|---| 30 > | T1590 Gather Victim Network Information | whois/ASN/netblocks, DNS, CT logs | 31 > | T1592 Gather Victim Host Information | Shodan/Censys/FOFA, cloud buckets, tech stack | 32 > | T1589 Gather Victim Identity Information | employee/email/username harvesting, breach corpora | 33 > | T1596 Search Open Technical Databases | Shodan, Censys, DNS dumps, GitHub search | 34 > | T1593 Search Open Websites/Domains | dorks, job postings, Wayback Machine | 35 > | T1598 Phishing for Information | *out of scope here — this stage stays passive* | 36 37 --- 38 39 ## 1. Certificate Transparency → subdomains (crt.sh, certspotter) 40 41 Every TLS cert ever issued for the org is public. CT logs are the single best passive subdomain source — and they remember **expired** certs, which point at forgotten, often-unpatched infrastructure. 42 43 **Enumerate** 44 ```bash 45 # clean subdomain list, wildcards stripped, feeds the resolver loop 46 curl -s "https://crt.sh/?q=TARGET.com&output=json" | jq -r '.[].name_value' \ 47 | sed 's/\*\.//g' | sort -u | grep -v "^TARGET.com$" > subdomains.txt 48 # %25 = URL-encoded % wildcard → also pulls EXPIRED certs = forgotten, often-unpatched subdomains 49 curl -s "https://crt.sh/?q=%25.TARGET.com&output=json" | jq -r '.[].name_value' | sort -u 50 51 # certspotter — streaming JSON API, no jq breakage on newline-joined name_value 52 curl -s "https://api.certspotter.com/v1/issuances?domain=TARGET.com&include_subdomains=true&expand=dns_names" \ 53 | jq -r '.[].dns_names[]' | sed 's/\*\.//g' | sort -u 54 ``` 55 56 > [!tip] Why CT first 57 > CT data includes certs for internal-only names that leaked into public logs (`vpn.`, `dev.`, `staging.`, `confluence.`, `gitlab.`). Wildcards are useless as targets but the *label* list around them is not. Also grab `issuer` fields — the CA (Let's Encrypt vs. internal CA vs. DigiCert) hints at which teams manage which boxes. 58 59 --- 60 61 ## 2. Subdomain enumeration — the resolver pipeline 62 63 **What to look for** → union of every passive source, then validate with DNS resolution (still passive — queries go to resolvers, not the target). 64 65 > [!tools] Stage this 66 > - [subfinder](https://github.com/projectdiscovery/subfinder) — fast passive union from 40+ sources (best default) 67 > - [amass](https://github.com/owasp-amass/amass) — OWASP; `enum` (passive+active) and `intel` (ASN/netblock → reverse discovery) 68 > - [assetfinder](https://github.com/tomnomnom/assetfinder) — tomnomnom's tiny fast one; great in pipes 69 > - [findomain](https://github.com/Findomain/Findomain) — single binary, CT + APIs, `-q` quiet mode 70 > - chaos — ProjectDiscovery's hosted subdomain dataset (free API key, `chaos -d TARGET.com`) 71 > - [puredns](https://github.com/d3mondev/puredns) — resolver/bruteforce with wildcard filtering (needs a resolvers list, e.g. trickest/resolvers) 72 73 **Enumerate** 74 ```bash 75 # passive union (each tool alone is incomplete — union everything) 76 subfinder -d $DOMAIN -all -silent > subs_subfinder.txt 77 amass enum -passive -d $DOMAIN -o subs_amass.txt # passive only = no target contact 78 assetfinder --subs-only $DOMAIN > subs_assetfinder.txt 79 findomain -t $DOMAIN -q > subs_findomain.txt 80 cat subs_*.txt | sort -u > subdomains.txt 81 82 # resolve to live hosts + IPs (still passive — hits resolvers, not the target) 83 puredns resolve subdomains.txt -r resolvers.txt -w resolved.txt 84 # cheap fallback if puredns isn't handy: 85 while read s; do host "$s" 2>/dev/null | awk '/has address/{print $1, $4}'; done < subdomains.txt | tee resolved.txt 86 ``` 87 88 **amass intel** — org-wide surface from ASNs/netblocks (needs the ASN from §8): 89 ```bash 90 amass intel -asn <ASN> -whois -d $DOMAIN 91 amass intel -cidr <NETBLOCK>/24 -d $DOMAIN 92 ``` 93 94 > [!warning] Watch out 95 > - Wildcard DNS poisons resolution-based lists — puredns filters wildcards by design; a naive `host` loop doesn't and will report thousands of fake "live" hosts. Check first: `host randomstring123.$DOMAIN` — if it resolves, the zone is wildcarded. 96 > - `amass enum` without `-passive` sends queries to the target's nameservers (AXFR attempts, brute-force) — **that's active**. Keep Stage 00 passive unless scope says otherwise. 97 > - API keys (VirusTotal, SecurityTrails, Shodan, Censys) roughly triple subfinder/amass yield. On CPTS you don't need them; on real engagements you do. 98 99 --- 100 101 ## 3. DNS records & zone data 102 103 **Enumerate** 104 ```bash 105 for t in A AAAA MX NS TXT SOA CNAME; do echo "== $t =="; dig $t $DOMAIN +short; done 106 dig TXT $DOMAIN @8.8.8.8 +short # external resolver — compare vs internal → split-horizon = internal net 107 dig AXFR $DOMAIN @ns1.$DOMAIN # zone transfer — rarely works, dumps the whole zone if it does 108 dig -x $IP # reverse DNS — DCs and mail servers often have PTR records with real names 109 ``` 110 111 - **MX/SPF TXT** leak mail provider (O365 vs. on-prem Exchange → which spraying path later) and sometimes *internal IP ranges* in SPF `ip4:` includes. 112 - **SOA** gives the primary nameserver and an admin email (`hostmaster.$DOMAIN`) — free username-format sample. 113 - Historical DNS (SecurityTrails / ViewDNS / crt.sh IP observations) shows the **origin IP before Cloudflare** was put in front. 114 115 --- 116 117 ## 4. Search-engine dorks 118 119 The table below is the field-expedient subset. The operator reference, the traps that silently break a dork, per-objective recipes and the cross-engine translation table live on [Google Dorking](/sheets/enumeration/google-dorking) — which also ships [dorkforge](/sheets/enumeration/google-dorking#13-dorkforge--the-companion-script), a script that builds and explains these for you. 120 121 > [!example] Google / GitHub / Shodan dork cheatsheet 122 > | Engine | Dork | What it finds | 123 > |---|---|---| 124 > | Google | `site:$DOMAIN filetype:env \| filetype:sql \| filetype:log` | indexed config/dump/log files — `.env` = instant secrets | 125 > | Google | `site:$DOMAIN intitle:"login" \| inurl:/admin \| inurl:/phpmyadmin` | admin panels, phpMyAdmin | 126 > | Google | `site:*.$DOMAIN -site:www.$DOMAIN` | indexed subdomains DNS didn't list | 127 > | Google | `intext:"$DOMAIN" inurl:s3.amazonaws.com \| inurl:blob.core.windows.net \| inurl:storage.googleapis.com` | cloud buckets referencing the org | 128 > | Google | `site:$DOMAIN filetype:pdf \| filetype:docx` | public docs → `exiftool` metadata → usernames/authors | 129 > | Google | `site:linkedin.com/in "$ORG" engineer` | staff names for username derivation | 130 > | GitHub | `org:$ORG filename:.env` | committed env files | 131 > | GitHub | `"$DOMAIN" "BEGIN RSA PRIVATE KEY"` | leaked private keys | 132 > | GitHub | `"$DOMAIN" password \| passwd \| secret \| api_key` | hardcoded creds in code/issues | 133 > | GitHub | `"$DOMAIN" 10.0. \| "192.168."` | internal IP leakage → network map hints | 134 > | Shodan | `ssl.cert.subject.cn:$DOMAIN` | hosts presenting the org's certs (origin-IP hunting) | 135 > | Shodan | `http.html:"$DOMAIN"` | pages referencing the domain (phishing infra, partners) | 136 137 > [!warning] OPSEC — OSINT is still testing 138 > Only **public** data / repos — cloning a private repo or downloading a bucket without written scope can be unauthorised access. Never dork while logged into your real Google account, and authenticate GitHub with a **throwaway**. Report leaked live keys immediately in a real engagement. 139 140 --- 141 142 ## 5. Internet census: Shodan / Censys / FOFA (zero packets to target) 143 144 > [!tools] Stage this 145 > - [Shodan](https://www.shodan.io) — CLI + web; org/hostname/port/screenshot pivots 146 > - [Censys](https://search.censys.io) — deeper cert pivoting: `services.tls.certificates.leaf_data.subject.common_name: "$DOMAIN"` 147 > - [FOFA](https://en.fofa.info) — strong on non-US infrastructure: `domain="$DOMAIN"`, `cert="$DOMAIN"` 148 149 **Passive service intel — Shodan** 150 ```bash 151 shodan init <API_KEY> 152 shodan search --fields ip_str,port,org,hostnames 'org:"InlaneFreight"' # IPs DNS never showed you 153 shodan search 'hostname:TARGET.com port:445' # internet-exposed SMB = goldmine 154 shodan search 'org:"InlaneFreight" port:3389' # RDP 155 shodan search 'org:"InlaneFreight" has_screenshot:true' # see login panels without touching them 156 ``` 157 158 **The favicon mmh3 trick** — find all infra running the org's web app, regardless of domain: 159 ```bash 160 # hash the favicon of a known company site (e.g. their OWA / Jenkins / product login) 161 curl -s https://portal.$DOMAIN/favicon.ico | python3 -c \ 162 "import mmh3,codecs,sys; print(mmh3.hash(codecs.encode(sys.stdin.buffer.read(),'base64')))" 163 # then pivot in Shodan: 164 shodan search 'http.favicon.hash:<MMH3_INT>' --fields ip_str,port,hostnames 165 ``` 166 This surfaces staging/dev instances on unrelated hostnames and the **real origin behind Cloudflare**. 167 168 > [!tip] Public buckets & real origin IP 169 > **GrayHatWarfare** (buckets.grayhatwarfare.com) enumerates public S3/Azure/GCS contents — prioritise `id_rsa`/`.pem`/`.env`/`.sql`. **domain.glass/$DOMAIN** flags whether Cloudflare is proxying — if so the A record is a *Cloudflare* IP, not the origin; find the real one via cert transparency, historical DNS (SecurityTrails), or `shodan search ssl.cert.subject.cn:$DOMAIN`. 170 171 --- 172 173 ## 6. Cloud storage hunting 174 175 **What to look for** → buckets/blob containers named after the org and its products. Naming conventions are brutally predictable. 176 177 ```bash 178 # common permutations to test (HEAD request to the provider = passive-ish, touches the CLOUD not the client) 179 for name in $ORG $ORG-backup $ORG-backups $ORG-dev $ORG-staging $ORG-logs $ORG-data backup-$ORG; do 180 echo "== $name ==" 181 curl -s -o /dev/null -w "s3: %{http_code}\n" "https://$name.s3.amazonaws.com" 182 curl -s -o /dev/null -w "azure: %{http_code}\n" "https://$name.blob.core.windows.net" 183 curl -s -o /dev/null -w "gcs: %{http_code}\n" "https://storage.googleapis.com/$name" 184 done 185 ``` 186 187 - **S3**: `200` = public (list it: `aws s3 ls s3://$name --no-sign-request`), `403` = exists but private (still a finding — confirms naming scheme), `404` = nothing. 188 - **Azure**: containers under `https://$name.blob.core.windows.net/$container?restype=container&comp=list`. 189 - **GCS**: `https://storage.googleapis.com/storage/v1/b/$name/o` returns JSON listing when public. 190 - GrayHatWarfare pre-indexes all three providers — search the org keyword before rolling your own. 191 192 --- 193 194 ## 7. Secrets in code & git history — the highest-value external win 195 196 > [!tools] Stage this 197 > - [gitleaks](https://github.com/gitleaks/gitleaks) — regex/entropy scanner; v8+ subcommands `git|dir|stdin` 198 > - [trufflehog](https://github.com/trufflesecurity/trufflehog) — 800+ detectors, **verifies** creds live against providers 199 > - [GitTools](https://github.com/internetwache/GitTools) (Dumper/Extractor) — rebuild repo from exposed `/.git` 200 > - [git-dumper](https://github.com/arthaud/git-dumper) — same job, Python, pip-installable 201 202 **Enumerate** 203 ```bash 204 # GitHub web/code search (see dork table): org:TARGET-org filename:.env "TARGET.com" "BEGIN RSA PRIVATE KEY" 205 git log --all -p | grep -i "password" | head # deleted secrets still live in the diff history 206 trufflehog github --org=TARGET-org --only-verified # full history, verified only = no false positives 207 gitleaks git ./repo -v # v8+: `gitleaks git|dir|stdin` (replaced detect/protect) 208 209 # Exposed .git on a LIVE web server (this is an active request — Stage 02 territory): 210 git-dumper https://$TARGET/.git ./looted-repo # or: GitTools/Dumper/gitdumper.sh 211 # then: git log -p, git checkout -- . , trufflehog filesystem ./looted-repo 212 ``` 213 214 > [!info] Bridge to Stage 02 215 > `/.git` exposure, `/.env`, `/.svn` are discovered during **active web enumeration** in [Stage 02 — Web Enumeration and Exploitation](/sheets/pentest-workflow/web-enumeration-and-exploitation) — but the *tooling* (git-dumper, GitTools) and the *triage mindset* (creds first, then endpoints, then vulns in the code) are decided here. TruffleHog against a dumped repo routinely yields cloud keys that survive rotation audits because nobody knew the repo leaked. 216 217 --- 218 219 ## 8. Infrastructure: whois, rDNS, ASN & netblocks 220 221 **What to look for** → the org's own IP space (not the CDN's), sister domains, and registrant info. 222 223 ```bash 224 whois $DOMAIN # registrar, dates, nameservers, sometimes unmasked contacts 225 whois $IP | grep -Ei 'netrange|cidr|orgname|org-name' # who actually owns this IP block 226 dig -x $IP +short # PTR — real hostnames (dc01.corp.local style leaks happen) 227 228 # bgp.he.net — free BGP/ASN intelligence in the browser: 229 # search the org name → their ASNs → "Prefixes" tab = every announced netblock 230 whois -h whois.radb.net -- "-i origin AS12345" | grep route # netblocks for an ASN from CLI 231 ``` 232 233 - Feed ASNs/netblocks back into **amass intel** (§2) for org-wide subdomain discovery. 234 - **Certificate SANs + rDNS + PTR records** together often reveal the *internal* naming convention (`dc01.corp.local`) which predicts AD domain names before you ever touch the perimeter. 235 236 --- 237 238 ## 9. People: employees → usernames → emails 239 240 **What to look for** → the org's username/email convention and a candidate userlist. This list feeds Stage 01 validation and [Stage 08 — Password Attacks](/sheets/pentest-workflow/password-attacks-and-credential-hunting) spraying. 241 242 > [!tools] Stage this 243 > - [linkedin2username](https://github.com/initstring/linkedin2username) — scrape LinkedIn company staff into username permutations (needs a throwaway LinkedIn account) 244 > - [statistically-likely-usernames](https://github.com/insidetrust/statistically-likely-usernames) — given `name.txt` (first/last), emits ranked `jsmith`, `john.smith`, … 245 > - [username-anarchy](https://github.com/urbanadventurer/username-anarchy) — same job, also auto-detects format from known emails 246 > - [namemash.py](https://gist.github.com/superkojiman/11076951) — the classic 25-line name→username mangler 247 248 **Enumerate** 249 ```bash 250 # raw names → every plausible username format 251 python3 namemash.py names.txt > usernames.txt 252 ./username-anarchy -i names.txt --select-format first.last,f.last,firstl > usernames.txt 253 python3 statistically-likely-usernames/john.py names.txt > usernames.txt # jsmith-style ranked output 254 255 # infer the email format from ONE known address (press release, SOA, PDF metadata, hunter.io) 256 # john.smith@TARGET.com → format = first.last → user@domain = john.smith 257 python3 - <<'EOF' 258 names = [l.strip().split() for l in open("names.txt") if l.strip()] 259 for f,l in names: print(f"{f.lower()}.{l.lower()}@$DOMAIN") 260 EOF 261 ``` 262 263 - **hunter.io** (free tier) and **phonebook.crawlers**-style services list observed email patterns per domain — use them to *confirm* the format before generating 500 permutations. 264 - `exiftool` on public PDFs/DOCX: `Author`, `LastModifiedBy` fields are frequently raw AD usernames (`jsmith`, not `John Smith`) — the exact format kerbrute wants. 265 - Cross-reference LinkedIn (`site:linkedin.com/in "Company" "engineer"`). Job titles tell you the tech stack too (§10). 266 267 --- 268 269 ## 10. Wayback Machine, job postings & misc intel 270 271 **Wayback Machine** (web.archive.org) 272 ```bash 273 # historical pages for a domain — retired apps, old admin panels, dead APIs still answering 274 curl -s "http://web.archive.org/cdx/search/cdx?url=*.$DOMAIN/*&output=text&fl=original&collapse=urlkey&limit=5000" | sort -u 275 ``` 276 - Old `robots.txt` and sitemap.xml snapshots enumerate paths that were later "hidden". 277 - Snapshotted JS bundles from 2019 still contain API routes that exist today. Deeper URL harvesting ([waybackurls](https://github.com/tomnomnom/waybackurls), [gau](https://github.com/lc/gau)) happens in Stage 02. 278 279 **Job postings** — free, accurate tech-stack intel: a "Windows Systems Administrator" posting that demands *SCCM, Exchange 2016, VMware, Veeam* tells you what's inside the perimeter and what to expect post-exploitation. Check LinkedIn Jobs, Indeed, the org's careers page (cached if removed). 280 281 **Other quick wins** 282 - `haveibeenpwned`/DeHashed for the domain (§11) 283 - GitHub org members page → developer handles → their *personal* gists/repos (off-scope caution) 284 - StackOverflow/forum posts by employees pasting config snippets with internal hostnames 285 286 --- 287 288 ## 11. Breach corpora — has anyone here leaked before? 289 290 > [!tools] Stage this 291 > - [DeHashed](https://www.dehashed.com) — paid; search `domain:$DOMAIN` for cleartext/hash/email rows 292 > - [HaveIBeenPwned](https://haveibeenpwned.com) — API; domain search requires domain ownership proof → use per-email checks 293 > - [h8mail](https://github.com/khast3x/h8mail) — CLI aggregator over breach APIs/local dumps 294 > - [breach-parse](https://github.com/hmaverickadams/breach-parse) — extracts `user:pass` pairs for a domain from a local compilation 295 296 **Enumerate** 297 ```bash 298 h8mail -t "@$DOMAIN" -q dehashed -k "dehashed.email=...,dehashed.key=..." # API-backed 299 ./breach-parse.sh @$DOMAIN breached-$DOMAIN.txt # local corpus 300 ``` 301 302 - Old password reuse is the point: a 2019 cleartext leak of `jsmith:Summer2019!` → try `Summer2026!` permutations in [Stage 08](/sheets/pentest-workflow/password-attacks-and-credential-hunting). 303 - **Hash-only rows** go to hashcat in Stage 08 — crack offline, then feed validated pairs back. 304 - Breach hits also *validate the username format* — leaked logins show whether the org uses `jsmith` or `john.smith`. 305 306 > [!warning] Legal/scope 307 > Querying breach databases about a *client's* domain is normal OSINT; downloading full credential dumps often isn't covered by default ROE. Confirm in writing. Never use breached creds against systems outside scope. 308 309 --- 310 311 ## 12. Validate without touching? — the honest bridge 312 313 Everything above is passive. The moment you **send a packet to the target** — even "just checking if this username exists" — you've left Stage 00. The two most common "is it passive?" traps: 314 315 > [!warning] kerbrute userenum is ACTIVE — flag it as such 316 > [kerbrute](https://github.com/ropnop/kerbrute) `userenum` sends **KRB_AS_REQ** packets to the DC (port 88). No password is attempted and no lockout occurs (pre-auth is never attempted — it just reads `KDC_ERR_C_PRINCIPAL_UNKNOWN` vs. `KDC_ERR_PREAUTH_REQUIRED`), but every request is logged (Event 4768) and the DC sees your source IP. It is *stealthy* but it is **not passive** — use only once active enumeration is in scope, and record it as the moment you "touched" the target. 317 318 > [!tools] Stage this 319 > [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)) 320 > [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)) 321 > 322 > ```bash 323 > # validate a harvested userlist against the DC (Stage 01+, not Stage 00) 324 > ./kerbrute_linux_amd64 userenum -d $DOMAIN --dc $DC usernames.txt -o valid-users.txt 325 > # kerbrute.exe userenum -d $DOMAIN --dc $DC usernames.txt (Windows) 326 > ``` 327 > Realm must be **uppercase**, `--dc` must resolve — full Kerberos toolkit in [Stage 05 — Kerberos Attacks](/sheets/pentest-workflow/kerberos-attacks). 328 329 > [!tools] O365 / Azure validation (also active — hits Microsoft, not the client) 330 > [o365spray](https://github.com/0xZDH/o365spray) (`--validate --domain $DOMAIN`, `enum --domain $DOMAIN -U usernames.txt`) confirms tenant existence + valid O365 users via login endpoints. Alternatives staged in the vault: 331 > 332 > [Go365_linux_amd64.tar.gz](/downloads/pentest-workflow/Go365_linux_amd64.tar.gz) ([SHA-256](/downloads/pentest-workflow/Go365_linux_amd64.tar.gz.sha256) · [GPG signature](/downloads/pentest-workflow/Go365_linux_amd64.tar.gz.sha256.asc)) 333 > [MSOLSpray.ps1](/downloads/pentest-workflow/MSOLSpray.ps1) ([SHA-256](/downloads/pentest-workflow/MSOLSpray.ps1.sha256) · [GPG signature](/downloads/pentest-workflow/MSOLSpray.ps1.sha256.asc)) 334 > 335 > ```bash 336 > # Go365 (unpack first) — NO pure userenum mode: it enums users *via* a password attempt 337 > tar -xzf Go365_linux_amd64.tar.gz 338 > ./Go365 -endpoint graph -d $DOMAIN -ul usernames.txt -p 'Password123' -w 5 -o go365.out 339 > # then parse go365.out: valid users are separable from valid creds in the result codes 340 > # MSOLSpray.ps1 — spraying once you have valid users + a candidate password (Stage 08) 341 > # powershell -ep bypass ; Import-Module .\MSOLSpray.ps1 342 > # Invoke-MSOLSpray -UserList .\valid.txt -Password 'Winter2026!' -Verbose 343 > ``` 344 > These talk to `login.microsoftonline.com` — the *client's* logs (Azure sign-in logs) still record every attempt. Treat as loud-ish active recon with a Microsoft-shaped source address. 345 346 --- 347 348 ## 13. Document as you go — Stage 00 record template 349 350 > [!todo] What to record before moving on 351 > - [ ] **Scope anchors:** root domains, org legal name + subsidiaries, ASN(s), owned netblocks 352 > - [ ] **Subdomains:** union count, which resolved, which are CDN-fronted vs. origin 353 > - [ ] **Mail identity:** MX provider (O365/on-prem), SPF/DKIM/DMARC posture (spoofable = phishing note) 354 > - [ ] **People:** ≥1 confirmed email/username format + raw name list + generated userlist (count) 355 > - [ ] **Secrets:** any verified leaked creds/keys — *report immediately, flag for rotation* 356 > - [ ] **Cloud:** public buckets/containers found, their sensitivity class 357 > - [ ] **Breach hits:** accounts of interest, password-pattern hints 358 > - [ ] **Tech stack:** from job postings, Shodan banners, Wayback JS 359 > - [ ] **First-touch log:** exact time/tool of the first active packet (kerbrute/O365) — the report needs it 360 > 361 > Reporting mechanics and evidence standards: [Stage 11 — Documentation and Reporting](/sheets/pentest-workflow/documentation-and-reporting). 362 363 --- 364 365 > [!success] Handoff → Stage 01 366 > You now hold: live subdomains, owned netblocks, a validated-ish userlist, maybe leaked creds, and a tech-stack guess. Point it all at the perimeter in [Stage 01 — Recon and Host Discovery](/sheets/pentest-workflow/recon-and-host-discovery) — `/etc/hosts` discipline, clock sync, and the rustscan→nmap pipeline turn this paper surface into open ports. 367 368 > [!navigation] Continue the attack flow 369 > **Previous:** [HTB Pentest Attack Flow](/sheets/pentest-workflow/attack-flow-dashboard) 370 > 371 > **Dashboard:** [HTB Pentest Attack Flow](/sheets/pentest-workflow/attack-flow-dashboard) 372 > 373 > **Next:** [Stage 01 — Recon and Host Discovery](/sheets/pentest-workflow/recon-and-host-discovery)