bbot 2.6.1.6913rc0__py3-none-any.whl → 2.7.0__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 (51) hide show
  1. bbot/__init__.py +1 -1
  2. bbot/core/engine.py +1 -1
  3. bbot/core/helpers/bloom.py +6 -7
  4. bbot/core/helpers/dns/dns.py +0 -1
  5. bbot/core/helpers/dns/engine.py +0 -2
  6. bbot/core/helpers/files.py +2 -2
  7. bbot/core/helpers/git.py +17 -0
  8. bbot/core/helpers/misc.py +1 -0
  9. bbot/core/helpers/ntlm.py +0 -2
  10. bbot/core/helpers/regex.py +1 -1
  11. bbot/core/modules.py +0 -54
  12. bbot/defaults.yml +4 -2
  13. bbot/modules/base.py +11 -5
  14. bbot/modules/dnstlsrpt.py +0 -6
  15. bbot/modules/git_clone.py +46 -21
  16. bbot/modules/gitdumper.py +3 -13
  17. bbot/modules/graphql_introspection.py +5 -2
  18. bbot/modules/httpx.py +2 -0
  19. bbot/modules/iis_shortnames.py +0 -7
  20. bbot/modules/internal/unarchive.py +9 -3
  21. bbot/modules/lightfuzz/lightfuzz.py +5 -1
  22. bbot/modules/nuclei.py +1 -1
  23. bbot/modules/output/base.py +0 -5
  24. bbot/modules/retirejs.py +232 -0
  25. bbot/modules/securitytxt.py +0 -3
  26. bbot/modules/subdomaincenter.py +1 -16
  27. bbot/modules/telerik.py +6 -1
  28. bbot/modules/trufflehog.py +1 -1
  29. bbot/scanner/manager.py +7 -4
  30. bbot/scanner/scanner.py +1 -1
  31. bbot/scripts/benchmark_report.py +433 -0
  32. bbot/test/benchmarks/__init__.py +2 -0
  33. bbot/test/benchmarks/test_bloom_filter_benchmarks.py +105 -0
  34. bbot/test/benchmarks/test_closest_match_benchmarks.py +76 -0
  35. bbot/test/benchmarks/test_event_validation_benchmarks.py +438 -0
  36. bbot/test/benchmarks/test_excavate_benchmarks.py +291 -0
  37. bbot/test/benchmarks/test_ipaddress_benchmarks.py +143 -0
  38. bbot/test/benchmarks/test_weighted_shuffle_benchmarks.py +70 -0
  39. bbot/test/test_step_1/test_bbot_fastapi.py +2 -2
  40. bbot/test/test_step_1/test_events.py +0 -1
  41. bbot/test/test_step_1/test_scan.py +1 -8
  42. bbot/test/test_step_2/module_tests/base.py +6 -1
  43. bbot/test/test_step_2/module_tests/test_module_excavate.py +35 -6
  44. bbot/test/test_step_2/module_tests/test_module_lightfuzz.py +2 -2
  45. bbot/test/test_step_2/module_tests/test_module_retirejs.py +159 -0
  46. bbot/test/test_step_2/module_tests/test_module_telerik.py +1 -1
  47. {bbot-2.6.1.6913rc0.dist-info → bbot-2.7.0.dist-info}/METADATA +3 -2
  48. {bbot-2.6.1.6913rc0.dist-info → bbot-2.7.0.dist-info}/RECORD +51 -40
  49. {bbot-2.6.1.6913rc0.dist-info → bbot-2.7.0.dist-info}/LICENSE +0 -0
  50. {bbot-2.6.1.6913rc0.dist-info → bbot-2.7.0.dist-info}/WHEEL +0 -0
  51. {bbot-2.6.1.6913rc0.dist-info → bbot-2.7.0.dist-info}/entry_points.txt +0 -0
bbot/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  # version placeholder (replaced by poetry-dynamic-versioning)
2
- __version__ = "v2.6.1.6913rc"
2
+ __version__ = "v2.7.0"
3
3
 
