← Back 16 July 2026

PentesterLab Recon Series — Command Reference & Mini Walkthroughs

by C. Casquatch

A personal cheat sheet of the tools/commands used across the PentesterLab Recon series, compiled from public write-ups, tool docs, and my own notes as I go. No >answers/keys here — just the “how” so I can reapply these techniques to other targets and exercises.


Recon 00 — robots.txt

Concept: robots.txt tells crawlers what not to index. Webmasters use the Disallow: directive to keep specific pages out of search results — but that same directive is a direct hint to a human reader about which paths the owner considers sensitive enough to hide from Google, which is exactly why it’s the first thing worth checking on any target.

curl https://target.tld/robots.txt

or just visit https://target.tld/robots.txt in a browser.

What to look for: Any Disallow: line. A single Disallow: / means the entire site is telling crawlers to stay out — unusual for a normal public site, and often a sign the site is under development, staging, or otherwise not meant to be publicly indexed at all. More specific disallowed paths (Disallow: /admin/, Disallow: /internal-docs/) are even more directly useful, since they’re pointing you straight at something the owner didn’t want search engines to find.

Bonus/alternative discovery method: if robots.txt doesn’t exist or is empty, you can also try crawling/brute-forcing the site itself with a directory tool (see Recon 05) to find similarly “hidden” paths the same way a crawler would — robots.txt is really just the site owner’s own list of what not to crawl, so it’s the fastest possible shortcut before you resort to brute force.


Recon 01 — Forcing a 404

Concept: Error pages can leak server software, versions, or internal paths, and also give you a baseline for what a genuinely nonexistent page looks like on this specific server — which matters later when brute-forcing directories, since you need to know how to tell a “real” 404 apart from a custom one.

curl -i https://target.tld/some-random-nonexistent-path

What to look for:


Recon 02 — security.txt

Concept: security.txt (formalized as RFC 9116) is the standardized, machine-readable location where organizations publish how researchers should responsibly disclose vulnerabilities — contact emails, PGP keys, disclosure policy links, sometimes even a scope of what’s in-bounds for testing.

curl https://target.tld/.well-known/security.txt

Note: The .well-known/ directory itself is a broader standard (RFC 8615) used to store various “important info for automated systems” files beyond just security.txt — worth remembering as a pattern, since other .well-known/ files can be worth checking too (e.g. .well-known/change-password, .well-known/openid-configuration).

Some older or non-compliant sites put security.txt at the site root instead of under /.well-known/, so check both:

curl https://target.tld/security.txt
curl https://target.tld/.well-known/security.txt

Recon 03 — Directory listing

Concept: If a web server has directory indexing enabled (common misconfiguration, especially on Apache) and there’s no index.html/index.php file present in that directory, the server falls back to rendering a raw file listing instead — showing you every file in that directory, including ones never linked anywhere on the site itself.

Step 1: find candidate directories. Check the page source / view-source of the main site for any referenced paths (image directories, upload folders, asset paths):

curl -s https://target.tld/ | grep -oE 'href="[^"]*"|src="[^"]*"'

This pulls out every linked path/resource referenced in the HTML — a quick way to spot directory names worth checking directly (e.g. spotting /images/logo.png tells you /images/ exists and is worth browsing directly).

Step 2: browse the directory path directly (with a trailing slash, no filename) and see if it renders a listing instead of a 404 or the homepage:

curl -i https://target.tld/images/

A 200 with an HTML file listing (you’ll see literal <a href="..."> tags for every file, often in a plain table layout) — versus a 403 Forbidden or 404 Not Found — is the tell that indexing is enabled here.

What to look for once you find a listing: files that were clearly never meant to be public — old backups, .git/ folders, config files, or anything with a name suggesting it was left behind by a developer rather than deliberately published.


Recon 04 — Common admin paths

Concept: Web applications almost always ship with a predictable management/login path, and developers rarely rename it from the framework or CMS default. Manually guessing a shortlist of the most common ones is often faster than firing up a full brute-force tool.

