# The richest recon target is a file you already downloaded

> Passive recon isn't running quieter scans — it's re-reading the traffic you already captured. The JS bundle you loaded ships a source map that rebuilds the original codebase, a chunk manifest listing every hidden admin route, and a bootstrap state object with the current user's role. No new requests, nothing to detect.

**Key takeaways:**
- Source maps are the single highest-yield artifact: a trailing `//# sourceMappingURL=main.<hash>.js.map` rebuilds the original unminified source — real names, comments, folder structure, dev-only code paths. Devs forget to strip them in prod constantly.
- The entry bundle carries a webpack chunk manifest (chunkId → filename hash near `__webpack_require__.u`) that lists every lazily-loaded route, including the admin panel the UI only fetches for admins. Pull them all and grep for endpoints.
- SPAs inline initial state into the HTML — `window.__INITIAL_STATE__`, `__NEXT_DATA__`, `window.__NUXT__` — often with full API base URLs, feature flags, and the current user object WITH role. Read it from the HTML, not a new request.
- Passive means re-reading, not re-requesting. No new traffic, so no WAF, no rate limit, nothing to detect — and it's all grounded in ground truth, because a developer wrote every string.

**Series:** Recon Notes  
**Published:** July 1, 2026 · 7 min read  
**Canonical:** https://crusaderproxy.com/research/en/posts/passive-recon-from-proxy-history/

---


Most recon writeups open with a subdomain brute-force and a wordlist. That's fine, but it's loud, it's slow, and it runs *before* you understand the app. The problem isn't that people don't know how to enumerate — it's that they treat recon as something you go *out* and do, when the highest-yield artifact is already sitting in your proxy history. **You downloaded the whole codebase the moment the page loaded. You just haven't read it yet.**

> **Passive recon isn't running quieter scans. It's re-reading traffic you already have — and re-reading beats re-requesting, because a file you already downloaded can't trip a WAF.**

That reframe is the whole game. The JS bundle your browser pulled to render the app is not a black box. It ships, or points to, everything you'd otherwise spend a week fuzzing for: the original source, the full route table, every lazily-loaded admin chunk, and — inlined into the HTML — the current user's role. None of it costs a request.

## Source maps: the entire codebase, handed to you

This is the single highest-yield artifact in modern web recon, and most people walk right past it. Open the main bundle and look at the last line:

```
//# sourceMappingURL=main.4f2a91c.js.map
```

Fetch that `.map`. A source map is a JSON file that maps minified positions back to the original, and it embeds the original source *content*. Feed it to a reconstructor and you get the pre-build tree back:

```
unwebpack-sourcemap https://app.target.com/static/js/main.4f2a91c.js.map -o ./src
# or
sourcemapper -url https://app.target.com/static/js/main.4f2a91c.js.map -output ./src
```

Out comes the original **unminified** source: real variable names, developer comments, the folder structure (`src/features/billing/adminOverride.ts` tells you more than any wordlist), and dev-only code paths that minification flattened away. Even when the `sourceMappingURL` comment has been stripped, the map is very often still deployed at the guessable path — just append `.map` to the bundle URL and try `main.<hash>.js.map` directly. Teams disable the comment and forget the emit flag constantly. This is not a rare misconfiguration; it's the default state of a shocking number of production apps.

## The chunk manifest: every hidden route, enumerated for you