4
4
  from .scanner import Scanner, Preset
5
5
 
bbot/core/engine.py CHANGED
@@ -636,7 +636,7 @@ class EngineServer(EngineBase):
636
636
  """
637
637
  if tasks:
638
638
  try:
639
- done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED, timeout=timeout)
639
+ done, _ = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED, timeout=timeout)
640
640
  return done
641
641
  except BaseException as e:
642
642
  if isinstance(e, (TimeoutError, asyncio.exceptions.TimeoutError)):
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  import mmh3
3
3
  import mmap
4
+ import xxhash
4
5
 
5
6
 
6
7
  class BloomFilter:
@@ -55,14 +56,12 @@ class BloomFilter:
55
56
  if not isinstance(item, str):
56
57
  item = str(item)
57
58
  item = item.encode("utf-8")
58
- return [abs(hash(item)) % self.size, abs(mmh3.hash(item)) % self.size, abs(self._fnv1a_hash(item)) % self.size]
59
59
 
60
- def _fnv1a_hash(self, data):
61
- hash = 0x811C9DC5 # 2166136261
62
- for byte in data:
63
- hash ^= byte
64
- hash = (hash * 0x01000193) % 2**32 # 16777619
65
- return hash
60
+ return [
61
+ abs(hash(item)) % self.size,
62
+ abs(mmh3.hash(item)) % self.size,
63
+ abs(xxhash.xxh32(item).intdigest()) % self.size,
64
+ ]
66
65
 
67
66
  def close(self):
68
67
  """Explicitly close the memory-mapped file."""
@@ -38,7 +38,6 @@ class DNSHelper(EngineClient):
38
38
  _wildcard_cache (dict): Cache for wildcard detection results.
39
39
  _dns_cache (LRUCache): Cache for DNS resolution results, limited in size.
40
40
  resolver_file (Path): File containing system's current resolver nameservers.
41
- filter_bad_ptrs (bool): Whether to filter out DNS names that appear to be auto-generated PTR records. Defaults to True.
42
41
 
43
42
  Args:
44
43
  parent_helper: The parent helper object with configuration details and utilities.
@@ -86,8 +86,6 @@ class DNSEngine(EngineServer):
86
86
  self._debug = self.dns_config.get("debug", False)
87
87
  self._dns_cache = LRUCache(maxsize=10000)
88
88
 
89
- self.filter_bad_ptrs = self.dns_config.get("filter_ptrs", True)
90
-
91
89
  async def resolve(self, query, **kwargs):
92
90
  """Resolve DNS names and IP addresses to their corresponding results.
93
91
 
@@ -9,7 +9,7 @@ from .misc import rm_at_exit
9
9
  log = logging.getLogger("bbot.core.helpers.files")
10
10
 
11
11
 
12
- def tempfile(self, content, pipe=True):
12
+ def tempfile(self, content, pipe=True, extension=None):
13
13
  """
14
14
  Creates a temporary file or named pipe and populates it with content.
15
15
 
@@ -29,7 +29,7 @@ def tempfile(self, content, pipe=True):
29
29
  >>> tempfile(["Another", "temp", "file"], pipe=False)
30
30
  '/home/user/.bbot/temp/someotherfile'
31
31
  """
32
- filename = self.temp_filename()
32
+ filename = self.temp_filename(extension)
33
33
  rm_at_exit(filename)
34
34
  try:
35
35
  if type(content) not in (set, list, tuple):
@@ -0,0 +1,17 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def sanitize_git_repo(repo_folder: Path):
5
+ # sanitizing the git config is infeasible since there are too many different ways to do evil things
6
+ # instead, we move it out of .git and into the repo folder, so we don't miss any secrets etc. inside
7
+ config_file = repo_folder / ".git" / "config"
8
+ if config_file.exists():
9
+ config_file.rename(repo_folder / "git_config_original")
10
+ # move the index file
11
+ index_file = repo_folder / ".git" / "index"
12
+ if index_file.exists():
13
+ index_file.rename(repo_folder / "git_index_original")
14
+ # move the hooks folder
15
+ hooks_folder = repo_folder / ".git" / "hooks"
16
+ if hooks_folder.exists():
17
+ hooks_folder.rename(repo_folder / "git_hooks_original")
bbot/core/helpers/misc.py CHANGED
@@ -17,6 +17,7 @@ from unidecode import unidecode # noqa F401
17
17
  from asyncio import create_task, gather, sleep, wait_for # noqa
