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.
Files changed (144) hide show
  1. bbot/__init__.py +1 -1
  2. bbot/cli.py +22 -8
  3. bbot/core/engine.py +1 -1
  4. bbot/core/event/__init__.py +2 -2
  5. bbot/core/event/base.py +138 -110
  6. bbot/core/flags.py +1 -0
  7. bbot/core/helpers/bloom.py +6 -7
  8. bbot/core/helpers/command.py +5 -2
  9. bbot/core/helpers/depsinstaller/installer.py +78 -7
  10. bbot/core/helpers/dns/dns.py +0 -1
  11. bbot/core/helpers/dns/engine.py +0 -2
  12. bbot/core/helpers/files.py +2 -2
  13. bbot/core/helpers/git.py +17 -0
  14. bbot/core/helpers/helper.py +6 -5
  15. bbot/core/helpers/misc.py +15 -28
  16. bbot/core/helpers/names_generator.py +5 -0
  17. bbot/core/helpers/ntlm.py +0 -2
  18. bbot/core/helpers/regex.py +1 -1
  19. bbot/core/helpers/regexes.py +25 -8
  20. bbot/core/helpers/web/engine.py +1 -1
  21. bbot/core/helpers/web/web.py +2 -1
  22. bbot/core/modules.py +22 -60
  23. bbot/core/shared_deps.py +38 -0
  24. bbot/defaults.yml +4 -2
  25. bbot/modules/apkpure.py +2 -2
  26. bbot/modules/aspnet_bin_exposure.py +80 -0
  27. bbot/modules/baddns.py +1 -1
  28. bbot/modules/baddns_direct.py +1 -1
  29. bbot/modules/baddns_zone.py +1 -1
  30. bbot/modules/badsecrets.py +1 -1
  31. bbot/modules/base.py +129 -40
  32. bbot/modules/bucket_amazon.py +1 -1
  33. bbot/modules/bucket_digitalocean.py +1 -1
  34. bbot/modules/bucket_firebase.py +1 -1
  35. bbot/modules/bucket_google.py +1 -1
  36. bbot/modules/{bucket_azure.py → bucket_microsoft.py} +2 -2
  37. bbot/modules/builtwith.py +4 -2
  38. bbot/modules/c99.py +1 -1
  39. bbot/modules/dnsbimi.py +1 -4
  40. bbot/modules/dnsbrute.py +6 -1
  41. bbot/modules/dnscommonsrv.py +1 -0
  42. bbot/modules/dnsdumpster.py +35 -52
  43. bbot/modules/dnstlsrpt.py +0 -6
  44. bbot/modules/docker_pull.py +2 -2
  45. bbot/modules/emailformat.py +17 -1
  46. bbot/modules/ffuf.py +4 -1
  47. bbot/modules/ffuf_shortnames.py +6 -3
  48. bbot/modules/filedownload.py +8 -5
  49. bbot/modules/fullhunt.py +1 -1
  50. bbot/modules/git_clone.py +47 -22
  51. bbot/modules/gitdumper.py +5 -15
  52. bbot/modules/github_workflows.py +6 -5
  53. bbot/modules/gitlab_com.py +31 -0
  54. bbot/modules/gitlab_onprem.py +84 -0
  55. bbot/modules/gowitness.py +60 -30
  56. bbot/modules/graphql_introspection.py +145 -0
  57. bbot/modules/httpx.py +2 -0
  58. bbot/modules/hunt.py +10 -3
  59. bbot/modules/iis_shortnames.py +16 -7
  60. bbot/modules/internal/cloudcheck.py +65 -72
  61. bbot/modules/internal/unarchive.py +9 -3
  62. bbot/modules/lightfuzz/lightfuzz.py +6 -2
  63. bbot/modules/lightfuzz/submodules/esi.py +42 -0
  64. bbot/modules/{deadly/medusa.py → medusa.py} +4 -7
  65. bbot/modules/nuclei.py +2 -2
  66. bbot/modules/otx.py +9 -2
  67. bbot/modules/output/base.py +3 -11
  68. bbot/modules/paramminer_headers.py +10 -7
  69. bbot/modules/passivetotal.py +1 -1
  70. bbot/modules/portfilter.py +2 -0
  71. bbot/modules/portscan.py +1 -1
  72. bbot/modules/postman_download.py +2 -2
  73. bbot/modules/retirejs.py +232 -0
  74. bbot/modules/securitytxt.py +0 -3
  75. bbot/modules/sslcert.py +2 -2
  76. bbot/modules/subdomaincenter.py +1 -16
  77. bbot/modules/telerik.py +7 -2
  78. bbot/modules/templates/bucket.py +24 -4
  79. bbot/modules/templates/gitlab.py +98 -0
  80. bbot/modules/trufflehog.py +7 -4
  81. bbot/modules/wafw00f.py +2 -2
  82. bbot/presets/web/dotnet-audit.yml +1 -0
  83. bbot/presets/web/lightfuzz-heavy.yml +1 -1
  84. bbot/presets/web/lightfuzz-medium.yml +1 -1
  85. bbot/presets/web/lightfuzz-superheavy.yml +1 -1
  86. bbot/scanner/manager.py +44 -37
  87. bbot/scanner/scanner.py +17 -4
  88. bbot/scripts/benchmark_report.py +433 -0
  89. bbot/test/benchmarks/__init__.py +2 -0
  90. bbot/test/benchmarks/test_bloom_filter_benchmarks.py +105 -0
  91. bbot/test/benchmarks/test_closest_match_benchmarks.py +76 -0
  92. bbot/test/benchmarks/test_event_validation_benchmarks.py +438 -0
  93. bbot/test/benchmarks/test_excavate_benchmarks.py +291 -0
  94. bbot/test/benchmarks/test_ipaddress_benchmarks.py +143 -0
  95. bbot/test/benchmarks/test_weighted_shuffle_benchmarks.py +70 -0
  96. bbot/test/conftest.py +1 -1
  97. bbot/test/test_step_1/test_bbot_fastapi.py +2 -2
  98. bbot/test/test_step_1/test_events.py +22 -21
  99. bbot/test/test_step_1/test_helpers.py +20 -0
  100. bbot/test/test_step_1/test_manager_scope_accuracy.py +45 -0
  101. bbot/test/test_step_1/test_modules_basic.py +40 -15
  102. bbot/test/test_step_1/test_python_api.py +2 -2
  103. bbot/test/test_step_1/test_regexes.py +21 -4
  104. bbot/test/test_step_1/test_scan.py +7 -8
  105. bbot/test/test_step_1/test_web.py +46 -0
  106. bbot/test/test_step_2/module_tests/base.py +6 -1
  107. bbot/test/test_step_2/module_tests/test_module_aspnet_bin_exposure.py +73 -0
  108. bbot/test/test_step_2/module_tests/test_module_bucket_amazon.py +52 -18
  109. bbot/test/test_step_2/module_tests/test_module_bucket_google.py +1 -1
  110. bbot/test/test_step_2/module_tests/{test_module_bucket_azure.py → test_module_bucket_microsoft.py} +7 -5
  111. bbot/test/test_step_2/module_tests/test_module_cloudcheck.py +19 -31
  112. bbot/test/test_step_2/module_tests/test_module_dnsbimi.py +2 -1
  113. bbot/test/test_step_2/module_tests/test_module_dnsdumpster.py +3 -5
  114. bbot/test/test_step_2/module_tests/test_module_emailformat.py +1 -1
  115. bbot/test/test_step_2/module_tests/test_module_emails.py +2 -2
  116. bbot/test/test_step_2/module_tests/test_module_excavate.py +64 -5
  117. bbot/test/test_step_2/module_tests/test_module_extractous.py +13 -1
  118. bbot/test/test_step_2/module_tests/test_module_github_workflows.py +10 -1
  119. bbot/test/test_step_2/module_tests/test_module_gitlab_com.py +66 -0
  120. bbot/test/test_step_2/module_tests/{test_module_gitlab.py → test_module_gitlab_onprem.py} +4 -69
  121. bbot/test/test_step_2/module_tests/test_module_gowitness.py +5 -5
  122. bbot/test/test_step_2/module_tests/test_module_graphql_introspection.py +34 -0
  123. bbot/test/test_step_2/module_tests/test_module_iis_shortnames.py +46 -1
  124. bbot/test/test_step_2/module_tests/test_module_jadx.py +9 -0
  125. bbot/test/test_step_2/module_tests/test_module_lightfuzz.py +71 -3
  126. bbot/test/test_step_2/module_tests/test_module_nuclei.py +8 -6
  127. bbot/test/test_step_2/module_tests/test_module_otx.py +3 -0
  128. bbot/test/test_step_2/module_tests/test_module_portfilter.py +2 -0
  129. bbot/test/test_step_2/module_tests/test_module_retirejs.py +161 -0
  130. bbot/test/test_step_2/module_tests/test_module_telerik.py +1 -1
  131. bbot/test/test_step_2/module_tests/test_module_trufflehog.py +10 -1
  132. bbot/test/test_step_2/module_tests/test_module_unarchive.py +9 -0
  133. {bbot-2.5.0.dist-info → bbot-2.7.2.7424rc0.dist-info}/METADATA +12 -9
  134. {bbot-2.5.0.dist-info → bbot-2.7.2.7424rc0.dist-info}/RECORD +137 -124
  135. {bbot-2.5.0.dist-info → bbot-2.7.2.7424rc0.dist-info}/WHEEL +1 -1
  136. {bbot-2.5.0.dist-info → bbot-2.7.2.7424rc0.dist-info/licenses}/LICENSE +98 -58
  137. bbot/modules/binaryedge.py +0 -42
  138. bbot/modules/censys.py +0 -98
  139. bbot/modules/gitlab.py +0 -141
  140. bbot/modules/zoomeye.py +0 -77
  141. bbot/test/test_step_2/module_tests/test_module_binaryedge.py +0 -33
  142. bbot/test/test_step_2/module_tests/test_module_censys.py +0 -83
  143. bbot/test/test_step_2/module_tests/test_module_zoomeye.py +0 -35
  144. {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
- event = self.make_event(*args, **event_kwargs)
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
- self._event_handler_watchdog_task = asyncio.create_task(
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
- result = await self.setup()
646
- if type(result) == tuple and len(result) == 2:
647
- status, msg = result
648
- else:
649
- status = result
650
- msg = status_codes[status]
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
- # exclude certain URLs (e.g. javascript):
786
- # TODO: revisit this after httpx rework
787
- if event.type.startswith("URL") and self.name != "httpx" and "httpx-only" in event.tags:
788
- return False, "its extension was listed in url_extension_httpx_only"
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
- return float(retry_after)
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):
@@ -16,7 +16,7 @@ class bucket_amazon(bucket_template):
16
16
  }