curl -i https://target.tld/admin/

Common ones worth trying manually, roughly in order of likelihood:

/admin/
/administrator/
/manager/
/management/
/login/
/wp-admin/        (WordPress)
/wp-login.php      (WordPress)
/user/login        (Drupal)
/admin/login
/cpanel/
/phpmyadmin/

Reading the response: a 200 or a 30x redirect (often to a login page) confirms the path exists. A 403 Forbidden also confirms existence — the path is there, just access-restricted, which is still useful information (it tells you something is guarding that path, worth investigating further). A 404 means it’s genuinely not there under that name.


Recon 05 — Directory brute-forcing

Concept: Automate the guessing from Recon 04 using a full wordlist instead of a handful of manual guesses — this is the same idea scaled up with a tool doing hundreds or thousands of requests instead of you typing them one at a time.

Wordlists (Kali ships these by default)

ls /usr/share/wordlists/dirb/
cat /usr/share/wordlists/dirb/common.txt

dirb’s bundled wordlists (common.txt, small.txt, big.txt) are a reasonable default starting point — small.txt for a quick pass, big.txt when you want to be thorough and have time to spare.

Option A: ffuf (modern, fast, actively maintained)

ffuf -w /usr/share/wordlists/dirb/common.txt -u https://target.tld/FUZZ

Option B: wfuzz

wfuzz -c -z file,/usr/share/wordlists/dirb/common.txt --hc 404 https://target.tld/FUZZ

Option C: gobuster

gobuster dir -u https://target.tld -w /usr/share/wordlists/dirb/common.txt

Option D: dirb (older, but still bundled on Kali and quick to reach for)

dirb http://target.tld /usr/share/wordlists/dirb/small.txt

dirb is slower than ffuf/gobuster but requires zero setup — a reasonable first tool to reach for since it’s likely already installed.

Option E: DirBuster (GUI, OWASP legacy tool)

dirbuster

In the GUI: enter target URL → load wordlist → uncheck “Recursive” for a flat top-level scan → Start.

Reading results: 200 = exists and accessible, 403 = exists but forbidden (still worth noting), 404 = doesn’t exist, 30x = redirect (often to a login page, still confirms existence).


Recon 06 — Default vhost (HTTP)

Concept: A single physical server often hosts multiple websites/applications, and it decides which one to serve based on the Host header the client sends — this is “virtual hosting” (vhosts). Every server also has a default vhost: whatever it serves when the Host header doesn’t match any of its configured sites. That default is sometimes a totally different, less-locked-down application than the “real” site you were pointed at — it might be a default install page, an internal admin tool, or leftover test content.

Step 1: resolve the target to its IP, since you’ll be hitting the IP directly rather than the hostname:

dig +short target.tld

Step 2: request the IP directly with a bogus/random Host header:

curl -H "Host: some-random-string.com" http://TARGET_IP

Why this works conceptually: the DNS hostname and the actual server are two separate things — DNS just tells your client which IP to connect to; once connected, the Host header is what tells that server which site to serve. Bypassing DNS entirely (by hitting the IP) and supplying an arbitrary Host header exposes whatever’s “underneath” the routing logic.


Recon 07 — Default vhost over TLS

Concept: Same idea as Recon 06, but over HTTPS — with the added wrinkle that the TLS certificate itself is tied to a specific hostname (via SNI, Server Name Indication), so requesting the IP directly or supplying a mismatched Host header means the certificate presented won’t validate against what you asked for.

curl -H "Host: some-random-string.com" https://TARGET_IP -v -k

Reading the verbose output: -v will show you the certificate details during the handshake (issuer, the hostname(s) it’s actually valid for) even before you see the response body — useful context on its own, separate from whatever default-vhost content comes back.

Related check: this is also a natural moment to verify what TLS versions/ciphers a server accepts, since -v surfaces the negotiated TLS version directly in the handshake output — older accepted versions (TLS 1.0/1.1) can themselves be a finding worth noting.


