FLOW ^: Pentest Workflow

Stage 00 — Passive External Recon

CPTS attack-flow reference for stage 00 — passive external recon in an authorised engagement.

intermediate updated 2026-08-29 crt.sh · dig · Shodan · Gitleaks

[!dashboard] Attack-flow navigation Dashboard: HTB Pentest Attack Flow

Section: 01 of 17 · Focus: Stage 00 — Passive External Recon

Previous: HTB Pentest Attack Flow · Next: Stage 01 — Recon and Host Discovery


🌐 STAGE 0 — Passive & External Recon (OSINT)

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.

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.

[!abstract] MITRE ATT&CK — Reconnaissance (TA0043)

TechniqueWhere it lands here
T1590 Gather Victim Network Informationwhois/ASN/netblocks, DNS, CT logs
T1592 Gather Victim Host InformationShodan/Censys/FOFA, cloud buckets, tech stack
T1589 Gather Victim Identity Informationemployee/email/username harvesting, breach corpora
T1596 Search Open Technical DatabasesShodan, Censys, DNS dumps, GitHub search
T1593 Search Open Websites/Domainsdorks, job postings, Wayback Machine
T1598 Phishing for Informationout of scope here — this stage stays passive

1. Certificate Transparency → subdomains (crt.sh, certspotter)

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.

Enumerate

# clean subdomain list, wildcards stripped, feeds the resolver loop
curl -s "https://crt.sh/?q=TARGET.com&output=json" | jq -r '.[].name_value' \
  | sed 's/\*\.//g' | sort -u | grep -v "^TARGET.com$" > subdomains.txt
# %25 = URL-encoded % wildcard → also pulls EXPIRED certs = forgotten, often-unpatched subdomains
curl -s "https://crt.sh/?q=%25.TARGET.com&output=json" | jq -r '.[].name_value' | sort -u

# certspotter — streaming JSON API, no jq breakage on newline-joined name_value
curl -s "https://api.certspotter.com/v1/issuances?domain=TARGET.com&include_subdomains=true&expand=dns_names" \
  | jq -r '.[].dns_names[]' | sed 's/\*\.//g' | sort -u

[!tip] Why CT first 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.


2. Subdomain enumeration — the resolver pipeline

What to look for → union of every passive source, then validate with DNS resolution (still passive — queries go to resolvers, not the target).

[!tools] Stage this

  • subfinder — fast passive union from 40+ sources (best default)
  • amass — OWASP; enum (passive+active) and intel (ASN/netblock → reverse discovery)
  • assetfinder — tomnomnom’s tiny fast one; great in pipes
  • findomain — single binary, CT + APIs, -q quiet mode
  • chaos — ProjectDiscovery’s hosted subdomain dataset (free API key, chaos -d TARGET.com)
  • puredns — resolver/bruteforce with wildcard filtering (needs a resolvers list, e.g. trickest/resolvers)

Enumerate

# passive union (each tool alone is incomplete — union everything)
subfinder -d $DOMAIN -all -silent > subs_subfinder.txt
amass enum -passive -d $DOMAIN -o subs_amass.txt          # passive only = no target contact
assetfinder --subs-only $DOMAIN > subs_assetfinder.txt
findomain -t $DOMAIN -q > subs_findomain.txt
cat subs_*.txt | sort -u > subdomains.txt

# resolve to live hosts + IPs (still passive — hits resolvers, not the target)
puredns resolve subdomains.txt -r resolvers.txt -w resolved.txt
# cheap fallback if puredns isn't handy:
while read s; do host "$s" 2>/dev/null | awk '/has address/{print $1, $4}'; done < subdomains.txt | tee resolved.txt

amass intel — org-wide surface from ASNs/netblocks (needs the ASN from §8):

amass intel -asn <ASN> -whois -d $DOMAIN
amass intel -cidr <NETBLOCK>/24 -d $DOMAIN

[!warning] Watch out

  • 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.
  • 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.
  • API keys (VirusTotal, SecurityTrails, Shodan, Censys) roughly triple subfinder/amass yield. On CPTS you don’t need them; on real engagements you do.

3. DNS records & zone data

Enumerate

