Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
492 changes: 380 additions & 112 deletions bbot/core/helpers/dns/dns.py

Large diffs are not rendered by default.

663 changes: 0 additions & 663 deletions bbot/core/helpers/dns/engine.py

This file was deleted.

98 changes: 42 additions & 56 deletions bbot/core/helpers/dns/helpers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,52 @@
import logging

from bbot.core.helpers.regexes import dns_name_extraction_regex
from bbot.core.helpers.misc import clean_dns_record, smart_decode
from bbot.core.helpers.misc import clean_dns_record

log = logging.getLogger("bbot.core.helpers.dns")


# Default rdtypes BBOT cares about during recursive resolution
all_rdtypes = ["A", "AAAA", "SRV", "MX", "NS", "SOA", "CNAME", "TXT"]


def extract_targets(record):
"""Hostnames/IPs worth following from a blastdns ``Record``.

For structured rdata (A/AAAA/CNAME/NS/PTR/MX/SOA/SRV/etc), blastdns has
already extracted the embedded names in Rust -- we just hand those back.

For TXT records we additionally apply a hostname regex to the text content,
since SPF / DKIM / similar TXT payloads commonly embed hostnames worth
pivoting on. That regex extraction is BBOT-specific and stays here.
"""
results = set()
for rdtype, host in record.extract_targets():
cleaned = clean_dns_record(host)
if cleaned:
results.add((rdtype, cleaned))

# TXT: pull additional hostnames out of the free-form text content
is_txt = "TXT" in record.rdata
if is_txt:
text = record.to_text()
for match in dns_name_extraction_regex.finditer(text):
cleaned = clean_dns_record(text[match.start() : match.end()])
if cleaned:
results.add(("TXT", cleaned))

return results


def record_to_text(record):
"""Presentation-format text for a blastdns ``Record``.

Equivalent to dnspython's ``answer.to_text()``. blastdns produces this on
the Rust side via hickory's ``Display`` impl, so this is a thin pass-through.
"""
return record.to_text()


# the following are the result of a 1-day internet survey to find the top SRV records
# the scan resulted in 36,282 SRV records. the count for each one is shown.
common_srvs = [
Expand Down Expand Up @@ -154,61 +195,6 @@
]


def extract_targets(record):
"""
Extracts hostnames or IP addresses from a given DNS record.

This method reads the DNS record's type and based on that, extracts the target
hostnames or IP addresses it points to. The type of DNS record
(e.g., "A", "MX", "CNAME", etc.) determines which fields are used for extraction.

Args:
record (dns.rdata.Rdata): The DNS record to extract information from.

Returns:
set: A set of tuples, each containing the DNS record type and the extracted value.

Examples:
>>> from dns.rrset import from_text
>>> record = from_text('www.example.com', 3600, 'IN', 'A', '192.0.2.1')
>>> extract_targets(record[0])
{('A', '192.0.2.1')}

>>> record = from_text('example.com', 3600, 'IN', 'MX', '10 mail.example.com.')
>>> extract_targets(record[0])
{('MX', 'mail.example.com')}

"""
results = set()

def add_result(rdtype, _record):
cleaned = clean_dns_record(_record)
if cleaned:
results.add((rdtype, cleaned))

rdtype = str(record.rdtype.name).upper()
if rdtype in ("A", "AAAA", "NS", "CNAME", "PTR"):
add_result(rdtype, record)
elif rdtype == "SOA":
add_result(rdtype, record.mname)
elif rdtype == "MX":
add_result(rdtype, record.exchange)
elif rdtype == "SRV":
add_result(rdtype, record.target)
elif rdtype == "TXT":
for s in record.strings:
s = smart_decode(s)
for match in dns_name_extraction_regex.finditer(s):
start, end = match.span()
host = s[start:end]
add_result(rdtype, host)
elif rdtype == "NSEC":
add_result(rdtype, record.next)
else:
log.warning(f'Unknown DNS record type "{rdtype}"')
return results