18
18
  from urllib.parse import urlparse, quote, unquote, urlunparse, urljoin # noqa F401
19
19
 
20
+ from .git import * # noqa F401
20
21
  from .url import * # noqa F401
21
22
  from ... import errors
22
23
  from . import regexes as bbot_regexes
bbot/core/helpers/ntlm.py CHANGED
@@ -17,11 +17,9 @@ class StrStruct(object):
17
17
  self.alloc = alloc
18
18
  self.offset = offset
19
19
  self.raw = raw[offset : offset + length]
20
- self.utf16 = False
21
20
 
22
21
  if len(self.raw) >= 2 and self.raw[1] == "\0":
23
22
  self.string = self.raw.decode("utf-16")
24
- self.utf16 = True
25
23
  else:
26
24
  self.string = self.raw
27
25
 
@@ -65,7 +65,7 @@ class RegexHelper:
65
65
 
66
66
  while tasks: # While there are tasks pending
67
67
  # Wait for the first task to complete
68
- done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
68
+ done, _ = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
69
69
 
70
70
  for task in done:
71
71
  result = task.result()
bbot/core/modules.py CHANGED
@@ -512,60 +512,6 @@ class ModuleLoader:
512
512
  # then we have a module
513
513
  return value
514
514
 
515
- def recommend_dependencies(self, modules):
516
- """
517
- Returns a dictionary containing missing dependencies and their suggested resolutions
518
-
519
- Needs work. For this we should probably be building a dependency graph
520
- """
521
- resolve_choices = {}
522
- # step 1: build a dictionary containing event types and their associated modules
523
- # {"IP_ADDRESS": set("masscan", "ipneighbor", ...)}
524
- watched = {}
525
- produced = {}
526
- for modname in modules:
527
- preloaded = self._preloaded.get(modname)
528
- if preloaded:
529
- for event_type in preloaded.get("watched_events", []):
530
- self.add_or_create(watched, event_type, modname)
531
- for event_type in preloaded.get("produced_events", []):
532
- self.add_or_create(produced, event_type, modname)
533
- watched_all = {}
534
- produced_all = {}
535
- for modname, preloaded in self.preloaded().items():
536
- if preloaded:
537
- for event_type in preloaded.get("watched_events", []):
538
- self.add_or_create(watched_all, event_type, modname)
539
- for event_type in preloaded.get("produced_events", []):
540
- self.add_or_create(produced_all, event_type, modname)
541
-
542
- # step 2: check to see if there are missing dependencies
543
- for modname in modules:
544
- preloaded = self._preloaded.get(modname)
545
- module_type = preloaded.get("type", "unknown")
546
- if module_type != "scan":
547
- continue
548
- watched_events = preloaded.get("watched_events", [])
549
- missing_deps = {e: not self.check_dependency(e, modname, produced) for e in watched_events}
550
- if all(missing_deps.values()):
551
- for event_type in watched_events:
552
- if event_type == "SCAN":
553
- continue
554
- choices = produced_all.get(event_type, [])
555
- choices = set(choices)
556
- with suppress(KeyError):
557
- choices.remove(modname)
558
- if event_type not in resolve_choices:
559
- resolve_choices[event_type] = {}
560
- deps = resolve_choices[event_type]
561
- self.add_or_create(deps, "required_by", modname)
562
- for c in choices:
563
- choice_type = self._preloaded.get(c, {}).get("type", "unknown")
564
- if choice_type == "scan":
565
- self.add_or_create(deps, "recommended", c)
566
-
567
- return resolve_choices
568
-
569
515
  def check_dependency(self, event_type, modname, produced):
