subseeker is a single-file, multi-mode DNS reconnaissance tool. It speaks DNS
through dnspython, which — unlike a
getaddrinfo-style call — keeps full visibility into CNAME chains, multi-record
answers, arbitrary record types, choice of resolver, and its own
timeouts/retries. It also has a Certificate Transparency mode that pulls
hostnames from CT logs, surfacing names that were never discoverable by querying
the apex or guessing with a generic wordlist.
Authorized use only. Every mode performs active DNS reconnaissance. Run it only against domains and networks you own or have explicit permission to assess. Zone-transfer, cache-snooping, and CT probes in particular can be disruptive or considered hostile if run without authorization.
This is the Python port of the original Zig implementation. The Python version
trades the zero-dependency static binary for one dependency (dnspython) and
the huge simplification that HTTPS/TLS — needed for CT log fetching — is a
solved problem in the standard library.
- Python 3.9+
- dnspython (
pip install dnspython)
pip install -r requirements.txt # or: pip install dnspython
chmod +x subseeker.py
./subseeker.py --helpNo build step — it is a single script. Drop it anywhere on your PATH if you
want it available as subseeker.
subseeker <mode> [options]
| Mode | What it does |
|---|---|
std |
General records (SOA, NS, A, AAAA, MX, TXT) + an automatic zone-transfer try |
brt |
Brute-force subdomains from a wordlist (A/AAAA/CNAME) with wildcard detection |
axfr |
Attempt a full zone transfer (AXFR) against every nameserver |
srv |
Enumerate common SRV service records under the domain |
tld |
Top-level-domain expansion: try the domain's base name across many TLDs |
ptr |
Reverse-lookup (PTR) an IPv4 range or CIDR |
snoop |
DNS cache snooping (RD=0): ask a resolver whether hosts are cached |
ct |
Certificate Transparency: pull names from CT logs (certspotter + crt.sh) |
| Flag | Meaning |
|---|---|
-d, --domain <domain> |
Target domain (std/brt/axfr/srv/tld/ct) |
-w, --wordlist <file> |
Wordlist of labels (brt) or full hostnames (snoop) |
--range <cidr|a-b> |
IPv4 range or CIDR for ptr (e.g. 192.0.2.0/24, a.b.c.d-e.f.g.h) |
-r, --resolver <ip> |
IPv4 resolver, repeatable and rotated. snoop uses the first. |
-t, --threads <n> |
Concurrent workers (default 50) |
--timeout <ms> |
Per-query timeout in milliseconds (default 3000) |
--retries <n> |
Retries per query on failure (default 2) |
-o, --output <fmt> |
Output format: text (default) or json (NDJSON) |
--outfile <file> |
Also write results to a file, in the chosen format |
-6, --ipv6 |
Also query AAAA where relevant (brt/tld/ct) |
--no-wildcard |
Disable wildcard detection (brt) |
--show-wildcard |
Report wildcard hits instead of suppressing them (brt) |
--resolve |
Resolve CT names through the worker pool (ct) |
--infile <file> |
Read crt.sh JSON from a file instead of the network (ct) |
--fetch |
Force a network CT fetch even when stdin is a pipe (ct) |
-v, --verbose |
Verbose diagnostics on stderr |
-h, --help |
Show help |
Default resolvers (when no -r is given): 1.1.1.1, 8.8.8.8, 9.9.9.9,
8.8.4.4.
The banner and live progress go to stderr; results go to stdout, so
-o json on stdout stays a clean, pipeable NDJSON stream.
Every scanning mode (brt, srv, tld, ptr, snoop, and ct --resolve)
runs its lookups through a thread pool — up to --threads in flight at once
(default 50). DNS is I/O-bound and Python releases the GIL while waiting on the
network, so the workers genuinely overlap.
Effective concurrency is also capped by your resolvers: all workers rotate
across the resolvers you give (four public ones by default), so very high
--threads against public resolvers can trigger rate-limiting, timeouts, and
retries that make a scan slower. For large wordlists, prefer pointing -r at
resolvers that tolerate volume over simply raising -t.
CT logs record every TLS certificate ever issued, which leaks hostnames that
plain DNS queries never reveal. ct reads them from two sources and merges the
results, so a single flaky provider can't sink the run:
- certspotter (
api.certspotter.com) — queried first; cleaner and more reliable. SetCERTSPOTTER_TOKENin your environment to raise the anonymous rate limit. - crt.sh — queried second; frequently returns 502s/timeouts, which is tolerated because certspotter usually already answered.
Names are stripped of wildcards (*.ei.example.com → ei.example.com),
lowercased, filtered to the target domain, and deduplicated across both sources.
It runs as a single command with no cache file to manage:
./subseeker.py ct -d example.com # list names from CT logs
./subseeker.py ct -d example.com --resolve # list + resolve which still answer
./subseeker.py ct -d example.com --resolve -o jsonText output is one bare hostname per line — a clean wordlist you can pipe straight into other modes:
./subseeker.py ct -d example.com | ./subseeker.py snoop -w /dev/stdin -r 8.8.8.8Offline / cached input is still supported for reproducibility or when both
sources are down — feed crt.sh's output=json in via a file or stdin:
curl -s 'https://crt.sh/?q=%25.example.com&output=json' > ct.json
./subseeker.py ct -d example.com --infile ct.json --resolveText (default) is one tab-separated record per line:
example.com A 192.0.2.10
www.example.com CNAME example.com.
--output json emits NDJSON — one JSON object per line, uniform across every
mode (name, type, value, plus an optional wildcard: true on brute-mode
wildcard hits):
{"name":"www.example.com","type":"A","value":"192.0.2.10"}
{"name":"dev.example.com","type":"A","value":"192.0.2.11","wildcard":true}NDJSON streams as results land. To collect it into one JSON document, slurp with
jq:
./subseeker.py brt -d example.com -w words.txt -o json | jq -s .# General records + zone-transfer attempt
./subseeker.py std -d example.com
# Brute-force subdomains, save NDJSON to a file
./subseeker.py brt -d example.com -w words.txt -o json --outfile found.json
# Zone transfer against every NS (a SUCCESS here is a finding to fix)
./subseeker.py axfr -d example.com
# SRV service discovery, piped through jq
./subseeker.py srv -d example.com -o json | jq .
# TLD expansion of the base name
./subseeker.py tld -d example.com
# Reverse DNS over a CIDR
./subseeker.py ptr --range 192.0.2.0/24
# Cache snooping against a specific resolver
./subseeker.py snoop -w hosts.txt -r 8.8.8.8
# Certificate Transparency, native, no cache
./subseeker.py ct -d example.com --resolveWordlists are plain text, one entry per line; blank lines and lines starting
with # are ignored. For brt each line is a label (e.g. www, dev); for
snoop each line is a full hostname.
- TLD and SRV lists are representative subsets, not the full IANA/service
registries. They are plain lists near the top of
subseeker.py(TLD_LIST,SRV_LIST) — add entries freely. ptrranges are capped at 65,536 addresses (a/16) to avoid accidental huge scans. WidenPTR_CAPif you need to.snoopis a point-in-time probe. An empty result usually just means the name isn't currently cached by that resolver, not that the mode failed. Large shared resolvers are anycast, so different backends hold different caches.- crt.sh is flaky. That is why
ctqueries certspotter first and only falls back to crt.sh; if both are down the run reports it and you simply retry.
MIT License