In this lesson, you’ll combine the parser, wordlist loader, URL builder, fixture request, and error handling into the final Project 1 loop. Each part is small because the earlier lessons already gave it a job.
Now that we’ve processed our wordlist, we’re going to build the simplest type of subdomain brute forcing: the web kind. The real internet version can get complicated quickly, so this edition keeps the target fixture-backed and deterministic while preserving the shape of the tool.
Concepts Covered
Loading runtime options
Iterating over wordlist candidates
Building URLs consistently
Continuing after connection failures
Reporting discovered fixture hosts
Why This Matters
The final loop is where a small script starts behaving like a tool. It should be predictable, safe by default, and calm when most candidates don’t respond.
Objective
Print the fixture-backed hosts that exist under example.test.
The Final Shape
The complete loop has four helper functions: one for command-line options, one for loading the wordlist, one for building URLs, and one for checking each candidate.
This is the part where the command centre, wordlist handling, and request code all meet. Each function should have one job so that when something breaks, you know which little room of the house to inspect.
import argparsefrom pathlib import Pathimport requestsfrom fixtures.local_targets import course_root, fake_requests_getdef 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)return parserdef load_wordlist(path: Path) ->list[str]:with path.open(encoding="utf-8") asfile:return [line.strip() for line infileif line.strip()]def build_url(scheme: str, label: str, domain: str) ->str:returnf"{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:continueif response.status_code ==200: found.append(f"{label}.{domain}")return foundargs = build_argument_parser().parse_args(["example.test"])labels = load_wordlist(Path(args.wordlist))for hostname in find_subdomains(args.domain, labels, args.scheme, args.timeout):print(f"Found: {hostname}")
Found: www.example.test
Found: mail.example.test
Reading the Loop
The important behaviour is the continue inside the ConnectionError handler. Because most wordlist entries will miss, the tool should skip those misses and keep checking the next candidate.
Creating too many open connections or waiting forever can be problematic for servers and miserable for users. That’s why the request layer accepts a timeout, and why expected misses are handled as part of the normal loop instead of crashing the whole application.
Once everything’s working as expected, you’ve got the simplest form of a subdomain bruteforcer. You’ll have plenty of improvements you can make later, such as DNS lookups, certificate transparency sources, or richer output, but the core shape is here.
Checkpoint
Build the final fixture bruteforcer in your project script. It should:
parse domain, --wordlist, --scheme, and --timeout
load candidate labels from the wordlist
request each fixture-backed URL
print discovered hosts
continue after ConnectionError
With the default course wordlist and example.test, your output should include:
Found: www.example.test
Found: mail.example.test
Run your application. If everything went the way we expected, misses should disappear quietly and matches should print in a tidy little list.
def find_subdomains(domain, labels, scheme="https", timeout=2.0, http_get=fake_requests_get): found = []# Loop over labels, build each URL, handle ConnectionError, and collect matches.return found
Extension Activity
Add a --limit option so learners can test the first few wordlist entries before running a larger list, and apply the limit before the loop starts.
This is a useful habit when tool-building: give yourself a smaller, faster way to test the loop before pointing it at a bigger input file, and your future debugging sessions will thank you.