How Take-Over Uses Go to Find Subdomain Takeover Vulnerabilities
Subdomain takeover is one of those bugs that looks boring until it is your domain serving someone else’s content.
The usual shape is simple: a DNS record still points at a cloud resource that no longer exists. Maybe it was a deleted S3 bucket, an old Heroku app, a dangling GitHub Pages site, or another SaaS endpoint. If an attacker can claim the missing resource, your subdomain becomes their publishing surface.
Take-Over is a small Go tool for checking that class of problem. It is not a huge concurrent scanner. The current source is much simpler than that: read one target or a list, make HTTP requests, compare response bodies against known service fingerprints, and optionally write vulnerable hits to JSON.
That simplicity is what makes it useful to read.
The CLI stays thin
The command entry is split between main.go and cmd/. main.go calls into the command layer. cmd/root.go owns the top-level command flow, and cmd/flags.go parses a small set of flags:
--urlfor one target--listfor a file of targets--outputfor JSON output--verbosefor printing non-vulnerable and HTTP-error cases
There is no big CLI framework here. The parser walks os.Args directly. For a tool this small, that is a fair trade. You get fewer dependencies and the behavior is easy to audit.
The parsed values are passed into:
type Config struct {
URL string
ListPath string
Output string
Verbose bool
}
That lives in runner/config.go. It is plain data, which keeps the command layer separate from the scanning logic.
Fingerprints are data, not code
The detection rules live in fingerprints.json, and runner/fingerprints.go loads them into this struct:
type Fingerprint struct {
CICDPass bool `json:"cicd_pass"`
CName []string `json:"cname"`
Discussion string `json:"discussion"`
Documentation string `json:"documentation"`
Fingerprint string `json:"fingerprint"`
HTTPStatus *int `json:"http_status"`
NXDomain bool `json:"nxdomain"`
Service string `json:"service"`
Status string `json:"status"`
Vulnerable bool `json:"vulnerable"`
}
This is the right call. Subdomain takeover detection changes as providers change their error pages and as new services are added. Keeping fingerprints in JSON means the rules can grow without turning the scanner into a giant switch statement.
The current matching code uses the Fingerprint string as a regular expression. There is a special case for fingerprints containing NXDOMAIN, where it checks the uppercased response body for that text.
Reading targets
runner/reader.go handles list input. It reads a line-delimited file, trims whitespace, and ignores empty lines. That is all this tool needs.
There is a small lesson there: a security scanner does not always need a complex ingestion layer. If the useful boundary is “one target per line,” make that boundary boring. It lets users pipe output from other discovery tools into your scanner without learning a new format.
The scan path
The scan path lives in runner/worker.go, although the name is a little misleading in the current version. It is not a worker-pool implementation. The actual Run function in runner/process.go loops through targets sequentially and calls scanTarget.
scanTarget does a few straightforward things:
- Normalize the target with
ensureURL, defaulting tohttps://when no scheme is provided. - Make a GET request with a 10-second timeout.
- Read the response body.
- Try each fingerprint regex against the body.
- Return a vulnerable result when a matching fingerprint is marked
Vulnerable.
The HTTP client uses a custom transport with InsecureSkipVerify: true. That makes sense for this kind of scanner because dangling or misconfigured hosts may not present a clean certificate chain. It also means the tool is not validating server identity; it is looking for provider-specific takeover signals in the response.
Output is deliberately narrow
When a target matches a vulnerable fingerprint, Take-Over prints:
[VULNERABLE] sub.example.com (ServiceName)
If --output is set, it sorts vulnerable results by target and writes pretty-printed JSON.
The output result shape is small:
type Result struct {
Target string `json:"target"`
Vulnerable bool `json:"vulnerable"`
Service string `json:"service,omitempty"`
HTTPError bool `json:"http_error,omitempty"`
}
That is enough for a first pass. You can hand the JSON to another tool, archive it in CI, or diff it between runs.
What I would change first
The obvious future improvement is concurrency. The current loop is easy to read, but a long subdomain list will be slow. The clean Go shape would be a bounded worker pool: send targets on a channel, scan with N workers, collect results on another channel, and keep the HTTP timeout.
I would also consider matching against more than the body. The fingerprint data already includes fields like cname, http_status, nxdomain, and documentation links. Today the scanner is primarily response-body driven. Folding in DNS and status-code checks would reduce false positives and make the JSON rule file more valuable.
But as a compact example of a Go security tool, Take-Over is still worth reading. The code keeps CLI parsing, target reading, fingerprint loading, scanning, and output separate. No ceremony, no framework gravity, just enough structure to make the scanner understandable.