daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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)