Recon 08 — TLS certificate Subject Alternative Names (SANs)

Concept: A single TLS certificate can be issued to cover multiple hostnames at once (its Subject Alternative Names, or SANs) — a common practice so one cert can serve target.tld, www.target.tld, api.target.tld, etc. Reading the cert directly can reveal every hostname it was issued for, including ones that were never linked anywhere on the public-facing site.

Quickest with openssl:

openssl s_client -connect target.tld:443 -servername target.tld < /dev/null 2>/dev/null | openssl x509 -noout -text

Look for the X509v3 Subject Alternative Name: section — it lists every hostname the cert covers, comma-separated.

One-liner to just grab the SANs (skip the rest of the cert output):

echo | openssl s_client -connect target.tld:443 -servername target.tld 2>/dev/null | openssl x509 -noout -ext subjectAltName

Browser alternative:

Safari/Chrome dev tools → Security tab, or click the padlock/site info icon (see notes on viewing certs without a padlock — Web Inspector’s Security tab, or an external tool like SSL Labs’ SSL Server Test, both work too and often present the SAN list more readably than raw openssl output).

What to do with a new hostname found here: treat it exactly like a newly discovered subdomain — re-run Recon 00–07’s checks against it (robots.txt, directory brute-force, default vhost check) since it’s effectively a brand-new target you didn’t know about a moment ago.


Recon 09 — Reading response headers

Concept: HTTP response headers carry a lot of metadata beyond just the page content, and checking them explicitly (rather than just glancing at a rendered page) is a habit worth building for every request during recon — not just the ones where something looks obviously wrong.

curl -i http://target.tld/

What to look for, systematically:

If headers alone don’t show what you need, try requesting with -v instead of -i to also see the request headers curl sent, useful for comparing what you sent vs. what came back.


Recon 10 — Visual reconnaissance across many hosts

Concept: When a target exposes a large or programmatically-generated set of subdomains/paths (for example, a whole family of hosts named like 0x[hex-digit].a.target.tld), checking each one by hand in a browser doesn’t scale — the practical approach is to script the enumeration and pull down a specific visual asset (like a logo image) from each one, then eyeball the results side by side to spot the one that’s different.

Step 1: script the list of hosts to check, if they follow a predictable pattern:

for i in $(seq 0 15); do
  hex=$(printf '%x' $i)
  echo "0x${hex}.a.target.tld"
done

This generates every hostname in the pattern (0x0.a.target.tld through 0xf.a.target.tld, for a hex-digit-based naming scheme) without typing each one manually.

Step 2: download the visual asset from each host.

for i in $(seq 0 15); do
  hex=$(printf '%x' $i)
  wget "https://0x${hex}.a.target.tld/logo.png" -O "logo_0x${hex}.png"
done

Step 3: review the images — open each file directly, or batch them into an image viewer/gallery to flip through quickly. The goal is spotting the one image that’s visually different from all the others (a different color, an extra element, text overlaid on it — whatever the “odd one out” signal is for that specific exercise).

General pattern: any time recon turns up a large family of similarly-named hosts rather than one-off subdomains, scripting the enumeration (a bash loop, or a quick Python script) beats manual browsing every time — the pattern is: generate the list → hit every host the same way → diff/compare the results to find the one that stands out. For larger host lists than a manual visual scan can handle, screenshot-automation tools like aquatone or gowitness scale this same idea up further:

cat hosts.txt | aquatone -ports 80,443
gowitness scan file -f hosts.txt

Recon 11 — Subdomain enumeration / DNS

Concept: Finding subdomains you weren’t given directly — via brute force, zone transfers, or passive sources.

dig — manual DNS queries

dig target.tld            # default A record lookup
dig A target.tld
dig target.tld NS         # nameservers
dig target.tld TXT        # TXT records
dig target.tld MX         # mail servers
dig -x 1.2.3.4             # reverse lookup
dig @ns1.target.tld target.tld   # query a specific nameserver

