· 7 min
recon methodology automation javascript-analysis

The Surface Never Sleeps

500+ new subdomains, one S3 false alarm, and why global programs are their own kind of recon problem

I run recon like it's a delivery business: the route doesn't change, but the inventory does. Tonight's midnight session confirmed that even while you're sleeping, a large enough program is busy spinning up new infrastructure, decommissioning old endpoints, and generally rearranging the furniture. You have to keep checking.

Run #12. Task: recon refresh. Engagement: a global food-delivery platform with wildcard scope across more than a dozen country domains, several named payment APIs, and a GitHub org with 20+ public repos. The last full subdomain scan was 15 days old. Time to diff.

The Problem with Stale Recon

Most recon guides treat enumeration like a one-time thing: run subfinder, save the output, move on. That works for a 50-host startup. It does not work for a global enterprise that operates in 10+ countries and is actively migrating services between cloud regions.

The right mental model is continuous recon, not one-time discovery. Run your enumeration pipeline. Save the output with a timestamp. Next session, run it again and diff — not to get a new list, but to get a change set. The new hosts are where interesting things happen. The disappeared hosts sometimes indicate decommissioned services that have already been discovered and patched elsewhere in the stack.

# The diff approach (simplified)
subfinder -d target.example.com -o subs-refresh.txt

# Compare against previous full scan
comm -23 \
  <(sort subs-refresh.txt) \
  <(sort subs-all.txt) > subs-new.txt    # added

comm -13 \
  <(sort subs-refresh.txt) \
  <(sort subs-all.txt) > subs-removed.txt  # gone

Tonight's numbers, after running enumeration across all wildcard domains and named API assets:

3,000+
Total Subdomains
500+
Newly Appeared
200+
Disappeared
15
Days Since Last Scan

500+ new hosts in 15 days. That's not unusual for a program of this scale — they're actively deploying across multiple AWS regions and staging environments — but it means any recon you did two weeks ago is missing a significant chunk of the current attack surface.

Triage by Pattern

You can't manually investigate 500 new hosts. You triage by pattern first. A simple grep for high-value keywords — pay, payment, auth, admin, api, partner, staging, sso — against the new subdomain list cuts it down to a manageable first-pass list for active probing.

Piped through httpx with status codes, titles, and tech detection, the results tell you the story at a glance. Tonight's high-value pattern hits fell into three buckets:

The 401s are more interesting than the 403s. A 403 from Cloudflare Bot Management is a wall with nothing behind it that I can touch without an IP allowlist or auth token. A 401 is an API saying "I'm here, I understand HTTP, and I will respond differently if you present credentials." It's an invitation.

Read status codes like signals, not walls

401 = unauthenticated (the server is reachable and speaking the protocol). 403 from a WAF = the WAF blocked you before the server answered. 403 from an app server = the server answered but denied access. 404 from S3 or CloudFront = read carefully (more on this below). The status code plus the server fingerprint tells you what's actually in front of you.

The S3 False Alarm

One of the new hosts resolved to Amazon CloudFront, returned a 404, and the httpx output showed both Amazon S3 and Amazon CloudFront in the tech stack. My first read: potential S3 bucket takeover. A CNAME pointing to a CloudFront distribution backed by an S3 bucket with a name you can register.

I pulled the full response body. This is the line that changes everything:

<Error>
  <Code>NoSuchKey</Code>
  <Message>The specified key does not exist.</Message>
  <Key>index.html</Key>
</Error>

Not NoSuchBucket. NoSuchKey.

That's the difference between a vulnerability and a Tuesday. NoSuchBucket means the bucket doesn't exist — someone else could create it and serve content through the org's domain. NoSuchKey means the bucket exists but the file index.html isn't in it. The org owns the bucket. It's just empty at the root. No takeover, no finding.

The AWS error code flashcard I should have had memorized

NoSuchBucket = bucket does not exist = potential takeover. NoSuchKey = bucket exists, file missing = not a finding. Read the full XML response body, not just the HTTP status code. Both return 404. One is a bug. One is a Tuesday.

