Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

subcred

A personal bug-bounty recon tool: it watches domains you add and continuously pulls in subdomain/IP intel from several passive, legal sources (see below), flags likely subdomain takeovers, automatically checks which subdomains are actually alive and what status code they return (httpx-style), and — only when you explicitly opt a domain in, optionally down to specific subdomains — port-scans, HTTP-fingerprints, screenshots, and runs real nuclei vulnerability templates against what it finds. A web dashboard (meant to be reached over your own Tailscale network) shows the results, a priority score and an evidence-based vulnerability gauge per subdomain, and lets you triage it all from your phone — plus an on-demand toolbox (curl/dig/whois/banner-grab) for one-off lookups against anything — with a push notification when something's actually worth a look.

Data sources

subcred deliberately does not crawl or scan the internet itself. Instead it queries services that already did their own (authorized) scanning/crawling, for domains you explicitly watch. All of these run automatically, on their own cadence, for every watched domain regardless of that domain's active_scan flag — none of them send traffic to the target itself:

Source What it does Default cadence
crt.sh New subdomains from public Certificate Transparency logs every 30 min
DNS brute-force Checks ~5,000 common subdomain names against public DNS (SecLists wordlist) weekly
Wayback Machine + Common Crawl Subdomains/paths found in existing web-archive/crawl datasets daily
GitHub code search Domain mentions in public repos (leaked subdomains, keys, config) daily, opt-in
ASN / IP-range (BGPView) The watched company's whole announced IP block, not just DNS names weekly
Shodan InternetDB Per-IP open ports/CPEs/known CVEs/tags — data Shodan already collected daily per IP
Subdomain takeover (passive tier) Walks the CNAME chain (dnspython); flags a dangling target (matches a known vulnerable-service suffix, or just fails to resolve at all) daily per subdomain

One deliberate exception to "none of them send traffic to the target": automatic reachability checking (httpx-style) — a single HEAD request per (subdomain, scheme), reading no body/title/server/tech, just "is something answering, what status code." This runs unconditionally for every subdomain of every watched domain, not gated behind active_scan or restricted by scan_scope — an explicit, discussed decision that a bare status-code check is a materially lighter touch than deep fingerprinting or port scanning. It's the primary at-a-glance triage view: reconctl reachable <domain> or the dashboard's Reachable column show every subdomain's https/http status side by side. Still gated by scanner.is_scannable_ip (won't touch a private/internal address regardless). Default cadence: daily per (subdomain, scheme).

One real bug found while dogfooding this, worth knowing about if your own results ever look wrong: the daemon picks one resolved IP per subdomain to check (not every IP a dual-stack host might have) — the original pick was arbitrary and briefly landed on an IPv6 address in an environment with no working outbound IPv6 route, reporting a domain that was genuinely up over IPv4 as "unreachable." Fixed to prefer IPv4 when both are available.

Takeover detection has a second, active tier (fetches the actual HTTP response body to confirm a match against a known "unclaimed page" fingerprint) that only runs when active_scan is on for that domain — see below.

GitHub search only runs if you set a token (it's free, public-repo scope is enough): generate one at https://github.com/settings/tokens, then either export SUBCRED_GITHUB_TOKEN=... before starting the service or add Environment=SUBCRED_GITHUB_TOKEN=... via systemctl --user edit subcred-daemon.service. Without it, GitHub search is silently skipped — that's expected, not a misconfiguration.

ASN/IP-range discovery is capped at ASN_MAX_IPS_PER_DOMAIN (default 500) total per domain, spent across however many prefixes that company announces — every IP found this way only ever goes through the passive Shodan lookup above, never through subcred's own port scanner. That boundary (ASN-derived IPs can't reach active scanning under any code path) was the single most scrutinized part of this feature during review.

DNS brute-force needs a wordlist on disk — it defaults to SecLists' subdomains-top1million-5000.txt (/usr/share/seclists/Discovery/DNS/...), which needs apt install seclists (or the Kali/parrot equivalent) if it isn't already present; if the file's missing, that source just logs a warning and skips itself rather than failing the whole sweep.