17
17
  scope_distance_modifier = 3
18
18
 
19
- cloud_helper_name = "amazon"
19
+ cloudcheck_provider_name = "Amazon"
20
20
  delimiters = ("", ".", "-")
21
21
  base_domains = ["s3.amazonaws.com"]
22
22
  regions = [None]
@@ -15,7 +15,7 @@ class bucket_digitalocean(bucket_template):
15
15
  "permutations": "Whether to try permutations",
16
16
  }
17
17
 
18
- cloud_helper_name = "digitalocean"
18
+ cloudcheck_provider_name = "DigitalOcean"
19
19
  delimiters = ("", "-")
20
20
  base_domains = ["digitaloceanspaces.com"]
21
21
  regions = ["ams3", "fra1", "nyc3", "sfo2", "sfo3", "sgp1"]
@@ -15,7 +15,7 @@ class bucket_firebase(bucket_template):
15
15
  "permutations": "Whether to try permutations",
16
16
  }
17
17
 
18
- cloud_helper_name = "google"
18
+ cloudcheck_provider_name = "Google"
19
19
  delimiters = ("", "-")
20
20
  base_domains = ["firebaseio.com"]
21
21
 
@@ -19,7 +19,7 @@ class bucket_google(bucket_template):
19
19
  "permutations": "Whether to try permutations",
