bbot 2.2.0.5263rc0__py3-none-any.whl → 2.2.0.5309rc0__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.

Potentially problematic release.


This version of bbot might be problematic. Click here for more details.

Files changed (71) hide show
  1. bbot/__init__.py +1 -1
  2. bbot/cli.py +1 -1
  3. bbot/core/engine.py +2 -2
  4. bbot/core/event/base.py +23 -2
  5. bbot/core/helpers/bloom.py +8 -1
  6. bbot/core/helpers/depsinstaller/installer.py +8 -5
  7. bbot/core/helpers/dns/helpers.py +2 -2
  8. bbot/core/helpers/helper.py +4 -3
  9. bbot/core/helpers/misc.py +29 -5
  10. bbot/core/helpers/regexes.py +2 -1
  11. bbot/core/helpers/web/web.py +1 -1
  12. bbot/defaults.yml +3 -0
  13. bbot/modules/anubisdb.py +1 -1
  14. bbot/modules/baddns.py +1 -1
  15. bbot/modules/bevigil.py +2 -2
  16. bbot/modules/binaryedge.py +1 -1
  17. bbot/modules/bufferoverrun.py +2 -3
  18. bbot/modules/builtwith.py +2 -2
  19. bbot/modules/c99.py +4 -2
  20. bbot/modules/certspotter.py +4 -2
  21. bbot/modules/chaos.py +4 -2
  22. bbot/modules/columbus.py +1 -1
  23. bbot/modules/crt.py +4 -2
  24. bbot/modules/digitorus.py +1 -1
  25. bbot/modules/dnscaa.py +3 -3
  26. bbot/modules/fullhunt.py +1 -1
  27. bbot/modules/hackertarget.py +4 -2
  28. bbot/modules/internal/excavate.py +2 -3
  29. bbot/modules/internal/speculate.py +34 -24
  30. bbot/modules/leakix.py +6 -5
  31. bbot/modules/myssl.py +1 -1
  32. bbot/modules/otx.py +4 -2
  33. bbot/modules/passivetotal.py +4 -2
  34. bbot/modules/rapiddns.py +2 -7
  35. bbot/modules/securitytrails.py +4 -2
  36. bbot/modules/shodan_dns.py +1 -1
  37. bbot/modules/subdomaincenter.py +1 -1
  38. bbot/modules/templates/subdomain_enum.py +3 -3
  39. bbot/modules/trickest.py +1 -1
  40. bbot/modules/virustotal.py +2 -7
  41. bbot/modules/zoomeye.py +5 -3
  42. bbot/presets/fast.yml +16 -0
  43. bbot/presets/spider.yml +4 -0
  44. bbot/scanner/manager.py +1 -2
  45. bbot/scanner/preset/args.py +20 -4
  46. bbot/scanner/preset/path.py +3 -1
  47. bbot/scanner/preset/preset.py +18 -12
  48. bbot/scanner/scanner.py +7 -2
  49. bbot/scanner/target.py +236 -434
  50. bbot/test/bbot_fixtures.py +5 -2
  51. bbot/test/conftest.py +95 -83
  52. bbot/test/test_step_1/test_bloom_filter.py +2 -0
  53. bbot/test/test_step_1/test_cli.py +36 -0
  54. bbot/test/test_step_1/test_dns.py +2 -1
  55. bbot/test/test_step_1/test_events.py +16 -3
  56. bbot/test/test_step_1/test_helpers.py +17 -0
  57. bbot/test/test_step_1/test_modules_basic.py +0 -3
  58. bbot/test/test_step_1/test_presets.py +51 -38
  59. bbot/test/test_step_1/test_python_api.py +4 -0
  60. bbot/test/test_step_1/test_scan.py +8 -2
  61. bbot/test/test_step_1/test_target.py +227 -129
  62. bbot/test/test_step_1/test_web.py +3 -0
  63. bbot/test/test_step_2/module_tests/test_module_dastardly.py +1 -1
  64. bbot/test/test_step_2/module_tests/test_module_dotnetnuke.py +0 -6
  65. bbot/test/test_step_2/module_tests/test_module_ffuf_shortnames.py +1 -1
  66. bbot/test/test_step_2/module_tests/test_module_leakix.py +5 -1
  67. {bbot-2.2.0.5263rc0.dist-info → bbot-2.2.0.5309rc0.dist-info}/METADATA +4 -4
  68. {bbot-2.2.0.5263rc0.dist-info → bbot-2.2.0.5309rc0.dist-info}/RECORD +71 -70
  69. {bbot-2.2.0.5263rc0.dist-info → bbot-2.2.0.5309rc0.dist-info}/LICENSE +0 -0
  70. {bbot-2.2.0.5263rc0.dist-info → bbot-2.2.0.5309rc0.dist-info}/WHEEL +0 -0
  71. {bbot-2.2.0.5263rc0.dist-info → bbot-2.2.0.5309rc0.dist-info}/entry_points.txt +0 -0
