Skip to content

Passive vs active subdomain enumeration, and when each one lies

subfinder returns over six hundred names for tesla.com; well under half resolve. Passive and active subdomain enumeration fail in opposite directions.

9 min read

Subdomain enumeration is the work of finding the hostnames that exist under a domain, and it comes in two families that fail in opposite directions. Passive enumeration reads what third parties already recorded, so it hands back names that no longer exist. Active enumeration generates candidates and resolves them, so it only finds names somebody already thought to ask for.

One subfinder run against tesla.com returns more than six hundred names in seconds, and well under half of them still resolve. Running one family on its own gives you a confident and wrong picture, which is why everything below runs both and then checks the output of both.

Everything below uses the tools people actually run: subfinder, dnsx, puredns and httpx. Passive enumeration only reads public logs, so it runs against tesla.com, which has published a bug bounty since 2015 and whose certificates and DNS records are already in databases anyone can query. Active enumeration sends packets, so it runs against vulnweb.com, a set of deliberately vulnerable hosts Acunetix publishes for people to point scanners at. That split is a rule about authorisation rather than a formality, and the last section says why.

The two families of subdomain enumeration

Passive enumeration collects subdomain names from third parties that already recorded them. Certificate transparency logs, passive DNS aggregators, search indexes, and autonomous system number (ASN) and WHOIS pivots. You never send a packet to the target, because you are querying somebody else's copy of the data.

Active enumeration generates candidate names and resolves them against the nameservers that answer for the domain. Every query is traffic that reaches the target's DNS infrastructure, and a brute force is a lot of it.

PassiveActive
What it queriesThird-party logs and aggregatorsThe domain's own nameservers
What the target seesNothingEvery lookup you make
Tools used heresubfinderpuredns, dnsx
FindsNames that were published somewhere, onceNames matching a wordlist you supply
Fails byReturning dead names, and missing anything never loggedMissing anything unconventional, and trusting bad resolvers

The first is quiet and turns up things nobody meant to publish. The second is loud and turns up things nobody published at all. Neither is optional, and the rest of this post is why.

Passive enumeration with subfinder

subfinder queries around thirty passive sources at once, unions the answers, and deduplicates them. Certificate transparency, passive DNS, threat-intel feeds, all in one pass. It is the fastest way to see most of what has ever been published about a domain, and it touches nothing the target owns.

bash
subfinder -d tesla.com
output
A subfinder run against tesla.com: the tool banner, three INF lines, then the first dozen subdomains it found
Around thirty passive sources, unioned and deduplicated, without a single packet sent to the target

More than six hundred names, in seconds, and you never announced yourself. That number is the trap. It reads like an inventory, and an inventory is exactly what it is not.

Resolving the passive list with dnsx

Certificate transparency is append-only, and that changes everything about how you read this list. A certificate issued in 2021 is in the log forever, even if the host it covered was torn down the week after. So a name in subfinder's output tells you one thing only: this name was published somewhere, once. Whether it points at anything today is a separate question, and DNS is the only thing that can answer it.

dnsx takes the list and resolves it. Pipe subfinder straight into it.

bash
subfinder -d tesla.com -silent | dnsx -a -resp -r 1.1.1.1,8.8.8.8
output
dnsx output: eleven akamai-apigateway hostnames under tesla.com, each followed by a blue [A] record type and a green IP address, and every one of those addresses is 23.193.108.68
Each name that survives, with the address it points at. Every name in this frame answers with the same one

Well under half of the names survive. The rest are ghosts: retired staging environments, per-deploy hostnames that lived for an afternoon, services shut down years ago whose certificates outlived them. An unresolved subdomain list is more fiction than fact, and it will not tell you which part is which. That is the work dnsx just did for you.

One detail that is not cosmetic: -r 1.1.1.1,8.8.8.8. The default resolver pool rate-limited a bulk run this size down to nothing halfway through, and naming two fast public resolvers is what made the result reproducible. Bad resolvers are the single largest source of wrong answers in active enumeration, and every section below comes back to them.

Testing for a wildcard DNS record

From here on, every command sends packets, so the target is vulnweb.com.

The moment you start inventing names instead of resolving found ones, one DNS setting can invalidate all of it: a wildcard record. A wildcard is an entry such as *.example.com that answers for any name at that level, so every name you make up resolves too, and a brute force will proudly report your entire wordlist as live.

Catch it in three lines. Resolve names that cannot possibly exist, and read the response status rather than the answer, because a timeout and a genuinely missing name both print nothing and an empty answer cannot tell them apart.

bash
for i in 1 2 3; do
  n="$(openssl rand -hex 6).vulnweb.com"
  printf '%-38s %s\n' "$n" "$(dig "$n" +noall +comment | grep -oE 'status: [A-Z]+')"
done
output
Three random subdomain labels under vulnweb.com, each answered NXDOMAIN
Three labels that cannot exist, each refused by name. No wildcard here

Each of the three comes back NXDOMAIN, the DNS status code for a non-existent domain, meaning the nameserver is stating that the name does not exist rather than failing to answer. No wildcard, so a positive result here means something. Run the same test on a domain that answers a wildcard and all three come back with an address, at which point a 100,000-word brute force reports 100,000 live hosts and not one of them is evidence.

Do this per branch, not once at the top. *.dev.example.com can exist while the apex has no wildcard at all, so a single test proves nothing about the rest of the tree.

Brute forcing subdomains with puredns

With the wildcard cleared, you can invent names and resolve them. puredns drives massdns through a wordlist, resolves against a pool of public resolvers, filters wildcards, and re-checks every hit against a trusted resolver so a flaky answer never becomes a false positive.

