bbot 2.5.0__py3-none-any.whl → 2.7.2.7424rc0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bbot/__init__.py +1 -1
- bbot/cli.py +22 -8
- bbot/core/engine.py +1 -1
- bbot/core/event/__init__.py +2 -2
- bbot/core/event/base.py +138 -110
- bbot/core/flags.py +1 -0
- bbot/core/helpers/bloom.py +6 -7
- bbot/core/helpers/command.py +5 -2
- bbot/core/helpers/depsinstaller/installer.py +78 -7
- bbot/core/helpers/dns/dns.py +0 -1
- bbot/core/helpers/dns/engine.py +0 -2
- bbot/core/helpers/files.py +2 -2
- bbot/core/helpers/git.py +17 -0
- bbot/core/helpers/helper.py +6 -5
- bbot/core/helpers/misc.py +15 -28
- bbot/core/helpers/names_generator.py +5 -0
- bbot/core/helpers/ntlm.py +0 -2
- bbot/core/helpers/regex.py +1 -1
- bbot/core/helpers/regexes.py +25 -8
- bbot/core/helpers/web/engine.py +1 -1
- bbot/core/helpers/web/web.py +2 -1
- bbot/core/modules.py +22 -60
- bbot/core/shared_deps.py +38 -0
- bbot/defaults.yml +4 -2
- bbot/modules/apkpure.py +2 -2
- bbot/modules/aspnet_bin_exposure.py +80 -0
- bbot/modules/baddns.py +1 -1
- bbot/modules/baddns_direct.py +1 -1
- bbot/modules/baddns_zone.py +1 -1
- bbot/modules/badsecrets.py +1 -1
- bbot/modules/base.py +129 -40
- bbot/modules/bucket_amazon.py +1 -1
- bbot/modules/bucket_digitalocean.py +1 -1
- bbot/modules/bucket_firebase.py +1 -1
- bbot/modules/bucket_google.py +1 -1
- bbot/modules/{bucket_azure.py → bucket_microsoft.py} +2 -2
- bbot/modules/builtwith.py +4 -2
- bbot/modules/c99.py +1 -1
- bbot/modules/dnsbimi.py +1 -4
- bbot/modules/dnsbrute.py +6 -1
- bbot/modules/dnscommonsrv.py +1 -0
- bbot/modules/dnsdumpster.py +35 -52
- bbot/modules/dnstlsrpt.py +0 -6
- bbot/modules/docker_pull.py +2 -2
- bbot/modules/emailformat.py +17 -1
- bbot/modules/ffuf.py +4 -1
- bbot/modules/ffuf_shortnames.py +6 -3
- bbot/modules/filedownload.py +8 -5
- bbot/modules/fullhunt.py +1 -1
- bbot/modules/git_clone.py +47 -22
- bbot/modules/gitdumper.py +5 -15
- bbot/modules/github_workflows.py +6 -5
- bbot/modules/gitlab_com.py +31 -0
- bbot/modules/gitlab_onprem.py +84 -0
- bbot/modules/gowitness.py +60 -30
- bbot/modules/graphql_introspection.py +145 -0
- bbot/modules/httpx.py +2 -0
- bbot/modules/hunt.py +10 -3
- bbot/modules/iis_shortnames.py +16 -7
- bbot/modules/internal/cloudcheck.py +65 -72
- bbot/modules/internal/unarchive.py +9 -3
- bbot/modules/lightfuzz/lightfuzz.py +6 -2
- bbot/modules/lightfuzz/submodules/esi.py +42 -0
- bbot/modules/{deadly/medusa.py → medusa.py} +4 -7
- bbot/modules/nuclei.py +2 -2
- bbot/modules/otx.py +9 -2
- bbot/modules/output/base.py +3 -11
- bbot/modules/paramminer_headers.py +10 -7
- bbot/modules/passivetotal.py +1 -1
- bbot/modules/portfilter.py +2 -0
- bbot/modules/portscan.py +1 -1
- bbot/modules/postman_download.py +2 -2
- bbot/modules/retirejs.py +232 -0
- bbot/modules/securitytxt.py +0 -3
- bbot/modules/sslcert.py +2 -2
- bbot/modules/subdomaincenter.py +1 -16
- bbot/modules/telerik.py +7 -2
- bbot/modules/templates/bucket.py +24 -4
- bbot/modules/templates/gitlab.py +98 -0
- bbot/modules/trufflehog.py +7 -4
- bbot/modules/wafw00f.py +2 -2
- bbot/presets/web/dotnet-audit.yml +1 -0
- bbot/presets/web/lightfuzz-heavy.yml +1 -1
- bbot/presets/web/lightfuzz-medium.yml +1 -1
- bbot/presets/web/lightfuzz-superheavy.yml +1 -1
- bbot/scanner/manager.py +44 -37
- bbot/scanner/scanner.py +17 -4
- bbot/scripts/benchmark_report.py +433 -0
- bbot/test/benchmarks/__init__.py +2 -0
- bbot/test/benchmarks/test_bloom_filter_benchmarks.py +105 -0
- bbot/test/benchmarks/test_closest_match_benchmarks.py +76 -0
- bbot/test/benchmarks/test_event_validation_benchmarks.py +438 -0
- bbot/test/benchmarks/test_excavate_benchmarks.py +291 -0
- bbot/test/benchmarks/test_ipaddress_benchmarks.py +143 -0
- bbot/test/benchmarks/test_weighted_shuffle_benchmarks.py +70 -0
- bbot/test/conftest.py +1 -1
- bbot/test/test_step_1/test_bbot_fastapi.py +2 -2
- bbot/test/test_step_1/test_events.py +22 -21
- bbot/test/test_step_1/test_helpers.py +20 -0
- bbot/test/test_step_1/test_manager_scope_accuracy.py +45 -0
- bbot/test/test_step_1/test_modules_basic.py +40 -15
- bbot/test/test_step_1/test_python_api.py +2 -2
- bbot/test/test_step_1/test_regexes.py +21 -4
- bbot/test/test_step_1/test_scan.py +7 -8
- bbot/test/test_step_1/test_web.py +46 -0
- bbot/test/test_step_2/module_tests/base.py +6 -1
- bbot/test/test_step_2/module_tests/test_module_aspnet_bin_exposure.py +73 -0
- bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py +52 -18
- bbot/test/test_step_2/module_tests/test_module_bucket_google.py +1 -1
- bbot/test/test_step_2/module_tests/{test_module_bucket_azure.py → test_module_bucket_microsoft.py} +7 -5
- bbot/test/test_step_2/module_tests/test_module_cloudcheck.py +19 -31
- bbot/test/test_step_2/module_tests/test_module_dnsbimi.py +2 -1
- bbot/test/test_step_2/module_tests/test_module_dnsdumpster.py +3 -5
- bbot/test/test_step_2/module_tests/test_module_emailformat.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_emails.py +2 -2
- bbot/test/test_step_2/module_tests/test_module_excavate.py +64 -5
- bbot/test/test_step_2/module_tests/test_module_extractous.py +13 -1
- bbot/test/test_step_2/module_tests/test_module_github_workflows.py +10 -1
- bbot/test/test_step_2/module_tests/test_module_gitlab_com.py +66 -0
- bbot/test/test_step_2/module_tests/{test_module_gitlab.py → test_module_gitlab_onprem.py} +4 -69
- bbot/test/test_step_2/module_tests/test_module_gowitness.py +5 -5
- bbot/test/test_step_2/module_tests/test_module_graphql_introspection.py +34 -0
- bbot/test/test_step_2/module_tests/test_module_iis_shortnames.py +46 -1
- bbot/test/test_step_2/module_tests/test_module_jadx.py +9 -0
- bbot/test/test_step_2/module_tests/test_module_lightfuzz.py +71 -3
- bbot/test/test_step_2/module_tests/test_module_nuclei.py +8 -6
- bbot/test/test_step_2/module_tests/test_module_otx.py +3 -0
- bbot/test/test_step_2/module_tests/test_module_portfilter.py +2 -0
- bbot/test/test_step_2/module_tests/test_module_retirejs.py +161 -0
- bbot/test/test_step_2/module_tests/test_module_telerik.py +1 -1
- bbot/test/test_step_2/module_tests/test_module_trufflehog.py +10 -1
- bbot/test/test_step_2/module_tests/test_module_unarchive.py +9 -0
- {bbot-2.5.0.dist-info → bbot-2.7.2.7424rc0.dist-info}/METADATA +12 -9
- {bbot-2.5.0.dist-info → bbot-2.7.2.7424rc0.dist-info}/RECORD +137 -124
- {bbot-2.5.0.dist-info → bbot-2.7.2.7424rc0.dist-info}/WHEEL +1 -1
- {bbot-2.5.0.dist-info → bbot-2.7.2.7424rc0.dist-info/licenses}/LICENSE +98 -58
- bbot/modules/binaryedge.py +0 -42
- bbot/modules/censys.py +0 -98
- bbot/modules/gitlab.py +0 -141
- bbot/modules/zoomeye.py +0 -77
- bbot/test/test_step_2/module_tests/test_module_binaryedge.py +0 -33
- bbot/test/test_step_2/module_tests/test_module_censys.py +0 -83
- bbot/test/test_step_2/module_tests/test_module_zoomeye.py +0 -35
- {bbot-2.5.0.dist-info → bbot-2.7.2.7424rc0.dist-info}/entry_points.txt +0 -0
bbot/modules/base.py
CHANGED
|
@@ -4,9 +4,10 @@ import traceback
|
|
|
4
4
|
from sys import exc_info
|
|
5
5
|
from contextlib import suppress
|
|
6
6
|
|
|
7
|
-
from ..errors import ValidationError
|
|
8
7
|
from ..core.helpers.misc import get_size # noqa
|
|
8
|
+
from ..errors import ValidationError, WebError
|
|
9
9
|
from ..core.helpers.async_helpers import TaskCounter, ShuffleQueue
|
|
10
|
+
from ..core.event import is_event
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class BaseModule:
|
|
@@ -53,6 +54,8 @@ class BaseModule:
|
|
|
53
54
|
|
|
54
55
|
in_scope_only (bool): Accept only explicitly in-scope events, regardless of the scan's search distance. Default is False.
|
|
55
56
|
|
|
57
|
+
accept_url_special (bool): Accept "special" URLs not typically distributed to web modules, e.g. JS URLs. Default is False.
|
|
58
|
+
|
|
56
59
|
options (Dict): Customizable options for the module, e.g., {"api_key": ""}. Empty dict by default.
|
|
57
60
|
|
|
58
61
|
options_desc (Dict): Descriptions for options, e.g., {"api_key": "API Key"}. Empty dict by default.
|
|
@@ -67,6 +70,8 @@ class BaseModule:
|
|
|
67
70
|
|
|
68
71
|
_stats_exclude (bool): Whether to exclude this module from scan statistics. Default is False.
|
|
69
72
|
|
|
73
|
+
_disable_auto_module_deps (bool): Whether to disable automatic module dependencies. This is useful e.g. if the module consumes URLs, but you don't want to automatically enable the httpx module. Default is False.
|
|
74
|
+
|
|
70
75
|
_qsize (int): Outgoing queue size (0 for infinite). Default is 0.
|
|
71
76
|
|
|
72
77
|
_priority (int): Priority level of the module. Lower values are higher priority. Default is 3.
|
|
@@ -97,17 +102,20 @@ class BaseModule:
|
|
|
97
102
|
scope_distance_modifier = 0
|
|
98
103
|
target_only = False
|
|
99
104
|
in_scope_only = False
|
|
100
|
-
|
|
105
|
+
accept_url_special = False
|
|
101
106
|
_module_threads = 1
|
|
102
107
|
_batch_size = 1
|
|
103
108
|
|
|
104
109
|
# disable the module after this many failed attempts in a row
|
|
105
110
|
_api_failure_abort_threshold = 3
|
|
111
|
+
# whether to retry on 429s when first pinging the API at scan start
|
|
112
|
+
_ping_retry_on_http_429 = False
|
|
106
113
|
|
|
107
114
|
default_discovery_context = "{module} discovered {event.type}: {event.data}"
|
|
108
115
|
|
|
109
116
|
_preserve_graph = False
|
|
110
117
|
_stats_exclude = False
|
|
118
|
+
_disable_auto_module_deps = False
|
|
111
119
|
_qsize = 1000
|
|
112
120
|
_priority = 3
|
|
113
121
|
_name = "base"
|
|
@@ -161,7 +169,6 @@ class BaseModule:
|
|
|
161
169
|
self._default_handle_batch_timeout = self.scan.config.get(
|
|
162
170
|
"module_handle_batch_timeout", 60 * 60 * 2
|
|
163
171
|
) # 2 hours
|
|
164
|
-
self._event_handler_watchdog_task = None
|
|
165
172
|
self._event_handler_watchdog_interval = self.event_handler_timeout / 10
|
|
166
173
|
|
|
167
174
|
# used for optional "per host" tracking
|
|
@@ -209,6 +216,14 @@ class BaseModule:
|
|
|
209
216
|
|
|
210
217
|
return True
|
|
211
218
|
|
|
219
|
+
async def setup_deps(self):
|
|
220
|
+
"""
|
|
221
|
+
Similar to setup(), but reserved for installing dependencies not covered by Ansible.
|
|
222
|
+
|
|
223
|
+
This should always be used to install static dependencies like AI models, wordlists, etc.
|
|
224
|
+
"""
|
|
225
|
+
return True
|
|
226
|
+
|
|
212
227
|
async def handle_event(self, event, **kwargs):
|
|
213
228
|
"""Asynchronously handles incoming events that the module is configured to watch.
|
|
214
229
|
|
|
@@ -381,8 +396,9 @@ class BaseModule:
|
|
|
381
396
|
"""
|
|
382
397
|
if url is None:
|
|
383
398
|
url = getattr(self, "ping_url", "")
|
|
399
|
+
retry_on_http_429 = getattr(self, "_ping_retry_on_http_429", False)
|
|
384
400
|
if url:
|
|
385
|
-
r = await self.api_request(url)
|
|
401
|
+
r = await self.api_request(url, retry_on_http_429=retry_on_http_429)
|
|
386
402
|
if getattr(r, "status_code", 0) != 200:
|
|
387
403
|
response_text = getattr(r, "text", "no response from server")
|
|
388
404
|
raise ValueError(response_text)
|
|
@@ -507,6 +523,12 @@ class BaseModule:
|
|
|
507
523
|
if (not args) or getattr(args[0], "module", None) is None:
|
|
508
524
|
kwargs["module"] = self
|
|
509
525
|
try:
|
|
526
|
+
if args and is_event(args[0]):
|
|
527
|
+
raise ValidationError(
|
|
528
|
+
f"{self.__class__.__name__}.make_event() does not accept an existing event "
|
|
529
|
+
f"({type(args[0]).__name__}) as the first argument. "
|
|
530
|
+
"Use update_event(event, ...) or emit_event(event, ...) instead."
|
|
531
|
+
)
|
|
510
532
|
event = self.scan.make_event(*args, **kwargs)
|
|
511
533
|
except ValidationError as e:
|
|
512
534
|
if raise_error:
|
|
@@ -515,6 +537,39 @@ class BaseModule:
|
|
|
515
537
|
return
|
|
516
538
|
return event
|
|
517
539
|
|
|
540
|
+
def update_event(self, event, **kwargs):
|
|
541
|
+
"""Update an existing event for the scan.
|
|
542
|
+
|
|
543
|
+
This is the counterpart to :meth:`make_event` for modifying an existing
|
|
544
|
+
:class:`bbot.core.event.base.BaseEvent` instance.
|
|
545
|
+
|
|
546
|
+
Raises a validation error if the update could not be applied, unless
|
|
547
|
+
``raise_error`` is set to False.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
event: The event object to update.
|
|
551
|
+
**kwargs: Keyword arguments to be passed to the scan's update_event method.
|
|
552
|
+
raise_error (bool, optional): Whether to raise a validation error if the event could not be updated. Defaults to False.
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
Event or None: The updated event, or None if a validation error occurred and raise_error was False.
|
|
556
|
+
|
|
557
|
+
Raises:
|
|
558
|
+
ValidationError: If the event could not be validated and raise_error is True.
|
|
559
|
+
"""
|
|
560
|
+
raise_error = kwargs.pop("raise_error", False)
|
|
561
|
+
module = kwargs.pop("module", None)
|
|
562
|
+
if module is None and getattr(event, "module", None) is None:
|
|
563
|
+
kwargs["module"] = self
|
|
564
|
+
try:
|
|
565
|
+
updated = self.scan.update_event(event, **kwargs)
|
|
566
|
+
except ValidationError as e:
|
|
567
|
+
if raise_error:
|
|
568
|
+
raise
|
|
569
|
+
self.warning(f"{e}")
|
|
570
|
+
return
|
|
571
|
+
return updated
|
|
572
|
+
|
|
518
573
|
async def emit_event(self, *args, **kwargs):
|
|
519
574
|
"""Emit an event to the event queue and distribute it to interested modules.
|
|
520
575
|
|
|
@@ -550,7 +605,23 @@ class BaseModule:
|
|
|
550
605
|
v = event_kwargs.pop(o, None)
|
|
551
606
|
if v is not None:
|
|
552
607
|
emit_kwargs[o] = v
|
|
553
|
-
|
|
608
|
+
|
|
609
|
+
# Two entry points:
|
|
610
|
+
# - emit_event(data, ...) -> create a new event via make_event()
|
|
611
|
+
# - emit_event(existing_event, ...) -> update and re‑emit that event
|
|
612
|
+
if args and is_event(args[0]):
|
|
613
|
+
event, *rest = args
|
|
614
|
+
if rest:
|
|
615
|
+
self.warning(
|
|
616
|
+
f"emit_event() was called on {self.name} with an existing event and extra "
|
|
617
|
+
f"positional args ({rest}); extra args are ignored. "
|
|
618
|
+
"Pass only the event plus keyword arguments, or call make_event() explicitly."
|
|
619
|
+
)
|
|
620
|
+
# Update the existing event (e.g. tags/context/module) before emitting
|
|
621
|
+
event = self.update_event(event, **event_kwargs)
|
|
622
|
+
else:
|
|
623
|
+
event = self.make_event(*args, **event_kwargs)
|
|
624
|
+
|
|
554
625
|
if event is not None:
|
|
555
626
|
children = event.children
|
|
556
627
|
for e in [event] + children:
|
|
@@ -610,44 +681,32 @@ class BaseModule:
|
|
|
610
681
|
asyncio.create_task(self._worker(), name=f"{self.scan.name}.{self.name}._worker()")
|
|
611
682
|
for _ in range(self.module_threads)
|
|
612
683
|
]
|
|
613
|
-
|
|
684
|
+
watchdog_task = asyncio.create_task(
|
|
614
685
|
self._event_handler_watchdog(),
|
|
615
686
|
name=f"{self.scan.name}.{self.name}._event_handler_watchdog()",
|
|
616
687
|
)
|
|
688
|
+
self._tasks.append(watchdog_task)
|
|
617
689
|
|
|
618
|
-
async def _setup(self):
|
|
619
|
-
"""
|
|
620
|
-
Asynchronously sets up the module by invoking its 'setup()' method.
|
|
621
|
-
|
|
622
|
-
This method catches exceptions during setup, sets the module's error state if necessary, and determines the
|
|
623
|
-
status code based on the result of the setup process.
|
|
624
|
-
|
|
625
|
-
Args:
|
|
626
|
-
None
|
|
627
|
-
|
|
628
|
-
Returns:
|
|
629
|
-
tuple: A tuple containing the module's name, status (True for success, False for hard-fail, None for soft-fail),
|
|
630
|
-
and an optional status message.
|
|
631
|
-
|
|
632
|
-
Raises:
|
|
633
|
-
Exception: Captured exceptions from the 'setup()' method are logged, but not propagated.
|
|
634
|
-
|
|
635
|
-
Notes:
|
|
636
|
-
- The 'setup()' method can return either a simple boolean status or a tuple of status and message.
|
|
637
|
-
- A WordlistError exception triggers a soft-fail status.
|
|
638
|
-
- The debug log will contain setup status information for the module.
|
|
639
|
-
"""
|
|
690
|
+
async def _setup(self, deps_only=False):
|
|
691
|
+
""" """
|
|
640
692
|
status_codes = {False: "hard-fail", None: "soft-fail", True: "success"}
|
|
641
693
|
|
|
642
694
|
status = False
|
|
643
695
|
self.debug(f"Setting up module {self.name}")
|
|
644
696
|
try:
|
|
645
|
-
|
|
646
|
-
if
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
697
|
+
funcs = [self.setup_deps]
|
|
698
|
+
if not deps_only:
|
|
699
|
+
funcs.append(self.setup)
|
|
700
|
+
for func in funcs:
|
|
701
|
+
self.debug(f"Running {self.name}.{func.__name__}()")
|
|
702
|
+
result = await func()
|
|
703
|
+
if type(result) == tuple and len(result) == 2:
|
|
704
|
+
status, msg = result
|
|
705
|
+
else:
|
|
706
|
+
status = result
|
|
707
|
+
msg = status_codes[status]
|
|
708
|
+
if status is False:
|
|
709
|
+
break
|
|
651
710
|
self.debug(f"Finished setting up module {self.name}")
|
|
652
711
|
except Exception as e:
|
|
653
712
|
self.set_error_state(f"Unexpected error during module setup: {e}", critical=True)
|
|
@@ -735,6 +794,9 @@ class BaseModule:
|
|
|
735
794
|
|
|
736
795
|
@property
|
|
737
796
|
def max_scope_distance(self):
|
|
797
|
+
"""
|
|
798
|
+
Maximum scope distance for events that are accepted by the module.
|
|
799
|
+
"""
|
|
738
800
|
if self.in_scope_only or self.target_only:
|
|
739
801
|
return 0
|
|
740
802
|
if self.scope_distance_modifier is None:
|
|
@@ -782,10 +844,14 @@ class BaseModule:
|
|
|
782
844
|
if "target" not in event.tags:
|
|
783
845
|
return False, "it did not meet target_only filter criteria"
|
|
784
846
|
|
|
785
|
-
#
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
847
|
+
# limit js URLs to modules that opt in to receive them
|
|
848
|
+
if (not self.accept_url_special) and event.type.startswith("URL"):
|
|
849
|
+
extension = getattr(event, "url_extension", "")
|
|
850
|
+
if extension in self.scan.url_extension_special:
|
|
851
|
+
return (
|
|
852
|
+
False,
|
|
853
|
+
f"it is a special URL (extension {extension}) but the module does not opt in to receive special URLs",
|
|
854
|
+
)
|
|
789
855
|
|
|
790
856
|
return True, "precheck succeeded"
|
|
791
857
|
|
|
@@ -1200,6 +1266,7 @@ class BaseModule:
|
|
|
1200
1266
|
- cancelling after too many failed attempts
|
|
1201
1267
|
"""
|
|
1202
1268
|
url = args[0] if args else kwargs.pop("url", "")
|
|
1269
|
+
retry_on_http_429 = kwargs.pop("retry_on_http_429", True)
|
|
1203
1270
|
|
|
1204
1271
|
# loop until we have a successful request
|
|
1205
1272
|
for _ in range(self.api_retries):
|
|
@@ -1225,7 +1292,7 @@ class BaseModule:
|
|
|
1225
1292
|
else:
|
|
1226
1293
|
# sleep for a bit if we're being rate limited
|
|
1227
1294
|
retry_after = self._get_retry_after(r)
|
|
1228
|
-
if retry_after or status_code == 429:
|
|
1295
|
+
if (retry_after or status_code == 429) and retry_on_http_429:
|
|
1229
1296
|
sleep_interval = int(retry_after) if retry_after is not None else self._429_sleep_interval
|
|
1230
1297
|
if retry_after and retry_after > self._429_max_sleep_interval:
|
|
1231
1298
|
self.verbose(
|
|
@@ -1244,6 +1311,24 @@ class BaseModule:
|
|
|
1244
1311
|
|
|
1245
1312
|
return r
|
|
1246
1313
|
|
|
1314
|
+
async def api_download(self, url, **kwargs):
|
|
1315
|
+
"""
|
|
1316
|
+
A wrapper around the `download()` web helper that incorporates API key cycling.
|
|
1317
|
+
"""
|
|
1318
|
+
error = None
|
|
1319
|
+
raise_error = kwargs.pop("raise_error", False)
|
|
1320
|
+
for _ in range(self.api_retries):
|
|
1321
|
+
new_url, kwargs = self.prepare_api_request(url, kwargs)
|
|
1322
|
+
if "raise_error" not in kwargs:
|
|
1323
|
+
kwargs["raise_error"] = True
|
|
1324
|
+
try:
|
|
1325
|
+
return await self.helpers.download(new_url, **kwargs)
|
|
1326
|
+
except WebError as e:
|
|
1327
|
+
error = e
|
|
1328
|
+
self.cycle_api_key()
|
|
1329
|
+
if raise_error:
|
|
1330
|
+
raise error
|
|
1331
|
+
|
|
1247
1332
|
def _get_retry_after(self, r):
|
|
1248
1333
|
# try to get retry_after from headers first
|
|
1249
1334
|
headers = getattr(r, "headers", {})
|
|
@@ -1255,7 +1340,10 @@ class BaseModule:
|
|
|
1255
1340
|
if isinstance(body_json, dict):
|
|
1256
1341
|
retry_after = body_json.get("retry_after", None)
|
|
1257
1342
|
if retry_after is not None:
|
|
1258
|
-
|
|
1343
|
+
# we don't allow retry-after smaller than 1 second
|
|
1344
|
+
# this is to prevent cases where APIs erroneously return a retry-after value of 0
|
|
1345
|
+
# e.g. https://github.com/blacklanternsecurity/bbot/issues/2826
|
|
1346
|
+
return max(1.0, float(retry_after))
|
|
1259
1347
|
|
|
1260
1348
|
def _prepare_api_iter_req(self, url, page, page_size, offset, **requests_kwargs):
|
|
1261
1349
|
"""
|
|
@@ -1711,6 +1799,7 @@ class BaseInterceptModule(BaseModule):
|
|
|
1711
1799
|
"""
|
|
1712
1800
|
|
|
1713
1801
|
accept_dupes = True
|
|
1802
|
+
accept_url_special = True
|
|
1714
1803
|
_intercept = True
|
|
1715
1804
|
|
|
1716
1805
|
async def _worker(self):
|
bbot/modules/bucket_amazon.py
CHANGED
|
@@ -15,7 +15,7 @@ class bucket_digitalocean(bucket_template):
|
|
|
15
15
|
"permutations": "Whether to try permutations",
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
cloudcheck_provider_name = "DigitalOcean"
|
|
19
19
|
delimiters = ("", "-")
|
|
20
20
|
base_domains = ["digitaloceanspaces.com"]
|
|
21
21
|
regions = ["ams3", "fra1", "nyc3", "sfo2", "sfo3", "sgp1"]
|
bbot/modules/bucket_firebase.py
CHANGED
bbot/modules/bucket_google.py
CHANGED
|
@@ -19,7 +19,7 @@ class bucket_google(bucket_template):
|
|
|
19
19
|
"permutations": "Whether to try permutations",
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
cloudcheck_provider_name = "Google"
|
|
23
23
|
delimiters = ("", "-", ".", "_")
|
|
24
24
|
base_domains = ["storage.googleapis.com"]
|
|
25
25
|
bad_permissions = [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from bbot.modules.templates.bucket import bucket_template
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
class
|
|
4
|
+
class bucket_microsoft(bucket_template):
|
|
5
5
|
watched_events = ["DNS_NAME", "STORAGE_BUCKET"]
|
|
6
6
|
produced_events = ["STORAGE_BUCKET", "FINDING"]
|
|
7
7
|
flags = ["active", "safe", "cloud-enum", "web-basic"]
|
|
@@ -15,7 +15,7 @@ class bucket_azure(bucket_template):
|
|
|
15
15
|
"permutations": "Whether to try permutations",
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
cloudcheck_provider_name = "Microsoft"
|
|
19
19
|
delimiters = ("", "-")
|
|
20
20
|
base_domains = ["blob.core.windows.net"]
|
|
21
21
|
# Dirbusting is required to know whether a bucket is public
|
bbot/modules/builtwith.py
CHANGED
|
@@ -33,7 +33,8 @@ class builtwith(subdomain_enum_apikey):
|
|
|
33
33
|
subdomains = await self.query(query, parse_fn=self.parse_domains, request_fn=self.request_domains)
|
|
34
34
|
if subdomains:
|
|
35
35
|
for s in subdomains:
|
|
36
|
-
|
|
36
|
+
# `s` is a hostname string; compare against the event's data, not the Event object itself.
|
|
37
|
+
if s != event.data:
|
|
37
38
|
await self.emit_event(
|
|
38
39
|
s,
|
|
39
40
|
"DNS_NAME",
|
|
@@ -45,7 +46,8 @@ class builtwith(subdomain_enum_apikey):
|
|
|
45
46
|
redirects = await self.query(query, parse_fn=self.parse_redirects, request_fn=self.request_redirects)
|
|
46
47
|
if redirects:
|
|
47
48
|
for r in redirects:
|
|
48
|
-
|
|
49
|
+
# `r` is a hostname string; compare against the event's data, not the Event object itself.
|
|
50
|
+
if r != event.data:
|
|
49
51
|
await self.emit_event(
|
|
50
52
|
r,
|
|
51
53
|
"DNS_NAME",
|
bbot/modules/c99.py
CHANGED
|
@@ -19,7 +19,7 @@ class c99(subdomain_enum_apikey):
|
|
|
19
19
|
|
|
20
20
|
async def ping(self):
|
|
21
21
|
url = f"{self.base_url}/randomnumber?key={{api_key}}&between=1,100&json"
|
|
22
|
-
response = await self.api_request(url)
|
|
22
|
+
response = await self.api_request(url, retry_on_http_429=False)
|
|
23
23
|
assert response.json()["success"] is True, getattr(response, "text", "no response from server")
|
|
24
24
|
|
|
25
25
|
async def request_url(self, query):
|
bbot/modules/dnsbimi.py
CHANGED
|
@@ -39,7 +39,7 @@ import re
|
|
|
39
39
|
# Handle "v=BIMI1; l=https://bimi.entrust.net/example.com/logo.svg;"
|
|
40
40
|
# Handle "v=BIMI1;l=https://bimi.entrust.net/example.com/logo.svg;a=https://bimi.entrust.net/example.com/certchain.pem"
|
|
41
41
|
# Handle "v=BIMI1; l=https://bimi.entrust.net/example.com/logo.svg;a=https://bimi.entrust.net/example.com/certchain.pem;"
|
|
42
|
-
_bimi_regex = r"^v=(?P<v>BIMI1)
|
|
42
|
+
_bimi_regex = r"^v=(?P<v>BIMI1);\s?(?:l=(?P<l>https?://[^;\s]{1,255})?)?;?(?:\s?a=(?P<a>https://[^;\s]{1,255})?;?)?$"
|
|
43
43
|
bimi_regex = re.compile(_bimi_regex, re.I)
|
|
44
44
|
|
|
45
45
|
|
|
@@ -140,6 +140,3 @@ class dnsbimi(BaseModule):
|
|
|
140
140
|
|
|
141
141
|
async def handle_event(self, event):
|
|
142
142
|
await self.inspectBIMI(event, event.host)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
# EOF
|
bbot/modules/dnsbrute.py
CHANGED
|
@@ -23,9 +23,14 @@ class dnsbrute(subdomain_enum):
|
|
|
23
23
|
dedup_strategy = "lowest_parent"
|
|
24
24
|
_qsize = 10000
|
|
25
25
|
|
|
26
|
+
async def setup_deps(self):
|
|
27
|
+
self.subdomain_file = await self.helpers.wordlist(self.config.get("wordlist"))
|
|
28
|
+
# tell the dnsbrute helper to fetch the resolver file
|
|
29
|
+
await self.helpers.dns.brute.resolver_file()
|
|
30
|
+
return True
|
|
31
|
+
|
|
26
32
|
async def setup(self):
|
|
27
33
|
self.max_depth = max(1, self.config.get("max_depth", 5))
|
|
28
|
-
self.subdomain_file = await self.helpers.wordlist(self.config.get("wordlist"))
|
|
29
34
|
self.subdomain_list = set(self.helpers.read_file(self.subdomain_file))
|
|
30
35
|
self.wordlist_size = len(self.subdomain_list)
|
|
31
36
|
return await super().setup()
|
bbot/modules/dnscommonsrv.py
CHANGED
|
@@ -8,6 +8,7 @@ class dnscommonsrv(subdomain_enum):
|
|
|
8
8
|
flags = ["subdomain-enum", "active", "safe"]
|
|
9
9
|
meta = {"description": "Check for common SRV records", "created_date": "2022-05-15", "author": "@TheTechromancer"}
|
|
10
10
|
dedup_strategy = "lowest_parent"
|
|
11
|
+
deps_common = ["massdns"]
|
|
11
12
|
|
|
12
13
|
options = {"max_depth": 2}
|
|
13
14
|
options_desc = {"max_depth": "The maximum subdomain depth to brute-force SRV records"}
|
bbot/modules/dnsdumpster.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import json
|
|
2
2
|
|
|
3
3
|
from bbot.modules.templates.subdomain_enum import subdomain_enum
|
|
4
4
|
|
|
@@ -15,78 +15,61 @@ class dnsdumpster(subdomain_enum):
|
|
|
15
15
|
|
|
16
16
|
base_url = "https://dnsdumpster.com"
|
|
17
17
|
|
|
18
|
+
async def setup(self):
|
|
19
|
+
self.apikey_regex = self.helpers.re.compile(r'<form[^>]*data-form-id="mainform"[^>]*hx-headers=\'([^\']*)\'')
|
|
20
|
+
return True
|
|
21
|
+
|
|
18
22
|
async def query(self, domain):
|
|
19
23
|
ret = []
|
|
20
|
-
# first, get the
|
|
24
|
+
# first, get the JWT token from the main page
|
|
21
25
|
res1 = await self.api_request(self.base_url)
|
|
22
26
|
status_code = getattr(res1, "status_code", 0)
|
|
23
|
-
if status_code in [
|
|
24
|
-
self.verbose(f'Too many requests "{status_code}"')
|
|
25
|
-
return ret
|
|
26
|
-
elif status_code not in [200]:
|
|
27
|
+
if status_code not in [200]:
|
|
27
28
|
self.verbose(f'Bad response code "{status_code}" from DNSDumpster')
|
|
28
29
|
return ret
|
|
29
|
-
else:
|
|
30
|
-
self.debug(f'Valid response code "{status_code}" from DNSDumpster')
|
|
31
|
-
|
|
32
|
-
html = self.helpers.beautifulsoup(res1.content, "html.parser")
|
|
33
|
-
if html is False:
|
|
34
|
-
self.verbose("BeautifulSoup returned False")
|
|
35
|
-
return ret
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
# Extract JWT token from the form's hx-headers attribute using regex
|
|
32
|
+
jwt_token = None
|
|
39
33
|
try:
|
|
40
|
-
for
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
except AttributeError:
|
|
50
|
-
pass
|
|
34
|
+
# Look for the form with data-form-id="mainform" and extract hx-headers
|
|
35
|
+
form_match = await self.helpers.re.search(self.apikey_regex, res1.text)
|
|
36
|
+
if form_match:
|
|
37
|
+
headers_json = form_match.group(1)
|
|
38
|
+
headers_data = json.loads(headers_json)
|
|
39
|
+
jwt_token = headers_data.get("Authorization")
|
|
40
|
+
except (AttributeError, json.JSONDecodeError, KeyError):
|
|
41
|
+
self.log.warning("Error obtaining JWT token")
|
|
42
|
+
return ret
|
|
51
43
|
|
|
52
|
-
# Abort if we didn't get the
|
|
53
|
-
if not
|
|
54
|
-
self.verbose("Error obtaining
|
|
44
|
+
# Abort if we didn't get the JWT token
|
|
45
|
+
if not jwt_token:
|
|
46
|
+
self.verbose("Error obtaining JWT token")
|
|
55
47
|
self.errorState = True
|
|
56
48
|
return ret
|
|
57
49
|
else:
|
|
58
|
-
self.debug("Successfully obtained
|
|
50
|
+
self.debug("Successfully obtained JWT token")
|
|
59
51
|
|
|
60
52
|
if self.scan.stopping:
|
|
61
|
-
return
|
|
53
|
+
return ret
|
|
62
54
|
|
|
63
|
-
#
|
|
64
|
-
subdomains = set()
|
|
55
|
+
# Query the API with the JWT token
|
|
65
56
|
res2 = await self.api_request(
|
|
66
|
-
|
|
57
|
+
"https://api.dnsdumpster.com/htmld/",
|
|
67
58
|
method="POST",
|
|
68
|
-
|
|
69
|
-
data={
|
|
70
|
-
"csrfmiddlewaretoken": csrfmiddlewaretoken,
|
|
71
|
-
"targetip": str(domain).lower(),
|
|
72
|
-
"user": "free",
|
|
73
|
-
},
|
|
59
|
+
data={"target": str(domain).lower()},
|
|
74
60
|
headers={
|
|
75
|
-
"
|
|
76
|
-
"
|
|
61
|
+
"Authorization": jwt_token,
|
|
62
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
63
|
+
"Origin": "https://dnsdumpster.com",
|
|
64
|
+
"Referer": "https://dnsdumpster.com/",
|
|
65
|
+
"HX-Request": "true",
|
|
66
|
+
"HX-Target": "results",
|
|
67
|
+
"HX-Current-URL": "https://dnsdumpster.com/",
|
|
77
68
|
},
|
|
78
69
|
)
|
|
79
70
|
status_code = getattr(res2, "status_code", 0)
|
|
80
71
|
if status_code not in [200]:
|
|
81
|
-
self.verbose(f'Bad response code "{status_code}" from DNSDumpster')
|
|
82
|
-
return ret
|
|
83
|
-
html = self.helpers.beautifulsoup(res2.content, "html.parser")
|
|
84
|
-
if html is False:
|
|
85
|
-
self.verbose("BeautifulSoup returned False")
|
|
72
|
+
self.verbose(f'Bad response code "{status_code}" from DNSDumpster API')
|
|
86
73
|
return ret
|
|
87
|
-
escaped_domain = re.escape(domain)
|
|
88
|
-
match_pattern = re.compile(r"^[\w\.-]+\." + escaped_domain + r"$")
|
|
89
|
-
for subdomain in html.findAll(text=match_pattern):
|
|
90
|
-
subdomains.add(str(subdomain).strip().lower())
|
|
91
74
|
|
|
92
|
-
return
|
|
75
|
+
return await self.scan.extract_in_scope_hostnames(res2.text)
|
bbot/modules/dnstlsrpt.py
CHANGED
|
@@ -44,20 +44,17 @@ class dnstlsrpt(BaseModule):
|
|
|
44
44
|
"emit_emails": True,
|
|
45
45
|
"emit_raw_dns_records": False,
|
|
46
46
|
"emit_urls": True,
|
|
47
|
-
"emit_vulnerabilities": True,
|
|
48
47
|
}
|
|
49
48
|
options_desc = {
|
|
50
49
|
"emit_emails": "Emit EMAIL_ADDRESS events",
|
|
51
50
|
"emit_raw_dns_records": "Emit RAW_DNS_RECORD events",
|
|
52
51
|
"emit_urls": "Emit URL_UNVERIFIED events",
|
|
53
|
-
"emit_vulnerabilities": "Emit VULNERABILITY events",
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
async def setup(self):
|
|
57
55
|
self.emit_emails = self.config.get("emit_emails", True)
|
|
58
56
|
self.emit_raw_dns_records = self.config.get("emit_raw_dns_records", False)
|
|
59
57
|
self.emit_urls = self.config.get("emit_urls", True)
|
|
60
|
-
self.emit_vulnerabilities = self.config.get("emit_vulnerabilities", True)
|
|
61
58
|
return await super().setup()
|
|
62
59
|
|
|
63
60
|
def _incoming_dedup_hash(self, event):
|
|
@@ -139,6 +136,3 @@ class dnstlsrpt(BaseModule):
|
|
|
139
136
|
tags=tags.append(f"tlsrpt-record-{key}"),
|
|
140
137
|
parent=event,
|
|
141
138
|
)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
# EOF
|
bbot/modules/docker_pull.py
CHANGED
|
@@ -8,7 +8,7 @@ from bbot.modules.base import BaseModule
|
|
|
8
8
|
class docker_pull(BaseModule):
|
|
9
9
|
watched_events = ["CODE_REPOSITORY"]
|
|
10
10
|
produced_events = ["FILESYSTEM"]
|
|
11
|
-
flags = ["passive", "safe", "slow", "code-enum"]
|
|
11
|
+
flags = ["passive", "safe", "slow", "code-enum", "download"]
|
|
12
12
|
meta = {
|
|
13
13
|
"description": "Download images from a docker repository",
|
|
14
14
|
"created_date": "2024-03-24",
|
|
@@ -38,7 +38,7 @@ class docker_pull(BaseModule):
|
|
|
38
38
|
if output_folder:
|
|
39
39
|
self.output_dir = Path(output_folder) / "docker_images"
|
|
40
40
|
else:
|
|
41
|
-
self.output_dir = self.
|
|
41
|
+
self.output_dir = self.scan.temp_dir / "docker_images"
|
|
42
42
|
self.helpers.mkdir(self.output_dir)
|
|
43
43
|
return await super().setup()
|
|
44
44
|
|
bbot/modules/emailformat.py
CHANGED
|
@@ -15,13 +15,29 @@ class emailformat(BaseModule):
|
|
|
15
15
|
|
|
16
16
|
base_url = "https://www.email-format.com"
|
|
17
17
|
|
|
18
|
+
async def setup(self):
|
|
19
|
+
self.cfemail_regex = self.helpers.re.compile(r'data-cfemail="([0-9a-z]+)"')
|
|
20
|
+
return True
|
|
21
|
+
|
|
18
22
|
async def handle_event(self, event):
|
|
19
23
|
_, query = self.helpers.split_domain(event.data)
|
|
20
24
|
url = f"{self.base_url}/d/{self.helpers.quote(query)}/"
|
|
21
25
|
r = await self.api_request(url)
|
|
22
26
|
if not r:
|
|
23
27
|
return
|
|
24
|
-
|
|
28
|
+
|
|
29
|
+
encrypted_emails = await self.helpers.re.findall(self.cfemail_regex, r.text)
|
|
30
|
+
|
|
31
|
+
for enc in encrypted_emails:
|
|
32
|
+
enc_len = len(enc)
|
|
33
|
+
|
|
34
|
+
if enc_len < 2 or enc_len % 2 != 0:
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
key = int(enc[:2], 16)
|
|
38
|
+
|
|
39
|
+
email = "".join([chr(int(enc[i : i + 2], 16) ^ key) for i in range(2, enc_len, 2)]).lower()
|
|
40
|
+
|
|
25
41
|
if email.endswith(query):
|
|
26
42
|
await self.emit_event(
|
|
27
43
|
email,
|
bbot/modules/ffuf.py
CHANGED
|
@@ -37,12 +37,15 @@ class ffuf(BaseModule):
|
|
|
37
37
|
|
|
38
38
|
in_scope_only = True
|
|
39
39
|
|
|
40
|
+
async def setup_deps(self):
|
|
41
|
+
self.wordlist = await self.helpers.wordlist(self.config.get("wordlist"))
|
|
42
|
+
return True
|
|
43
|
+
|
|
40
44
|
async def setup(self):
|
|
41
45
|
self.proxy = self.scan.web_config.get("http_proxy", "")
|
|
42
46
|
self.canary = "".join(random.choice(string.ascii_lowercase) for i in range(10))
|
|
43
47
|
wordlist_url = self.config.get("wordlist", "")
|
|
44
48
|
self.debug(f"Using wordlist [{wordlist_url}]")
|
|
45
|
-
self.wordlist = await self.helpers.wordlist(wordlist_url)
|
|
46
49
|
self.wordlist_lines = self.generate_wordlist(self.wordlist)
|
|
47
50
|
self.tempfile, tempfile_len = self.generate_templist()
|
|
48
51
|
self.rate = self.config.get("rate", 0)
|