Zone transfer attempt (misconfigured DNS servers only)

dig axfr @ns1.target.tld target.tld

axfr requests a full zone transfer — if the nameserver is misconfigured to allow this to anyone, it dumps every record in the zone in one shot. Try this against every nameserver returned by dig target.tld NS.

Subdomain brute force with dnsrecon

dnsrecon -d target.tld -D /usr/share/wordlists/dnsmap.txt -t brt

Subdomain brute force with dnsenum

dnsenum --dnsserver <DNS_IP> --enum -p 0 -s 0 -o subdomains.txt -f /usr/share/wordlists/dnsmap.txt target.tld

Manual bash loop (no extra tool needed)

for sub in $(cat wordlist.txt); do
  dig $sub.target.tld | grep -v ';\|SOA' | sed -r '/^\s*$/d' | grep $sub
done

Passive/OSINT sources (no active scanning against the target)


Recon 12 — Multiple backends behind a load balancer

Concept: A single hostname can round-robin (or otherwise route) across several different backend servers. Each backend may be running different code, a different version, or have different quirks/vulnerabilities — but a single request only ever shows you one of them. Repeating the same request enough times can surface a different backend’s response.

curl balancer.hackycorp.com

Just keep re-running it:

for i in $(seq 1 20); do curl -s balancer.hackycorp.com; echo; done

or as a one-liner loop directly in the shell:

while true; do curl -s balancer.hackycorp.com; echo; sleep 0.2; done

(Ctrl+C to stop once you spot a different response.)

What to look for: Any response that differs from the baseline (<h1>Not yet</h1> in this case) — different HTML, different headers, different status code, different timing. That different response is a signal you’ve hit a different backend server, which may expose different content, a debug endpoint, or a vulnerability that isn’t present on the “default” backend.

Why this matters in the wild: load-balanced infrastructure often has backends at different patch levels or configs (a canary/staging box mixed in with production, a backend mid-deploy, etc.). Repeated requests are a cheap way to fingerprint that inconsistency before targeting it further.

Useful headers to diff between responses, once you’re comparing outputs:

curl -sI balancer.hackycorp.com

Compare Server, Date, X-Powered-By, or any custom headers across multiple runs — sometimes the body looks identical but the headers give away a different backend.


Recon 13 — Retrieving a DNS TXT record

Concept: TXT records are free-form text fields in DNS, commonly used for domain ownership verification (SPF, DKIM, site verification codes) or to stash config/service info. Worth checking on any subdomain you find, since they sometimes leak more than the owner intended.

dig key.z.hackycorp.com

Basic lookup first — confirms the host resolves and shows its default record type (usually A).

dig -t TXT key.z.hackycorp.com

What to look for: the ANSWER SECTION of the output — that’s where the actual TXT record content shows up, quoted in the response.

General pattern: any time you want a specific DNS record type instead of the default, use -t <TYPE>:

dig -t A domain.tld
dig -t MX domain.tld
dig -t NS domain.tld
dig -t TXT domain.tld
dig -t CNAME domain.tld

Recon 14 — DNS zone transfer (AXFR)

Concept: Zone transfers exist so DNS servers can sync records with each other, but they’re only supposed to be allowed for trusted secondary nameservers. If a server is misconfigured to allow AXFR from anyone, it’ll hand over its entire zone — every record it holds for that domain — in one request.

dig AXFR z.hackycorp.com

Run bare like this, it’ll almost always fail — by default dig queries your resolver/the domain’s general nameservers, not necessarily the actual authoritative server, and most servers refuse AXFR from random sources anyway.

Step 1: find the authoritative nameserver.

dig -t SOA z.hackycorp.com

Step 2: retry AXFR against that specific nameserver.

dig AXFR z.hackycorp.com @z.hackycorp.com

What to look for: if the transfer succeeds, the output dumps every record in the zone (A, TXT, CNAME, NS, MX, etc.) — including subdomains that were never publicly linked anywhere.