for t in A AAAA MX NS TXT SOA CNAME; do echo "== $t =="; dig $t $DOMAIN +short; done
dig TXT $DOMAIN @8.8.8.8 +short      # external resolver — compare vs internal → split-horizon = internal net
dig AXFR $DOMAIN @ns1.$DOMAIN        # zone transfer — rarely works, dumps the whole zone if it does
dig -x $IP                            # reverse DNS — DCs and mail servers often have PTR records with real names
  • MX/SPF TXT leak mail provider (O365 vs. on-prem Exchange → which spraying path later) and sometimes internal IP ranges in SPF ip4: includes.
  • SOA gives the primary nameserver and an admin email (hostmaster.$DOMAIN) — free username-format sample.
  • Historical DNS (SecurityTrails / ViewDNS / crt.sh IP observations) shows the origin IP before Cloudflare was put in front.

4. Search-engine dorks

[!example] Google / GitHub / Shodan dork cheatsheet

EngineDorkWhat it finds
Googlesite:$DOMAIN filetype:env | filetype:sql | filetype:logindexed config/dump/log files — .env = instant secrets
Googlesite:$DOMAIN intitle:"login" | inurl:/admin | inurl:/phpmyadminadmin panels, phpMyAdmin
Googlesite:*.$DOMAIN -site:www.$DOMAINindexed subdomains DNS didn’t list
Googleintext:"$DOMAIN" inurl:s3.amazonaws.com | inurl:blob.core.windows.net | inurl:storage.googleapis.comcloud buckets referencing the org
Googlesite:$DOMAIN filetype:pdf | filetype:docxpublic docs → exiftool metadata → usernames/authors
Googlesite:linkedin.com/in "$ORG" engineerstaff names for username derivation
GitHuborg:$ORG filename:.envcommitted env files
GitHub"$DOMAIN" "BEGIN RSA PRIVATE KEY"leaked private keys
GitHub"$DOMAIN" password | passwd | secret | api_keyhardcoded creds in code/issues
GitHub"$DOMAIN" 10.0. | "192.168."internal IP leakage → network map hints
Shodanssl.cert.subject.cn:$DOMAINhosts presenting the org’s certs (origin-IP hunting)
Shodanhttp.html:"$DOMAIN"pages referencing the domain (phishing infra, partners)

[!warning] OPSEC — OSINT is still testing 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.


5. Internet census: Shodan / Censys / FOFA (zero packets to target)

[!tools] Stage this

  • Shodan — CLI + web; org/hostname/port/screenshot pivots
  • Censys — deeper cert pivoting: services.tls.certificates.leaf_data.subject.common_name: "$DOMAIN"
  • FOFA — strong on non-US infrastructure: domain="$DOMAIN", cert="$DOMAIN"

Passive service intel — Shodan

shodan init <API_KEY>
shodan search --fields ip_str,port,org,hostnames 'org:"InlaneFreight"'   # IPs DNS never showed you
shodan search 'hostname:TARGET.com port:445'      # internet-exposed SMB = goldmine
shodan search 'org:"InlaneFreight" port:3389'     # RDP
shodan search 'org:"InlaneFreight" has_screenshot:true'   # see login panels without touching them

The favicon mmh3 trick — find all infra running the org’s web app, regardless of domain:

# hash the favicon of a known company site (e.g. their OWA / Jenkins / product login)
curl -s https://portal.$DOMAIN/favicon.ico | python3 -c \
  "import mmh3,codecs,sys; print(mmh3.hash(codecs.encode(sys.stdin.buffer.read(),'base64')))"
# then pivot in Shodan:
shodan search 'http.favicon.hash:<MMH3_INT>' --fields ip_str,port,hostnames

This surfaces staging/dev instances on unrelated hostnames and the real origin behind Cloudflare.

[!tip] Public buckets & real origin IP 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.


6. Cloud storage hunting

What to look for → buckets/blob containers named after the org and its products. Naming conventions are brutally predictable.

# common permutations to test (HEAD request to the provider = passive-ish, touches the CLOUD not the client)
for name in $ORG $ORG-backup $ORG-backups $ORG-dev $ORG-staging $ORG-logs $ORG-data backup-$ORG; do
  echo "== $name =="
  curl -s -o /dev/null -w "s3: %{http_code}\n" "https://$name.s3.amazonaws.com"
  curl -s -o /dev/null -w "azure: %{http_code}\n" "https://$name.blob.core.windows.net"
  curl -s -o /dev/null -w "gcs: %{http_code}\n" "https://storage.googleapis.com/$name"