570
516
  if event_type not in produced:
571
517
  return False
bbot/defaults.yml CHANGED
@@ -187,8 +187,10 @@ url_extension_blacklist:
187
187
  - mov
188
188
  - flv
189
189
  - webm
190
- # Distribute URLs with these extensions only to httpx (these are omitted from output)
191
- url_extension_httpx_only:
190
+
191
+ # URLs with these extensions are not distributed to modules unless the module opts in via `accept_url_special = True`
192
+ # They are also excluded from output. If you want to see them in output, remove them from this list.
193
+ url_extension_special:
192
194
  - js
193
195
 
194
196
  # These url extensions are almost always static, so we exclude them from modules that fuzz things
bbot/modules/base.py CHANGED
@@ -53,6 +53,8 @@ class BaseModule:
53
53
 
54
54
  in_scope_only (bool): Accept only explicitly in-scope events, regardless of the scan's search distance. Default is False.
55
55
 
56
+ accept_url_special (bool): Accept "special" URLs not typically distributed to web modules, e.g. JS URLs. Default is False.
57
+
56
58
  options (Dict): Customizable options for the module, e.g., {"api_key": ""}. Empty dict by default.
57
59
 
58
60
  options_desc (Dict): Descriptions for options, e.g., {"api_key": "API Key"}. Empty dict by default.
@@ -97,7 +99,7 @@ class BaseModule:
97
99
  scope_distance_modifier = 0
98
100
  target_only = False
99
101
  in_scope_only = False
100
-
102
+ accept_url_special = False
101
103
  _module_threads = 1
102
104
  _batch_size = 1
103
105
 
@@ -785,10 +787,14 @@ class BaseModule:
785
787
  if "target" not in event.tags:
786
788
  return False, "it did not meet target_only filter criteria"
787
789
 
788
- # exclude certain URLs (e.g. javascript):
789
- # TODO: revisit this after httpx rework
790
- if event.type.startswith("URL") and self.name != "httpx" and "httpx-only" in event.tags:
791
- return False, "its extension was listed in url_extension_httpx_only"
790
+ # limit js URLs to modules that opt in to receive them
791
+ if (not self.accept_url_special) and event.type.startswith("URL"):
792
+ extension = getattr(event, "url_extension", "")
793
+ if extension in self.scan.url_extension_special:
794
+ return (
795
+ False,
796
+ f"it is a special URL (extension {extension}) but the module does not opt in to receive special URLs",
797
+ )
792
798
 
793
799
  return True, "precheck succeeded"
794
800
 
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/git_clone.py CHANGED
@@ -24,44 +24,69 @@ class git_clone(github):
24
24
 
25
25
  async def setup(self):
26
26
  output_folder = self.config.get("output_folder")
27
- if output_folder:
28
- self.output_dir = Path(output_folder) / "git_repos"
29
- else:
30
- self.output_dir = self.scan.temp_dir / "git_repos"
27
+ self.output_dir = Path(output_folder) / "git_repos" if output_folder else self.scan.temp_dir / "git_repos"
31
28
  self.helpers.mkdir(self.output_dir)
32
29
  return await super().setup()
33
30
 
34
31
  async def filter_event(self, event):
35
- if event.type == "CODE_REPOSITORY":
36
- if "git" not in event.tags:
37
- return False, "event is not a git repository"
32
+ if event.type == "CODE_REPOSITORY" and "git" not in event.tags:
33
+ return False, "event is not a git repository"
38
34
  return True
39
35
 
40
36
  async def handle_event(self, event):
