Skip to content

Signatures

The signatures used by several BadDNS modules are ultimately sourced from two main projects:

  • Nuclei Templates. The nuclei templates pertaining to subdomain takeovers are automatically converted and imported into BadDNS
  • dnsReaper. Another excellent project devoted to discovering subdomain takeovers, whose signatures are imported and converted into BadDNS.

GitHub automation pipelines monitor these projects for any new signatures. If they are found, they are converted into BadDNS's format. Tests will be run against the converted signatures. If the tests pass and the signatures look good, they can be imported into BadDNS with one click by the maintainers.

BadDNS can also maintain it's own, unique, signatures when necessary.

Rather than contribute further to the fragmentation of DNS-based signatures by creating another set of them, BadDNS aims to utilize and support existing repositories of signatures.

Currently, the ultimate source of many of these signatures is the https://github.com/EdOverflow/can-i-take-over-xyz repository. The issues section of this repo has become a centralized discussion place for signatures. Many of these discussions are used to create signatures within Nuclei. Ultimately, these Nuclei signatures are converted and used within BadDNS.

We highly encourage contributions to the signatures on these projects and to the discussions on can-i-take-over-xyz, as ultimately these are fed into BadDNS and benefit the entire community.

Signature Format

Signatures are YAML files in baddns/signatures/. Each signature has the following fields:

Field Description
service_name Name of the service (e.g., "Netlify", "AWS Route53")
source Origin of the signature: dnsreaper, nucleitemplates, or self
mode Detection mode (see below)
identifiers Patterns to match against DNS records
matcher_rule HTTP response matching rules (required for http mode, must be null for DNS modes)
negative_signature Optional. Set to true to mark a provider as not exploitable (default: false)

Modes

  • http — Matches CNAME identifiers, then checks HTTP response body/headers against matcher_rule. Used by the CNAME module.
  • dns_nxdomain — Matches CNAME identifiers where the CNAME target returns NXDOMAIN. Used by the CNAME module.
  • dns_nosoa — Matches nameserver identifiers where NS records exist but no SOA is returned. Used by the NS module.

Identifiers

identifiers:
  cnames:            # CNAME patterns to match (used by http and dns_nxdomain modes)
  - type: word
    value: netlify.app
  not_cnames: []     # CNAME patterns that exclude a match
  ips: []            # IP addresses to match (exact, word only)
  nameservers:       # Nameserver patterns to match (used by dns_nosoa mode)
  - awsdns

cnames, not_cnames and nameservers each take word or regex identifiers:

  • word — a plain string. In dns_nxdomain mode it matches as a suffix of the CNAME target; in http mode and for nameservers it matches as a substring.
  • regex — a Python regular expression, matched with re.search against the whole name, so it can anchor with ^ and $. Patterns are compiled when the signature loads, and one that doesn't compile is rejected there rather than silently never matching.

Use regex when a substring is too loose to express the shape of a claimable name:

not_cnames:
# an Elastic Beanstalk name with a dotted prefix carries a hash label, so it can't be
# recreated in another account
- type: regex
  value: ^[^.]+\.[^.]+\..+\.elasticbeanstalk\.com$

A bare string is accepted anywhere an identifier is expected and is treated as word, which is how nameservers and dnsReaper-sourced ips are written. ips are compared exactly, so regex is rejected there.

Examples

HTTP mode (CNAME + response matching):

identifiers:
  cnames:
  - type: word
    value: netlify.app
  ips: []
  nameservers: []
  not_cnames: []
matcher_rule:
  matchers:
  - condition: or
    part: body
    type: word
    words:
    - 'Not Found - Request ID:'
  matchers-condition: and
mode: http
service_name: Netlify
source: dnsreaper

DNS NS mode (dangling nameservers):

identifiers:
  cnames: []
  ips: []
  nameservers:
  - awsdns
  not_cnames: []
matcher_rule: null
mode: dns_nosoa
service_name: AWS Route53
source: dnsreaper

Negative Signatures

Negative signatures mark providers as not exploitable, suppressing generic low-confidence findings. When the NS module finds dangling NS records but no positive signature match, it checks negative signatures before falling back to a generic finding. If a negative signature matches, no finding is reported.

Positive signatures always take priority over negative ones.

To create a negative signature, add negative_signature: true to a standard signature file. By convention, name these files with a negative_ prefix:

identifiers:
  cnames: []
  ips: []
  nameservers:
  - type: word
    value: ultradns.com
  - type: word
    value: ultradns.net
  - type: word
    value: ultradns.org
  - type: word
    value: ultradns.biz
  not_cnames: []
matcher_rule: null
mode: dns_nosoa
negative_signature: true
service_name: UltraDNS
source: self

The SignatureBot

A nightly workflow re-imports every upstream template and opens a PR for anything that differs from what we ship. It decides what has changed by comparing the freshly imported signature against the shipped file byte for byte, which has two consequences worth knowing about.

Signature files have one canonical serialization

Because the comparison is byte for byte, a shipped signature that says the same thing as the imported one still has to serialize the same way. If the serialized form ever changes -- a new identifier shape, a new field -- every shipped signature stops matching at once and the bot re-proposes all of them.

After any change to the serialized form, rewrite the shipped signatures to match:

python3 baddns/scripts/normalize_signatures.py

tests/signature_test.py::test_all_shipped_signatures_canonical fails if any shipped signature has drifted out of canonical form, so this cannot go unnoticed.

Hand-tuned fields are preserved across re-imports

Fields listed in PRESERVED_FIELDS in baddns/scripts/readsources.py (currently confidence) have no upstream equivalent. The importer copies them from the shipped signature of the same name, so a bot update never silently reverts them. Add a field there if it is tuned by hand after import.

Dropping a signature for good

Removing a signature file is not enough on its own: upstream still has the template, so the next run imports it and proposes it again. Record the decision by name in baddns/signatures/blocked_signatures.txt and the bot will skip it permanently.

The bot also tracks content hashes, in signature_history.txt and in the content of PRs closed with the signature-blocked label, but those only suppress one exact version of a signature. Use them to reject a particular upstream revision; use blocked_signatures.txt to reject the signature itself.