from fixtures.local_targets import fake_public_ip
external_ip = fake_public_ip()
print(f"Documentation address: {external_ip}")Documentation address: 203.0.113.42
In this lesson, you’ll connect the project to the networking ideas behind subdomain enumeration without sending traffic to the internet. The examples use deterministic fixtures that behave like small pieces of requests and DNS-adjacent infrastructure.
Normally when we talk about subdomain brute forcing we also talk about enumeration. Enumeration can draw from search engines, DNS records, certificate transparency logs, and other sources, which makes it more complex very quickly. Those are all things you can build in later, but first we’ll start with the basics.
Subdomain discovery is a reconnaissance technique, and reconnaissance can touch real systems quickly. Fixture-backed practice lets you learn the programming shape before running anything against an authorised target.
Build fixture-backed URLs, request them safely, and recognise when a candidate host responds.
Before we talk about enumerating subdomains, we need to have a chat about DNS. Long story short, DNS is the phone book of the internet: clients can look up names instead of memorising IP addresses.
A subdomain is a name below another domain. In api.example.com, api is a label under example.com. In real reconnaissance, subdomains can reveal forgotten applications, staging systems, alternate front doors, or services that weren’t supposed to see the light of day.
Common records you’ll see in reconnaissance notes include:
| DNS Record | Usage |
|---|---|
| A | Maps a hostname to one or more IPv4 addresses |
| AAAA | Maps a hostname to one or more IPv6 addresses |
| CNAME | Aliases one hostname to another hostname |
| MX | Identifies mail exchangers for a domain |
| NS | Identifies authoritative name servers for a zone |
This course uses documentation and fixture addresses instead of live targets. 203.0.113.42, for example, comes from a documentation-only range reserved by RFC 5737.
In the original workshop we had a training web server set up specifically for practice. In this edition, the same idea lives inside the repository so the book can render safely and you can experiment without making surprise internet traffic.
from fixtures.local_targets import fake_public_ip
external_ip = fake_public_ip()
print(f"Documentation address: {external_ip}")Documentation address: 203.0.113.42
The bruteforcer combines three pieces: a URL scheme, a candidate subdomain label, and the authorised parent domain.
You’ve got a few ways to build that string: Python supports concatenation with +, the older format() method, and f-strings. In Python 3.6 and higher, f-strings are usually the clearest option, and yes, they’re my personal favourite.
scheme = "https"
label = "www"
domain = "example.test"
url = f"{scheme}://{label}.{domain}"
print(url)http or https.
https://www.example.test
Here’s what’s happening: without the scheme, a web request doesn’t know whether it should use http or https. Keeping the scheme explicit avoids the classic “almost a URL” problem.
The real function in a live script is requests.get(). In this notebook, fake_requests_get() accepts the same kind of URL and returns a small response object without leaving your machine.
Requests is a third-party Python library with a much friendlier interface than the lower-level tools in the standard library. For this course we keep the interface shape but replace the network with a fixture, because learning the control flow shouldn’t require poking a real domain.
from fixtures.local_targets import fake_requests_get
response = fake_requests_get("https://www.example.test", timeout=2)
print(response.status_code)
print(response.text)200
www.example.test is alive
A bruteforcer should expect many candidates not to answer. Handle that as normal control flow, not as a surprising crash.
If you recall Python’s motto, this is “ask for forgiveness, not permission” in action. We try the request, and if the candidate doesn’t exist in the fixture, we catch the connection error and move on.
import requests
try:
fake_requests_get("https://missing.example.test", timeout=2)
except requests.exceptions.ConnectionError:
print("No response from fixture")No response from fixture
Write a check_subdomain(label, domain) function. It should build a fixture URL, call fake_requests_get(), and return True only when the fixture returns status code 200.
A quick test should produce:
True
False
Run it with one label you expect to exist and one you expect to miss. If everything went the way we expected, your function should be calm in both cases.
from fixtures.local_targets import fake_requests_get
import requests
def check_subdomain(label: str, domain: str) -> bool:
# Build the URL, call fake_requests_get(), and handle ConnectionError.
passReturn the full hostname for successful checks instead of only True. For example, www.example.test is more useful in final output than True.
Once that works, think about where you would add a timeout option in the real command-line parser. Slow or missing hosts are normal in this kind of tool, and waiting forever isn’t a strategy.