✦ H3LL0 W0RLD - HACK THE PLANET - FOLLOW THE WHITE RABBIT ✦
← back to projects
Security● online

recon-chain

node://recon-chain

Individually, recon-ng, httpx and nuclei each do one job well. Chained, they answer a question none of them answers alone: given a domain, what is actually exposed right now? recon-ng finds what exists, httpx works out what is alive, and nuclei tells you what is wrong with it.

The interesting engineering is not in any of the three tools. It is in the seams between them — the filtering, the boundary crossing, and getting three different output formats to end up in one report.

The line down the middle

Everything recon-ng does is passive. It asks third parties — certificate logs, DNS, WHOIS, breach indexes — about a target, and the target never hears about it. httpx and nuclei are the opposite: they send live traffic to hosts and read what comes back.

That difference is not a detail, it is the whole reason the pipeline has a seam there instead of running end to end. Authorization to research a domain is not authorization to scan it. So the chain deliberately stops, and the active half requires its own explicit go-ahead — a separate confirmation, not an inherited one.

Everything below that line is opt-in, and nothing in the passive half depends on it.

Never trust the host list

The first version of this fed recon-ng's hosts.csv straight into the scanner. That was wrong in a way worth describing, because it is the kind of wrong that does not look wrong.

recon-ng's IP-pivot modules find hosts by asking "what else lives at this address?" On shared infrastructure, the answer is everyone. On one target, hosts.csv came back with a thousand rows, and 999 of them were unrelated domains sharing a Cloudflare edge IP. Exactly one row was the target.

Point an active scanner at that list and you are not scanning your client. You are scanning 999 strangers.

So there is a filter between the halves, and it is the most important stage in the chain despite doing nothing clever. Keep a host only if its registered domain actually matches the target:

python
kept = sorted(h for h in hosts if h == target or h.endswith("." + target))

Blunt, and deliberately so. Brand-alias domains on other TLDs get dropped too, because ownership cannot be confirmed from DNS alone. Those are reported to the operator as found, not scanned rather than silently discarded — the operator can confirm ownership and add them back, which is a decision a filter has no business making.

httpx earns its place in the middle

The obvious question is why httpx is there at all, when nuclei can take hostnames directly. Two reasons, both learned by doing it the slow way.

It resolves scheme. A hostname is not a target — http:// and https:// are different services, and guessing wrong wastes a probe on every host. And it prunes the dead: a list from certificate logs is full of names that resolved years ago and answer nothing today. Handing those to nuclei means every one of them burns a full template run before timing out.

bash
httpx -l nuclei_targets.txt -timeout 5 -retries 1 -silent -o live_hosts.txt < /dev/null

Two details in that line are scar tissue. -timeout 5 -retries 1 stops one unresponsive host from stalling the batch. And < /dev/null is there because httpx hung indefinitely when run in the background without stdin explicitly redirected — it sits waiting for input that will never arrive, and looks exactly like a slow scan.

There is also a trap in the name itself. On this machine, typing bare httpx runs Python's HTTP client, not ProjectDiscovery's recon tool — the Python one lands earlier on PATH. Same command name, entirely different program, no error. Every invocation in the pipeline uses the absolute path for that reason alone.

Scope the templates or wait all day

nuclei ships with roughly 8,000 templates and will happily run all of them. Against just twelve hosts, the unscoped default took over six minutes. That does not scale to a real host list, and worse, it buries anything interesting in noise.

bash
nuclei -l live_hosts.txt \
  -tags tech,cve,exposure,misconfig,default-login \
  -jsonl -o nuclei_results.jsonl < /dev/null

Five tags, chosen because they are the high-signal ones for an external posture check: what is running, known vulnerabilities, exposed files, misconfiguration, default credentials. Severity filtering (-s critical,high,medium) trims further when a list is large. JSONL out, because the next stage has to parse it.

One report, not three

This is the part I would point at. A pipeline that ends with three output files in three formats has not really chained anything — it has just run three tools in a row and left the correlation to a human at 2am.

So nuclei's findings get merged back into recon-ng's own vulnerabilities table, and the reports are regenerated from there. One HTML report, containing both the passive enumeration and the active findings, sorted together.

The merge is where the sharp edges live:

  • Idempotent by construction. Every inserted row is tagged [nuclei:<template-id>], and the merge deletes previously tagged rows for that workspace before inserting. Re-running after a follow-up scan replaces rather than duplicates.
  • That tagging is not cosmetic. recon-ng's db insert forces the module column to user_defined regardless of source, so the tag is the only thing that makes a nuclei-sourced row identifiable later.
  • Insert-or-ignore will eat your updates. The table has a uniqueness constraint on host plus reference, and a second insert with the same reference but richer notes is silently discarded, not merged. Delete-then-insert is the only version that actually updates.
  • Real CVE IDs where they exist. The reference is nuclei's info.classification.cve-id when the template matched a CVE, falling back to the template ID otherwise, so findings stay cross-referenceable.

Everything nuclei emits is treated as untrusted input, because it is: the values come from whatever a remote server chose to send back. They go through argv rather than a shell, and characters that could break out of a delimited insert are stripped. A scanner's output is attacker-influenced data, and a pipeline that string-interpolates it into a command is one hostile banner away from a bad day.

Where it stands

The chain works end to end and is what actually runs on authorized engagements. It lives as step 7 of the recon-ng driver skill rather than as a standalone binary, which is honest about what it is: a documented sequence with one Python script doing the merge, not a product.

The parts I would still change: the eTLD+1 filter is string matching rather than a real public-suffix list, which is correct for the common case and wrong for domains under multi-part suffixes. And the passive and active halves write to different places before the merge pulls them together, which is one more moving part than it needs.

Everything it finds is written outside the repository, and none of it has ever been published anywhere.