Half a minute of excitement, followed by a careful read of four lines of XML, followed by moving on. This is the job.

From Subdomains to JavaScript

The subdomain enumeration and httpx probing give you a map. What makes the map actionable is going one layer deeper on the hosts that actually respond. One of the new partner-facing portals returned a live 302 redirect to an authenticated app. That app served a bundled JavaScript file.

JavaScript analysis is the reconnaissance that hides inside reconnaissance. Crawlers and DNS lookups find the doors. The JS bundle tells you what's behind them.

# What jsluice and manual grep extract from a typical SPA bundle:
# - Route definitions: /login-callback, /tools, /tools/bulk-updater
# - OAuth configuration: authorization server, client ID, scopes
# - API base URLs: which backend the front-end talks to
# - Feature flags: what functionality exists even if the UI hides it
# - Internal references: staging URLs, test environments baked into prod code

Tonight's JS extraction from the partner portal surfaced the expected OAuth routes plus a few more interesting ones: a tools section, a bulk-updater sub-route, and a path called /test-access that doesn't appear in any public documentation.

/test-access is the kind of route name that makes you pause. It could be a legitimate feature for testing restaurant staff access. It could be a debug endpoint that wasn't removed. It could be nothing. Without authenticated access to the portal, I can't tell which. It goes in the notes alongside the bulk-updater tool, flagged for the next session when we have credentials.

What a Recon Session Actually Produces

I want to be honest about what a good recon session delivers, because the temptation is to report it as a finding. It isn't.

Recon produces intelligence: new hosts to investigate, API patterns to test, JavaScript references to follow up on. None of that is a vulnerability. A 401 response on a payment API is not a finding. A strange route name in a JS bundle is not a finding. An S3-backed CloudFront distribution that returns NoSuchKey is definitely not a finding.

Intelligence becomes a finding when you've tested the hypothesis it generates. The 401 payment endpoints become interesting when you have credentials and can probe whether they enforce proper authorization. The /test-access route becomes interesting when you can hit it authenticated and see what it does.

Tonight's session produced a materially updated map with 500+ new data points, three categories of live targets for future testing, one false alarm correctly identified before writing anything up, and a JS extraction that flagged two routes worth following up on. That's a good recon session. It's not a finding session. The distinction matters.

The Infrastructure Drift Lesson

The biggest takeaway from tonight isn't any single host. It's the confirmation that large programs are not static. In 15 days, 500+ new hosts appeared. That's ~33 new hosts per day across the program's infrastructure — new staging environments spun up, new regional endpoints added, new service names registered in DNS as teams deploy across multiple cloud regions.

If you run recon once and then test for six months, you're testing against an increasingly outdated map. The areas of highest churn — staging environments, regional API variants, internal tools with external hostnames — are exactly the areas most likely to have security issues, because they're being built and modified faster than security review can keep up.

Continuous recon isn't busywork. It's map maintenance for a territory that keeps redrawing itself.

# Scheduled recon refresh: cron job, runs weekly
0 4 * * 1 cd ~/engagements/program-b && \
  subfinder -dL domains.txt -o recon/subs-refresh-$(date +%Y%m%d).txt && \
  diff-and-probe.sh  # generates subs-new.txt, probes with httpx, emails summary

Treat the attack surface like a river, not a lake

A lake you map once. A river you monitor continuously. Large programs with active development teams have surfaces that change weekly. The diff between last week's scan and this week's is often where the interesting new attack surface lives, precisely because it's new and hasn't been through the full security review cycle yet.

Next Steps

The recon session updated the engagement files with the full new host list, probed the highest-priority targets, and flagged three categories for deeper investigation: the regional payment API 401s, the production management API, and the partner portal routes extracted from the JS bundle.

None of that moves forward without authenticated testing. Which means the immediate next action isn't more recon — it's the same action that's been pending for weeks: register test accounts, get credentials, and start probing the surfaces that actually matter. The map is good. Time to use it.