Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ jobs:
uv run ruff check
uv run ruff format --check
- name: Run tests
env:
BBOT_IO_API_KEY: ${{ secrets.BBOT_IO_API_KEY }}
run: |
uv run pytest -vv --reruns 2 -o timeout_func_only=true --timeout 1200 --disable-warnings --log-cli-level=INFO --cov-config=bbot/test/coverage.cfg --cov-report xml:cov.xml --cov=bbot .
- name: Upload Debug Logs
Expand Down
7 changes: 1 addition & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,7 @@ include:
- paramminer
- dirbust-light
- web-screenshots
- baddns-intense

config:
modules:
baddns:
enable_references: True
- baddns-heavy

```

Expand Down
150 changes: 107 additions & 43 deletions bbot/modules/baddns.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,37 @@

import logging

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

SUBMODULE_MAX_SEVERITY = {
"CNAME": "MEDIUM",
"NS": "MEDIUM",
"MX": "MEDIUM",
"TXT": "LOW",
"references": "MEDIUM",
"NSEC": "INFO",
"zonetransfer": "INFO",
"DMARC": "INFO",
"SPF": "MEDIUM",
"MTA-STS": "HIGH",
"WILDCARD": "HIGH",
}

SUBMODULE_MAX_CONFIDENCE = {
"CNAME": "CONFIRMED",
"NS": "HIGH",
"MX": "CONFIRMED",
"TXT": "CONFIRMED",
"references": "CONFIRMED",
"NSEC": "CONFIRMED",
"zonetransfer": "CONFIRMED",
"DMARC": "CONFIRMED",
"SPF": "CONFIRMED",
"MTA-STS": "CONFIRMED",
"WILDCARD": "CONFIRMED",
}


class baddns(BaseModule):
watched_events = ["DNS_NAME", "DNS_NAME_UNRESOLVED"]
Expand All @@ -14,14 +45,15 @@ class baddns(BaseModule):
"created_date": "2024-01-18",
"author": "@liquidsec",
}
options = {"custom_nameservers": [], "only_high_confidence": False, "enabled_submodules": []}
options = {"custom_nameservers": [], "min_severity": "LOW", "min_confidence": "MODERATE", "enabled_submodules": []}
options_desc = {
"custom_nameservers": "Force BadDNS to use a list of custom nameservers",
"only_high_confidence": "Do not emit low-confidence or generic detections",
"min_severity": "Minimum severity to emit (INFO, LOW, MEDIUM, HIGH, CRITICAL)",
"min_confidence": "Minimum confidence to emit (UNKNOWN, LOW, MODERATE, 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~=1.12.294"]
deps_pip = ["baddns~=2.0.0"]

def select_modules(self):
selected_submodules = []
Expand All @@ -35,14 +67,50 @@ def set_modules(self):
if self.enabled_submodules == []:
self.enabled_submodules = ["CNAME", "MX", "TXT"]

def _filter_submodules(self):
filtered = []
for name in self.enabled_submodules:
max_sev = SUBMODULE_MAX_SEVERITY.get(name)
max_conf = SUBMODULE_MAX_CONFIDENCE.get(name)
if max_sev is None or max_conf is None:
filtered.append(name)
continue
sev_idx = SEVERITY_LEVELS.index(max_sev) if max_sev in SEVERITY_LEVELS else 0
conf_idx = CONFIDENCE_LEVELS.index(max_conf) if max_conf in CONFIDENCE_LEVELS else 0
if sev_idx < self._min_sev_idx or conf_idx < self._min_conf_idx:
self.verbose(
f"Auto-disabling submodule [{name}]: max_severity={max_sev}, max_confidence={max_conf} below configured thresholds"
)
else:
filtered.append(name)
return filtered

def _meets_threshold(self, severity, confidence):
sev_idx = SEVERITY_LEVELS.index(severity) if severity in SEVERITY_LEVELS else 0
conf_idx = CONFIDENCE_LEVELS.index(confidence) if confidence in CONFIDENCE_LEVELS else 0
return sev_idx >= self._min_sev_idx and conf_idx >= self._min_conf_idx

async def setup(self):
self.preset.core.logger.include_logger(logging.getLogger("baddns"))
self.custom_nameservers = self.config.get("custom_nameservers", []) or None
if self.custom_nameservers:
self.custom_nameservers = self.helpers.chain_lists(self.custom_nameservers)
self.only_high_confidence = self.config.get("only_high_confidence", False)
min_severity = self.config.get("min_severity", "LOW").upper()
min_confidence = self.config.get("min_confidence", "MODERATE").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._min_sev_idx = SEVERITY_LEVELS.index(min_severity)
self._min_conf_idx = CONFIDENCE_LEVELS.index(min_confidence)
self.signatures = load_signatures()
self.set_modules()
self.enabled_submodules = self._filter_submodules()
if not self.enabled_submodules:
self.warning("All submodules were disabled by severity/confidence thresholds")
return False
all_submodules_list = [m.name for m in get_all_modules()]
for m in self.enabled_submodules:
if m not in all_submodules_list:
Expand All @@ -62,11 +130,25 @@ async def _run_module(self, module_instance):
self.warning(f"Task for {module_instance} raised an error: {e}")
return module_instance, None

def _new_http_client(self, *args, **kwargs):
"""Create a non-cached HTTP client for baddns submodules.

baddns submodules close their HTTP clients during cleanup, so we can't
use the caching ``web.AsyncClient`` factory — that would let one
submodule close a client that another submodule is still using.

TODO: revisit this when we switch to blasthttp — the caching/lifecycle
model will be different and this workaround may no longer be needed.
"""
from bbot.core.helpers.web.client import BBOTAsyncClient

return BBOTAsyncClient.from_config(self.scan.config, self.scan.target, *args, persist_cookies=False, **kwargs)

async def handle_event(self, event):
coroutines = []
for ModuleClass in self.select_modules():
kwargs = {
"http_client_class": self.scan.helpers.web.AsyncClient,
"http_client_class": self._new_http_client,
"dns_client": self.scan.helpers.dns.resolver,
"custom_nameservers": self.custom_nameservers,
"signatures": self.signatures,
Expand Down Expand Up @@ -96,46 +178,28 @@ async def handle_event(self, event):
r_dict = r.to_dict()

confidence = r_dict["confidence"]
severity = r_dict["severity"]

if confidence in ["CONFIRMED", "PROBABLE"]:
data = {
"severity": "MEDIUM",
"name": f"BadDNS {r_dict['signature']}",
"confidence": "HIGH",
"description": f"{r_dict['description']}. Confidence: [{confidence}] Signature: [{r_dict['signature']}] Indicator: [{r_dict['indicator']}] Trigger: [{r_dict['trigger']}] baddns Module: [{r_dict['module']}]",
"host": str(event.host),
}
await self.emit_event(
data,
"FINDING",
event,
tags=[f"baddns-{module_instance.name.lower()}"],
context=f'{{module}}\'s "{r_dict["module"]}" module found {{event.type}}: {r_dict["description"]}',
if not self._meets_threshold(severity, confidence):
self.debug(
f"Skipping result below threshold (severity={severity}, confidence={confidence})"
)

elif confidence in ["UNLIKELY", "POSSIBLE"]:
if not self.only_high_confidence:
data = {
"name": f"BadDNS {r_dict['signature']}",
"description": f"{r_dict['description']} Confidence: [{confidence}] Signature: [{r_dict['signature']}] Indicator: [{r_dict['indicator']}] Trigger: [{r_dict['trigger']}] baddns Module: [{r_dict['module']}]",
"host": str(event.host),
"severity": "MEDIUM",
"confidence": "LOW",
}
await self.emit_event(
data,
"FINDING",
event,
tags=[f"baddns-{module_instance.name.lower()}"],
context=f'{{module}}\'s "{r_dict["module"]}" module found {{event.type}}: {r_dict["description"]}',
)
else:
self.debug(
f"Skipping low-confidence result due to only_high_confidence setting: {confidence}"
)

else:
self.warning(f"Got unrecognized confidence level: {confidence}")
continue

data = {
"severity": severity,
"name": f"BadDNS {r_dict['signature']}",
"confidence": confidence,
"description": f"{r_dict['description']}. Confidence: [{confidence}] Signature: [{r_dict['signature']}] Indicator: [{r_dict['indicator']}] Trigger: [{r_dict['trigger']}] baddns Module: [{r_dict['module']}]",
"host": str(event.host),
}
await self.emit_event(
data,
"FINDING",
event,
tags=[f"baddns-{module_instance.name.lower()}"],
context=f'{{module}}\'s "{r_dict["module"]}" module found {{event.type}}: {r_dict["description"]}',
)

found_domains = r_dict.get("found_domains", None)
if found_domains:
Expand Down
42 changes: 17 additions & 25 deletions bbot/modules/baddns_direct.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
from baddns.base import get_all_modules
from baddns.lib.loader import load_signatures
from .base import BaseModule
from .baddns import baddns as baddns_module

import logging


class baddns_direct(BaseModule):
class baddns_direct(baddns_module):
watched_events = ["URL", "STORAGE_BUCKET"]
produced_events = ["FINDING"]
flags = ["active", "safe", "subdomain-enum", "baddns", "cloud-enum"]
Expand All @@ -14,30 +10,19 @@ class baddns_direct(BaseModule):
"created_date": "2024-01-29",
"author": "@liquidsec",
}
options = {"custom_nameservers": []}
options = {"custom_nameservers": [], "min_severity": "LOW", "min_confidence": "MODERATE"}
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)",
}
module_threads = 8
deps_pip = ["baddns~=1.12.294"]
deps_pip = ["baddns~=2.0.0"]

scope_distance_modifier = 1

async def setup(self):
self.preset.core.logger.include_logger(logging.getLogger("baddns"))
self.custom_nameservers = self.config.get("custom_nameservers", []) or None
if self.custom_nameservers:
self.custom_nameservers = self.helpers.chain_lists(self.custom_nameservers)
self.only_high_confidence = self.config.get("only_high_confidence", False)
self.signatures = load_signatures()
return True

def select_modules(self):
selected_modules = []
for m in get_all_modules():
if m.name in ["CNAME"]:
selected_modules.append(m)
return selected_modules
def set_modules(self):
self.enabled_submodules = ["CNAME"]

async def handle_event(self, event):
CNAME_direct_module = self.select_modules()[0]
Expand All @@ -56,12 +41,19 @@ async def handle_event(self, event):
for r in results:
r_dict = r.to_dict()

severity = r_dict["severity"]
confidence = r_dict["confidence"]

if not self._meets_threshold(severity, confidence):
self.debug(f"Skipping result below threshold (severity={severity}, confidence={confidence})")
continue

data = {
"name": f"BadDNS {r_dict['signature']}",
"description": f"Possible [{r_dict['signature']}] via direct BadDNS analysis. Indicator: [{r_dict['indicator']}] Trigger: [{r_dict['trigger']}] baddns Module: [{r_dict['module']}]",
"host": str(event.host),
"severity": "HIGH",
"confidence": "MEDIUM",
"severity": severity,
"confidence": confidence,
}

await self.emit_event(
Expand Down
7 changes: 4 additions & 3 deletions bbot/modules/baddns_zone.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ class baddns_zone(baddns_module):
"created_date": "2024-01-29",
"author": "@liquidsec",
}
options = {"custom_nameservers": [], "only_high_confidence": False}
options = {"custom_nameservers": [], "min_severity": "INFO", "min_confidence": "MODERATE"}
options_desc = {
"custom_nameservers": "Force BadDNS to use a list of custom nameservers",
"only_high_confidence": "Do not emit low-confidence or generic detections",
"min_severity": "Minimum severity to emit (INFO, LOW, MEDIUM, HIGH, CRITICAL)",
"min_confidence": "Minimum confidence to emit (UNKNOWN, LOW, MODERATE, HIGH, CONFIRMED)",
}
module_threads = 8
deps_pip = ["baddns~=1.12.294"]
deps_pip = ["baddns~=2.0.0"]

def set_modules(self):
self.enabled_submodules = ["NSEC", "zonetransfer"]
Expand Down
6 changes: 3 additions & 3 deletions bbot/modules/badsecrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class badsecrets(BaseModule):
options_desc = {
"custom_secrets": "Include custom secrets loaded from a local file",
}
deps_pip = ["badsecrets~=0.13.47"]
deps_pip = ["badsecrets~=1.0.0"]

async def setup(self):
self.custom_secrets = None
Expand Down Expand Up @@ -86,8 +86,8 @@ async def handle_event(self, event):
# Only vulnerable (SecretFound) JWT results are worth emitting from badsecrets.
if r["detecting_module"] == "Generic_JWT":
continue
# There is little value to presenting a non-vulnerable asp.net viewstate, as it is not crackable without a Matrioshka brain. Just emit a technology instead.
if r["detecting_module"] == "ASPNET_Viewstate":
# There is little value to presenting a non-vulnerable asp.net viewstate/resource, as it is not crackable without a Matrioshka brain. Just emit a technology instead.
if r["detecting_module"] in ("ASPNET_Viewstate", "ASPNET_Resource"):
technology = "microsoft asp.net"
await self.emit_event(
{"technology": technology, "url": event.data["url"], "host": str(event.host)},
Expand Down
21 changes: 21 additions & 0 deletions bbot/presets/baddns-heavy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
description: Run all baddns modules and submodules.

include:
- baddns

modules:
- baddns_zone
- baddns_direct

config:
modules:
baddns:
enabled_submodules: [CNAME, NS, MX, TXT, references, DMARC, SPF, MTA-STS, WILDCARD]
min_severity: INFORMATIONAL
min_confidence: UNKNOWN
baddns_zone:
min_severity: INFORMATIONAL
min_confidence: UNKNOWN
baddns_direct:
min_severity: INFORMATIONAL
min_confidence: UNKNOWN
12 changes: 0 additions & 12 deletions bbot/presets/baddns-intense.yml

This file was deleted.

11 changes: 11 additions & 0 deletions bbot/presets/baddns.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
description: Check for subdomain takeovers and other DNS issues.

modules:
- baddns

config:
modules:
baddns:
enabled_submodules: [CNAME, MX, TXT]
min_severity: LOW
min_confidence: MODERATE
7 changes: 1 addition & 6 deletions bbot/presets/kitchen-sink.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,4 @@ include:
- paramminer
- dirbust-light
- web-screenshots
- baddns-intense

config:
modules:
baddns:
enable_references: True
- baddns-heavy
Loading
Loading