Take-Over is a small Go scanner that checks targets against service fingerprints. The useful lesson is how much you can do with plain files, net/http, and careful matching.

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:

  • --url for one target
  • --list for a file of targets
  • --output for JSON output
  • --verbose for 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:

  1. Normalize the target with ensureURL, defaulting to https:// when no scheme is provided.
  2. Make a GET request with a 10-second timeout.
  3. Read the response body.
  4. Try each fingerprint regex against the body.
  5. 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.