General pattern: always check the SOA/NS records first, then attempt AXFR against each nameserver individually — a zone can have multiple nameservers, and only one might be misconfigured to allow it.

dig NS z.hackycorp.com                      # list all nameservers for the zone
dig AXFR z.hackycorp.com @ns1.z.hackycorp.com
dig AXFR z.hackycorp.com @ns2.z.hackycorp.com

Recon 15 — Zone transfer on an internal zone via a known nameserver

Concept: A nameserver can be authoritative (or at least willing to answer) for more than one zone — including internal/private zones that were never meant to be queried from outside. If you already know a working nameserver from a previous step, it’s worth asking that same server about other zone names too, not just the one you found it through.

Step 1: confirm the nameserver you already have.

dig -t NS z.hackycorp.com

This is the server you’ll target directly — no need to look up a separate SOA for the internal zone, since you’re leveraging a nameserver you already know answers AXFR requests.

Step 2: attempt AXFR on the internal zone name directly.

dig AXFR int @z.hackycorp.com

What to look for: if the server is misconfigured to answer for internal zones as well as public ones, this dumps the internal zone’s full record set — hostnames, IPs, and services that were never meant to be externally visible.

General pattern: once you find any nameserver willing to do a zone transfer, don’t stop at the zone you found it through — try common/guessable internal zone names against it too (int, internal, corp, local, intranet, dev, staging):

dig AXFR internal @known-nameserver.tld
dig AXFR corp @known-nameserver.tld
dig AXFR local @known-nameserver.tld

Recon 16 — Fingerprinting the BIND version

Concept: BIND is one of the most widely used DNS server implementations, and by default it responds to a special query asking for its own version string. This is a classic DNS fingerprinting trick — knowing the exact version can point you toward known CVEs for that release.

dig +short -c chaos -t txt version.bind @NAMESERVER_IP

What to look for: the output is literally the BIND version string (e.g. 9.16.1), which you can then cross-reference against known vulnerabilities for that release.

General pattern: CHAOS-class queries are a good “does this reveal too much” check on any DNS server you find during recon — worth trying by default alongside SOA/NS/AXFR checks:

dig +short -c chaos -t txt version.bind @NAMESERVER_IP
dig +short -c chaos -t txt hostname.bind @NAMESERVER_IP   # sometimes reveals the server's hostname too

Recon 17 — GitHub OSINT: finding a developer via commit history

Concept: Organizations often have a public GitHub presence, and even throwaway/test repos can leak real names, emails, or usernames of the people who work there. This one’s less about tooling and more about manual OSINT technique — knowing where to look on GitHub itself.

Step 1: find the org’s GitHub account.

Google: "hackycorp github"

or search directly on GitHub:

https://github.com/search?q=hackycorp&type=organizations

Step 2: browse the org’s repositories until you find the one named (in this case test1).

Step 3: find the committer.

General pattern: this same approach — org → repo list → commit history → author — is a repeatable technique any time you’re doing recon on a company’s GitHub presence:


Recon 18 — Browsing an org’s developers’ public repos

Concept: GitHub organizations list their People (members) publicly by default, and each of those individuals often has their own personal repos alongside anything under the org itself. A developer’s personal projects are usually far less curated than official company repos — old test scripts, personal notes, forgotten API keys/configs, side projects that reference internal tooling — so once you know who works there (from a step like Recon 17), checking what they’ve personally pushed is a natural next move.

Step 1: find the org’s People tab.

https://github.com/orgs/hackycorp/people

or from the org’s main page, click the People tab.

Step 2: click into an individual member’s profile, which shows their own public repositories separate from the org’s.

Step 3: browse their personal repos the same way you’d browse an org’s — look for anything oddly named, clearly a test/scratch project, or something that references the company’s real infrastructure.

General pattern: this is the natural follow-up to Recon 17 — once a name/username surfaces from a commit, always check:


Recon 19 — Finding a mismatched commit email in git history