20
20
  }
21
21
 
22
- cloud_helper_name = "google"
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 bucket_azure(bucket_template):
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
- cloud_helper_name = "azure"
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
- if s != event:
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
- if r != event:
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);* *(l=(?P<l>https*://[^;]*|)|);*( *a=((?P<a>https://[^;]*|)|);*)*$"
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()
@@ -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"}
@@ -1,4 +1,4 @@
1
- import re
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 CSRF tokens
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 [429]:
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
- csrftoken = None
38
- csrfmiddlewaretoken = None
31
+ # Extract JWT token from the form's hx-headers attribute using regex
32
+ jwt_token = None
39
33
  try:
40
- for cookie in res1.headers.get("set-cookie", "").split(";"):
41
- try:
42
- k, v = cookie.split("=", 1)
43
- except ValueError:
44
- self.verbose("Error retrieving cookie")
45
- return ret
46
- if k == "csrftoken":
47
- csrftoken = str(v)
48
- csrfmiddlewaretoken = html.find("input", {"name": "csrfmiddlewaretoken"}).attrs.get("value", None)
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 tokens
53
- if not csrftoken or not csrfmiddlewaretoken:
54
- self.verbose("Error obtaining CSRF tokens")
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 CSRF tokens")
50
+ self.debug("Successfully obtained JWT token")
59
51
 
60
52
  if self.scan.stopping:
61
- return
53
+ return ret
62
54
 
63
- # Otherwise, do the needful
64
- subdomains = set()
55
+ # Query the API with the JWT token
65
56
  res2 = await self.api_request(
66
- f"{self.base_url}/",
57
+ "https://api.dnsdumpster.com/htmld/",
67
58
  method="POST",
68
- cookies={"csrftoken": csrftoken},
69
- data={
70
- "csrfmiddlewaretoken": csrfmiddlewaretoken,
71
- "targetip": str(domain).lower(),
72
- "user": "free",
73
- },
59
+ data={"target": str(domain).lower()},
74
60
  headers={
75
- "origin": "https://dnsdumpster.com",
76
- "referer": "https://dnsdumpster.com/",
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 list(subdomains)
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
@@ -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.helpers.temp_dir / "docker_images"
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
 
@@ -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
- for email in await self.helpers.re.extract_emails(r.text):
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)