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,203 @@
1
+ import importlib
2
+ from bbot.modules.base import BaseModule
3
+
4
+ from bbot.errors import InteractshError
5
+
6
+
7
+ class lightfuzz(BaseModule):
8
+ watched_events = ["URL", "WEB_PARAMETER"]
9
+ produced_events = ["FINDING", "VULNERABILITY"]
10
+ flags = ["active", "aggressive", "web-thorough", "deadly"]
11
+
12
+ options = {
13
+ "force_common_headers": False,
14
+ "enabled_submodules": ["sqli", "cmdi", "xss", "path", "ssti", "crypto", "serial", "nosqli"],
15
+ "disable_post": False,
16
+ }
17
+ options_desc = {
18
+ "force_common_headers": "Force emit commonly exploitable parameters that may be difficult to detect",
19
+ "enabled_submodules": "A list of submodules to enable. Empty list enabled all modules.",
20
+ "disable_post": "Disable processing of POST parameters, avoiding form submissions.",
21
+ }
22
+
23
+ meta = {
24
+ "description": "Find Web Parameters and Lightly Fuzz them using a heuristic based scanner",
25
+ "author": "@liquidsec",
26
+ "created_date": "2024-06-28",
27
+ }
28
+ common_headers = ["x-forwarded-for", "user-agent"]
29
+ in_scope_only = True
30
+
31
+ _module_threads = 4
32
+
33
+ async def setup(self):
34
+ self.event_dict = {}
35
+ self.interactsh_subdomain_tags = {}
36
+ self.interactsh_instance = None
37
+ self.disable_post = self.config.get("disable_post", False)
38
+ self.enabled_submodules = self.config.get("enabled_submodules")
39
+ self.interactsh_disable = self.scan.config.get("interactsh_disable", False)
40
+ self.submodules = {}
41
+
42
+ if not self.enabled_submodules:
43
+ return False, "Lightfuzz enabled without any submodules. Must enable at least one submodule."
44
+
45
+ for submodule_name in self.enabled_submodules:
46
+ try:
47
+ submodule_module = importlib.import_module(f"bbot.modules.lightfuzz.submodules.{submodule_name}")
48
+ submodule_class = getattr(submodule_module, submodule_name)
49
+ except ImportError:
50
+ return False, f"Invalid Lightfuzz submodule ({submodule_name}) specified in enabled_modules"
51
+ self.submodules[submodule_name] = submodule_class
52
+
53
+ interactsh_needed = any(submodule.uses_interactsh for submodule in self.submodules.values())
54
+
55
+ if interactsh_needed and not self.interactsh_disable:
56
+ try:
57
+ self.interactsh_instance = self.helpers.interactsh()
58
+ self.interactsh_domain = await self.interactsh_instance.register(callback=self.interactsh_callback)
59
+ except InteractshError as e:
60
+ self.warning(f"Interactsh failure: {e}")
61
+ return True
62
+
63
+ async def interactsh_callback(self, r):
64
+ full_id = r.get("full-id", None)
65
+ if full_id:
66
+ if "." in full_id:
67
+ details = self.interactsh_subdomain_tags.get(full_id.split(".")[0])
68
+ if not details["event"]:
69
+ return
70
+ # currently, this is only used by the cmdi submodule. Later, when other modules use it, we will need to store description data in the interactsh_subdomain_tags dictionary
71
+ await self.emit_event(
72
+ {
73
+ "severity": "CRITICAL",
74
+ "host": str(details["event"].host),
75
+ "url": details["event"].data["url"],
76
+ "description": f"OS Command Injection (OOB Interaction) Type: [{details['type']}] Parameter Name: [{details['name']}] Probe: [{details['probe']}]",
77
+ },
78
+ "VULNERABILITY",
79
+ details["event"],
80
+ )
81
+ else:
82
+ # this is likely caused by something trying to resolve the base domain first and can be ignored
83
+ self.debug("skipping result because subdomain tag was missing")
84
+
85
+ def _outgoing_dedup_hash(self, event):
86
+ return hash(
87
+ (
88
+ "lightfuzz",
89
+ str(event.host),
90
+ event.data["url"],
91
+ event.data["description"],
92
+ event.data.get("type", ""),
93
+ event.data.get("name", ""),
94
+ )
95
+ )
96
+
97
+ async def run_submodule(self, submodule, event):
98
+ submodule_instance = submodule(self, event)
99
+ await submodule_instance.fuzz()
100
+ if len(submodule_instance.results) > 0:
101
+ for r in submodule_instance.results:
102
+ event_data = {"host": str(event.host), "url": event.data["url"], "description": r["description"]}
103
+
104
+ envelopes = getattr(event, "envelopes", None)
105
+ envelope_summary = getattr(envelopes, "summary", None)
106
+ if envelope_summary:
107
+ # Append the envelope summary to the description
108
+ event_data["description"] += f" Envelopes: [{envelope_summary}]"
109
+
110
+ if r["type"] == "VULNERABILITY":
111
+ event_data["severity"] = r["severity"]
112
+ await self.emit_event(
113
+ event_data,
114
+ r["type"],
115
+ event,
116
+ )
117
+
118
+ async def handle_event(self, event):
119
+ if event.type == "URL":
120
+ if self.config.get("force_common_headers", False) is False:
121
+ return False
122
+
123
+ # If force_common_headers is True, we force the emission of a WEB_PARAMETER for each of the common headers to force fuzzing against them
124
+ for h in self.common_headers:
125
+ description = f"Speculative (Forced) Header [{h}]"
126
+ data = {
127
+ "host": str(event.host),
128
+ "type": "HEADER",
129
+ "name": h,
130
+ "original_value": None,
131
+ "url": event.data,
132
+ "description": description,
133
+ }
134
+ await self.emit_event(data, "WEB_PARAMETER", event)
135
+
136
+ elif event.type == "WEB_PARAMETER":
137
+ # check connectivity to url
138
+ connectivity_test = await self.helpers.request(event.data["url"], timeout=10)
139
+
140
+ if connectivity_test:
141
+ for submodule_name, submodule in self.submodules.items():
142
+ self.debug(f"Starting {submodule_name} fuzz()")
143
+ await self.run_submodule(submodule, event)
144
+ else:
145
+ self.debug(f"WEB_PARAMETER URL {event.data['url']} failed connectivity test, aborting")
146
+
147
+ async def cleanup(self):
148
+ if self.interactsh_instance:
149
+ try:
150
+ await self.interactsh_instance.deregister()
151
+ self.debug(
152
+ f"successfully deregistered interactsh session with correlation_id {self.interactsh_instance.correlation_id}"
153
+ )
154
+ except InteractshError as e:
155
+ self.warning(f"Interactsh failure: {e}")
156
+
157
+ async def finish(self):
158
+ if self.interactsh_instance:
159
+ await self.helpers.sleep(5)
160
+ try:
161
+ for r in await self.interactsh_instance.poll():
162
+ await self.interactsh_callback(r)
163
+ except InteractshError as e:
164
+ self.debug(f"Error in interact.sh: {e}")
165
+
166
+ # If we've disabled fuzzing POST parameters, back out of POSTPARAM WEB_PARAMETER events as quickly as possible
167
+ async def filter_event(self, event):
168
+ if event.type == "WEB_PARAMETER" and self.disable_post and event.data["type"] == "POSTPARAM":
169
+ return False, "POST parameter disabled in lightfuzz module"
170
+ return True
171
+
172
+ @classmethod
173
+ def help_text(self):
174
+ # Call the base class help_text method
175
+ base_help_text = super().help_text()
176
+
177
+ import importlib
178
+
179
+ submodules = {}
180
+ for submodule_name in self.options.get("enabled_submodules", []):
181
+ try:
182
+ submodule_module = importlib.import_module(f"bbot.modules.lightfuzz.submodules.{submodule_name}")
183
+ submodule_class = getattr(submodule_module, submodule_name)
184
+ submodules[submodule_name] = submodule_class
185
+ except ImportError:
186
+ continue
187
+
188
+ # Find all submodules
189
+ submodules_info = "\nLightfuzz Submodules:\n"
190
+ for submodule_name, submodule_class in submodules.items():
191
+ try:
192
+ friendly_name = getattr(submodule_class, "friendly_name", submodule_name)
193
+ description = (
194
+ submodule_class.__doc__.strip() if submodule_class.__doc__ else "No description available"
195
+ )
196
+ indented_description = " " + description.replace("\n", "\n ")
197
+ submodules_info += f" - {submodule_name} ({friendly_name}):\n"
198
+ submodules_info += f"{indented_description}\n\n"
199
+ except AttributeError:
200
+ continue
201
+
202
+ # Combine the base help text with the submodules information
203
+ return base_help_text + submodules_info
File without changes
@@ -0,0 +1,312 @@
1
+ import copy
2
+ import base64
3
+ import binascii
4
+ from urllib.parse import quote
5
+
6
+
7
+ class BaseLightfuzz:
8
+ friendly_name = ""
9
+ uses_interactsh = False
10
+
11
+ def __init__(self, lightfuzz, event):
12
+ self.lightfuzz = lightfuzz
13
+ self.event = event
14
+ self.results = []
15
+ self.parameter_name = self.event.data["name"]
16
+
17
+ @staticmethod
18
+ def is_hex(s):
19
+ try:
20
+ bytes.fromhex(s)
21
+ return True
22
+ except ValueError:
23
+ return False
24
+
25
+ @staticmethod
26
+ def is_base64(s):
27
+ try:
28
+ if base64.b64encode(base64.b64decode(s)).decode() == s:
29
+ return True
30
+ except (binascii.Error, UnicodeDecodeError):
31
+ return False
32
+ return False
33
+
34
+ # WEB_PARAMETER event may contain additional_params (e.g. other parameters in the same form or query string). These will be sent unchanged along with the probe.
35
+ def additional_params_process(self, additional_params, additional_params_populate_empty):
36
+ """
37
+ Processes additional parameters by populating blank or empty values with random strings if specified.
38
+
39
+ Parameters:
40
+ - additional_params (dict): A dictionary of additional parameters to process.
41
+ - additional_params_populate_blank_empty (bool): If True, populates blank or empty parameter values with random numeric strings.
42
+
43
+ Returns:
44
+ - dict: A dictionary with processed additional parameters, where blank or empty values are replaced with random strings if specified.
45
+
46
+ The function iterates over the provided additional parameters and replaces any blank or empty values with a random numeric string
47
+ of length 10, if the flag is set to True. Otherwise, it returns the parameters unchanged.
48
+ """
49
+ if not additional_params or not additional_params_populate_empty:
50
+ return additional_params
51
+
52
+ return {
53
+ k: self.lightfuzz.helpers.rand_string(10, numeric_only=True) if v in ("", None) else v
54
+ for k, v in additional_params.items()
55
+ }
56
+
57
+ def conditional_urlencode(self, probe, event_type, skip_urlencoding=False):
58
+ """Conditionally url-encodes the probe if the event type requires it and encoding is not skipped by the submodule.
59
+ We also don't encode if any envelopes are present.
60
+ """
61
+ if event_type in ["GETPARAM", "COOKIE"] and not skip_urlencoding and getattr(self.event, "envelopes", None):
62
+ # Exclude '&' from being encoded since we are operating on full query strings
63
+ return quote(probe, safe="&")
64
+ return probe
65
+
66
+ def build_query_string(self, probe, parameter_name, additional_params=None):
67
+ """Constructs a URL with query parameters from the given probe and additional parameters."""
68
+ url = f"{self.event.data['url']}?{parameter_name}={probe}"
69
+ if additional_params:
70
+ url = self.lightfuzz.helpers.add_get_params(url, additional_params, encode=False).geturl()
71
+ return url
72
+
73
+ def prepare_request(
74
+ self,
75
+ event_type,
76
+ probe,
77
+ cookies,
78
+ additional_params=None,
79
+ speculative_mode="GETPARAM",
80
+ parameter_name_suffix="",
81
+ additional_params_populate_empty=False,
82
+ skip_urlencoding=False,
83
+ ):
84
+ """
85
+ Prepares the request parameters by processing the probe and constructing the request based on the event type.
86
+ """
87
+
88
+ if parameter_name_suffix:
89
+ parameter_name = f"{self.parameter_name}{parameter_name_suffix}"
90
+ else:
91
+ parameter_name = self.parameter_name
92
+ additional_params = self.additional_params_process(additional_params, additional_params_populate_empty)
93
+
94
+ # Transparently pack the probe value into the envelopes, if present
95
+ probe = self.outgoing_probe_value(probe)
96
+
97
+ # URL Encode the probe if the event type is GETPARAM or COOKIE, if there are no envelopes, and the submodule did not opt-out with skip_urlencoding
98
+ probe = self.conditional_urlencode(probe, event_type, skip_urlencoding)
99
+
100
+ if event_type == "SPECULATIVE":
101
+ event_type = speculative_mode
102
+
103
+ # Construct request parameters based on the event type
104
+ if event_type == "GETPARAM":
105
+ url = self.build_query_string(probe, parameter_name, additional_params)
106
+ return {"method": "GET", "cookies": cookies, "url": url}
107
+ elif event_type == "COOKIE":
108
+ cookies_probe = {parameter_name: probe}
109
+ return {"method": "GET", "cookies": {**cookies, **cookies_probe}, "url": self.event.data["url"]}
110
+ elif event_type == "HEADER":
111
+ headers = {parameter_name: probe}
112
+ return {"method": "GET", "headers": headers, "cookies": cookies, "url": self.event.data["url"]}
113
+ elif event_type in ["POSTPARAM", "BODYJSON"]:
114
+ # Prepare data for POSTPARAM and BODYJSON event types
115
+ data = {parameter_name: probe}
116
+ if additional_params:
117
+ data.update(additional_params)
118
+ if event_type == "BODYJSON":
119
+ return {"method": "POST", "json": data, "cookies": cookies, "url": self.event.data["url"]}
120
+ else:
121
+ return {"method": "POST", "data": data, "cookies": cookies, "url": self.event.data["url"]}
122
+
123
+ def compare_baseline(
124
+ self,
125
+ event_type,
126
+ probe,
127
+ cookies,
128
+ additional_params_populate_empty=False,
129
+ speculative_mode="GETPARAM",
130
+ skip_urlencoding=False,
131
+ parameter_name_suffix="",
132
+ parameter_name_suffix_additional_params="",
133
+ ):
134
+ """
135
+ Compares the baseline using prepared request parameters.
136
+ """
137
+ additional_params = copy.deepcopy(self.event.data.get("additional_params", {}))
138
+
139
+ if additional_params and parameter_name_suffix_additional_params:
140
+ # Add suffix to each key in additional_params
141
+ additional_params = {
142
+ f"{k}{parameter_name_suffix_additional_params}": v for k, v in additional_params.items()
143
+ }
144
+
145
+ request_params = self.prepare_request(
146
+ event_type,
147
+ probe,
148
+ cookies,
149
+ additional_params,
150
+ speculative_mode,
151
+ parameter_name_suffix,
152
+ additional_params_populate_empty,
153
+ skip_urlencoding,
154
+ )
155
+ request_params.update({"include_cache_buster": False})
156
+ return self.lightfuzz.helpers.http_compare(**request_params)
157
+
158
+ async def baseline_probe(self, cookies):
159
+ """
160
+ Executes a baseline probe to establish a baseline for comparison.
161
+ """
162
+ if self.event.data.get("eventtype") in ["POSTPARAM", "BODYJSON"]:
163
+ method = "POST"
164
+ else:
165
+ method = "GET"
166
+
167
+ return await self.lightfuzz.helpers.request(
168
+ method=method,
169
+ cookies=cookies,
170
+ url=self.event.data.get("url"),
171
+ allow_redirects=False,
172
+ retries=1,
173
+ timeout=10,
174
+ )
175
+
176
+ async def compare_probe(
177
+ self,
178
+ http_compare,
179
+ event_type,
180
+ probe,
181
+ cookies,
182
+ additional_params_populate_empty=False,
183
+ additional_params_override={},
184
+ speculative_mode="GETPARAM",
185
+ skip_urlencoding=False,
186
+ parameter_name_suffix="",
187
+ parameter_name_suffix_additional_params="",
188
+ ):
189
+ # Deep copy to avoid modifying original additional_params
190
+ additional_params = copy.deepcopy(self.event.data.get("additional_params", {}))
191
+
192
+ # Override additional parameters if provided
193
+ additional_params.update(additional_params_override)
194
+
195
+ if additional_params and parameter_name_suffix_additional_params:
196
+ # Add suffix to each key in additional_params
197
+ additional_params = {
198
+ f"{k}{parameter_name_suffix_additional_params}": v for k, v in additional_params.items()
199
+ }
200
+
201
+ # Prepare request parameters
202
+ request_params = self.prepare_request(
203
+ event_type,
204
+ probe,
205
+ cookies,
206
+ additional_params,
207
+ speculative_mode,
208
+ parameter_name_suffix,
209
+ additional_params_populate_empty,
210
+ skip_urlencoding,
211
+ )
212
+ # Perform the comparison using the constructed request parameters
213
+ url = request_params.pop("url")
214
+ return await http_compare.compare(url, **request_params)
215
+
216
+ async def standard_probe(
217
+ self,
218
+ event_type,
219
+ cookies,
220
+ probe,
221
+ timeout=10,
222
+ additional_params_populate_empty=False,
223
+ speculative_mode="GETPARAM",
224
+ allow_redirects=False,
225
+ skip_urlencoding=False,
226
+ ):
227
+ request_params = self.prepare_request(
228
+ event_type,
229
+ probe,
230
+ cookies,
231
+ self.event.data.get("additional_params"),
232
+ speculative_mode,
233
+ "",
234
+ additional_params_populate_empty,
235
+ skip_urlencoding,
236
+ )
237
+ request_params.update({"allow_redirects": allow_redirects, "retries": 0, "timeout": timeout})
238
+ self.debug(f"standard_probe requested URL: [{request_params['url']}]")
239
+ return await self.lightfuzz.helpers.request(**request_params)
240
+
241
+ def metadata(self):
242
+ metadata_string = f"Parameter: [{self.event.data['name']}] Parameter Type: [{self.event.data['type']}]"
243
+ if self.event.data["original_value"] != "" and self.event.data["original_value"] is not None:
244
+ metadata_string += (
245
+ f" Original Value: [{self.lightfuzz.helpers.truncate_string(self.event.data['original_value'], 200)}]"
246
+ )
247
+ return metadata_string
248
+
249
+ def incoming_probe_value(self, populate_empty=True):
250
+ """
251
+ Transparently modifies the incoming probe value (the original value of the WEB_PARAMETER), given any envelopes that may have been identified, so that fuzzing within the envelopes can occur.
252
+ """
253
+ envelopes = getattr(self.event, "envelopes", None)
254
+ probe_value = ""
255
+ if envelopes is not None:
256
+ probe_value = envelopes.get_subparam()
257
+ self.debug(f"incoming_probe_value (after unpacking): {probe_value} with envelopes [{envelopes}]")
258
+ if not probe_value:
259
+ if populate_empty is True:
260
+ probe_value = self.lightfuzz.helpers.rand_string(10, numeric_only=True)
261
+ else:
262
+ probe_value = ""
263
+ probe_value = str(probe_value)
264
+ return probe_value
265
+
266
+ def outgoing_probe_value(self, outgoing_probe_value):
267
+ """
268
+ Transparently modifies the outgoing probe value (fuzz probe being sent to the target), given any envelopes that may have been identified, so that fuzzing within the envelopes can occur.
269
+ """
270
+ self.debug(f"outgoing_probe_value (before packing): {outgoing_probe_value} / {self.event}")
271
+ envelopes = getattr(self.event, "envelopes", None)
272
+ if envelopes is not None:
273
+ envelopes.set_subparam(value=outgoing_probe_value)
274
+ outgoing_probe_value = envelopes.pack()
275
+ self.debug(
276
+ f"outgoing_probe_value (after packing): {outgoing_probe_value} with envelopes [{envelopes}] / {self.event}"
277
+ )
278
+ return outgoing_probe_value
279
+
280
+ def get_submodule_name(self):
281
+ """Extracts the submodule name from the class name."""
282
+ return self.__class__.__name__.replace("Lightfuzz", "").lower()
283
+
284
+ def log(self, level, message, *args, **kwargs):
285
+ submodule_name = self.get_submodule_name()
286
+ prefixed_message = f"[{submodule_name}] {message}"
287
+ log_method = getattr(self.lightfuzz, level)
288
+ log_method(prefixed_message, *args, **kwargs)
289
+
290
+ def debug(self, message, *args, **kwargs):
291
+ self.log("debug", message, *args, **kwargs)
292
+
293
+ def verbose(self, message, *args, **kwargs):
294
+ self.log("verbose", message, *args, **kwargs)
295
+
296
+ def info(self, message, *args, **kwargs):
297
+ self.log("info", message, *args, **kwargs)
298
+
299
+ def hugeinfo(self, message, *args, **kwargs):
300
+ self.log("hugeinfo", message, *args, **kwargs)
301
+
302
+ def warning(self, message, *args, **kwargs):
303
+ self.log("warning", message, *args, **kwargs)
304
+
305
+ def hugewarning(self, message, *args, **kwargs):
306
+ self.log("hugewarning", message, *args, **kwargs)
307
+
308
+ def error(self, message, *args, **kwargs):
309
+ self.log("error", message, *args, **kwargs)
310
+
311
+ def critical(self, message, *args, **kwargs):
312
+ self.log("critical", message, *args, **kwargs)
@@ -0,0 +1,106 @@
1
+ from bbot.errors import HttpCompareError
2
+ from .base import BaseLightfuzz
3
+
4
+ import urllib.parse
5
+
6
+
7
+ class cmdi(BaseLightfuzz):
8
+ """
9
+ Detects command injection vulnerabilities.
10
+
11
+ Techniques:
12
+
13
+ * Echo Canary Detection:
14
+ - Injects command delimiters (;, &&, ||, &, |) along with an echo command
15
+ - Checks if the echoed canary appears in the response without the "echo" itself
16
+ - Uses a false positive probe to validate findings
17
+
18
+ * Blind Command Injection:
19
+ - Injects nslookup commands with unique subdomain tags
20
+ - Detects command execution through DNS resolution via Interactsh
21
+ """
22
+
23
+ friendly_name = "Command Injection"
24
+ uses_interactsh = True
25
+
26
+ async def fuzz(self):
27
+ cookies = self.event.data.get(
28
+ "assigned_cookies", {}
29
+ ) # Retrieve assigned cookies from WEB_PARAMETER event data, if present
30
+ probe_value = self.incoming_probe_value()
31
+
32
+ canary = self.lightfuzz.helpers.rand_string(10, numeric_only=True)
33
+ http_compare = self.compare_baseline(
34
+ self.event.data["type"], probe_value, cookies
35
+ ) # Initialize the http_compare object and establish a baseline HTTP response
36
+
37
+ cmdi_probe_strings = [
38
+ "AAAA", # False positive probe
39
+ ";",
40
+ "&&",
41
+ "||",
42
+ "&",
43
+ "|",
44
+ ]
45
+
46
+ positive_detections = []
47
+ for p in cmdi_probe_strings:
48
+ try:
49
+ # add "echo" to the cmdi probe value to construct the command to be executed
50
+ echo_probe = f"{probe_value}{p} echo {canary} {p}"
51
+ # we have to handle our own URL-encoding here, because our payloads include the & character
52
+ if self.event.data["type"] == "GETPARAM":
53
+ echo_probe = urllib.parse.quote(echo_probe.encode(), safe="")
54
+
55
+ # send cmdi probe and compare with baseline response
56
+ cmdi_probe = await self.compare_probe(
57
+ http_compare, self.event.data["type"], echo_probe, cookies, skip_urlencoding=True
58
+ )
59
+
60
+ # ensure we received an HTTP response
61
+ if cmdi_probe[3]:
62
+ # check if the canary is in the response and the word "echo" is NOT in the response text, ruling out mere reflection of the entire probe value without execution
63
+ if canary in cmdi_probe[3].text and "echo" not in cmdi_probe[3].text:
64
+ self.debug(f"canary [{canary}] found in response when sending probe [{p}]")
65
+ if p == "AAAA": # Handle detection false positive probe
66
+ self.warning(
67
+ f"False Postive Probe appears to have been triggered for {self.event.data['url']}, aborting remaining detection"
68
+ )
69
+ return
70
+ positive_detections.append(p) # Add detected probes to positive detections
71
+ except HttpCompareError as e:
72
+ self.debug(e)
73
+ continue
74
+ if len(positive_detections) > 0:
75
+ self.results.append(
76
+ {
77
+ "type": "FINDING",
78
+ "description": f"POSSIBLE OS Command Injection. {self.metadata()} Detection Method: [echo canary] CMD Probe Delimeters: [{' '.join(positive_detections)}]",
79
+ }
80
+ )
81
+
82
+ # Blind OS Command Injection
83
+ if self.lightfuzz.interactsh_instance:
84
+ self.lightfuzz.event_dict[self.event.data["url"]] = self.event # Store the event associated with the URL
85
+ for p in cmdi_probe_strings:
86
+ # generate a random subdomain tag and associate it with the event, type, name, and probe
87
+ subdomain_tag = self.lightfuzz.helpers.rand_string(4, digits=False)
88
+ self.lightfuzz.interactsh_subdomain_tags[subdomain_tag] = {
89
+ "event": self.event,
90
+ "type": self.event.data["type"],
91
+ "name": self.event.data["name"],
92
+ "probe": p,
93
+ }
94
+ # payload is an nslookup command that includes the interactsh domain prepended the previously generated subdomain tag
95
+ interactsh_probe = f"{p} nslookup {subdomain_tag}.{self.lightfuzz.interactsh_domain} {p}"
96
+ # we have to handle our own URL-encoding here, because our payloads include the & character
97
+ if self.event.data["type"] == "GETPARAM":
98
+ interactsh_probe = urllib.parse.quote(interactsh_probe.encode(), safe="")
99
+ # we send the probe here, and any positive detections are processed in the interactsh_callback defined in lightfuzz.py
100
+ await self.standard_probe(
101
+ self.event.data["type"],
102
+ cookies,
103
+ f"{probe_value}{interactsh_probe}",
104
+ timeout=15,
105
+ skip_urlencoding=True,
106
+ )