Read this before you enable active scanning

Everything by default is passive: crt.sh lookups are public Certificate Transparency data, and DNS resolution is a normal lookup — neither sends traffic to the target. Active scanning (port scan + HTTP fingerprint) does send traffic directly to a host, and is off by default for every domain you add. Only flip active_scan on (reconctl enable-scan <domain>, or the confirm-gated toggle in the /watchlist page) for domains you own or have explicit written authorization to test — e.g. an in-scope target in a bug bounty program you're enrolled in. This is on you; the tool gates the switch, it doesn't police what you point it at. Every scan (daemon cycle, manual poll, on-demand rescan) re-validates each resolved IP against scanner.is_scannable_ip immediately before touching it, rejecting private/ loopback/link-local/reserved ranges regardless of what a domain's DNS claims — so a misconfigured or rebound DNS answer can't trick it into scanning your own LAN.

Narrowing scope within a domain. active_scan is a whole-domain switch, but your actual bug bounty authorization is very often a specific list of assets, not "every subdomain that happens to resolve under this parent domain." Use reconctl scope add <subdomain> to restrict active scanning to only specific subdomains you've explicitly listed — leave it unrestricted (the default) and the whole domain stays in scope, exactly as before.

reconctl scope add app.example.com     # narrow to just this one (and any others you add)
reconctl scope list example.com        # see current restriction, or "whole domain in scope"
reconctl scope remove app.example.com  # drop one from the restricted list
reconctl scope clear example.com       # remove all restrictions, back to whole-domain

The same toggle exists per-subdomain on the /domain/<id> dashboard page. This restriction applies identically to all five active-touch behaviors (port scan, HTTP fingerprint, takeover confirmation, screenshots, nuclei) — a review pass caught one real bypass here worth knowing about: the dashboard's manual "Rescan" button initially ignored scope entirely and would actively touch a subdomain regardless of restriction; fixed and verified (an out-of-scope subdomain's rescan now correctly skips the active part, an in-scope one still works normally).

Four things are gated behind active_scan, all sharing that same re-checked-live gate and is_scannable_ip discipline, each on its own cadence (longer = heavier):

Feature What it does Default cadence
Port scan + HTTP fingerprint Connects to each resolved IP, checks a fixed port list, grabs title/server header daily
Takeover HTTP-confirmation Fetches the response body, matches against known "unclaimed service" fingerprints daily
Screenshot triage Headless Chromium (Playwright), full-page render, saved as a PNG per (subdomain, scheme) weekly
Nuclei vulnerability scan Real CVE/misconfiguration/exposure templates — see below weekly