Even without a source map, the entry bundle betrays the whole app. Code-split apps ship a **webpack chunk manifest** — a big map of `chunkId → filename hash`, sitting near `__webpack_require__.u` (the function that builds a chunk's URL). It exists so the app can lazily fetch routes on demand, which means it enumerates *every* route, including the ones the UI only loads *after* it decides you're an admin.

Grep the bundle for the manifest, pull every chunk filename it names, download them all, and grep the lot for endpoints:

```
# find the manifest / chunk map
grep -oE '__webpack_require__\.u\s*=\s*function[^}]+' main.js
# then pull each chunk hash it references and grep them for routes
grep -rhoE '"/(api|admin|internal)[a-zA-Z0-9/_{}$.-]*"' ./chunks/
```

The admin panel, the billing-ops screen, the internal debug view — the UI gates their *rendering* behind your role, but the *code* to render them was shipped to your browser anyway, and the manifest tells you exactly which file to open. There is no navigation link to `/admin/impersonate`; there is a chunk that references it.

While you're grepping the reconstructed source and chunks, the **client-side route table** is the other free map. React Router / Vue Router configs enumerate every path in one array — grep for the route definitions and you get `/admin`, `/internal`, `/debug` with no nav entry anywhere in the DOM.

## The bootstrap state object: role and internals, in the HTML

Before you make a single new request, read the HTML you already have. SPAs inline their initial state directly into the page to avoid a first-paint round-trip. Look for one of these globals in a `<script>` block:

```
window.__INITIAL_STATE__ = {...}
__NEXT_DATA__            (Next.js, in <script id="__NEXT_DATA__">)
window.__NUXT__          (Nuxt)
window.__CONFIG__
```

These routinely contain the full API base URLs (including the internal ones the SPA quietly calls), every feature-flag value, and — the part people miss — **the current user object, with its `role`**. Occasionally a poorly-scoped serializer dumps *other* users' data into the same blob. You read all of it out of the HTML you already loaded. No request, no log line.

## Mining tokens and secrets from what you captured

Any request you made carrying `Authorization: Bearer eyJ...` is a free intelligence drop. A JWT is three base64url segments; decode the **middle** one:

```
# middle segment = claims
echo '<middle-segment>' | base64 -d 2>/dev/null | jq
```

Read `sub` (the user/subject id), `role`, `tenant`, and `exp` (session length — short expiry tells you where to expect refresh flows). The gold is `iss` and `aud`, which routinely leak internal hostnames like `auth.internal.corp` — new surface you'd never have guessed from the outside.

Then grep everything you've reconstructed — JS, JSON responses, and the source-map output — for the prefixes that actually pay. These are exact enough to run blind:

```
AIza                 # Google API key
AKIA | ASIA          # AWS access key id
xox[baprs]-          # Slack token
sk_live_             # Stripe live secret key
ghp_ | gho_          # GitHub token
glpat-               # GitLab PAT
-----BEGIN           # private key block
eyJ                  # JWT
```

Front-end secrets are embarrassingly common, and a source map surfaces the ones that got minified into an unsearchable soup — because the map hands you back the original string literals with their variable names attached.

## The detection trap: never trust a single host list

The mistake that quietly caps your surface: assuming the app talks to the hosts you'd guess. It doesn't. The SPA on `app.target.com` fans out to `api.`, `events.`, `uploads.`, `flags.` subdomains — some on the apex, some on S3, some on a vendor. **Group your history by host *first*.** Every distinct host is distinct surface, and half of them never appear in any nav link or marketing page. If you started with a hand-written host list and fuzzed against it, you never saw the `events.` host that logs to a different backend with different auth. The bootstrap state object and the reconstructed source will name these hosts for you — cross-reference them against what your history actually contacted, and the deltas are the interesting part.

While grouping, flag the second-order signals: a stray `/api/v1/` call under a `/api/v3/` app (old version, often missing the auth v3 added), a `/graphql` endpoint (introspection may be on — one query dumps the entire schema), and any parameter you can now *see is real* because you found the code that reads it. That's the natural bridge to [mining the parameters the app never documents]({{< relref "posts/hidden-parameter-mining.md" >}}) — the source map tells you which ones exist before you send a single value.

## Why this beats active scanning

Two reasons, and they compound. First, it's **grounded in ground truth**: every endpoint, host, and parameter came from code a developer actually wrote, not a wordlist guess — so your hit rate is real, and you skip the 90% of any wordlist that was never deployed. Second, it's **silent by construction**: the highest-value work happens on files already in your history, so it generates *zero* new requests. No rate limit to trip, no WAF rule to fire, no anomaly detector to spike. A scanner announces itself on every line; re-reading a source map you downloaded during a normal login announces nothing. You go active later — surgically, only against the routes and params you've already confirmed exist.

## The point

Active recon asks the server questions and hopes it answers honestly. Passive recon reads the answers the app already shipped to your browser: the source map that rebuilds its codebase, the chunk manifest that lists its hidden admin routes, the bootstrap blob that hands over the current user's role. The bugs were reachable the whole time — the app told you where, in a file you'd already downloaded and never opened.

Crusader is built to make that re-read the default loop, not an export-and-grep chore. Your HTTP/2 history is searchable and groupable by host, path, and parameter, and the JS responses — bundles, chunks, and the HTML with its inline state — are right there to mine, source maps and all. Browse a target for ten minutes, then read what you caught. [download Crusader free](https://crusaderproxy.com/#install) and start your next engagement by reading the map before you draw on it.


---

*Original research by Crusader Research. Try Crusader free: https://crusaderproxy.com/#install*
