bbot 2.4.2__py3-none-any.whl → 2.4.2.6590rc0__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 (64) 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 +21 -0
  44. bbot/presets/web/paramminer.yml +8 -5
  45. bbot/scanner/preset/args.py +26 -0
  46. bbot/scanner/scanner.py +6 -0
  47. bbot/test/test_step_1/test__module__tests.py +1 -1
  48. bbot/test/test_step_1/test_helpers.py +7 -0
  49. bbot/test/test_step_1/test_presets.py +2 -2
  50. bbot/test/test_step_1/test_web.py +20 -0
  51. bbot/test/test_step_1/test_web_envelopes.py +343 -0
  52. bbot/test/test_step_2/module_tests/test_module_excavate.py +404 -29
  53. bbot/test/test_step_2/module_tests/test_module_httpx.py +29 -0
  54. bbot/test/test_step_2/module_tests/test_module_hunt.py +18 -1
  55. bbot/test/test_step_2/module_tests/test_module_lightfuzz.py +1947 -0
  56. bbot/test/test_step_2/module_tests/test_module_paramminer_getparams.py +4 -1
  57. bbot/test/test_step_2/module_tests/test_module_paramminer_headers.py +46 -2
  58. bbot/test/test_step_2/module_tests/test_module_reflected_parameters.py +226 -0
  59. bbot/wordlists/paramminer_parameters.txt +0 -8
  60. {bbot-2.4.2.dist-info → bbot-2.4.2.6590rc0.dist-info}/METADATA +2 -1
  61. {bbot-2.4.2.dist-info → bbot-2.4.2.6590rc0.dist-info}/RECORD +64 -42
  62. {bbot-2.4.2.dist-info → bbot-2.4.2.6590rc0.dist-info}/LICENSE +0 -0
  63. {bbot-2.4.2.dist-info → bbot-2.4.2.6590rc0.dist-info}/WHEEL +0 -0
  64. {bbot-2.4.2.dist-info → bbot-2.4.2.6590rc0.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,21 @@
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
+ modules:
3
+ - httpx
4
+ - lightfuzz
5
+ - paramminer_getparams
6
+ - reflected_parameters
7
+ - portfilter
8
+
9
+ config:
10
+ url_querystring_remove: False
11
+ url_querystring_collapse: False
12
+ modules:
13
+ lightfuzz:
14
+ enabled_submodules: [xss]
15
+ disable_post: True
16
+
17
+ conditions:
18
+ - |
19
+ {% if config.web.spider_distance == 0 %}
20
+ {{ warn("The lightfuzz-xss preset works much better with spider enabled! Consider adding 'spider' or 'spider-intense' preset.") }}
21
+ {% 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"]
bbot/scanner/scanner.py CHANGED
@@ -214,6 +214,12 @@ class Scanner:
214
214
  self.warning(
215
215
  "You have enabled custom HTTP headers. These will be attached to all in-scope requests and all requests made by httpx."
216
216
  )
217
+ # custom HTTP cookies warning
218
+ self.custom_http_cookies = self.web_config.get("http_cookies", {})
219
+ if self.custom_http_cookies:
220
+ self.warning(
221
+ "You have enabled custom HTTP cookies. These will be attached to all in-scope requests and all requests made by httpx."
222
+ )
217
223
 
218
224
  # url file extensions
219
225
  self.url_extension_blacklist = {e.lower() for e in self.config.get("url_extension_blacklist", [])}
@@ -18,7 +18,7 @@ def test__module__tests():
18
18
  preset = Preset()
19
19
 
20
20
  # make sure each module has a .py file
21
- for module_name in preset.module_loader.preloaded():
21
+ for module_name, preloaded in preset.module_loader.preloaded().items():
22
22
  module_name = module_name.lower()
23
23
  assert module_name in module_test_files, f'No test file found for module "{module_name}"'
24
24
 
@@ -460,6 +460,13 @@ async def test_helpers_misc(helpers, scan, bbot_scanner, bbot_httpserver):
460
460
  s = "asdf {unused} {used}"
461
461
  assert helpers.safe_format(s, used="fdsa") == "asdf {unused} fdsa"
462
462
 
463
+ # is_printable
464
+ assert helpers.is_printable("asdf") is True
465
+ assert helpers.is_printable(r"""~!@#$^&*()_+=-<>:"?,./;'[]\{}|""") is True
466
+ assert helpers.is_printable("ドメイン.テスト") is True
467
+ assert helpers.is_printable("4") is True
468
+ assert helpers.is_printable("asdf\x00") is False
469
+
463
470
  # punycode
464
471
  assert helpers.smart_encode_punycode("ドメイン.テスト") == "xn--eckwd4c7c.xn--zckzah"
465
472
  assert helpers.smart_decode_punycode("xn--eckwd4c7c.xn--zckzah") == "ドメイン.テスト"
@@ -596,7 +596,7 @@ class TestModule1(BaseModule):
596
596
  from bbot.modules.output.base import BaseOutputModule
597
597
 
598
598
  class TestModule2(BaseOutputModule):
599
- pass
599
+ watched_events = []
600
600
  """
601
601
  )
602
602
 
@@ -607,7 +607,7 @@ class TestModule2(BaseOutputModule):
607
607
  from bbot.modules.internal.base import BaseInternalModule
608
608
 
609
609
  class TestModule3(BaseInternalModule):
610
- pass
610
+ watched_events = []
611
611
  """
612
612
  )
613
613
 
@@ -478,3 +478,23 @@ async def test_web_cookies(bbot_scanner, httpx_mock):
478
478
  assert not client2.cookies
479
479
 
480
480
  await scan._cleanup()
481
+
482
+
483
+ @pytest.mark.asyncio
484
+ async def test_http_sendcookies(bbot_scanner, bbot_httpserver):
485
+ endpoint = "/"
486
+ url = bbot_httpserver.url_for(endpoint)
487
+ from werkzeug.wrappers import Response
488
+
489
+ def echo_cookies_handler(request):
490
+ cookies = request.cookies
491
+ cookie_str = "; ".join([f"{key}={value}" for key, value in cookies.items()])
492
+ return Response(f"Echoed Cookies: {cookie_str}\nEchoed Headers: {request.headers}")
493
+
494
+ bbot_httpserver.expect_request(uri=endpoint).respond_with_handler(echo_cookies_handler)
495
+ scan1 = bbot_scanner("127.0.0.1", config={"web": {"debug": True}})
496
+ r1 = await scan1.helpers.request(url, cookies={"foo": "bar"})
497
+
498
+ assert r1 is not None, "Request to self-signed SSL server went through even with ssl_verify=True"
499
+ assert "bar" in r1.text
500
+ await scan1._cleanup()