Every one of these connects to the already-resolved, already-vetted IP directly and only ever uses the hostname for the HTTP Host header / TLS SNI — none of them let anything (a raw socket, Playwright's browser, or the nuclei subprocess) re-resolve the hostname itself. That matters: a second, independent DNS lookup could return a different address under DNS rebinding, bypassing the is_scannable_ip check entirely. This was verified empirically for all three mechanisms while building them (a local test server + a deliberately wrong/nonexistent hostname, confirming the pinned IP was the one actually contacted) — nuclei's version of this needed a second fix after review found that template-driven redirects bypass the pin: nuclei follows a Location header with its own HTTP client, doing real DNS resolution and dropping the pinned Host header on the follow-up request, with zero is_scannable_ip check on that second connection. Confirmed against 902 templates in the actually-configured scan scope that set redirects: true. Fixed by disabling redirect-following outright (-dr) — verified after the fix that a redirect to a second local test server received zero requests.

Real vulnerability scanning (nuclei)

The one feature here that sends genuinely test-shaped requests, not just a fingerprint GET — real CVE/misconfiguration/exposure checks via nuclei, reusing whatever template checkout you already have at ~/nuclei-templates. Deliberately scoped conservative:

  • Template directories: http/cves/, http/vulnerabilities/, http/misconfiguration/, http/exposures/, http/exposed-panels/, ssl/ — HTTP/TLS categories only, ~4,100+ CVE templates alone. Configurable via SUBCRED_NUCLEI_TEMPLATE_DIRS, but any entry that would resolve outside the templates root (an absolute path, a ../ escape — checked after a review found the naive version of this silently vulnerable to exactly that) is rejected and logged rather than run.
  • Tags dos,intrusive,fuzz excluded by default (SUBCRED_NUCLEI_EXCLUDE_TAGS) — denial-of-service and deliberately aggressive templates stay out of scope.
  • Rate-limited to 30 req/s by default (SUBCRED_NUCLEI_RATE_LIMIT, nuclei's own default is 150) and capped at NUCLEI_TIMEOUT_SEC (180s default) per subdomain-scan.

Real timing tradeoff worth knowing: 30 req/s × several thousand templates in the default scope means a single subdomain's scan can legitimately approach or exceed that 180s budget even under good network conditions — confirmed directly while testing (a scan against a live target hit the timeout on its first attempt). A scan that times out is not recorded as clean — it just stays due and retries next cycle — but if you want scans to reliably finish, raise SUBCRED_NUCLEI_TIMEOUT_SEC, raise the rate limit, or narrow SUBCRED_NUCLEI_TEMPLATE_DIRS. nuclei itself must be on PATH (or set SUBCRED_NUCLEI_BIN) — it's a separate system binary, not a pip package; this machine already has it via the existing ~/nuclei-templates checkout.

A newly-appearing critical/high nuclei finding (a template match that wasn't there on the previous scan) triggers a push notification, same as a new takeover candidate.

JS endpoint/secret mining

Also gated behind active_scan + scan_scope, on its own weekly cadence: fetches a subdomain's page and its same-origin <script src> JavaScript files, then mines the JS text for two things — hardcoded API endpoints (paths referenced in frontend code that no DNS-based enumeration would ever surface, since they're just routes on a host you already know about) and credential-shaped strings (AWS keys, generic API keys, JWTs, Slack tokens, Google API keys, PEM private-key headers).

Found secrets are never stored or displayed in full, anywhere, by design — not a partial implementation. Every regex match is hashed (sha256, for dedup — "is this the same secret as last time" without retaining it) and masked (a few leading/trailing characters, fixed-length asterisks in between so even the true length isn't revealed) in the same breath it's matched; the raw value never gets assigned to a variable that outlives that single expression, so it can't leak into the database, a log line, an exception message, or a push notification even by accident. This was the single most scrutinized property of this whole feature — verified with planted fake secrets, grepping the actual database and log files afterward to confirm the raw value is genuinely absent from both. Third-party CDN-hosted scripts are skipped entirely (same-origin only) — they won't contain target-specific secrets, and fetching them would touch infrastructure that was never part of the authorization anyway. A newly- found secret (not a repeat of one already on record) triggers a push notification containing only the type and masked value.

Push notifications (ntfy.sh)

Fully passive from the target's side — this only ever talks to ntfy.sh (or a self-hosted instance via SUBCRED_NTFY_SERVER), never the watched domain. One consolidated notification per sweep tick (never one per event) whenever something notable happened: new subdomains, a new takeover candidate, a newly-grown InternetDB CVE list, or a new critical/high nuclei finding.

The topic name is auto-generated and persisted at ~/.local/share/subcred/ntfy_topic on first use — treat it like a secret: on the public ntfy.sh server, anyone who knows the topic name can subscribe and read your notifications. The dashboard's /status page shows it partially masked; a review pass caught an earlier version of that masking that revealed a complete short custom topic (anything ≤10 characters set via SUBCRED_NTFY_TOPIC) verbatim behind a misleading "…" — fixed to never reveal more than a few leading characters regardless of topic length.

Test your setup any time with:

reconctl notify-test

Priority score and vulnerability gauge — two different things

  • Priority (priority.score()): an operator-interestingness heuristic — combines takeover status, InternetDB CVEs/tags, open ports, and your own triage tag into a high/medium/low tier. An educated guess, not a claim.
  • Vulnerability gauge (priority.vuln_gauge()): evidence-based, driven by actual nuclei findings and InternetDB CVEs — critical/high/medium/low/ clean (nuclei ran, found nothing) or unscanned (never ran — active_scan is off, or it just hasn't come due yet). Deliberately not a fabricated percentage — every level names the real finding behind it, and "clean" vs. "unscanned" are shown as visually distinct states so a genuine clean scan is never mistaken for "we don't know."

On-demand recon toolbox

A /toolbox page (and matching reconctl curl/dig/whois/banner commands) for one-off manual lookups against any hostname or IP you type — deliberately not restricted to the watchlist or scan_scope, since these are ad-hoc checks you make your own authorization judgment about, the same as running a terminal directly:

  • curl — real curl binary, any method/custom headers, full raw response. Pinned to the resolved IP via --resolve so curl never does its own (unvetted) DNS lookup, and redirects are never followed (no -L) — the exact bypass class that hit nuclei (below) doesn't apply here because it's simply never given the chance to.
  • dig — real dig binary, pick a record type, see the raw answer.
  • whois — no whois binary is installed, so this is a small self-contained two-hop WHOIS client (query IANA for the TLD's registry referral, then query that registry directly) — no new system dependency.
  • Banner grabncat isn't installed either, and its full feature set (listener mode, exec mode) is more than this needs; a plain Python socket connect-and-read instead, optionally sending a payload after connecting.

curl and banner-grab still refuse private/loopback/link-local/reserved addresses (scanner.is_scannable_ip, the same gate used everywhere else) — this page is reachable from your phone over Tailscale, and an unrestricted "fetch any address" tool would let a leaked auth token pivot into this machine's own local network. dig and whois need no such gate: they're protocol lookups against public infrastructure, not a fetch of arbitrary target content. Nothing here is persisted to the database — results are shown once and gone, the same as running the command yourself.

Two real bugs review found and fixed while building this: a literal IPv6 URL host (e.g. http://[2606:...]/) broke curl's --resolve pinning syntax entirely (fails closed, not a security issue, but every such request errored out) — fixed by skipping the pin when the host is already a literal IP, since there's nothing left to resolve; and the whois/banner-grab read loops only bounded each individual read call, not the overall wall-clock time, so a deliberately slow peer trickling data could hold a request open far longer than the configured timeout — fixed with an actual deadline.

Real limits — what this is and isn't

  • Not a "complete hacking DB." crt.sh only knows about hostnames that got a certificate logged publicly. It misses anything HTTPS-only-internal or that never got a public cert. It's one strong, free, passive signal — not omniscient.
  • crt.sh is unreliable. It's a community-run, single-operator service with no published rate-limit or uptime guarantee, and it does go down or time out (confirmed first-hand while building this — a plain fetch of crt.sh's homepage timed out completely at one point, while the rest of the internet worked fine). The client retries 3x with exponential backoff, then gives up cleanly and logs it — "nothing new this cycle" is the normal outcome, not a bug to chase.
  • "24/7" depends on this machine staying up. The daemon and dashboard only run while this host is powered on and not suspended. Both come back automatically (systemd Restart=on-failure handles the daemon; the dashboard is just stateless Flask) but there's a real gap in coverage while the machine is asleep — a subdomain could theoretically appear and disappear in that window unseen.
  • A single sweep can take a while if third parties are slow. Every source retries with backoff before giving up (observed first-hand: crt.sh 502s, Wayback timeouts, and a transient BGPView DNS failure all happened in the same test run) — that's correct, patient behavior, not a hang, but it does mean one sweep cycle can stretch to several minutes if multiple services are having a bad day simultaneously. The daemon just moves on to the next tick after; nothing blocks on it long-term.
  • DNS brute-force and GitHub search return unvalidated candidates. Every hostname either of them turns up gets DNS-resolved before it's ever trusted as a real subdomain — GitHub search in particular is just regex matching over public code/text, so plenty of its raw hits won't resolve to anything and are silently discarded, exactly as intended.
  • Storage is trivial for the database, not for the tooling. DB rows are short text (hostnames, IPs, timestamps, headers) — tens of MB even under heavy long-term use, measured directly (~1.3MB per heavily-populated domain). The log self-rotates so it can't grow unbounded. The one real disk cost is Playwright's Chromium download for screenshots — ~650MB, one-time, for the screenshot feature alone. Not a lot on a modern disk, but nothing like the KB/MB scale of everything else here.
  • Screenshots are transient, not a steady background cost. A capture is ~0.75s and peaks around 156MB of RAM — but that's only during the capture itself; Chromium launches fresh per screenshot and exits, it's not a resident process.
  • No mass scanning. This only ever touches domains you explicitly add. There's no discovery-of-arbitrary-hosts mode and none is planned.

Layout

subcred/
  config.py               # paths, intervals, ports list, web token, ntfy topic
  db.py                    # SQLite schema + shared upsert helpers
  crtsh.py                 # crt.sh client (retry/backoff, never raises)
  dnsbrute.py              # DNS brute-force against a SecLists wordlist
  history.py               # Wayback CDX + Common Crawl index clients
  github_search.py         # GitHub code search client (opt-in, needs a token)
  asn.py                   # BGPView ASN/IP-range lookup + pure CIDR math
  internetdb.py            # Shodan InternetDB client (passive per-IP lookup)
  takeover.py              # subdomain takeover: passive DNS tier + active HTTP-confirmation tier
  takeover_fingerprints.py # static data extracted once from ~/nuclei-templates -- no runtime nuclei dep
  screenshot.py            # Playwright/Chromium screenshot capture, DNS-pinned
  nuclei.py                # real nuclei CVE/misconfig scan, DNS-pinned, conservative scope
  ntfy.py                  # ntfy.sh push notification client
  priority.py              # priority.score() heuristic + vuln_gauge() evidence-based tier
  jsintel.py                # JS endpoint/secret mining -- masks+hashes secrets at the source, never stores raw
  toolbox.py               # on-demand curl/dig/whois/banner-grab, freeform targets, not persisted
  scanner.py               # DNS resolve, is_scannable_ip guard, port scan, HTTP fingerprint, reachability check
  daemon.py                # the 24/7 sweep loop -- schedules all of the above
  cli.py                   # `reconctl` — watchlist management from the terminal
  webapp.py                # Flask dashboard + JSON API
  templates/               # dashboard HTML (Jinja, autoescaped)
systemd/                   # --user unit files (staged by install.sh, not enabled by it)
install.sh                 # venv + deps + Chromium download + systemd unit staging -- never enables/starts anything
requirements.txt

Setup

Clone this anywhere -- install.sh and the systemd units are self-locating (no hardcoded path/username), so it works the same regardless of where you put it or which machine it's on.

./install.sh

This creates .venv, installs Flask/waitress/Playwright/dnspython into it, downloads Chromium for Playwright (~650MB, one-time), stages the two systemd --user unit files into ~/.config/systemd/user/, and runs systemctl --user daemon-reload. It does not enable, start, or expose anything — it prints the exact next commands instead. It does not install nuclei itself (a separate system binary, not a pip package) — if it's not already on PATH, nuclei vulnerability scanning just logs a warning and skips itself, same graceful-degradation pattern as every other missing-dependency case here.

systemctl --user enable --now subcred-daemon.service
systemctl --user enable --now subcred-web.service

Check on them with:

systemctl --user status subcred-daemon.service subcred-web.service
journalctl --user -u subcred-daemon.service -u subcred-web.service -f

The dashboard's shared-secret auth token is generated on first access and persisted (owner-read-only, 0600) at ~/.local/share/subcred/web_token. You'll need it to log in.

Remote access over Tailscale

The web app binds only to 127.0.0.1:5900 — it has no awareness of Tailscale and can't be made to bind anywhere routable. Once subcred-web.service is running, expose it to your tailnet (and only your tailnet) with:

tailscale serve --bg 5900

That reverse-proxies a tailnet-only HTTPS endpoint (with a real cert, via Tailscale's own cert system) to the local port. Reachable from any device on your tailnet — your phone, the Fedora box, the Windows PC — nothing else. Do not use tailscale serve --set-path with funneltailscale funnel exposes a service to the public internet, which is not what you want here. Check what's currently exposed with tailscale serve status; tear it down with tailscale serve --https=443 off (or whatever port it reports).

One debugging note: the auth cookie is set with the Secure flag, since the only intended access path is HTTPS via tailscale serve. If you hit the app directly over plain http://127.0.0.1:5900 (bypassing tailscale serve, e.g. while poking at it locally), a real browser will silently refuse to store/send that cookie and login will appear to loop back to /login. That's expected friction flagging an off-path access method, not a bug.

CLI (reconctl)

source .venv/bin/activate   # or prefix commands with .venv/bin/python3 -m
export PYTHONPATH=$(pwd)

reconctl add example.com               # passive-only by default
reconctl add example.com --active-scan # opt in to active scanning immediately — only for authorized targets
reconctl enable-scan example.com       # flip active_scan on for an existing entry
reconctl disable-scan example.com
reconctl list                          # every watched domain + counts + poll times
reconctl subdomains example.com        # everything known for one domain
reconctl new --since 24h               # the money command: what's new, across every watched domain
reconctl scope add app.example.com     # narrow active scanning to specific subdomains (default: whole domain)
reconctl scope list example.com
reconctl takeovers                     # every currently-flagged subdomain takeover candidate, watchlist-wide
reconctl vulns                         # every subdomain with a current nuclei finding, highest severity first
reconctl reachable example.com         # httpx-style: https/http status code per subdomain, at a glance
reconctl secrets                       # every masked-only secret found in mined JS, watchlist-wide
reconctl endpoints                     # every API endpoint mined from JS, watchlist-wide
reconctl notify-test                   # send a test ntfy push to verify your notification setup
reconctl report example.com            # full dump: subdomains + IPs + ports + HTTP fingerprints + InternetDB + ASN ranges + takeover + nuclei + priority/vuln gauge
reconctl asn example.com               # just the discovered ASN/IP ranges + how many IPs checked so far
reconctl export example.com --format csv   # or json, to stdout
reconctl remove example.com            # cascades, deletes all its recon data
reconctl set-password                  # change the dashboard login (prompts interactively; takes effect immediately, no restart)

# on-demand toolbox -- any hostname/IP, not tied to the watchlist
reconctl curl https://example.com --method GET --header "X-Test: 1"
reconctl dig example.com --type MX
reconctl whois example.com
reconctl banner example.com 22         # SSH banner grab, or any TCP port

Run a single sweep manually (useful for testing, or as a cron fallback instead of the daemon):

python3 -m subcred.daemon --once

Web dashboard pages

  • / — the live feed: every subdomain across your whole watchlist, newest first, with a "new since you last checked" divider and filter chips.
  • /watchlist — add/remove domains, toggle active scanning (confirm-gated), force an out-of-cycle passive poll.
  • /domain/<id> — full sortable/searchable subdomain table for one domain (sortable by priority too), each row tagged with its discovery source, reachability badge (https/http status at a glance), InternetDB findings, and a vulnerability-gauge badge, plus an "ASN / IP ranges" section for that company's announced IP blocks. A takeover candidates banner appears above the table whenever one exists.
  • /subdomain/<id> — full history for one hostname (DNS, ports, HTTP fingerprints, reachability detail, InternetDB findings, takeover check, nuclei findings, screenshot, mined JS endpoints/secrets — masked values only) plus an on-demand rescan button and a triage tag/notes editor.
  • /screenshot/<id>/<scheme> — serves a captured screenshot PNG (same auth as every other route; scheme checked against a fixed allow-list before any filesystem access).
  • /status — daemon health: is it actually running, is any domain overdue for a poll, DB size, log tail, whether notifications are enabled and a masked ntfy server/topic.
  • /toolbox — on-demand curl/dig/whois/banner-grab against any hostname/IP, not tied to the watchlist. Nothing here is persisted.

Every page and API route requires the token from web_token (cookie after login, or an X-Auth-Token header for scripting/curl). State-changing requests additionally require the header even with a valid cookie present — that's the CSRF defense, since a forged cross-site form can only ever carry the ambient cookie, never a custom header.

What was security-reviewed

Every feature here went through the same pattern — design → parallel implementation → integration → an adversarial review pass — with findings independently verified (not just self-reported) and fixed. Baseline coverage across every module: XSS (every scraped/user field — subdomain names, HTTP titles, server headers, notes, InternetDB findings, ASN org names, takeover evidence, nuclei finding names — confirmed escaped via Jinja autoescaping, spot-checked by hand with deliberately hostile payloads), CSRF (cookie-only mutation attempts confirmed rejected against a live running instance), SQL parameterization across every module, and the active-scan is_scannable_ip gate re-verified for each new active-touch feature as it was added (port scan/fingerprint → takeover HTTP-confirmation → screenshots → nuclei).

Real findings from those reviews, since fixed and re-verified:

  • The 5 enrichment-source clients (crt.sh/dnsbrute/history/GitHub/ASN) — 3 of 5 could crash a sweep on a malformed third-party response instead of degrading gracefully; ASN_MAX_IPS_PER_DOMAIN was silently multiplied per announced prefix instead of being a true per-domain budget.
  • Screenshots — the daemon's active_scan re-check (present for the port scanner) was missing for screenshot triage, so a mid-sweep toggle-off wasn't honored for that feature specifically.
  • Notifications — ntfy.notify()'s "never raises" contract had a gap (an unguarded disk write on first use), and the /status page's topic-masking revealed a complete short custom topic verbatim behind a misleading "…" instead of actually masking it.
  • Takeover detection — the passive DNS-only recheck could silently downgrade an already-confirmed finding back to "possible," which then made the active tier re-report the same already-known takeover as "newly confirmed" every cycle, indefinitely — the exact steady-state case a notification system most needs to stay quiet for. Also two raw-socket file-descriptor leaks on TLS handshake failure (one in takeover.py, one inherited from the pre-existing scanner.py).
  • Nuclei — the most scrutinized addition. The DNS-pinning approach (target the vetted IP directly, hostname only for Host header/SNI) was verified empirically, but review found that nuclei's own redirect-following bypasses that pin entirely — a template hitting a redirect makes nuclei do a second, real DNS-resolved connection with the pinned Host header dropped, zero is_scannable_ip check, against 902 templates in the actually-configured scan scope. Fixed with -dr (disable redirects), re-verified with an escalating test (a redirect to a second local server received zero requests once fixed). Also found and fixed: a duplicate template_id from a single scan could raise an uncaught IntegrityError that silently discarded a real finding and left the subdomain permanently re-crashing; and template-directory paths weren't checked against escaping the configured root via ../ or an absolute path.
  • scan_scope — after adding fine-grained scope restriction, review found the dashboard's manual "Rescan" button was a complete bypass of it, active-scanning a subdomain regardless of whether it was excluded.
  • The on-demand toolbox — a literal IPv6 URL host broke curl's --resolve pinning syntax outright (failed closed, not a security gap, but every such request errored); a tempdir-creation failure could escape curl's "never raises" contract; no disk-write cap during download; and the whois/banner-grab read loops only bounded individual reads, not overall wall-clock time, letting a slow-drip peer hold a connection open indefinitely.
  • Reachability checking — not a security bug, but a real correctness one caught by my own live testing rather than the automated review: the IP-selection logic was non-deterministic and could pick an IPv6 address in an environment with no outbound IPv6 route, reporting a genuinely-up domain as "unreachable." Fixed to prefer IPv4.
  • JS endpoint/secret mining — the most heavily scrutinized addition (5 separate review passes on the raw-secret-never-leaks property alone, plus my own independent test with planted fake secrets grepping the actual database/log files afterward) came back clean on every angle.

This is a personal tool holding live bug-bounty recon data. The code is public on GitHub (see above); the data never is — it lives entirely outside this repo, in ~/.local/share/subcred, which is .gitignored and was never part of the project directory to begin with.

About

Personal bug-bounty recon: passive subdomain discovery, takeover detection, reachability checks, and gated active scanning (port scan, HTTP fingerprint, screenshots, real nuclei vuln scans) with a web dashboard + CLI.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages