FLOW^:Pentest Workflow

Stage 00 — Passive External Recon

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

intermediateupdated 2026-08-29crt.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

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 — which also ships dorkforge, a script that builds and explains these for you.

[!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

On this page

☧ CPTS · ordered posts

Locked

Unlock with your Main_Key through local GPG.

Start the local GPG helper

In your cheatsheet checkout, run:

npm run unlock:helper

Open the link it prints, or paste its pairing code below. Keep the terminal open until GPG finishes.

Import an index you already decrypted

Download the encrypted index, then decrypt with GPG:

gpg --output report-index.json --decrypt report-index.gpg

Your private key stays in GPG. The decrypted index is saved in this browser so you can reopen it after navigation, refreshes and restarts. Lock this browser or clear this site's browser data to remove the saved index. Nothing is uploaded.