Concept: Git records an author name + email on every single commit, and that email is whatever was set in the committer’s local git config at the time — not necessarily their work email. Developers frequently commit from a personal machine with a personal email still configured, which leaks personal accounts or hints at internal systems that differ from their usual company address.

Step 1: clone the repo.

git clone https://github.com/hackycorp/repo7.git
cd repo7
ls

Step 2: check the commit history for author emails.

git log

This shows every commit with its Author: Name <email> line. Scroll through looking for one email that doesn’t match the pattern of the others (different domain, personal-looking address like Gmail, or a naming convention that doesn’t match the rest).

Faster ways to isolate just the emails, once you know what you’re looking for:

git log --format='%ae'          # just author emails, one per commit
git log --format='%ae' | sort -u   # unique list, easier to spot the odd one out

What to look for: most commits will share a consistent company-domain email pattern (e.g. name@hackycorp.com). The one that stands out — a different domain, a personal provider, or an inconsistent naming style — is the target.

General pattern: this technique applies to any git repo you have access to, public or otherwise — always worth a quick git log --format='%ae' | sort -u pass on a new repo during recon, since it’s a cheap way to enumerate every identity that’s ever touched the code.


Recon 20 — Checking all branches of a repo

Concept: The default branch (main/master) is only ever part of the story. Developers often push experimental work, old configs, or debug/test code to other branches and never clean them up — and those branches stay fully accessible even if they’re not linked anywhere obvious in the UI.

Step 1: clone the repo (grabs all branches by default).

git clone https://github.com/hackycorp/repo3.git
cd repo3

Step 2: list every branch, including remote ones.

git branch -a

Step 3: switch to and inspect each branch.

git checkout branch-name
ls
git log

Repeat for every branch listed — check file contents, commit history, and commit messages on each, the same way you would on the main branch.

Faster way to browse without switching back and forth:

git log --all --oneline

What to look for: branches with unusual names (dev, test, backup, old, a developer’s first name, a ticket number) are worth prioritizing — these are exactly the kind of branches that get forgotten about and never merged or deleted.

General pattern: always run git branch -a (or check the branch dropdown on GitHub’s web UI) as a standard step right after cloning any repo during recon — never assume the default branch is the only one worth reading.


Recon 21 — Reading through code content across branches

Concept: Same starting point as Recon 20 (enumerate every branch), but the goal here is to actually read the code/file contents on each branch rather than just noting the branch exists — sensitive info tends to be buried inside files, not in branch names or commit messages alone.

Step 1: clone and list branches, as before.

git clone https://github.com/hackycorp/repo4.git
cd repo4
git branch -a

Step 2: switch to each branch and actually look through the files.

git checkout branch-name
ls -la

Then open/inspect anything that looks relevant:

cat filename
find . -type f -name "*.env"
find . -type f -name "*.config*"

Step 3: search file contents directly for likely secrets, across the whole branch.

grep -r "password" .
grep -r "key" .
grep -ri "secret\|token\|api_key" .

Step 4: diff a suspicious branch against main to see exactly what changed.

git diff main branch-name

This shows line-by-line differences, which is often faster than manually reading every file if the branch is large — you only see what’s actually different from the default branch.

Step 5: check history within a branch, not just its current state.

git log -p branch-name

What to look for: hardcoded credentials, internal hostnames/IPs, API keys, .env files, config files with connection strings — anything that reads like it wasn’t meant to be committed.

General pattern: treat every branch like its own mini recon target — checkout, ls, grep -r, and git log -p are the four commands worth running every time, not just git branch -a and moving on.


Recon 22 — Finding a deleted file in git history

Concept: Deleting a file and committing that deletion does not remove it from the repo’s history — it’s still fully present in every commit before the deletion. A common mistake pattern is: commit a secret → realize it → delete the file → commit again, thinking it’s “gone.” It isn’t. This is the direct follow-up to the git log -p habit from Recon 21.

Approach used: git log -p

git clone https://github.com/hackycorp/repo9.git
cd repo9
git log -p