bash
puredns bruteforce subdomains.txt vulnweb.com \
  -r resolvers.txt --resolvers-trusted resolvers.txt
output
A puredns brute force against vulnweb.com: banner, a configuration table, three completed progress bars, and three valid domains found - www, rest and localhost
Five thousand words resolved and validated. Three survive, and every one of them is a dictionary word

Three hits: www, rest, localhost. Every one is a plain word that was already in the list. Now look at what it missed: testphp, testasp, testaspnet, testhtml5, the actual applications on this domain, every one a live host that the passive pass and the probe below both see plainly. A wordlist cannot contain a name nobody put in it. So a brute force is really a statement about someone else's naming convention, and it goes quiet at exactly the point where the naming stops being conventional and starts being interesting.

This is the case for running passive first and permuting what it finds, instead of trusting a generic list to guess the names for you.

Probing live hosts with httpx

A name that resolves has an address. It does not follow that anything is listening there, or that what is listening is what you assume. httpx sends one request per host and reports what came back: status code, page title, and the stack it fingerprints.

These five names share one owner and, at the DNS layer, look nearly identical. They are five completely different applications.

bash
httpx -u testphp.vulnweb.com,testasp.vulnweb.com,testaspnet.vulnweb.com,rest.vulnweb.com,testhtml5.vulnweb.com \
      -status-code -title -tech-detect
output
httpx output: five vulnweb hosts, each 200, with different titles and detected stacks - Apache and PHP, IIS and ASP.NET, and an Invicti REST API
Same owner, same kind of DNS record, five different stacks. Nothing before this step could tell them apart

Every host returns 200, and no two are the same thing underneath: a PHP application on Apache, a classic ASP forum on Microsoft Internet Information Services (IIS), an ASP.NET application, an HTML5 site, an Invicti REST API. Resolution gave you five addresses. Only the probe tells you what actually runs there, on what stack, and therefore which one is worth your afternoon. One command, and the name list becomes a target list.

Every command from the wildcard test down sent a packet to a server, which is why they ran against vulnweb.com and the passive pass did not. That distinction is the last section, and it matters more than any command here.

Where passive enumeration is wrong

It hands you names that no longer resolve. The drop from the passive list to what actually answers is the whole story: on a long-lived domain with automated certificate issuance, expect well over half of the passive names to be dead on arrival.

It only sees names that were logged somewhere. An internal service behind a private certificate authority (CA), a plain-HTTP host that never got a certificate, a name that only ever appears as the target of a canonical name (CNAME) record: passive never sees any of them. A wildcard certificate is the worst case, one log entry, *.example.com, hiding every real name beneath it.

Its freshness is somebody else's decision. Passive DNS aggregators sample, and two of them queried on the same day return different sets. Neither is wrong; they are answering a slightly different question than the one you meant to ask. That is the argument for unioning several sources, which is exactly what subfinder does under the hood.

Where active enumeration is wrong

Your wordlist is a statement about someone else's naming convention. The brute force above is the proof: five thousand words found www, rest and localhost, and missed every testphp-style host on the domain. It finds api, staging and vpn because they are in every list. It never finds pf-edge-04 or tenant-7719 or jenkins-old-do-not-delete, and those are usually the ones worth finding. The fix is to permute the names you already have rather than lean on a generic list.

Resolvers change the answer. Public resolvers rate-limit, cache negatives aggressively, and some hand back an ad-server address for a name that does not exist. Run a brute force through a bad pool and you get false negatives from the rate limiting and false positives from the hijacking, in the same output, with nothing marking which is which. This is why puredns re-validates every hit against a trusted resolver, and why the dnsx run named its resolvers by hand. Check your resolvers against a known-good and a known-bad name before a long run, and again during it.

The full enumeration sequence, step by step

  1. Go passive first, and go wide. subfinder -d target unions thirty sources in one command. Resist the urge to throw away the odd-looking names; they are often the real ones.
  2. Test for wildcards at the apex and at every branch, before you trust a single positive.
  3. Resolve through named resolvers. dnsx -r 1.1.1.1,8.8.8.8, keeping the full response. Roughly half the passive list falls away here, and that is the point.
  4. Brute force, then permute. puredns bruteforce clears wildcards and validates hits, but a generic wordlist only finds conventional names. Seed the active pass with what resolved and permute those.
  5. Probe what survived. httpx -status-code -title -tech-detect. This is the step that turns a name list into a target list, and the only one that separates a live site from a dead record, or one stack from another.
  6. Diff against last time. The most valuable output of recon is not the list. It is what changed since the last run.

Step six is the one most methodologies skip and the one that pays off again and again. A first enumeration of a large domain gives you hundreds of names and no sense of which ones matter. The same enumeration a week later gives you eleven new ones, and those eleven are worth reading one at a time.

Authorisation, and choosing a target

The passive pass named tesla.com because certificate transparency and passive DNS are already public, and reading them touches nothing Tesla owns. Everything that sent a packet, the wildcard test, the brute force, the probe, ran against vulnweb.com, which Acunetix publishes specifically so people can point scanners at it.

That split is the whole point, not a formality. Passive data is public; acting on it is not automatically allowed. Active enumeration sends traffic to real infrastructure, and resolving, brute-forcing or probing a name is exactly that kind of traffic. Tesla runs a published bug bounty, so testing *.tesla.com inside its scope is authorized, but a scope is narrower than a whole domain, it names hosts that are explicitly off-limits, and "there is a bug bounty" is not the same sentence as "anything I do here is fine". When you do not have that authorization in writing, use a domain that exists to be tested. Get the scope, know which names are inside it, and know which ones belong to a shared-tenancy provider rather than your target. A finding outside the boundary is worth nothing, and it can cost you a great deal.