done
  • 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.
  • Azure: containers under https://$name.blob.core.windows.net/$container?restype=container&comp=list.
  • GCS: https://storage.googleapis.com/storage/v1/b/$name/o returns JSON listing when public.
  • GrayHatWarfare pre-indexes all three providers — search the org keyword before rolling your own.

7. Secrets in code & git history — the highest-value external win

[!tools] Stage this

  • gitleaks — regex/entropy scanner; v8+ subcommands git|dir|stdin
  • trufflehog — 800+ detectors, verifies creds live against providers
  • GitTools (Dumper/Extractor) — rebuild repo from exposed /.git
  • git-dumper — same job, Python, pip-installable

Enumerate

# GitHub web/code search (see dork table): org:TARGET-org  filename:.env  "TARGET.com" "BEGIN RSA PRIVATE KEY"
git log --all -p | grep -i "password" | head            # deleted secrets still live in the diff history
trufflehog github --org=TARGET-org --only-verified       # full history, verified only = no false positives
gitleaks git ./repo -v                                   # v8+: `gitleaks git|dir|stdin` (replaced detect/protect)

# Exposed .git on a LIVE web server (this is an active request — Stage 02 territory):
git-dumper https://$TARGET/.git ./looted-repo            # or: GitTools/Dumper/gitdumper.sh
# then: git log -p, git checkout -- . , trufflehog filesystem ./looted-repo

[!info] Bridge to Stage 02 /.git exposure, /.env, /.svn are discovered during active web enumeration in Stage 02 — 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.


8. Infrastructure: whois, rDNS, ASN & netblocks

What to look for → the org’s own IP space (not the CDN’s), sister domains, and registrant info.

whois $DOMAIN                       # registrar, dates, nameservers, sometimes unmasked contacts
whois $IP | grep -Ei 'netrange|cidr|orgname|org-name'   # who actually owns this IP block
dig -x $IP +short                   # PTR — real hostnames (dc01.corp.local style leaks happen)

# bgp.he.net — free BGP/ASN intelligence in the browser:
#   search the org name → their ASNs → "Prefixes" tab = every announced netblock
whois -h whois.radb.net -- "-i origin AS12345" | grep route   # netblocks for an ASN from CLI
  • Feed ASNs/netblocks back into amass intel (§2) for org-wide subdomain discovery.
  • 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.

9. People: employees → usernames → emails

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 spraying.

[!tools] Stage this

Enumerate

# raw names → every plausible username format
python3 namemash.py names.txt > usernames.txt
./username-anarchy -i names.txt --select-format first.last,f.last,firstl > usernames.txt
python3 statistically-likely-usernames/john.py names.txt > usernames.txt   # jsmith-style ranked output

# infer the email format from ONE known address (press release, SOA, PDF metadata, hunter.io)
#   john.smith@TARGET.com → format = first.last → user@domain = john.smith
python3 - <<'EOF'
names = [l.strip().split() for l in open("names.txt") if l.strip()]
for f,l in names: print(f"{f.lower()}.{l.lower()}@$DOMAIN")
EOF
  • 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.
  • exiftool on public PDFs/DOCX: Author, LastModifiedBy fields are frequently raw AD usernames (jsmith, not John Smith) — the exact format kerbrute wants.
  • Cross-reference LinkedIn (site:linkedin.com/in "Company" "engineer"). Job titles tell you the tech stack too (§10).

10. Wayback Machine, job postings & misc intel

Wayback Machine (web.archive.org)

# historical pages for a domain — retired apps, old admin panels, dead APIs still answering
curl -s "http://web.archive.org/cdx/search/cdx?url=*.$DOMAIN/*&output=text&fl=original&collapse=urlkey&limit=5000" | sort -u
  • Old robots.txt and sitemap.xml snapshots enumerate paths that were later “hidden”.
  • Snapshotted JS bundles from 2019 still contain API routes that exist today. Deeper URL harvesting (waybackurls, gau) happens in Stage 02.

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).