bbot/modules/myssl.py CHANGED
@@ -17,7 +17,7 @@ class myssl(subdomain_enum):
17
17
  url = f"{self.base_url}?domain={self.helpers.quote(query)}"
18
18
  return await self.api_request(url)
19
19
 
20
- def parse_results(self, r, query):
20
+ async def parse_results(self, r, query):
21
21
  results = set()
22
22
  json = r.json()
23
23
  if json and isinstance(json, dict):
bbot/modules/otx.py CHANGED
@@ -17,10 +17,12 @@ class otx(subdomain_enum):
17
17
  url = f"{self.base_url}/api/v1/indicators/domain/{self.helpers.quote(query)}/passive_dns"
18
18
  return self.api_request(url)
19
19
 
20
- def parse_results(self, r, query):
20
+ async def parse_results(self, r, query):
21
+ results = set()
21
22
  j = r.json()
22
23
  if isinstance(j, dict):
23
24
  for entry in j.get("passive_dns", []):
24
25
  subdomain = entry.get("hostname", "")
25
26
  if subdomain:
26
- yield subdomain
27
+ results.add(subdomain)
28
+ return results
@@ -39,6 +39,8 @@ class passivetotal(subdomain_enum_apikey):
39
39
  url = f"{self.base_url}/enrichment/subdomains?query={self.helpers.quote(query)}"
40
40
  return await self.api_request(url)
41
41
 
42
- def parse_results(self, r, query):
42
+ async def parse_results(self, r, query):
43
+ results = set()
43
44
  for subdomain in r.json().get("subdomains", []):
44
- yield f"{subdomain}.{query}"
45
+ results.add(f"{subdomain}.{query}")
46
+ return results
bbot/modules/rapiddns.py CHANGED
@@ -18,11 +18,6 @@ class rapiddns(subdomain_enum):
18
18
  response = await self.api_request(url, timeout=self.http_timeout + 10)
19
19
  return response
20
20
 
21
- def parse_results(self, r, query):
22
- results = set()
21
+ async def parse_results(self, r, query):
23
22
  text = getattr(r, "text", "")
24
- for match in self.helpers.regexes.dns_name_regex.findall(text):
25
- match = match.lower()
26
- if match.endswith(query):
27
- results.add(match)
28
- return results
23
+ return await self.scan.extract_in_scope_hostnames(text)
@@ -26,8 +26,10 @@ class securitytrails(subdomain_enum_apikey):
26
26
  response = await self.api_request(url)
27
27
  return response
28
28
 
29
- def parse_results(self, r, query):
29
+ async def parse_results(self, r, query):
30
+ results = set()
30
31
  j = r.json()
31
32
  if isinstance(j, dict):
32
33
  for host in j.get("subdomains", []):
33
- yield f"{host}.{query}"
34
+ results.add(f"{host}.{query}")
35
+ return results
@@ -22,5 +22,5 @@ class shodan_dns(shodan):
22
22
  def make_url(self, query):
23
23
  return f"{self.base_url}/dns/domain/{self.helpers.quote(query)}?key={{api_key}}&page={{page}}"
24
24
 
25
- def parse_results(self, json, query):
25
+ async def parse_results(self, json, query):
26
26
  return [f"{sub}.{query}" for sub in json.get("subdomains", [])]
@@ -33,7 +33,7 @@ class subdomaincenter(subdomain_enum):
33
33
  break
34
34
  return response
35
35
 
36
- def parse_results(self, r, query):
36
+ async def parse_results(self, r, query):
37
37
  results = set()
38
38
  json = r.json()