def service_record(host, rdtype=None):
"""
Indicates that the provided host name and optional rdtype is an SRV or related service record.
Expand Down
74 changes: 0 additions & 74 deletions bbot/core/helpers/dns/mock.py

This file was deleted.

13 changes: 5 additions & 8 deletions bbot/core/helpers/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2743,14 +2743,11 @@ def clean_dns_record(record):
>>> clean_dns_record('www.evilcorp.com.')
'www.evilcorp.com'

>>> from dns.rrset import from_text
>>> record = from_text('www.evilcorp.com', 3600, 'IN', 'A', '1.2.3.4')[0]
>>> clean_dns_record(record)
'1.2.3.4'
"""
if not isinstance(record, str):
record = str(record.to_text())
return str(record).rstrip(".").lower()
>>> clean_dns_record('*.evilcorp.com.')
'evilcorp.com'
"""
record = str(record).strip("*.").lower()
return record


def truncate_filename(file_path, max_length=255):
Expand Down
8 changes: 5 additions & 3 deletions bbot/defaults.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ dns:
disable: false
# Speed up scan by not creating any new DNS events, and only resolving A and AAAA records
minimal: false
# How many instances of the dns module to run concurrently
threads: 25
# How many threads to use per resolver (best way to increase speed is to put more resolvers in /etc/resolv.conf)
threads: 10
# How many DNS records to cache
cache_size: 100000
# How many concurrent DNS resolvers to use when brute-forcing
# (under the hood this is passed through directly to massdns -s)
brute_threads: 1000
Expand All @@ -67,7 +69,7 @@ dns:
wildcard_tests: 10
# Skip DNS requests for a certain domain and rdtype after encountering this many timeouts or SERVFAILs
# This helps prevent faulty DNS servers from hanging up the scan
abort_threshold: 50
abort_threshold: 20
# Treat hostnames discovered via PTR records as affiliates instead of in-scope
# This prevents rDNS results (e.g. 1-2-3-4.ptr.example.com) from triggering
# subdomain enumeration against unrelated domains when scanning IP ranges
Expand Down
16 changes: 8 additions & 8 deletions bbot/modules/baddns.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import logging

SEVERITY_LEVELS = ("INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL")
CONFIDENCE_LEVELS = ("UNKNOWN", "LOW", "MODERATE", "HIGH", "CONFIRMED")
CONFIDENCE_LEVELS = ("UNKNOWN", "LOW", "MEDIUM", "HIGH", "CONFIRMED")

SUBMODULE_MAX_SEVERITY = {
"CNAME": "MEDIUM",
Expand Down Expand Up @@ -45,15 +45,15 @@ class baddns(BaseModule):
"created_date": "2024-01-18",
"author": "@liquidsec",
}
options = {"custom_nameservers": [], "min_severity": "LOW", "min_confidence": "MODERATE", "enabled_submodules": []}
options = {"custom_nameservers": [], "min_severity": "LOW", "min_confidence": "MEDIUM", "enabled_submodules": []}
options_desc = {
"custom_nameservers": "Force BadDNS to use a list of custom nameservers",
"min_severity": "Minimum severity to emit (INFO, LOW, MEDIUM, HIGH, CRITICAL)",
"min_confidence": "Minimum confidence to emit (UNKNOWN, LOW, MODERATE, HIGH, CONFIRMED)",
"min_confidence": "Minimum confidence to emit (UNKNOWN, LOW, MEDIUM, HIGH, CONFIRMED)",
"enabled_submodules": "A list of submodules to enable. Empty list (default) enables CNAME, TXT and MX Only",
}
module_threads = 8
deps_pip = ["baddns~=2.0.0"]
deps_pip = ["baddns~=2.1.0"]

def select_modules(self):
selected_submodules = []
Expand Down Expand Up @@ -96,13 +96,13 @@ async def setup(self):
if self.custom_nameservers:
self.custom_nameservers = self.helpers.chain_lists(self.custom_nameservers)
min_severity = self.config.get("min_severity", "LOW").upper()
min_confidence = self.config.get("min_confidence", "MODERATE").upper()
min_confidence = self.config.get("min_confidence", "MEDIUM").upper()
if min_severity not in SEVERITY_LEVELS:
self.warning(f"Invalid min_severity: {min_severity}, defaulting to LOW")
min_severity = "LOW"
if min_confidence not in CONFIDENCE_LEVELS:
self.warning(f"Invalid min_confidence: {min_confidence}, defaulting to MODERATE")
min_confidence = "MODERATE"
self.warning(f"Invalid min_confidence: {min_confidence}, defaulting to MEDIUM")
min_confidence = "MEDIUM"
self._min_sev_idx = SEVERITY_LEVELS.index(min_severity)
self._min_conf_idx = CONFIDENCE_LEVELS.index(min_confidence)
self.signatures = load_signatures()
Expand Down Expand Up @@ -149,7 +149,7 @@ async def handle_event(self, event):
for ModuleClass in self.select_modules():
kwargs = {
"http_client_class": self._new_http_client,
"dns_client": self.scan.helpers.dns.resolver,
"dns_client": self.scan.helpers.dns.blastdns,
"custom_nameservers": self.custom_nameservers,
"signatures": self.signatures,
}
Expand Down
8 changes: 4 additions & 4 deletions bbot/modules/baddns_direct.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ class baddns_direct(baddns_module):
"created_date": "2024-01-29",
"author": "@liquidsec",
}
options = {"custom_nameservers": [], "min_severity": "LOW", "min_confidence": "MODERATE"}
options = {"custom_nameservers": [], "min_severity": "LOW", "min_confidence": "MEDIUM"}
options_desc = {
"custom_nameservers": "Force BadDNS to use a list of custom nameservers",
"min_severity": "Minimum severity to emit (INFO, LOW, MEDIUM, HIGH, CRITICAL)",
"min_confidence": "Minimum confidence to emit (UNKNOWN, LOW, MODERATE, HIGH, CONFIRMED)",
"min_confidence": "Minimum confidence to emit (UNKNOWN, LOW, MEDIUM, HIGH, CONFIRMED)",
}
module_threads = 8
deps_pip = ["baddns~=2.0.0"]
deps_pip = ["baddns~=2.1.0"]

scope_distance_modifier = 1

Expand All @@ -28,7 +28,7 @@ async def handle_event(self, event):
CNAME_direct_module = self.select_modules()[0]
kwargs = {
"http_client_class": self.scan.helpers.web.AsyncClient,
"dns_client": self.scan.helpers.dns.resolver,
"dns_client": self.scan.helpers.dns.blastdns,
"custom_nameservers": self.custom_nameservers,
"signatures": self.signatures,
"direct_mode": True,
Expand Down
6 changes: 3 additions & 3 deletions bbot/modules/baddns_zone.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ class baddns_zone(baddns_module):
"created_date": "2024-01-29",
"author": "@liquidsec",
}
options = {"custom_nameservers": [], "min_severity": "INFO", "min_confidence": "MODERATE"}
options = {"custom_nameservers": [], "min_severity": "INFO", "min_confidence": "MEDIUM"}
options_desc = {
"custom_nameservers": "Force BadDNS to use a list of custom nameservers",
"min_severity": "Minimum severity to emit (INFO, LOW, MEDIUM, HIGH, CRITICAL)",
"min_confidence": "Minimum confidence to emit (UNKNOWN, LOW, MODERATE, HIGH, CONFIRMED)",
"min_confidence": "Minimum confidence to emit (UNKNOWN, LOW, MEDIUM, HIGH, CONFIRMED)",
}
module_threads = 8
deps_pip = ["baddns~=2.0.0"]
deps_pip = ["baddns~=2.1.0"]

def set_modules(self):
self.enabled_submodules = ["NSEC", "zonetransfer"]
Expand Down
2 changes: 1 addition & 1 deletion bbot/modules/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1876,7 +1876,7 @@ async def _worker(self):
forward_event_reason = ""

if acceptable:
context = f"{self.name}.handle_event({event, kwargs})"
context = f"{self.name}.handle_event({event})"
self.scan.stats.event_consumed(event, self)
self.debug(f"Intercepting {event}")
try:
Expand Down
Loading
Loading