Other quick wins

  • haveibeenpwned/DeHashed for the domain (§11)
  • GitHub org members page → developer handles → their personal gists/repos (off-scope caution)
  • StackOverflow/forum posts by employees pasting config snippets with internal hostnames

11. Breach corpora — has anyone here leaked before?

[!tools] Stage this

  • DeHashed — paid; search domain:$DOMAIN for cleartext/hash/email rows
  • HaveIBeenPwned — API; domain search requires domain ownership proof → use per-email checks
  • h8mail — CLI aggregator over breach APIs/local dumps
  • breach-parse — extracts user:pass pairs for a domain from a local compilation

Enumerate

h8mail -t "@$DOMAIN" -q dehashed -k "dehashed.email=...,dehashed.key=..."   # API-backed
./breach-parse.sh @$DOMAIN breached-$DOMAIN.txt                             # local corpus
  • Old password reuse is the point: a 2019 cleartext leak of jsmith:Summer2019! → try Summer2026! permutations in Stage 08.
  • Hash-only rows go to hashcat in Stage 08 — crack offline, then feed validated pairs back.
  • Breach hits also validate the username format — leaked logins show whether the org uses jsmith or john.smith.

[!warning] Legal/scope 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.


12. Validate without touching? — the honest bridge

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:

[!warning] kerbrute userenum is ACTIVE — flag it as such 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.

[!tools] Stage this kerbrute_linux_amd64 (SHA-256 · GPG signature) kerbrute_windows_amd64.exe (SHA-256 · GPG signature)

# validate a harvested userlist against the DC (Stage 01+, not Stage 00)
./kerbrute_linux_amd64 userenum -d $DOMAIN --dc $DC usernames.txt -o valid-users.txt
# kerbrute.exe userenum -d $DOMAIN --dc $DC usernames.txt   (Windows)

Realm must be uppercase, --dc must resolve — full Kerberos toolkit in Stage 05 — Kerberos Attacks.

[!tools] O365 / Azure validation (also active — hits Microsoft, not the client) o365spray (--validate --domain $DOMAIN, enum --domain $DOMAIN -U usernames.txt) confirms tenant existence + valid O365 users via login endpoints. Alternatives staged in the vault:

Go365_linux_amd64.tar.gz (SHA-256 · GPG signature) MSOLSpray.ps1 (SHA-256 · GPG signature)

# Go365 (unpack first) — NO pure userenum mode: it enums users *via* a password attempt
tar -xzf Go365_linux_amd64.tar.gz
./Go365 -endpoint graph -d $DOMAIN -ul usernames.txt -p 'Password123' -w 5 -o go365.out
# then parse go365.out: valid users are separable from valid creds in the result codes
# MSOLSpray.ps1 — spraying once you have valid users + a candidate password (Stage 08)
#   powershell -ep bypass ; Import-Module .\MSOLSpray.ps1
#   Invoke-MSOLSpray -UserList .\valid.txt -Password 'Winter2026!' -Verbose

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.


13. Document as you go — Stage 00 record template

[!todo] What to record before moving on

  • Scope anchors: root domains, org legal name + subsidiaries, ASN(s), owned netblocks
  • Subdomains: union count, which resolved, which are CDN-fronted vs. origin
  • Mail identity: MX provider (O365/on-prem), SPF/DKIM/DMARC posture (spoofable = phishing note)
  • People: ≥1 confirmed email/username format + raw name list + generated userlist (count)
  • Secrets: any verified leaked creds/keys — report immediately, flag for rotation
  • Cloud: public buckets/containers found, their sensitivity class
  • Breach hits: accounts of interest, password-pattern hints
  • Tech stack: from job postings, Shodan banners, Wayback JS
  • First-touch log: exact time/tool of the first active packet (kerbrute/O365) — the report needs it

Reporting mechanics and evidence standards: Stage 11 — Documentation and Reporting.


[!success] Handoff → Stage 01 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/etc/hosts discipline, clock sync, and the rustscan→nmap pipeline turn this paper surface into open ports.

[!navigation] Continue the attack flow Previous: HTB Pentest Attack Flow

Dashboard: HTB Pentest Attack Flow

Next: Stage 01 — Recon and Host Discovery