39
39
  if json and isinstance(json, list):
@@ -106,7 +106,7 @@ class subdomain_enum(BaseModule):
106
106
  break
107
107
  return ".".join([s for s in query.split(".") if s != "_wildcard"])
108
108
 
109
- def parse_results(self, r, query=None):
109
+ async def parse_results(self, r, query=None):
110
110
  json = r.json()
111
111
  if json:
112
112
  for hostname in json:
@@ -123,7 +123,7 @@ class subdomain_enum(BaseModule):
123
123
  self.info(f'Query "{query}" failed (no response)')
124
124
  return []
125
125
  try:
126
- results = list(parse_fn(response, query))
126
+ results = list(await parse_fn(response, query))
127
127
  except Exception as e:
128
128
  if response:
129
129
  self.info(
@@ -144,7 +144,7 @@ class subdomain_enum(BaseModule):
144
144
  agen = self.api_page_iter(url, page_size=self.page_size, **self.api_page_iter_kwargs)
145
145
  try:
146
146
  async for response in agen:
147
- subdomains = self.parse_results(response, query)
147
+ subdomains = await self.parse_results(response, query)
148
148
  self.verbose(f'Got {len(subdomains):,} subdomains for "{query}"')
149
149
  if not subdomains:
150
150
  break
bbot/modules/trickest.py CHANGED
@@ -36,7 +36,7 @@ class Trickest(subdomain_enum_apikey):
36
36
  url += "&limit={page_size}&offset={offset}&select=hostname&orderby=hostname"
37
37
  return url
38
38
 
39
- def parse_results(self, j, query):
39
+ async def parse_results(self, j, query):
40
40
  results = j.get("results", [])
41
41
  subdomains = set()
42
42
  for item in results:
@@ -24,11 +24,6 @@ class virustotal(subdomain_enum_apikey):
24
24
  kwargs["headers"]["x-apikey"] = self.api_key
25
25
  return url, kwargs
26
26
 
27
- def parse_results(self, r, query):
28
- results = set()
27
+ async def parse_results(self, r, query):
29
28
  text = getattr(r, "text", "")
30
- for match in self.helpers.regexes.dns_name_regex.findall(text):
31
- match = match.lower()
32
- if match.endswith(query):
33
- results.add(match)
34
- return results
29
+ return await self.scan.extract_in_scope_hostnames(text)
bbot/modules/zoomeye.py CHANGED
@@ -60,7 +60,7 @@ class zoomeye(subdomain_enum_apikey):
60
60
  agen = self.api_page_iter(url)
61
61
  try:
62
62
  async for j in agen:
63
- r = list(self.parse_results(j))
63
+ r = list(await self.parse_results(j))
64
64
  if r:
65
65
  results.update(set(r))
66
66
  if not r or i >= (self.max_pages - 1):
@@ -70,6 +70,8 @@ class zoomeye(subdomain_enum_apikey):
70
70
  agen.aclose()
71
71
  return results
72
72
 
73
- def parse_results(self, r):
73
+ async def parse_results(self, r):
74
+ results = set()
74
75
  for entry in r.get("list", []):
75
- yield entry["name"]
76
+ results.add(entry["name"])
77
+ return results
bbot/presets/fast.yml ADDED
@@ -0,0 +1,16 @@
1
+ description: Scan only the provided targets as fast as possible - no extra discovery
2
+
3
+ exclude_modules:
4
+ - excavate
5
+
6
+ config:
7
+ # only scan the exact targets specified
8
+ scope:
9
+ strict: true
10
+ # speed up dns resolution by doing A/AAAA only - not MX/NS/SRV/etc
11
+ dns:
12
+ minimal: true
13
+ # essential speculation only
14
+ modules:
15
+ speculate:
16
+ essential_only: true
bbot/presets/spider.yml CHANGED
@@ -3,6 +3,10 @@ description: Recursive web spider
3
3
  modules:
4
4
  - httpx
5
5
 
6
+ blacklist:
7
+ # Prevent spider from invalidating sessions by logging out
8
+ - "RE:/.*(sign|log)[_-]?out"
9
+
6
10
  config:
7
11
  web:
8
12
  # how many links to follow in a row
bbot/scanner/manager.py CHANGED
@@ -38,7 +38,7 @@ class ScanIngress(BaseInterceptModule):
38
38
  - It also marks the Scan object as finished with initialization by setting `_finished_init` to True.
39
39
  """
40
40
  if events is None:
41
- events = self.scan.target.events
41
+ events = self.scan.target.seeds.events
42
42
  async with self.scan._acatch(self.init_events), self._task_counter.count(self.init_events):
43
43
  sorted_events = sorted(events, key=lambda e: len(e.data))
44
44
  for event in [self.scan.root_event] + sorted_events:
@@ -49,7 +49,6 @@ class ScanIngress(BaseInterceptModule):
49
49
  event.parent = self.scan.root_event
50
50
  if event.module is None:
51
51
  event.module = self.scan._make_dummy_module(name="TARGET", _type="TARGET")
52
- event.add_tag("target")
53
52
  if event != self.scan.root_event:
54
53
  event.discovery_context = f"Scan {self.scan.name} seeded with " + "{event.type}: {event.data}"
55
54
  self.verbose(f"Target: {event}")
@@ -91,7 +91,6 @@ class BBOTArgs:
91
91
  *self.parsed.targets,
92
92
  whitelist=self.parsed.whitelist,
93
93
  blacklist=self.parsed.blacklist,
94
- strict_scope=self.parsed.strict_scope,
95
94
  name="args_preset",
96
95
  )
97
96
 
@@ -149,6 +148,9 @@ class BBOTArgs:
149
148
  if self.parsed.force:
150
149
  args_preset.force_start = self.parsed.force
151
150
 
151
+ if self.parsed.proxy:
152
+ args_preset.core.merge_custom({"web": {"http_proxy": self.parsed.proxy}})
153
+
152
154
  if self.parsed.custom_headers:
153
155
  args_preset.core.merge_custom({"web": {"http_headers": self.parsed.custom_headers}})
154
156
 
@@ -165,6 +167,10 @@ class BBOTArgs:
165
167
  except Exception as e:
166
168
  raise BBOTArgumentError(f'Error parsing command-line config option: "{config_arg}": {e}')
167
169
 
170
+ # strict scope
171
+ if self.parsed.strict_scope:
172
+ args_preset.core.merge_custom({"scope": {"strict": True}})
173
+
168
174
  return args_preset
169
175
 
170
176
  def create_parser(self, *args, **kwargs):
@@ -217,7 +223,7 @@ class BBOTArgs:
217
223
  "--modules",
218
224
  nargs="+",
219
225
  default=[],
220
- help=f'Modules to enable. Choices: {",".join(self.preset.module_loader.scan_module_choices)}',
226
+ help=f'Modules to enable. Choices: {",".join(sorted(self.preset.module_loader.scan_module_choices))}',
221
227
  metavar="MODULE",
222
228
  )
223
229
  modules.add_argument("-l", "--list-modules", action="store_true", help=f"List available modules.")
@@ -232,7 +238,7 @@ class BBOTArgs:
232
238
  "--flags",
233
239
  nargs="+",
234
240
  default=[],
235
- help=f'Enable modules by flag. Choices: {",".join(self.preset.module_loader.flag_choices)}',
241
+ help=f'Enable modules by flag. Choices: {",".join(sorted(self.preset.module_loader.flag_choices))}',
236
242
  metavar="FLAG",
237
243
  )
238
244
  modules.add_argument("-lf", "--list-flags", action="store_true", help=f"List available flags.")
@@ -265,6 +271,11 @@ class BBOTArgs:
265
271
  help="Run scan even in the case of condition violations or failed module setups",
266
272
  )
267
273
  scan.add_argument("-y", "--yes", action="store_true", help="Skip scan confirmation prompt")
274
+ scan.add_argument(
275
+ "--fast-mode",
276
+ action="store_true",
277
+ help="Scan only the provided targets as fast as possible, with no extra discovery",
278
+ )
268
279
  scan.add_argument("--dry-run", action="store_true", help=f"Abort before executing scan")
269
280
  scan.add_argument(
270
281
  "--current-preset",
@@ -289,7 +300,7 @@ class BBOTArgs:
289
300
  "--output-modules",
290
301
  nargs="+",
291
302
  default=[],
292
- help=f'Output module(s). Choices: {",".join(self.preset.module_loader.output_module_choices)}',
303
+ help=f'Output module(s). Choices: {",".join(sorted(self.preset.module_loader.output_module_choices))}',
293
304
  metavar="MODULE",
294
305
  )
295
306
  output.add_argument("--json", "-j", action="store_true", help="Output scan data in JSON format")
@@ -310,6 +321,7 @@ class BBOTArgs:
310
321
 
311
322
  misc = p.add_argument_group(title="Misc")
312
323
  misc.add_argument("--version", action="store_true", help="show BBOT version and exit")
324
+ misc.add_argument("--proxy", help="Use this proxy for all HTTP requests", metavar="HTTP_PROXY")
313
325
  misc.add_argument(
314
326
  "-H",
315
327
  "--custom-headers",
@@ -359,6 +371,10 @@ class BBOTArgs:
359
371
  custom_headers_dict[k] = v
360
372
  self.parsed.custom_headers = custom_headers_dict
361
373
 
374
+ # --fast-mode
375
+ if self.parsed.fast_mode:
376
+ self.parsed.preset += ["fast"]
377
+
362
378
  def validate(self):
363
379
  # validate config options
364
380
  sentinel = object()
@@ -33,7 +33,9 @@ class PresetPath:
33
33
  if "/" in str(filename):
34
34
  if filename_path.parent not in paths_to_search:
35
35
  paths_to_search.append(filename_path.parent)
36
- log.debug(f"Searching for preset in {paths_to_search}, file candidates: {file_candidates_str}")
36
+ log.debug(
37
+ f"Searching for preset in {[str(p) for p in paths_to_search]}, file candidates: {file_candidates_str}"
38
+ )
37
39
  for path in paths_to_search:
38
40
  for candidate in file_candidates:
39
41
  for file in path.rglob(candidate):
@@ -47,7 +47,6 @@ class Preset:
47
47
  target (Target): Target(s) of scan.
48
48
  whitelist (Target): Scan whitelist (by default this is the same as `target`).
49
49
  blacklist (Target): Scan blacklist (this takes ultimate precedence).
50
- strict_scope (bool): If True, subdomains of targets are not considered to be in-scope.
51
50
  helpers (ConfigAwareHelper): Helper containing various reusable functions, regexes, etc.
52
51
  output_dir (pathlib.Path): Output directory for scan.
53
52
  scan_name (str): Name of scan. Defaults to random value, e.g. "demonic_jimmy".
@@ -87,7 +86,6 @@ class Preset:
87
86
  *targets,
88
87
  whitelist=None,
89
88
  blacklist=None,
90
- strict_scope=False,
91
89
  modules=None,
92
90
  output_modules=None,
93
91
  exclude_modules=None,
@@ -117,7 +115,6 @@ class Preset:
117
115
  *targets (str): Target(s) to scan. Types supported: hostnames, IPs, CIDRs, emails, open ports.
118
116
  whitelist (list, optional): Whitelisted target(s) to scan. Defaults to the same as `targets`.
119
117
  blacklist (list, optional): Blacklisted target(s). Takes ultimate precedence. Defaults to empty.
120
- strict_scope (bool, optional): If True, subdomains of targets are not in-scope.
121
118
  modules (list[str], optional): List of scan modules to enable for the scan. Defaults to empty list.
122
119
  output_modules (list[str], optional): List of output modules to use. Defaults to csv, human, and json.
123
120
  exclude_modules (list[str], optional): List of modules to exclude from the scan.
@@ -234,7 +231,6 @@ class Preset:
234
231
  self.module_dirs = module_dirs
235
232
 
236
233
  # target / whitelist / blacklist
237
- self.strict_scope = strict_scope
238
234
  # these are temporary receptacles until they all get .baked() together
239
235
  self._seeds = set(targets if targets else [])
240
236
  self._whitelist = set(whitelist) if whitelist else whitelist
@@ -245,7 +241,7 @@ class Preset:
245
241
  # "presets" is alias to "include"
246
242
  if presets and include:
247
243
  raise ValueError(
248
- 'Cannot use both "presets" and "include" args at the same time (presets is only an alias to include). Please pick only one :)'
244
+ 'Cannot use both "presets" and "include" args at the same time (presets is an alias to include). Please pick one or the other :)'
249
245
  )
250
246
  if presets and not include:
251
247
  include = presets
@@ -274,6 +270,12 @@ class Preset:
274
270
  raise ValueError("Cannot access target before preset is baked (use ._seeds instead)")
275
271
  return self._target
276
272
 
273
+ @property
274
+ def seeds(self):
275
+ if self._seeds is None:
276
+ raise ValueError("Cannot access target before preset is baked (use ._seeds instead)")
277
+ return self.target.seeds
278
+
277
279
  @property
278
280
  def whitelist(self):
279
281
  if self._target is None:
@@ -353,7 +355,6 @@ class Preset:
353
355
  else:
354
356
  self._whitelist.update(other._whitelist)
355
357
  self._blacklist.update(other._blacklist)
356
- self.strict_scope = self.strict_scope or other.strict_scope
357
358
 
358
359
  # module dirs
359
360
  self.module_dirs = self.module_dirs.union(other.module_dirs)
@@ -537,6 +538,14 @@ class Preset:
537
538
  def web_config(self):
538
539
  return self.core.config.get("web", {})
539
540
 
541
+ @property
542
+ def scope_config(self):
543
+ return self.config.get("scope", {})
544
+
545
+ @property
546
+ def strict_scope(self):
547
+ return self.scope_config.get("strict", False)
548
+
540
549
  def apply_log_level(self, apply_core=False):
541
550
  # silent takes precedence
542
551
  if self.silent:
@@ -635,7 +644,6 @@ class Preset:
635
644
  debug=preset_dict.get("debug", False),
636
645
  silent=preset_dict.get("silent", False),
637
646
  config=preset_dict.get("config"),
638
- strict_scope=preset_dict.get("strict_scope", False),
639
647
  module_dirs=preset_dict.get("module_dirs", []),
640
648
  include=list(preset_dict.get("include", [])),
641
649
  scan_name=preset_dict.get("scan_name"),
@@ -753,19 +761,17 @@ class Preset:
753
761
 
754
762
  # scope
755
763
  if include_target:
756
- target = sorted(str(t.data) for t in self.target.seeds)
764
+ target = sorted(self.target.seeds.inputs)
757
765
  whitelist = []
758
766
  if self.target.whitelist is not None:
759
- whitelist = sorted(str(t.data) for t in self.target.whitelist)
760
- blacklist = sorted(str(t.data) for t in self.target.blacklist)
767
+ whitelist = sorted(self.target.whitelist.inputs)
768
+ blacklist = sorted(self.target.blacklist.inputs)
761
769
  if target:
762
770
  preset_dict["target"] = target
763
771
  if whitelist and whitelist != target:
764
772
  preset_dict["whitelist"] = whitelist
765
773
  if blacklist:
766
774
  preset_dict["blacklist"] = blacklist
767
- if self.strict_scope:
768
- preset_dict["strict_scope"] = True
769
775
 
770
776
  # flags + modules
771
777
  if self.require_flags:
bbot/scanner/scanner.py CHANGED
@@ -269,7 +269,7 @@ class Scanner:
269
269
  f.write(self.preset.to_yaml())
270
270
 
271
271
  # log scan overview
272
- start_msg = f"Scan with {len(self.preset.scan_modules):,} modules seeded with {len(self.target):,} targets"
272
+ start_msg = f"Scan seeded with {len(self.seeds):,} targets"
273
273
  details = []
274
274
  if self.whitelist != self.target:
275
275
  details.append(f"{len(self.whitelist):,} in whitelist")
@@ -362,7 +362,8 @@ class Scanner:
362
362
 
363
363
  # distribute seed events
364
364
  self.init_events_task = asyncio.create_task(
365
- self.ingress_module.init_events(self.target.events), name=f"{self.name}.ingress_module.init_events()"
365
+ self.ingress_module.init_events(self.target.seeds.events),
366
+ name=f"{self.name}.ingress_module.init_events()",
366
367
  )
367
368
 
368
369
  # main scan loop
@@ -896,6 +897,10 @@ class Scanner:
896
897
  def target(self):
897
898
  return self.preset.target
898
899
 
900
+ @property
901
+ def seeds(self):
902
+ return self.preset.seeds
903
+
899
904
  @property
900
905
  def whitelist(self):
901
906
  return self.preset.whitelist