41
- repo_url = event.data.get("url")
42
- repo_path = await self.clone_git_repository(repo_url)
43
- if repo_path:
44
- self.verbose(f"Cloned {repo_url} to {repo_path}")
45
- codebase_event = self.make_event({"path": str(repo_path)}, "FILESYSTEM", tags=["git"], parent=event)
37
+ repository_url = event.data.get("url")
38
+ repository_path = await self.clone_git_repository(repository_url)
39
+ if repository_path:
40
+ self.verbose(f"Cloned {repository_url} to {repository_path}")
41
+ codebase_event = self.make_event({"path": str(repository_path)}, "FILESYSTEM", tags=["git"], parent=event)
46
42
  await self.emit_event(
47
43
  codebase_event,
48
- context=f"{{module}} downloaded git repo at {repo_url} to {{event.type}}: {repo_path}",
44
+ context=f"{{module}} cloned git repository at {repository_url} to {{event.type}}: {repository_path}",
49
45
  )
50
46
 
51
47
  async def clone_git_repository(self, repository_url):
52
48
  owner = repository_url.split("/")[-2]
53
49
  folder = self.output_dir / owner
54
50
  self.helpers.mkdir(folder)
55
- if self.api_key:
56
- url = repository_url.replace("https://github.com", f"https://user:{self.api_key}@github.com")
57
- else:
58
- url = repository_url
59
- command = ["git", "-C", folder, "clone", url]
51
+
52
+ command = ["git", "-C", folder, "clone", repository_url]
53
+ env = {"GIT_TERMINAL_PROMPT": "0"}
54
+
60
55
  try:
61
- output = await self.run_process(command, env={"GIT_TERMINAL_PROMPT": "0"}, check=True)
56
+ hostname = self.helpers.urlparse(repository_url).hostname
57
+ if hostname and self.api_key:
58
+ _, domain = self.helpers.split_domain(hostname)
59
+ # only use the api key if the domain is github.com
60
+ if domain == "github.com":
61
+ env["GIT_HELPER"] = (
62
+ f'!f() {{ case "$1" in get) '
63
+ f"echo username=x-access-token; "
64
+ f"echo password={self.api_key};; "
65
+ f'esac; }}; f "$@"'
66
+ )
67
+ command = (
68
+ command[:1]
69
+ + [
70
+ "-c",
71
+ "credential.helper=",
72
+ "-c",
73
+ "credential.useHttpPath=true",
74
+ "--config-env=credential.helper=GIT_HELPER",
75
+ ]
76
+ + command[1:]
77
+ )
78
+
79
+ output = await self.run_process(command, env=env, check=True)
62
80
  except CalledProcessError as e:
63
- self.debug(f"Error cloning {url}. STDERR: {repr(e.stderr)}")
81
+ self.debug(f"Error cloning {repository_url}. STDERR: {repr(e.stderr)}")
64
82
  return
65
83
 
66
84
  folder_name = output.stderr.split("Cloning into '")[1].split("'")[0]
67
- return folder / folder_name
85
+ repo_folder = folder / folder_name
86
+
87
+ # sanitize the repo
88
+ # this moves the git config, index file, and hooks folder out of the .git folder to prevent nasty things
89
+ # Note: the index file can be regenerated by running "git checkout HEAD -- ."
90
+ self.helpers.sanitize_git_repo(repo_folder)
91
+
92
+ return repo_folder
bbot/modules/gitdumper.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import regex as re
3
2
  from pathlib import Path
4
3
  from subprocess import CalledProcessError
5
4
  from bbot.modules.base import BaseModule
@@ -35,7 +34,6 @@ class gitdumper(BaseModule):
35
34
  else:
36
35
  self.output_dir = self.scan.temp_dir / "git_repos"
37
36
  self.helpers.mkdir(self.output_dir)
38
- self.unsafe_regex = self.helpers.re.compile(r"^\s*fsmonitor|sshcommand|askpass|editor|pager", re.IGNORECASE)
39
37
  self.ref_regex = self.helpers.re.compile(r"ref: refs/heads/([a-zA-Z\d_-]+)")
40
38
  self.obj_regex = self.helpers.re.compile(r"[a-f0-9]{40}")
41
39
  self.pack_regex = self.helpers.re.compile(r"pack-([a-f0-9]{40})\.pack")
@@ -131,7 +129,6 @@ class gitdumper(BaseModule):
131
129
  else:
132
130
  result = await self.git_fuzz(repo_url, repo_folder)
133
131
  if result:
134
- await self.sanitize_config(repo_folder)
135
132
  await self.git_checkout(repo_folder)
136
133
  codebase_event = self.make_event({"path": str(repo_folder)}, "FILESYSTEM", tags=["git"], parent=event)
137
134
  await self.emit_event(
@@ -251,15 +248,6 @@ class gitdumper(BaseModule):
251
248
  self.debug(f"Unable to download git files to {folder}")
252
249
  return False
253
250
 
254
- async def sanitize_config(self, folder):
255
- config_file = folder / ".git/config"
256
- if config_file.exists():
257
- with config_file.open("r", encoding="utf-8", errors="ignore") as file:
258
- content = file.read()
259
- sanitized = await self.helpers.re.sub(self.unsafe_regex, r"# \g<0>", content)
260
- with config_file.open("w", encoding="utf-8") as file:
261
- file.write(sanitized)
262
-
263
251
  async def git_catfile(self, hash, option="-t", folder=Path()):
264
252
  command = ["git", "cat-file", option, hash]
265
253
  try:
@@ -270,8 +258,10 @@ class gitdumper(BaseModule):
270
258
  return output.stdout
271
259
 
272
260
  async def git_checkout(self, folder):
261
+ self.helpers.sanitize_git_repo(folder)
273
262
  self.verbose(f"Running git checkout to reconstruct the git repository at {folder}")
274
- command = ["git", "checkout", "."]
263
+ # we do "checkout head -- ." because the sanitization deletes the index file, and it needs to be reconstructed
264
+ command = ["git", "checkout", "HEAD", "--", "."]
275
265
  try:
276
266
  await self.run_process(command, env={"GIT_TERMINAL_PROMPT": "0"}, cwd=folder, check=True)
277
267
  except CalledProcessError as e:
@@ -27,7 +27,6 @@ class graphql_introspection(BaseModule):
27
27
  self.output_dir = Path(output_folder) / "graphql-schemas"
28
28
  else:
29
29
  self.output_dir = self.scan.home / "graphql-schemas"
30
- self.helpers.mkdir(self.output_dir)
31
30
  return True
32
31
 
33
32
  async def filter_event(self, event):
@@ -120,7 +119,10 @@ fragment TypeRef on __Type {
120
119
  }
121
120
  response = await self.helpers.request(**request_args)
122
121
  if not response or response.status_code != 200:
123
- self.debug(f"Failed to get GraphQL schema for {url} (status code {response.status_code})")
122
+ self.debug(
123
+ f"Failed to get GraphQL schema for {url} "
124
+ f"{f'(status code {response.status_code})' if response else ''}"
125
+ )
124
126
  continue
125
127
  try:
126
128
  response_json = response.json()
@@ -128,6 +130,7 @@ fragment TypeRef on __Type {
128
130
  self.debug(f"Failed to parse JSON for {url}")
129
131
  continue
130
132
  if response_json.get("data", {}).get("__schema", {}).get("types", []):
133
+ self.helpers.mkdir(self.output_dir)
131
134
  filename = f"schema-{self.helpers.tagify(url)}.json"
132
135
  filename = self.output_dir / filename
133
136
  with open(filename, "w") as f:
bbot/modules/httpx.py CHANGED
@@ -50,6 +50,8 @@ class httpx(BaseModule):
50
50
  _shuffle_incoming_queue = False
51
51
  _batch_size = 500
52
52
  _priority = 2
53
+ # accept Javascript URLs
54
+ accept_url_special = True
53
55
 
54
56
  async def setup(self):
55
57
  self.threads = self.config.get("threads", 50)
@@ -116,13 +116,6 @@ class iis_shortnames(BaseModule):
116
116
 
117
117
  return duplicates
118
118
 
119
- async def threaded_request(self, method, url, affirmative_status_code, c):
120
- r = await self.helpers.request(method=method, url=url, allow_redirects=False, retries=2, timeout=10)
121
- if r is not None:
122
- if r.status_code == affirmative_status_code:
123
- return True, c
124
- return None, c
125
-
126
119
  async def solve_valid_chars(self, method, target, affirmative_status_code):
127
120
  confirmed_chars = []
128
121
  confirmed_exts = []
@@ -1,4 +1,5 @@
1
1
  from pathlib import Path
2
+ from contextlib import suppress
2
3
  from bbot.modules.internal.base import BaseInternalModule
3
4
  from bbot.core.helpers.libmagic import get_magic_info, get_compression
4
5
 
@@ -62,15 +63,20 @@ class unarchive(BaseInternalModule):
62
63
  context=f'extracted "{path}" to: {output_dir}',
63
64
  )
64
65
  else:
65
- output_dir.rmdir()
66
+ with suppress(OSError):
67
+ output_dir.rmdir()
66
68
 
67
69
  async def extract_file(self, path, output_dir):
68
70
  extension, mime_type, description, confidence = get_magic_info(path)
69
71
  compression_format = get_compression(mime_type)
70
72
  cmd_list = self.compression_methods.get(compression_format, [])
71
73
  if cmd_list:
72
- if not output_dir.exists():
73
- self.helpers.mkdir(output_dir)
74
+ # output dir must not already exist
75
+ try:
76
+ output_dir.mkdir(exist_ok=False)
77
+ except FileExistsError:
78
+ self.warning(f"Destination directory {output_dir} already exists, aborting unarchive for {path}")
79
+ return False
74
80
  command = [s.format(filename=path, extract_dir=output_dir) for s in cmd_list]
75
81
  try:
76
82
  await self.run_process(command, check=True)
@@ -34,6 +34,7 @@ class lightfuzz(BaseModule):
34
34
  self.event_dict = {}
35
35
  self.interactsh_subdomain_tags = {}
36
36
  self.interactsh_instance = None
37
+ self.interactsh_domain = None
37
38
  self.disable_post = self.config.get("disable_post", False)
38
39
  self.enabled_submodules = self.config.get("enabled_submodules")
39
40
  self.interactsh_disable = self.scan.config.get("interactsh_disable", False)
@@ -51,13 +52,16 @@ class lightfuzz(BaseModule):
51
52
  self.submodules[submodule_name] = submodule_class
52
53
 
53
54
  interactsh_needed = any(submodule.uses_interactsh for submodule in self.submodules.values())
54
-
55
55
  if interactsh_needed and not self.interactsh_disable:
56
56
  try:
57
57
  self.interactsh_instance = self.helpers.interactsh()
58
58
  self.interactsh_domain = await self.interactsh_instance.register(callback=self.interactsh_callback)
59
+ if not self.interactsh_domain:
60
+ self.warning("Interactsh failure: No domain returned from self.interactsh_instance.register()")
61
+ self.interactsh_instance = None
59
62
  except InteractshError as e:
60
63
  self.warning(f"Interactsh failure: {e}")
64
+ self.interactsh_instance = None
61
65
  return True
62
66
 
63
67
  async def interactsh_callback(self, r):
bbot/modules/nuclei.py CHANGED
@@ -15,7 +15,7 @@ class nuclei(BaseModule):
15
15
  }
16
16
 
17
17
  options = {
18
- "version": "3.4.7",
18
+ "version": "3.4.10",
19
19
  "tags": "",
20
20
  "templates": "",
21
21
  "severity": "",
@@ -38,11 +38,6 @@ class BaseOutputModule(BaseModule):
38
38
  if self._is_graph_important(event):
39
39
  return True, "event is critical to the graph"
40
40
 
41
- # exclude certain URLs (e.g. javascript):
42
- # TODO: revisit this after httpx rework
43
- if event.type.startswith("URL") and self.name != "httpx" and "httpx-only" in event.tags:
44
- return False, (f"Omitting {event} from output because it's marked as httpx-only")
45
-
46
41
  # omit certain event types
47
42
  if event._omit:
48
43
  if "target" in event.tags: