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.
- bbot/__init__.py +1 -1
- bbot/core/event/base.py +64 -4
- bbot/core/helpers/diff.py +10 -7
- bbot/core/helpers/helper.py +5 -1
- bbot/core/helpers/misc.py +48 -11
- bbot/core/helpers/regex.py +4 -0
- bbot/core/helpers/regexes.py +45 -8
- bbot/core/helpers/url.py +21 -5
- bbot/core/helpers/web/client.py +25 -5
- bbot/core/helpers/web/engine.py +9 -1
- bbot/core/helpers/web/envelopes.py +352 -0
- bbot/core/helpers/web/web.py +10 -2
- bbot/core/helpers/yara_helper.py +50 -0
- bbot/core/modules.py +23 -7
- bbot/defaults.yml +26 -1
- bbot/modules/base.py +4 -2
- bbot/modules/{deadly/dastardly.py → dastardly.py} +1 -1
- bbot/modules/{deadly/ffuf.py → ffuf.py} +1 -1
- bbot/modules/ffuf_shortnames.py +1 -1
- bbot/modules/httpx.py +14 -0
- bbot/modules/hunt.py +24 -6
- bbot/modules/internal/aggregate.py +1 -0
- bbot/modules/internal/excavate.py +356 -197
- bbot/modules/lightfuzz/lightfuzz.py +203 -0
- bbot/modules/lightfuzz/submodules/__init__.py +0 -0
- bbot/modules/lightfuzz/submodules/base.py +312 -0
- bbot/modules/lightfuzz/submodules/cmdi.py +106 -0
- bbot/modules/lightfuzz/submodules/crypto.py +474 -0
- bbot/modules/lightfuzz/submodules/nosqli.py +183 -0
- bbot/modules/lightfuzz/submodules/path.py +154 -0
- bbot/modules/lightfuzz/submodules/serial.py +179 -0
- bbot/modules/lightfuzz/submodules/sqli.py +187 -0
- bbot/modules/lightfuzz/submodules/ssti.py +39 -0
- bbot/modules/lightfuzz/submodules/xss.py +191 -0
- bbot/modules/{deadly/nuclei.py → nuclei.py} +1 -1
- bbot/modules/paramminer_headers.py +2 -0
- bbot/modules/reflected_parameters.py +80 -0
- bbot/modules/{deadly/vhost.py → vhost.py} +2 -2
- bbot/presets/web/lightfuzz-heavy.yml +16 -0
- bbot/presets/web/lightfuzz-light.yml +20 -0
- bbot/presets/web/lightfuzz-medium.yml +14 -0
- bbot/presets/web/lightfuzz-superheavy.yml +13 -0
- bbot/presets/web/lightfuzz-xss.yml +21 -0
- bbot/presets/web/paramminer.yml +8 -5
- bbot/scanner/preset/args.py +26 -0
- bbot/scanner/scanner.py +6 -0
- bbot/test/test_step_1/test__module__tests.py +1 -1
- bbot/test/test_step_1/test_helpers.py +7 -0
- bbot/test/test_step_1/test_presets.py +2 -2
- bbot/test/test_step_1/test_web.py +20 -0
- bbot/test/test_step_1/test_web_envelopes.py +343 -0
- bbot/test/test_step_2/module_tests/test_module_excavate.py +404 -29
- bbot/test/test_step_2/module_tests/test_module_httpx.py +29 -0
- bbot/test/test_step_2/module_tests/test_module_hunt.py +18 -1
- bbot/test/test_step_2/module_tests/test_module_lightfuzz.py +1947 -0
- bbot/test/test_step_2/module_tests/test_module_paramminer_getparams.py +4 -1
- bbot/test/test_step_2/module_tests/test_module_paramminer_headers.py +46 -2
- bbot/test/test_step_2/module_tests/test_module_reflected_parameters.py +226 -0
- bbot/wordlists/paramminer_parameters.txt +0 -8
- {bbot-2.4.2.dist-info → bbot-2.4.2.6590rc0.dist-info}/METADATA +2 -1
- {bbot-2.4.2.dist-info → bbot-2.4.2.6590rc0.dist-info}/RECORD +64 -42
- {bbot-2.4.2.dist-info → bbot-2.4.2.6590rc0.dist-info}/LICENSE +0 -0
- {bbot-2.4.2.dist-info → bbot-2.4.2.6590rc0.dist-info}/WHEEL +0 -0
- {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
|
+
)
|