DEV Community

ULNIT
ULNIT

Posted on

How I Automated My Bug Bounty Recon Pipeline With Python on a Raspberry Pi

Bug bounty hunting has a dirty secret: 80% of the work is reconnaissance, and most of it is boring, repetitive, and perfect for automation. While other hunters manually click through subdomain lists, you can have a Python pipeline doing the grunt work 24/7 — and wake up to a fresh list of interesting targets every morning.

In this tutorial, I'll walk you through the recon pipeline I run on a Raspberry Pi, and how a few hundred lines of Python turned a weekend of manual work into a daily automated sweep.

The Problem With Manual Recon

A typical recon session looks like this:

  1. Run subfinder / amass to enumerate subdomains
  2. Probe which hosts are actually alive
  3. Grab HTTP titles, status codes, and TLS certs
  4. Fingerprint the tech stack (is it WordPress? An S3 bucket? An admin panel?)
  5. Stare at the results and pick something juicy

Steps 1–4 are pure mechanics. If you're doing them by hand every day, you're losing hours — and worse, you're inconsistent. Automation fixes both.

Step 1: Subdomain Enumeration on a Schedule

I keep a target list in targets.txt and let subfinder chew through it on a cron schedule:

# crontab -e
0 3 * * * subfinder -dL /home/pi/bbrecon/targets.txt -o /home/pi/bbrecon/data/subs_$(date +\%F).txt -silent
Enter fullscreen mode Exit fullscreen mode

Running it at 3 AM means fresh results are ready with my morning coffee. On a Raspberry Pi 4 this is completely fine — subfinder is mostly network-bound, not CPU-bound.

Step 2: Probe for Live Hosts

Enumerated subdomains are mostly noise. The signal comes from probing. I use httpx for this, but here's the core idea in pure Python so you can extend it:

import asyncio
import aiohttp

async def probe(session, url):
    try:
        async with session.head(url, timeout=aiohttp.ClientTimeout(total=8),
                                ssl=False, allow_redirects=True) as r:
            return {"url": url, "status": r.status, "final": str(r.url)}
    except Exception:
        return None

async def main(subdomains):
    async with aiohttp.ClientSession() as session:
        tasks = [probe(session, f"https://{s}") for s in subdomains]
        results = await asyncio.gather(*tasks)
    return [r for r in results if r]

alive = asyncio.run(main(open("subs_today.txt").read().splitlines()))
Enter fullscreen mode Exit fullscreen mode

Concurrency matters here — 50 parallel workers will sweep 10,000 subdomains in a few minutes.

Step 3: Fingerprint What Matters

A live host isn't interesting by itself. You want to know what it is. For each alive URL, I fetch the page and extract:

  • Page title — titles like "Dashboard", "Admin", or "Welcome to nginx" are instant triage
  • Server header — outdated or unusual servers are worth a look
  • Known signatureswp-content means WordPress, /_next/ means Next.js, X-Amz-Bucket means S3
INTERESTING = ["admin", "dashboard", "login", "staging", "dev.", "internal", "jenkins", "grafana"]

def triage(result, html, headers):
    flags = []
    title = extract_title(html)
    if any(k in title.lower() for k in INTERESTING):
        flags.append("interesting-title")
    if headers.get("Server", "").startswith(("nginx/1.1", "Apache/2.2")):
        flags.append("old-server")
    if "wp-content" in html:
        flags.append("wordpress")
    return flags
Enter fullscreen mode Exit fullscreen mode

Everything gets written to a JSON-lines file, one entry per host, so I can query it later with jq:

jq -c 'select(.flags | contains(["interesting-title"]))' results.jsonl
Enter fullscreen mode Exit fullscreen mode

Step 4: Alerts, Not Dashboards

Don't build a dashboard. Build a filter that messages you. My pipeline diffs today's results against yesterday's and sends a Telegram message for anything new — a new subdomain, a newly open port, a title change. New stuff is where the bugs are, because it hasn't been tested yet.

new_hosts = today_hosts - yesterday_hosts
if new_hosts:
    send_telegram(f"🆕 {len(new_hosts)} new hosts on {target}: {list(new_hosts)[:5]}")
Enter fullscreen mode Exit fullscreen mode

The Full Kit

The pipeline above is the skeleton — the production version handles rate limiting, retries, screenshotting, nuclei template runs, and state tracking across dozens of targets. Wrapping all of that yourself is a solid week of work, so I packaged everything I actually use into a Bug Bounty Automation Kit: the full recon scripts, cron templates, alerting glue, and my triage rules — ready to drop onto a Pi or a VPS.

Final Tips

  1. Stay in scope. Automation makes it trivial to accidentally hammer something you shouldn't. Whitelist domains, respect robots and rate limits, and read the program policy twice.
  2. Small VPS > your laptop for 24/7 runs. A Raspberry Pi works great and costs nothing to run.
  3. Recon is a compound interest game. Day one you find nothing. Day thirty, your diff alerts start catching assets nobody else has seen yet.

Automation won't find the bugs for you — but it guarantees you're always looking at the freshest attack surface, while you spend your actual brainpower on exploitation. That's the edge.

Happy hunting! 🐛

Top comments (0)