Appendix J — Build the Bruteforce Loop Answers

Completed Check

The final loop is deliberately calm: connection failures are expected misses, so the tool skips them and continues. This is the simplest web-flavoured subdomain brute forcer: read a list, build a URL, ask the fixture, and keep going.

import argparse
from pathlib import Path

import requests

from fixtures.local_targets import course_root, fake_requests_get


def build_argument_parser():
    parser = argparse.ArgumentParser(prog="bernauer")
    parser.add_argument("domain", help="domain to test, e.g. example.test")
    parser.add_argument(
        "-w",
        "--wordlist",
        default=str(course_root() / "data" / "wordlist.txt"),
        help="path to a wordlist file",
    )
    parser.add_argument("--scheme", choices=("http", "https"), default="https")
    parser.add_argument("--timeout", type=float, default=2.0)
    parser.add_argument("--limit", type=int, help="only test the first N labels")
    return parser


def load_wordlist(path: Path) -> list[str]:
    with path.open(encoding="utf-8") as file:
        return [
            line.strip()
            for line in file
            if line.strip() and not line.lstrip().startswith("#")
        ]


def build_url(scheme: str, label: str, domain: str) -> str:
    return f"{scheme}://{label}.{domain}"


def find_subdomains(domain, labels, scheme="https", timeout=2.0, http_get=fake_requests_get):
    found = []
    for label in labels:
        url = build_url(scheme, label, domain)
        try:
            response = http_get(url, timeout=timeout)
        except requests.exceptions.ConnectionError:
            continue
        if response.status_code == 200:
            found.append(f"{label}.{domain}")
    return found


def main(args):
    labels = load_wordlist(Path(args.wordlist))
    if args.limit:
        labels = labels[: args.limit]

    for hostname in find_subdomains(args.domain, labels, args.scheme, args.timeout):
        print(f"Found: {hostname}")
    return 0


args = build_argument_parser().parse_args(["example.test", "--limit", "10"])
print(main(args))
Found: www.example.test
Found: mail.example.test
0

The --limit option gives you a smaller, faster loop while you’re debugging. It belongs before the loop starts because limiting the input list is simpler than making every later function understand partial runs.

References: argparse, pathlib, Requests Quickstart, and RFC 3986.