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,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.
|
|
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 %}
|
bbot/presets/web/paramminer.yml
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
spider_distance
|
|
12
|
-
|
|
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 %}
|
bbot/scanner/preset/args.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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()
|