Scroll through the patch output looking for a diff that only removes lines — in diff format, removed lines are prefixed with -. A commit whose diff is entirely - lines (nothing added) is a deletion commit, and scrolling to it shows you exactly what content was removed.

Faster ways to jump straight to deletions, without scrolling through everything:

git log --diff-filter=D --summary

Once you’ve found the commit that deleted the file, view the file as it existed right before that deletion:

git show <commit-hash>~1:path/to/deleted/file

Or restore it entirely into your working directory:

git checkout <commit-hash>~1 -- path/to/deleted/file

Note on tig: the instructor’s video used tig, a text-based UI for browsing git repos interactively (like a terminal git log browser). It’s a separate package, not built into git itself:

apt-get install tig

If it’s not installed and you don’t want to bother, plain git log -p (or the --diff-filter=D approach above) gets you the same information — just less visually browsable.


Recon 23 — Searching commit messages for sensitive keywords

Concept: Commit messages themselves can leak sensitive information, separate from anything in the actual file changes — a developer might write “removed hardcoded key: XXXX” or “fixed password issue” directly in the message. When a repo has a lot of commits, scrolling through them all is slow, so searching by keyword is far more efficient.

Step 1: clone the repo.

git clone https://github.com/hackycorp/repo0a.git
cd repo0a

Step 2: browse everything, if the repo is small enough.

git log --all

Step 3: search commit messages by keyword instead of scrolling manually.

git log --all --grep="key"

Useful variations:

git log --all --grep="key" -i          # case-insensitive search
git log --all --grep="key" --oneline   # condensed one-line-per-commit view of matches
git log --all -i --grep="password" --grep="secret" --all-match   # match commits containing BOTH terms

Note on tig: as in Recon 22, the instructor used tig (an interactive terminal UI for browsing git history) rather than raw git log. It’s not required — git log --all --grep= gets you the same result from the command line, just install tig (apt-get install tig) if you’d rather browse visually.

General pattern: --grep on commit messages is the natural companion to the git log -p content search from Recon 21/22 — one searches what changed, the other searches what the developer said about the change.


Recon 24 — Finding exposed files on an asset-hosting server

Concept: Sites often load JS/CSS from a separate subdomain (a CDN or dedicated asset host), and that asset server is a distinct target from the main site — it may have its own misconfigurations, and since it’s “just serving static files,” it doesn’t always get the same scrutiny as the main application.

Step 1: find the asset subdomain via the browser. Open the page, use Inspect Element → Network tab (or just view page source) and look at where .js/.css files are actually being loaded from. In this case, that’s assets.hackycorp.com rather than the main domain.

Step 2: resolve it to see what’s actually hosting it.

dig assets.hackycorp.com

The response here pointed to a cloudfront.net hostname — meaning the asset server is sitting behind AWS CloudFront (a CDN), not hosted directly by the company’s own infrastructure.

Step 3: try requesting files directly off that asset host.

curl https://assets.hackycorp.com/key.txt

or just visit it in a browser. Since it’s a plain static-file host, there’s no application logic to stop you from requesting any filename directly — if the file exists and isn’t access-restricted, it’s served as-is.

General pattern: any time you spot assets loading from a separate subdomain, treat it as its own recon target:


Recon 25 — Accessing an “Any Authenticated AWS User” S3 object

Concept: S3 buckets have a legacy ACL grant called “Any Authenticated AWS Users” that many people historically misunderstood as “only people in my own AWS account” — it actually means any AWS account at all, not just the bucket owner’s. If a bucket/object has this grant, anyone with any AWS CLI credentials configured (their own account, unrelated to the bucket owner) can read the file.

Step 1: try a direct download with the AWS CLI.

aws s3 cp s3://assets.hackycorp.com/key2.txt key2.txt

If you get a HeadObject 403/Forbidden error — this is a known quirk: aws s3 cp runs a HeadObject check before downloading, and that metadata-check permission isn’t always granted even when actual read access is. Skip it by calling the lower-level API directly:

aws s3api get-object --bucket assets.hackycorp.com --key key2.txt key2.txt

If you then get an AccessDenied error on GetObject itself — this usually isn’t the bucket’s problem, it’s your own IAM user’s. A freshly created AWS CLI user typically has no permissions at all by default (AWS denies by default until a policy is attached), so even if the bucket allows any authenticated account in, your IAM user still needs explicit permission to make S3 calls in the first place.

Check who you’re authenticated as:

aws sts get-caller-identity

Check what policies are already attached:

aws iam list-attached-user-policies --user-name YOUR_USERNAME

If nothing’s attached, grant yourself basic S3 read access:

aws iam attach-user-policy \
  --user-name YOUR_USERNAME \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess

Then retry the download — aws s3 cp or aws s3api get-object will both work once your own IAM user actually has S3 permissions:

aws s3 cp s3://assets.hackycorp.com/key2.txt key2.txt

Other useful checks along the way:

aws configure get region                                   # confirm region isn't blank/"none"
aws configure set region us-east-1                          # set explicitly if needed
aws s3api get-bucket-location --bucket assets.hackycorp.com  # find the bucket's actual region

Key takeaway: cp and get-object both hit the same underlying GetObject API — swapping between them doesn’t bypass an actual AccessDenied. If GetObject itself is denied, the fix is almost always on your own IAM user’s permissions, not the target bucket’s.


Recon 26 — Hardcoded keys in JavaScript files

Concept: Anything shipped to the browser as client-side JS is fully visible to the end user, no matter how it’s minified or obfuscated. Developers sometimes hardcode API keys, tokens, or internal endpoints directly into JS thinking it’s “just for the frontend to work” — but since the browser has to download and execute the raw file, so does anyone else.

Step 1: open dev tools and browse the loaded JS files.

Step 2: read through the files directly. For a large/minified file, browser dev tools usually offer a “pretty print” / {} button in the Sources panel to reformat minified JS into readable multi-line code — much easier to visually scan for anything key-shaped.

Step 3: search for likely keywords within the file (most dev tools support Ctrl+F / Cmd+F search within an open file):

key
token
secret
api_key
password
Authorization

Command-line alternative, if you’d rather grep than click through dev tools:

curl -s https://assets.hackycorp.com/somefile.js | grep -i "key\|token\|secret"

or save it locally and search:

curl -s https://assets.hackycorp.com/somefile.js -o somefile.js
grep -iE "key|token|secret|password" somefile.js

General pattern: any time you’re recon’ing a site, checking loaded JS files for hardcoded secrets is worth doing as a standard step alongside checking the asset host itself (Recon 24) — the two often go hand in hand, since discovering the asset subdomain is usually how you find the JS files to inspect in the first place.


General Tools Quick Reference

Tool Purpose Core command
curl HTTP requests, header manipulation curl -i -H "Host: X" URL
openssl s_client Inspect TLS certs directly openssl s_client -connect host:443
dig DNS queries dig DOMAIN TYPE
wfuzz Web fuzzing (dirs, params, vhosts) wfuzz -z file,LIST URL/FUZZ
ffuf Faster modern fuzzer ffuf -w LIST -u URL/FUZZ
gobuster Dir/DNS/vhost brute force gobuster dir -u URL -w LIST
dnsrecon / dnsenum DNS enumeration & zone transfers dnsrecon -d DOMAIN -D LIST -t brt
aquatone / gowitness Screenshot many hosts at once cat hosts.txt \| aquatone

Notes on Docker environments (from my recent setup)

Since I’ve been running these tools via Docker containers, a couple of reminders that keep biting:


Compiled from public write-ups (Medium: “Comprehensive Guide to Mastering Reconnaissance… on PentesterLab” by Shamil Abbas; HackTricks DNS pentesting reference; Hackviser DNS pentesting guide) plus standard tool documentation and my own experience with these exercises. Exercise-specific flags/keys intentionally omitted — this is meant as a command reference, not a solutions sheet.

tags: pentesterlab - recon