bbot 2.4.2.6109rc0__py3-none-any.whl → 2.4.2.6596rc0__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 (67) hide show
  1. bbot/__init__.py +1 -1
  2. bbot/core/event/base.py +64 -4
  3. bbot/core/helpers/diff.py +10 -7
  4. bbot/core/helpers/helper.py +5 -1
  5. bbot/core/helpers/misc.py +48 -11
  6. bbot/core/helpers/regex.py +4 -0
  7. bbot/core/helpers/regexes.py +45 -8
  8. bbot/core/helpers/url.py +21 -5
  9. bbot/core/helpers/web/client.py +25 -5
  10. bbot/core/helpers/web/engine.py +9 -1
  11. bbot/core/helpers/web/envelopes.py +352 -0
  12. bbot/core/helpers/web/web.py +10 -2
  13. bbot/core/helpers/yara_helper.py +50 -0
  14. bbot/core/modules.py +23 -7
  15. bbot/defaults.yml +26 -1
  16. bbot/modules/base.py +4 -2
  17. bbot/modules/{deadly/dastardly.py → dastardly.py} +1 -1
  18. bbot/modules/{deadly/ffuf.py → ffuf.py} +1 -1
  19. bbot/modules/ffuf_shortnames.py +1 -1
  20. bbot/modules/httpx.py +14 -0
  21. bbot/modules/hunt.py +24 -6
  22. bbot/modules/internal/aggregate.py +1 -0
  23. bbot/modules/internal/excavate.py +356 -197
  24. bbot/modules/lightfuzz/lightfuzz.py +203 -0
  25. bbot/modules/lightfuzz/submodules/__init__.py +0 -0
  26. bbot/modules/lightfuzz/submodules/base.py +312 -0
  27. bbot/modules/lightfuzz/submodules/cmdi.py +106 -0
  28. bbot/modules/lightfuzz/submodules/crypto.py +474 -0
  29. bbot/modules/lightfuzz/submodules/nosqli.py +183 -0
  30. bbot/modules/lightfuzz/submodules/path.py +154 -0
  31. bbot/modules/lightfuzz/submodules/serial.py +179 -0
  32. bbot/modules/lightfuzz/submodules/sqli.py +187 -0
  33. bbot/modules/lightfuzz/submodules/ssti.py +39 -0
  34. bbot/modules/lightfuzz/submodules/xss.py +191 -0
  35. bbot/modules/{deadly/nuclei.py → nuclei.py} +1 -1
  36. bbot/modules/paramminer_headers.py +2 -0
  37. bbot/modules/reflected_parameters.py +80 -0
  38. bbot/modules/{deadly/vhost.py → vhost.py} +2 -2
  39. bbot/presets/web/lightfuzz-heavy.yml +16 -0
  40. bbot/presets/web/lightfuzz-light.yml +20 -0
  41. bbot/presets/web/lightfuzz-medium.yml +14 -0
  42. bbot/presets/web/lightfuzz-superheavy.yml +13 -0
  43. bbot/presets/web/lightfuzz-xss.yml +22 -0
  44. bbot/presets/web/paramminer.yml +8 -5
  45. bbot/scanner/preset/args.py +26 -0
  46. bbot/scanner/preset/path.py +12 -10
  47. bbot/scanner/preset/preset.py +42 -37
  48. bbot/scanner/scanner.py +6 -0
  49. bbot/scripts/docs.py +5 -5
  50. bbot/test/test_step_1/test__module__tests.py +1 -1
  51. bbot/test/test_step_1/test_helpers.py +7 -0
  52. bbot/test/test_step_1/test_presets.py +2 -2
  53. bbot/test/test_step_1/test_web.py +20 -0
  54. bbot/test/test_step_1/test_web_envelopes.py +343 -0
  55. bbot/test/test_step_2/module_tests/test_module_excavate.py +404 -29
  56. bbot/test/test_step_2/module_tests/test_module_httpx.py +29 -0
  57. bbot/test/test_step_2/module_tests/test_module_hunt.py +18 -1
  58. bbot/test/test_step_2/module_tests/test_module_lightfuzz.py +1947 -0
  59. bbot/test/test_step_2/module_tests/test_module_paramminer_getparams.py +4 -1
  60. bbot/test/test_step_2/module_tests/test_module_paramminer_headers.py +46 -2
  61. bbot/test/test_step_2/module_tests/test_module_reflected_parameters.py +226 -0
  62. bbot/wordlists/paramminer_parameters.txt +0 -8
  63. {bbot-2.4.2.6109rc0.dist-info → bbot-2.4.2.6596rc0.dist-info}/METADATA +2 -1
  64. {bbot-2.4.2.6109rc0.dist-info → bbot-2.4.2.6596rc0.dist-info}/RECORD +67 -45
  65. {bbot-2.4.2.6109rc0.dist-info → bbot-2.4.2.6596rc0.dist-info}/LICENSE +0 -0
  66. {bbot-2.4.2.6109rc0.dist-info → bbot-2.4.2.6596rc0.dist-info}/WHEEL +0 -0
  67. {bbot-2.4.2.6109rc0.dist-info → bbot-2.4.2.6596rc0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,191 @@
1
+ from .base import BaseLightfuzz
2
+
3
+ import regex as re
4
+
5
+
6
+ class xss(BaseLightfuzz):
7
+ """
8
+ Detects Reflected Cross-Site Scripting vulnerabilities across multiple contexts and techniques
9
+
10
+ * Context Detection:
11
+ - Between HTML Tags: <tag>injection</tag>
12
+ - Within Tag Attributes: <tag attribute="injection">
13
+ - Inside JavaScript: <script>var x = 'injection'</script>
14
+
15
+ * Context-Specific Testing:
16
+ - Between Tags: Tests basic HTML injection and tag creation
17
+ - Tag Attributes: Tests quote escaping and JavaScript event handlers
18
+ - JavaScript Context: Tests string delimiter breaking and script tag termination
19
+ - Handles both single and double quote contexts in JavaScript
20
+
21
+ Can often detect through WAFs, since it does not attempt to construct an exploitation payload
22
+ """
23
+
24
+ friendly_name = "Cross-Site Scripting"
25
+
26
+ async def determine_context(self, cookies, html, random_string):
27
+ """
28
+ Determines the context of the random string in the HTML response.
29
+ With XSS, the context is what kind part of the page the injection is occuring in, which determine what payloads might be successful
30
+
31
+ https://portswigger.net/web-security/cross-site-scripting/contexts
32
+ """
33
+ between_tags = False
34
+ in_tag_attribute = False
35
+ in_javascript = False
36
+
37
+ between_tags_regex = re.compile(
38
+ rf"<(\/?\w+)[^>]*>.*?{random_string}.*?<\/?\w+>"
39
+ ) # The between tags context is when the injection occurs between HTML tags
40
+ in_tag_attribute_regex = re.compile(
41
+ rf'<(\w+)\s+[^>]*?(\w+)="([^"]*?{random_string}[^"]*?)"[^>]*>'
42
+ ) # The in tag attribute context is when the injection occurs in an attribute of an HTML tag
43
+ in_javascript_regex = re.compile(
44
+ rf"<script\b[^>]*>[^<]*(?:<(?!\/script>)[^<]*)*{random_string}[^<]*(?:<(?!\/script>)[^<]*)*<\/script>"
45
+ ) # The in javascript context is when the injection occurs within a <script> tag
46
+
47
+ between_tags_match = await self.lightfuzz.helpers.re.search(between_tags_regex, html)
48
+ if between_tags_match:
49
+ between_tags = True
50
+
51
+ in_tag_attribute_match = await self.lightfuzz.helpers.re.search(in_tag_attribute_regex, html)
52
+ if in_tag_attribute_match:
53
+ in_tag_attribute = True
54
+
55
+ in_javascript_match = await self.lightfuzz.helpers.re.search(in_javascript_regex, html)
56
+ if in_javascript_match:
57
+ in_javascript = True
58
+
59
+ return between_tags, in_tag_attribute, in_javascript
60
+
61
+ async def determine_javascript_quote_context(self, target, text):
62
+ # Define and compile regex patterns for double and single quotes
63
+ quote_patterns = {"double": re.compile(f'"[^"]*{target}[^"]*"'), "single": re.compile(f"'[^']*{target}[^']*'")}
64
+
65
+ # Split the text by semicolons to isolate JavaScript statements
66
+ statements = text.split(";")
67
+
68
+ # This function checks if the target string is balanced within a JavaScript statement
69
+ def is_balanced(section, target_index, quote_char):
70
+ left = section[:target_index]
71
+ right = section[target_index + len(target) :]
72
+ return left.count(quote_char) % 2 == 0 and right.count(quote_char) % 2 == 0
73
+
74
+ # For each javascript statement, attempt to determine the type of quote we are within, and therefore what will enable breaking out of it to result in a successful XSS
75
+ for statement in statements:
76
+ for quote_type, pattern in quote_patterns.items():
77
+ match = await self.lightfuzz.helpers.re.search(pattern, statement)
78
+ if match:
79
+ context = match.group(0)
80
+ target_index = context.find(target)
81
+ opposite_quote = "'" if quote_type == "double" else '"'
82
+ if is_balanced(context, target_index, opposite_quote):
83
+ return quote_type
84
+ # If we have no matches, the target string is most likely not within quotes
85
+ return "outside"
86
+
87
+ async def check_probe(self, cookies, probe, match, context):
88
+ # Send the defined probe and look for the expected match value in the response
89
+ probe_result = await self.standard_probe(self.event.data["type"], cookies, probe)
90
+ if probe_result and match in probe_result.text:
91
+ self.results.append(
92
+ {
93
+ "type": "FINDING",
94
+ "description": f"Possible Reflected XSS. Parameter: [{self.event.data['name']}] Context: [{context}] Parameter Type: [{self.event.data['type']}]",
95
+ }
96
+ )
97
+ return True
98
+ return False
99
+
100
+ async def fuzz(self):
101
+ lightfuzz_event = self.event.parent
102
+ cookies = self.event.data.get("assigned_cookies", {})
103
+
104
+ # If this came from paramminer_getparams and didn't have a http_reflection tag, we don't need to check again
105
+ if (
106
+ lightfuzz_event.type == "WEB_PARAMETER"
107
+ and str(lightfuzz_event.module) == "paramminer_getparams"
108
+ and "http-reflection" not in lightfuzz_event.tags
109
+ ):
110
+ self.debug("Got WEB_PARAMETER from paramminer, with no reflection tag - xss is not possible, aborting")
111
+ return
112
+
113
+ reflection = None
114
+ random_string = self.lightfuzz.helpers.rand_string(8)
115
+
116
+ reflection_probe_result = await self.standard_probe(self.event.data["type"], cookies, random_string)
117
+ # before continuing, check if the random string is reflected in the response - a prerequisite for XSS
118
+ if reflection_probe_result and random_string in reflection_probe_result.text:
119
+ reflection = True
120
+
121
+ if not reflection or reflection is False:
122
+ return
123
+
124
+ between_tags, in_tag_attribute, in_javascript = await self.determine_context(
125
+ cookies, reflection_probe_result.text, random_string
126
+ )
127
+ self.debug(
128
+ f"determine_context returned: between_tags [{between_tags}], in_tag_attribute [{in_tag_attribute}], in_javascript [{in_javascript}]"
129
+ )
130
+ tags = [
131
+ "z",
132
+ "svg",
133
+ "img",
134
+ ] # These represent easy to exploit tags, along with an arbitrary tag which is less likely to be blocked
135
+ if between_tags:
136
+ for tag in tags:
137
+ between_tags_probe = f"<{tag}>{random_string}</{tag}>"
138
+ result = await self.check_probe(
139
+ cookies, between_tags_probe, between_tags_probe, f"Between Tags ({tag} tag)"
140
+ ) # After reflection in the HTTP response, did the tags survive without url-encoding or other sanitization/escaping?
141
+ if result is True:
142
+ break
143
+
144
+ if in_tag_attribute:
145
+ in_tag_attribute_probe = f'{random_string}"'
146
+ in_tag_attribute_match = f'{random_string}"'
147
+ await self.check_probe(
148
+ cookies, in_tag_attribute_probe, in_tag_attribute_match, "Tag Attribute"
149
+ ) # After reflection in the HTTP response, did the quote survive without url-encoding or other sanitization/escaping?
150
+
151
+ in_tag_attribute_probe = f'{random_string}"'
152
+ in_tag_attribute_match = f'"{random_string}""'
153
+ await self.check_probe(
154
+ cookies, in_tag_attribute_probe, in_tag_attribute_match, "Tag Attribute (autoquote)"
155
+ ) # After reflection in the HTTP response, did the quote survive without url-encoding or other sanitization/escaping (and account for auto-quoting)
156
+
157
+ in_tag_attribute_probe = f"javascript:{random_string}"
158
+ in_tag_attribute_match = f'action="javascript:{random_string}'
159
+ await self.check_probe(
160
+ cookies, in_tag_attribute_probe, in_tag_attribute_match, "Form Action Injection"
161
+ ) # After reflection in the HTTP response, did the javascript sch
162
+
163
+ if in_javascript:
164
+ in_javascript_probe = rf"</script><script>{random_string}</script>"
165
+ result = await self.check_probe(
166
+ cookies, in_javascript_probe, in_javascript_probe, "In Javascript"
167
+ ) # After reflection in the HTTP response, did the script tags survive without url-encoding or other sanitization/escaping?
168
+ if result is False:
169
+ # To attempt this technique, we need to determine the type of quote we are within
170
+ quote_context = await self.determine_javascript_quote_context(
171
+ random_string, reflection_probe_result.text
172
+ )
173
+
174
+ # Skip the test if the context is outside
175
+ if quote_context == "outside":
176
+ return
177
+
178
+ # Update probes based on the quote context
179
+ if quote_context == "single":
180
+ in_javascript_escape_probe = rf"a\';zzzzz({random_string})\\"
181
+ in_javascript_escape_match = rf"a\\';zzzzz({random_string})\\"
182
+ elif quote_context == "double":
183
+ in_javascript_escape_probe = rf"a\";zzzzz({random_string})\\"
184
+ in_javascript_escape_match = rf'a\\";zzzzz({random_string})\\'
185
+
186
+ await self.check_probe(
187
+ cookies,
188
+ in_javascript_escape_probe,
189
+ in_javascript_escape_match,
190
+ f"In Javascript (escaping the escape character, {quote_context} quote)",
191
+ )
@@ -7,7 +7,7 @@ from bbot.modules.base import BaseModule
7
7
  class nuclei(BaseModule):
8
8
  watched_events = ["URL"]
9
9
  produced_events = ["FINDING", "VULNERABILITY", "TECHNOLOGY"]
10
- flags = ["active", "aggressive"]
10
+ flags = ["active", "aggressive", "deadly"]
11
11
  meta = {
12
12
  "description": "Fast and customisable vulnerability scanner",
13
13
  "created_date": "2022-03-12",
@@ -52,6 +52,7 @@ class paramminer_headers(BaseModule):
52
52
  "javascript",
53
53
  "keep-alive",
54
54
  "label",
55
+ "max-forwards",
55
56
  "negotiate",
56
57
  "proxy",
57
58
  "range",
@@ -148,6 +149,7 @@ class paramminer_headers(BaseModule):
148
149
  "type": paramtype,
149
150
  "description": description,
150
151
  "name": result,
152
+ "original_value": None,
151
153
  },
152
154
  "WEB_PARAMETER",
153
155
  event,
@@ -0,0 +1,80 @@
1
+ from bbot.modules.base import BaseModule
2
+
3
+
4
+ class reflected_parameters(BaseModule):
5
+ watched_events = ["WEB_PARAMETER"]
6
+ produced_events = ["FINDING"]
7
+ flags = ["active", "safe", "web-thorough"]
8
+ meta = {
9
+ "description": "Highlight parameters that reflect their contents in response body",
10
+ "author": "@liquidsec",
11
+ "created_date": "2024-10-29",
12
+ }
13
+
14
+ async def handle_event(self, event):
15
+ url = event.data.get("url")
16
+ reflection_detected = await self.detect_reflection(event, url)
17
+
18
+ if reflection_detected:
19
+ param_type = event.data.get("type", "UNKNOWN")
20
+ description = (
21
+ f"[{param_type}] Parameter value reflected in response body. Name: [{event.data['name']}] "
22
+ f"Source Module: [{str(event.module)}]"
23
+ )
24
+ if event.data.get("original_value"):
25
+ description += (
26
+ f" Original Value: [{self.helpers.truncate_string(str(event.data['original_value']), 200)}]"
27
+ )
28
+ data = {"host": str(event.host), "description": description, "url": url}
29
+ await self.emit_event(data, "FINDING", event)
30
+
31
+ async def detect_reflection(self, event, url):
32
+ """Detects reflection by sending a probe with a random value and a canary parameter."""
33
+ probe_parameter_name = event.data["name"]
34
+ probe_parameter_value = self.helpers.rand_string()
35
+ canary_parameter_value = self.helpers.rand_string()
36
+ probe_response = await self.send_probe_with_canary(
37
+ event,
38
+ probe_parameter_name,
39
+ probe_parameter_value,
40
+ canary_parameter_value,
41
+ cookies=event.data.get("assigned_cookies", {}),
42
+ timeout=10,
43
+ )
44
+
45
+ # Check if the probe parameter value is reflected AND the canary is not
46
+ if probe_response:
47
+ response_text = probe_response.text
48
+ reflection_result = probe_parameter_value in response_text and canary_parameter_value not in response_text
49
+ return reflection_result
50
+ return False
51
+
52
+ async def send_probe_with_canary(self, event, parameter_name, parameter_value, canary_value, cookies, timeout=10):
53
+ method = "GET"
54
+ url = event.data["url"]
55
+ headers = {}
56
+ data = None
57
+ json_data = None
58
+ params = {parameter_name: parameter_value, "c4n4ry": canary_value}
59
+
60
+ if event.data["type"] == "GETPARAM":
61
+ url = f"{url}?{parameter_name}={parameter_value}&c4n4ry={canary_value}"
62
+ elif event.data["type"] == "COOKIE":
63
+ cookies.update(params)
64
+ elif event.data["type"] == "HEADER":
65
+ headers.update(params)
66
+ elif event.data["type"] == "POSTPARAM":
67
+ method = "POST"
68
+ data = params
69
+ elif event.data["type"] == "BODYJSON":
70
+ method = "POST"
71
+ json_data = params
72
+
73
+ self.debug(
74
+ f"Sending {method} request to {url} with headers: {headers}, cookies: {cookies}, data: {data}, json: {json_data}"
75
+ )
76
+
77
+ response = await self.helpers.request(
78
+ method=method, url=url, headers=headers, cookies=cookies, data=data, json=json_data, timeout=timeout
79
+ )
80
+ return response
@@ -1,13 +1,13 @@
1
1
  import base64
2
2
  from urllib.parse import urlparse
3
3
 
4
- from bbot.modules.deadly.ffuf import ffuf
4
+ from bbot.modules.ffuf import ffuf
5
5
 
6
6
 
7
7
  class vhost(ffuf):
8
8
  watched_events = ["URL"]
9
9
  produced_events = ["VHOST", "DNS_NAME"]
10
- flags = ["active", "aggressive", "slow"]
10
+ flags = ["active", "aggressive", "slow", "deadly"]
11
11
  meta = {"description": "Fuzz for virtual hosts", "created_date": "2022-05-02", "author": "@liquidsec"}
12
12
 
13
13
  special_vhost_list = ["127.0.0.1", "localhost", "host.docker.internal"]
@@ -0,0 +1,16 @@
1
+ description: Discover web parameters and lightly fuzz them for vulnerabilities, with more intense discovery techniques, including POST parameters, which are more invasive. Uses all lightfuzz modules, and adds paramminer modules for parameter discovery.
2
+
3
+ include:
4
+ - lightfuzz-medium
5
+
6
+ flags:
7
+ - web-paramminer
8
+
9
+ modules:
10
+ - robots
11
+
12
+ config:
13
+ modules:
14
+ lightfuzz:
15
+ enabled_submodules: [cmdi,crypto,nosqli,path,serial,sqli,ssti,xss]
16
+ disable_post: False
@@ -0,0 +1,20 @@
1
+ description: Discover web parameters and lightly fuzz them for vulnerabilities, with only the most common vulnerabilities and minimal extra modules. Safest to run alongside larger scans.
2
+
3
+ modules:
4
+ - httpx
5
+ - lightfuzz
6
+ - portfilter
7
+
8
+ config:
9
+ url_querystring_remove: False # don't strip off the querystring (BBOT normally does this; but lightfuzz needs it)
10
+ url_querystring_collapse: True # in cases where the same parameter has multiple values, collapse them into a single parameter to save on fuzzing attempts
11
+ modules:
12
+ lightfuzz:
13
+ enabled_submodules: [path,sqli,xss] # only look for the most common vulnerabilities
14
+ disable_post: True # don't send POST requests (less aggressive)
15
+
16
+ conditions:
17
+ - |
18
+ {% if config.web.spider_distance == 0 %}
19
+ {{ warn("Lightfuzz works much better with spider enabled! Consider adding 'spider' or 'spider-intense' preset.") }}
20
+ {% endif %}
@@ -0,0 +1,14 @@
1
+ description: Discover web parameters and lightly fuzz them for vulnerabilities. Uses all lightfuzz modules, without some of the more intense discovery techniques. Does not send POST requests. This is the default lightfuzz preset; if you're not sure which one to use, this is a good starting point.
2
+
3
+ include:
4
+ - lightfuzz-light
5
+
6
+ modules:
7
+ - badsecrets
8
+ - hunt
9
+ - reflected_parameters
10
+
11
+ config:
12
+ modules:
13
+ lightfuzz:
14
+ enabled_submodules: [cmdi,crypto,nosqli,path,serial,sqli,ssti,xss]
@@ -0,0 +1,13 @@
1
+ description: Discover web parameters and lightly fuzz them for vulnerabilities, with the most intense discovery techniques, including POST parameters, which are more invasive. Uses all lightfuzz modules, adds paramminer modules for parameter discovery, and tests each unique parameter-value instance individually.
2
+
3
+ include:
4
+ - lightfuzz-heavy
5
+
6
+ config:
7
+ url_querystring_collapse: False # in cases where the same parameter is observed multiple times, fuzz them individually instead of collapsing them into a single parameter
8
+ modules:
9
+ lightfuzz:
10
+ force_common_headers: True # Fuzz common headers like X-Forwarded-For even if they're not observed on the target
11
+ enabled_submodules: [cmdi,crypto,nosqli,path,serial,sqli,ssti,xss]
12
+ excavate:
13
+ speculate_params: True # speculate potential parameters extracted from JSON/XML web responses
@@ -0,0 +1,22 @@
1
+ description: Discover web parameters and lightly fuzz them, limited to just GET-based xss vulnerabilities. This is an example of a custom lightfuzz preset, selectively enabling a single lightfuzz module.
2
+
3
+ modules:
4
+ - httpx
5
+ - lightfuzz
6
+ - paramminer_getparams
7
+ - reflected_parameters
8
+ - portfilter
9
+
10
+ config:
11
+ url_querystring_remove: False
12
+ url_querystring_collapse: False
13
+ modules:
14
+ lightfuzz:
15
+ enabled_submodules: [xss]
16
+ disable_post: True
17
+
18
+ conditions:
19
+ - |
20
+ {% if config.web.spider_distance == 0 %}
21
+ {{ warn("The lightfuzz-xss preset works much better with spider enabled! Consider adding 'spider' or 'spider-intense' preset.") }}
22
+ {% endif %}
@@ -1,12 +1,15 @@
1
- description: Discover new web parameters via brute-force
1
+ description: Discover new web parameters via brute-force, and analyze them with additional modules
2
2
 
3
3
  flags:
4
4
  - web-paramminer
5
5
 
6
6
  modules:
7
7
  - httpx
8
+ - reflected_parameters
9
+ - hunt
8
10
 
9
- config:
10
- web:
11
- spider_distance: 1
12
- spider_depth: 4
11
+ conditions:
12
+ - |
13
+ {% if config.web.spider_distance == 0 %}
14
+ {{ warn("The paramminer preset works much better with spider enabled! Consider adding 'spider' or 'spider-intense' preset.") }}
15
+ {% endif %}
@@ -168,6 +168,9 @@ class BBOTArgs:
168
168
  if self.parsed.custom_headers:
169
169
  args_preset.core.merge_custom({"web": {"http_headers": self.parsed.custom_headers}})
170
170
 
171
+ if self.parsed.custom_cookies:
172
+ args_preset.core.merge_custom({"web": {"http_cookies": self.parsed.custom_cookies}})
173
+
171
174
  if self.parsed.custom_yara_rules:
172
175
  args_preset.core.merge_custom(
173
176
  {"modules": {"excavate": {"custom_yara_rules": self.parsed.custom_yara_rules}}}
@@ -375,6 +378,13 @@ class BBOTArgs:
375
378
  default=[],
376
379
  help="List of custom headers as key value pairs (header=value).",
377
380
  )
381
+ misc.add_argument(
382
+ "-C",
383
+ "--custom-cookies",
384
+ nargs="+",
385
+ default=[],
386
+ help="List of custom cookies as key value pairs (cookie=value).",
387
+ )
378
388
  misc.add_argument("--custom-yara-rules", "-cy", help="Add custom yara rules to excavate")
379
389
 
380
390
  misc.add_argument("--user-agent", "-ua", help="Set the user-agent for all HTTP requests")
@@ -420,6 +430,22 @@ class BBOTArgs:
420
430
  custom_headers_dict[k] = v
421
431
  self.parsed.custom_headers = custom_headers_dict
422
432
 
433
+ # Custom Cookie Parsing / Validation
434
+ custom_cookies_dict = {}
435
+ custom_cookie_example = "Example: --custom-cookies foo=bar foo2=bar2"
436
+
437
+ for i in self.parsed.custom_cookies:
438
+ parts = i.split("=", 1)
439
+ if len(parts) != 2:
440
+ raise ValidationError(f"Custom cookies not formatted correctly (missing '='). {custom_cookie_example}")
441
+ k, v = parts
442
+ if not k or not v:
443
+ raise ValidationError(
444
+ f"Custom cookies not formatted correctly (missing cookie name or value). {custom_cookie_example}"
445
+ )
446
+ custom_cookies_dict[k] = v
447
+ self.parsed.custom_cookies = custom_cookies_dict
448
+
423
449
  # --fast-mode
424
450
  if self.parsed.fast_mode:
425
451
  self.parsed.preset += ["fast"]
@@ -6,6 +6,7 @@ from bbot.errors import *
6
6
  log = logging.getLogger("bbot.presets.path")
7
7
 
8
8
  DEFAULT_PRESET_PATH = Path(__file__).parent.parent.parent / "presets"
9
+ DEFAULT_PRESET_PATH = DEFAULT_PRESET_PATH.expanduser().resolve()
9
10
 
10
11
 
11
12
  class PresetPath:
@@ -17,7 +18,7 @@ class PresetPath:
17
18
  self.paths = [DEFAULT_PRESET_PATH]
18
19
 
19
20
  def find(self, filename):
20
- filename_path = Path(filename).resolve()
21
+ filename_path = Path(filename).expanduser().resolve()
21
22
  extension = filename_path.suffix.lower()
22
23
  file_candidates = set()
23
24
  extension_candidates = {".yaml", ".yml"}
@@ -29,16 +30,12 @@ class PresetPath:
29
30
  file_candidates.add(f"{filename_path.stem}{ext}")
30
31
  file_candidates = sorted(file_candidates)
31
32
  file_candidates_str = ",".join([str(s) for s in file_candidates])
32
- paths_to_search = self.paths
33
33
  if "/" in str(filename):
34
- if filename_path.parent not in paths_to_search:
35
- paths_to_search.append(filename_path.parent)
36
- log.debug(
37
- f"Searching for preset in {[str(p) for p in paths_to_search]}, file candidates: {file_candidates_str}"
38
- )
39
- for path in paths_to_search:
34
+ self.add_path(filename_path.parent)
35
+ log.debug(f"Searching for {file_candidates_str} in {[str(p) for p in self.paths]}")
36
+ for path in self.paths:
40
37
  for candidate in file_candidates:
41
- for file in path.rglob(candidate):
38
+ for file in path.rglob(f"**/{candidate}"):
42
39
  if file.is_file():
43
40
  log.verbose(f'Found preset matching "{filename}" at {file}')
44
41
  self.add_path(file.parent)
@@ -51,14 +48,19 @@ class PresetPath:
51
48
  return ":".join([str(s) for s in self.paths])
52
49
 
53
50
  def add_path(self, path):
54
- path = Path(path).resolve()
51
+ path = Path(path).expanduser().resolve()
52
+ # skip if already in paths
55
53
  if path in self.paths:
56
54
  return
55
+ # skip if path is a subdirectory of any path in paths
57
56
  if any(path.is_relative_to(p) for p in self.paths):
58
57
  return
58
+ # skip if path is not a directory
59
59
  if not path.is_dir():
60
60
  log.debug(f'Path "{path.resolve()}" is not a directory')
61
61
  return
62
+ # preemptively remove any paths that are subdirectories of the new path
63
+ self.paths = [p for p in self.paths if not p.is_relative_to(path)]
62
64
  self.paths.append(path)
63
65
 
64
66
  def __iter__(self):