10  Build the Bruteforce Loop

Introduction

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 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)
    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()]


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


args = 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.

Answer

References