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,352 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import base64
|
|
3
|
+
import binascii
|
|
4
|
+
import xmltodict
|
|
5
|
+
from contextlib import suppress
|
|
6
|
+
from urllib.parse import unquote, quote
|
|
7
|
+
from xml.parsers.expat import ExpatError
|
|
8
|
+
|
|
9
|
+
from bbot.core.helpers.misc import is_printable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# TODO: This logic is perfect for extracting params. We should expand it outwards to include other higher-level envelopes:
|
|
13
|
+
# - QueryStringEnvelope
|
|
14
|
+
# - MultipartFormEnvelope
|
|
15
|
+
# - HeaderEnvelope
|
|
16
|
+
# - CookieEnvelope
|
|
17
|
+
#
|
|
18
|
+
# Once we start ingesting HTTP_REQUEST events, this will make them instantly fuzzable
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EnvelopeChildTracker(type):
|
|
22
|
+
"""
|
|
23
|
+
Keeps track of all the child envelope classes
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
children = []
|
|
27
|
+
|
|
28
|
+
def __new__(mcs, name, bases, class_dict):
|
|
29
|
+
# Create the class
|
|
30
|
+
cls = super().__new__(mcs, name, bases, class_dict)
|
|
31
|
+
# Don't register the base class itself
|
|
32
|
+
if bases and not name.startswith("Base"): # Only register if it has base classes (i.e., is a child)
|
|
33
|
+
EnvelopeChildTracker.children.append(cls)
|
|
34
|
+
EnvelopeChildTracker.children.sort(key=lambda x: x.priority)
|
|
35
|
+
return cls
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BaseEnvelope(metaclass=EnvelopeChildTracker):
|
|
39
|
+
__slots__ = ["subparams", "selected_subparam", "singleton"]
|
|
40
|
+
|
|
41
|
+
# determines the order of the envelope detection
|
|
42
|
+
priority = 5
|
|
43
|
+
# whether the envelope is the final format, e.g. raw text/binary
|
|
44
|
+
end_format = False
|
|
45
|
+
ignore_exceptions = (Exception,)
|
|
46
|
+
envelope_classes = EnvelopeChildTracker.children
|
|
47
|
+
# transparent envelopes (i.e. TextEnvelope) are not counted as envelopes or included in the finding descriptions
|
|
48
|
+
transparent = False
|
|
49
|
+
|
|
50
|
+
def __init__(self, s):
|
|
51
|
+
unpacked_data = self.unpack(s)
|
|
52
|
+
|
|
53
|
+
if self.end_format:
|
|
54
|
+
inner_envelope = unpacked_data
|
|
55
|
+
else:
|
|
56
|
+
inner_envelope = self.detect(unpacked_data)
|
|
57
|
+
|
|
58
|
+
self.selected_subparam = None
|
|
59
|
+
# if we have subparams, our inner envelope will be a dictionary
|
|
60
|
+
if isinstance(inner_envelope, dict):
|
|
61
|
+
self.subparams = inner_envelope
|
|
62
|
+
self.singleton = False
|
|
63
|
+
# otherwise if we just have one value, we make a dictionary with a default key
|
|
64
|
+
else:
|
|
65
|
+
self.subparams = {"__default__": inner_envelope}
|
|
66
|
+
self.singleton = True
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def final_envelope(self):
|
|
70
|
+
try:
|
|
71
|
+
return self.unpacked_data(recursive=False).final_envelope
|
|
72
|
+
except AttributeError:
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def friendly_name(self):
|
|
77
|
+
if self.friendly_name:
|
|
78
|
+
return self.friendly_name
|
|
79
|
+
else:
|
|
80
|
+
return self.name
|
|
81
|
+
|
|
82
|
+
def pack(self, data=None):
|
|
83
|
+
if data is None:
|
|
84
|
+
data = self.unpacked_data(recursive=False)
|
|
85
|
+
with suppress(AttributeError):
|
|
86
|
+
data = data.pack()
|
|
87
|
+
return self._pack(data)
|
|
88
|
+
|
|
89
|
+
def unpack(self, s):
|
|
90
|
+
return self._unpack(s)
|
|
91
|
+
|
|
92
|
+
def _pack(self, s):
|
|
93
|
+
"""
|
|
94
|
+
Encodes the string using the class's unique encoder (adds the outer envelope)
|
|
95
|
+
"""
|
|
96
|
+
raise NotImplementedError("Envelope.pack() must be implemented")
|
|
97
|
+
|
|
98
|
+
def _unpack(self, s):
|
|
99
|
+
"""
|
|
100
|
+
Decodes the string using the class's unique encoder (removes the outer envelope)
|
|
101
|
+
"""
|
|
102
|
+
raise NotImplementedError("Envelope.unpack() must be implemented")
|
|
103
|
+
|
|
104
|
+
def unpacked_data(self, recursive=True):
|
|
105
|
+
try:
|
|
106
|
+
unpacked = self.subparams["__default__"]
|
|
107
|
+
if recursive:
|
|
108
|
+
with suppress(AttributeError):
|
|
109
|
+
return unpacked.unpacked_data(recursive=recursive)
|
|
110
|
+
return unpacked
|
|
111
|
+
except KeyError:
|
|
112
|
+
return self.subparams
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def detect(cls, s):
|
|
116
|
+
"""
|
|
117
|
+
Detects the type of envelope used to encode the packed_data
|
|
118
|
+
"""
|
|
119
|
+
if not isinstance(s, str):
|
|
120
|
+
raise ValueError(f"Invalid data passed to detect(): {s} ({type(s)})")
|
|
121
|
+
# if the value is empty, we just return the text envelope
|
|
122
|
+
if not s.strip():
|
|
123
|
+
return TextEnvelope(s)
|
|
124
|
+
for envelope_class in cls.envelope_classes:
|
|
125
|
+
with suppress(*envelope_class.ignore_exceptions):
|
|
126
|
+
envelope = envelope_class(s)
|
|
127
|
+
if envelope is not False:
|
|
128
|
+
# make sure the envelope is not just the original string, to prevent unnecessary envelope detection. For example, "10" is technically valid JSON, but nothing is being encapsulated
|
|
129
|
+
if str(envelope.unpacked_data()) == s:
|
|
130
|
+
return TextEnvelope(s)
|
|
131
|
+
else:
|
|
132
|
+
return envelope
|
|
133
|
+
del envelope
|
|
134
|
+
raise Exception(f"No envelope detected for data: '{s}' ({type(s)})")
|
|
135
|
+
|
|
136
|
+
def get_subparams(self, key=None, data=None, recursive=True):
|
|
137
|
+
if data is None:
|
|
138
|
+
data = self.unpacked_data(recursive=recursive)
|
|
139
|
+
if key is None:
|
|
140
|
+
key = []
|
|
141
|
+
|
|
142
|
+
if isinstance(data, dict):
|
|
143
|
+
for k, v in data.items():
|
|
144
|
+
full_key = key + [k]
|
|
145
|
+
if isinstance(v, dict):
|
|
146
|
+
yield from self.get_subparams(full_key, v)
|
|
147
|
+
else:
|
|
148
|
+
yield full_key, v
|
|
149
|
+
else:
|
|
150
|
+
yield [], data
|
|
151
|
+
|
|
152
|
+
def get_subparam(self, key=None, recursive=True):
|
|
153
|
+
if key is None:
|
|
154
|
+
key = self.selected_subparam
|
|
155
|
+
envelope = self
|
|
156
|
+
if recursive:
|
|
157
|
+
envelope = self.final_envelope
|
|
158
|
+
data = envelope.unpacked_data(recursive=False)
|
|
159
|
+
if key is None:
|
|
160
|
+
if envelope.singleton:
|
|
161
|
+
key = []
|
|
162
|
+
else:
|
|
163
|
+
raise ValueError("No subparam selected")
|
|
164
|
+
else:
|
|
165
|
+
for segment in key:
|
|
166
|
+
data = data[segment]
|
|
167
|
+
return data
|
|
168
|
+
|
|
169
|
+
def set_subparam(self, key=None, value=None, recursive=True):
|
|
170
|
+
envelope = self
|
|
171
|
+
if recursive:
|
|
172
|
+
envelope = self.final_envelope
|
|
173
|
+
|
|
174
|
+
# if there's only one value to set, we can just set it directly
|
|
175
|
+
if envelope.singleton:
|
|
176
|
+
envelope.subparams["__default__"] = value
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
# if key isn't specified, use the selected subparam
|
|
180
|
+
if key is None:
|
|
181
|
+
key = self.selected_subparam
|
|
182
|
+
if key is None:
|
|
183
|
+
raise ValueError(f"{self} -> {envelope}: No subparam selected")
|
|
184
|
+
|
|
185
|
+
data = envelope.unpacked_data(recursive=False)
|
|
186
|
+
for segment in key[:-1]:
|
|
187
|
+
data = data[segment]
|
|
188
|
+
data[key[-1]] = value
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def name(self):
|
|
192
|
+
return self.__class__.__name__
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def num_envelopes(self):
|
|
196
|
+
num_envelopes = 0 if self.transparent else 1
|
|
197
|
+
if self.end_format:
|
|
198
|
+
return num_envelopes
|
|
199
|
+
for envelope in self.subparams.values():
|
|
200
|
+
with suppress(AttributeError):
|
|
201
|
+
num_envelopes += envelope.num_envelopes
|
|
202
|
+
return num_envelopes
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def summary(self):
|
|
206
|
+
if self.transparent:
|
|
207
|
+
return ""
|
|
208
|
+
self_string = f"{self.friendly_name}"
|
|
209
|
+
with suppress(AttributeError):
|
|
210
|
+
child_envelope = self.unpacked_data(recursive=False)
|
|
211
|
+
child_summary = child_envelope.summary
|
|
212
|
+
if child_summary:
|
|
213
|
+
self_string += f" -> {child_summary}"
|
|
214
|
+
|
|
215
|
+
if self.selected_subparam:
|
|
216
|
+
self_string += f" [{'.'.join(self.selected_subparam)}]"
|
|
217
|
+
return self_string
|
|
218
|
+
|
|
219
|
+
def to_dict(self):
|
|
220
|
+
return self.summary
|
|
221
|
+
|
|
222
|
+
def __str__(self):
|
|
223
|
+
return self.summary
|
|
224
|
+
|
|
225
|
+
__repr__ = __str__
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class HexEnvelope(BaseEnvelope):
|
|
229
|
+
"""
|
|
230
|
+
Hexadecimal encoding
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
friendly_name = "Hexadecimal-Encoded"
|
|
234
|
+
|
|
235
|
+
ignore_exceptions = (ValueError, UnicodeDecodeError)
|
|
236
|
+
|
|
237
|
+
def _pack(self, s):
|
|
238
|
+
return s.encode().hex()
|
|
239
|
+
|
|
240
|
+
def _unpack(self, s):
|
|
241
|
+
return bytes.fromhex(s).decode()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class B64Envelope(BaseEnvelope):
|
|
245
|
+
"""
|
|
246
|
+
Base64 encoding
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
friendly_name = "Base64-Encoded"
|
|
250
|
+
|
|
251
|
+
ignore_exceptions = (binascii.Error, UnicodeDecodeError, ValueError)
|
|
252
|
+
|
|
253
|
+
def unpack(self, s):
|
|
254
|
+
# it's easy to have a small value that accidentally decodes to base64
|
|
255
|
+
if len(s) < 8 and not s.endswith("="):
|
|
256
|
+
raise ValueError("Data is too small to be sure")
|
|
257
|
+
return super().unpack(s)
|
|
258
|
+
|
|
259
|
+
def _pack(self, s):
|
|
260
|
+
return base64.b64encode(s.encode()).decode()
|
|
261
|
+
|
|
262
|
+
def _unpack(self, s):
|
|
263
|
+
return base64.b64decode(s).decode()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class URLEnvelope(BaseEnvelope):
|
|
267
|
+
"""
|
|
268
|
+
URL encoding
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
friendly_name = "URL-Encoded"
|
|
272
|
+
|
|
273
|
+
def unpack(self, s):
|
|
274
|
+
unpacked = super().unpack(s)
|
|
275
|
+
if unpacked == s:
|
|
276
|
+
raise ValueError("Data is not URL-encoded")
|
|
277
|
+
return unpacked
|
|
278
|
+
|
|
279
|
+
def _pack(self, s):
|
|
280
|
+
return quote(s)
|
|
281
|
+
|
|
282
|
+
def _unpack(self, s):
|
|
283
|
+
return unquote(s)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class TextEnvelope(BaseEnvelope):
|
|
287
|
+
"""
|
|
288
|
+
Text encoding
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
end_format = True
|
|
292
|
+
# lowest priority means text is the ultimate fallback
|
|
293
|
+
priority = 10
|
|
294
|
+
transparent = True
|
|
295
|
+
ignore_exceptions = ()
|
|
296
|
+
|
|
297
|
+
def _pack(self, s):
|
|
298
|
+
return s
|
|
299
|
+
|
|
300
|
+
def _unpack(self, s):
|
|
301
|
+
if not is_printable(s):
|
|
302
|
+
raise ValueError(f"Non-printable data detected in TextEnvelope: '{s}' ({type(s)})")
|
|
303
|
+
return s
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# class BinaryEnvelope(BaseEnvelope):
|
|
307
|
+
# """
|
|
308
|
+
# Binary encoding
|
|
309
|
+
# """
|
|
310
|
+
# end_format = True
|
|
311
|
+
|
|
312
|
+
# def pack(self, s):
|
|
313
|
+
# return s
|
|
314
|
+
|
|
315
|
+
# def unpack(self, s):
|
|
316
|
+
# if is_printable(s):
|
|
317
|
+
# raise Exception("Non-binary data detected in BinaryEnvelope")
|
|
318
|
+
# return s
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class JSONEnvelope(BaseEnvelope):
|
|
322
|
+
"""
|
|
323
|
+
JSON encoding
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
friendly_name = "JSON-formatted"
|
|
327
|
+
end_format = True
|
|
328
|
+
priority = 8
|
|
329
|
+
ignore_exceptions = (json.JSONDecodeError,)
|
|
330
|
+
|
|
331
|
+
def _pack(self, s):
|
|
332
|
+
return json.dumps(s)
|
|
333
|
+
|
|
334
|
+
def _unpack(self, s):
|
|
335
|
+
return json.loads(s)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class XMLEnvelope(BaseEnvelope):
|
|
339
|
+
"""
|
|
340
|
+
XML encoding
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
friendly_name = "XML-formatted"
|
|
344
|
+
end_format = True
|
|
345
|
+
priority = 9
|
|
346
|
+
ignore_exceptions = (ExpatError,)
|
|
347
|
+
|
|
348
|
+
def _pack(self, s):
|
|
349
|
+
return xmltodict.unparse(s)
|
|
350
|
+
|
|
351
|
+
def _unpack(self, s):
|
|
352
|
+
return xmltodict.parse(s)
|
bbot/core/helpers/web/web.py
CHANGED
|
@@ -349,6 +349,7 @@ class WebHelper(EngineClient):
|
|
|
349
349
|
curl_command.append("-k")
|
|
350
350
|
|
|
351
351
|
headers = kwargs.get("headers", {})
|
|
352
|
+
cookies = kwargs.get("cookies", {})
|
|
352
353
|
|
|
353
354
|
ignore_bbot_global_settings = kwargs.get("ignore_bbot_global_settings", False)
|
|
354
355
|
|
|
@@ -362,10 +363,17 @@ class WebHelper(EngineClient):
|
|
|
362
363
|
if "User-Agent" not in headers:
|
|
363
364
|
headers["User-Agent"] = user_agent
|
|
364
365
|
|
|
365
|
-
# only add custom headers if the URL is in-scope
|
|
366
|
+
# only add custom headers / cookies if the URL is in-scope
|
|
366
367
|
if self.parent_helper.preset.in_scope(url):
|
|
367
368
|
for hk, hv in self.web_config.get("http_headers", {}).items():
|
|
368
|
-
headers
|
|
369
|
+
# Only add the header if it doesn't already exist in the headers dictionary
|
|
370
|
+
if hk not in headers:
|
|
371
|
+
headers[hk] = hv
|
|
372
|
+
|
|
373
|
+
for ck, cv in self.web_config.get("http_cookies", {}).items():
|
|
374
|
+
# don't clobber cookies
|
|
375
|
+
if ck not in cookies:
|
|
376
|
+
cookies[ck] = cv
|
|
369
377
|
|
|
370
378
|
# add the timeout
|
|
371
379
|
if "timeout" not in kwargs:
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import yara
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class YaraHelper:
|
|
5
|
+
def __init__(self, parent_helper):
|
|
6
|
+
self.parent_helper = parent_helper
|
|
7
|
+
|
|
8
|
+
def compile_strings(self, strings: list[str], nocase=False):
|
|
9
|
+
"""
|
|
10
|
+
Compile a list of strings into a YARA rule
|
|
11
|
+
"""
|
|
12
|
+
# Format each string as a YARA string definition
|
|
13
|
+
yara_strings = []
|
|
14
|
+
for i, s in enumerate(strings):
|
|
15
|
+
s = s.replace('"', '\\"')
|
|
16
|
+
yara_string = f'$s{i} = "{s}"'
|
|
17
|
+
if nocase:
|
|
18
|
+
yara_string += " nocase"
|
|
19
|
+
yara_strings.append(yara_string)
|
|
20
|
+
yara_strings = "\n ".join(yara_strings)
|
|
21
|
+
|
|
22
|
+
# Create the complete YARA rule
|
|
23
|
+
yara_rule = f"""
|
|
24
|
+
rule strings_match
|
|
25
|
+
{{
|
|
26
|
+
strings:
|
|
27
|
+
{yara_strings}
|
|
28
|
+
condition:
|
|
29
|
+
any of them
|
|
30
|
+
}}
|
|
31
|
+
"""
|
|
32
|
+
# Compile and return the rule
|
|
33
|
+
return self.compile(source=yara_rule)
|
|
34
|
+
|
|
35
|
+
def compile(self, *args, **kwargs):
|
|
36
|
+
return yara.compile(*args, **kwargs)
|
|
37
|
+
|
|
38
|
+
async def match(self, compiled_rules, text):
|
|
39
|
+
"""
|
|
40
|
+
Given a compiled YARA rule and a body of text, return a list of strings that match the rule
|
|
41
|
+
"""
|
|
42
|
+
matched_strings = []
|
|
43
|
+
matches = await self.parent_helper.run_in_executor(compiled_rules.match, data=text)
|
|
44
|
+
if matches:
|
|
45
|
+
for match in matches:
|
|
46
|
+
for string_match in match.strings:
|
|
47
|
+
for instance in string_match.instances:
|
|
48
|
+
matched_string = instance.matched_data.decode("utf-8")
|
|
49
|
+
matched_strings.append(matched_string)
|
|
50
|
+
return matched_strings
|
bbot/core/modules.py
CHANGED
|
@@ -104,8 +104,9 @@ class ModuleLoader:
|
|
|
104
104
|
|
|
105
105
|
def file_filter(self, file):
|
|
106
106
|
file = file.resolve()
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
for part in file.parts:
|
|
108
|
+
if part.endswith("_submodules") or part == "templates":
|
|
109
|
+
return False
|
|
109
110
|
return file.suffix.lower() == ".py" and file.stem not in ["base", "__init__"]
|
|
110
111
|
|
|
111
112
|
def preload(self, module_dirs=None):
|
|
@@ -158,12 +159,11 @@ class ModuleLoader:
|
|
|
158
159
|
namespace = f"bbot.modules.{module_dir.name}"
|
|
159
160
|
try:
|
|
160
161
|
preloaded = self.preload_module(module_file)
|
|
162
|
+
if preloaded is None:
|
|
163
|
+
continue
|
|
161
164
|
module_type = "scan"
|
|
162
165
|
if module_dir.name in ("output", "internal"):
|
|
163
166
|
module_type = str(module_dir.name)
|
|
164
|
-
elif module_dir.name not in ("modules"):
|
|
165
|
-
flags = set(preloaded["flags"] + [module_dir.name])
|
|
166
|
-
preloaded["flags"] = sorted(flags)
|
|
167
167
|
|
|
168
168
|
# derive module dependencies from watched event types (only for scan modules)
|
|
169
169
|
if module_type == "scan":
|
|
@@ -327,12 +327,28 @@ class ModuleLoader:
|
|
|
327
327
|
deps_apt = []
|
|
328
328
|
deps_common = []
|
|
329
329
|
ansible_tasks = []
|
|
330
|
+
config = {}
|
|
331
|
+
options_desc = {}
|
|
330
332
|
python_code = open(module_file).read()
|
|
331
333
|
# take a hash of the code so we can keep track of when it changes
|
|
332
334
|
module_hash = sha1(python_code).hexdigest()
|
|
333
335
|
parsed_code = ast.parse(python_code)
|
|
334
|
-
|
|
335
|
-
|
|
336
|
+
|
|
337
|
+
# discard if the module isn't a valid BBOT module
|
|
338
|
+
is_bbot_module = False
|
|
339
|
+
for root_element in parsed_code.body:
|
|
340
|
+
if type(root_element) == ast.ClassDef:
|
|
341
|
+
for class_attr in root_element.body:
|
|
342
|
+
if type(class_attr) == ast.Assign and any(
|
|
343
|
+
target.id in ("watched_events", "produced_events") for target in class_attr.targets
|
|
344
|
+
):
|
|
345
|
+
is_bbot_module = True
|
|
346
|
+
break
|
|
347
|
+
|
|
348
|
+
if not is_bbot_module:
|
|
349
|
+
log.debug(f"Skipping {module_file} as it is not a valid BBOT module")
|
|
350
|
+
return
|
|
351
|
+
|
|
336
352
|
for root_element in parsed_code.body:
|
|
337
353
|
# look for classes
|
|
338
354
|
if type(root_element) == ast.ClassDef:
|
bbot/defaults.yml
CHANGED
|
@@ -227,7 +227,32 @@ url_extension_static:
|
|
|
227
227
|
- bz2
|
|
228
228
|
- 7z
|
|
229
229
|
- rar
|
|
230
|
-
|
|
230
|
+
|
|
231
|
+
parameter_blacklist:
|
|
232
|
+
- __VIEWSTATE
|
|
233
|
+
- __EVENTARGUMENT
|
|
234
|
+
- __EVENTVALIDATION
|
|
235
|
+
- __EVENTTARGET
|
|
236
|
+
- __EVENTARGUMENT
|
|
237
|
+
- __VIEWSTATEGENERATOR
|
|
238
|
+
- __SCROLLPOSITIONY
|
|
239
|
+
- __SCROLLPOSITIONX
|
|
240
|
+
- ASP.NET_SessionId
|
|
241
|
+
- PHPSESSID
|
|
242
|
+
- __cf_bm
|
|
243
|
+
- f5_cspm
|
|
244
|
+
|
|
245
|
+
parameter_blacklist_prefixes:
|
|
246
|
+
- TS01
|
|
247
|
+
- BIGipServer
|
|
248
|
+
- incap_
|
|
249
|
+
- visid_incap_
|
|
250
|
+
- AWSALB
|
|
251
|
+
- utm_
|
|
252
|
+
- ApplicationGatewayAffinity
|
|
253
|
+
- JSESSIONID
|
|
254
|
+
- ARRAffinity
|
|
255
|
+
|
|
231
256
|
# Don't output these types of events (they are still distributed to modules)
|
|
232
257
|
omit_event_types:
|
|
233
258
|
- HTTP_RESPONSE
|
bbot/modules/base.py
CHANGED
|
@@ -535,8 +535,10 @@ class BaseModule:
|
|
|
535
535
|
if v is not None:
|
|
536
536
|
emit_kwargs[o] = v
|
|
537
537
|
event = self.make_event(*args, **event_kwargs)
|
|
538
|
-
if event:
|
|
539
|
-
|
|
538
|
+
if event is not None:
|
|
539
|
+
children = event.children
|
|
540
|
+
for e in [event] + children:
|
|
541
|
+
await self.queue_outgoing_event(e, **emit_kwargs)
|
|
540
542
|
return event
|
|
541
543
|
|
|
542
544
|
async def _events_waiting(self, batch_size=None):
|
|
@@ -5,7 +5,7 @@ from bbot.modules.base import BaseModule
|
|
|
5
5
|
class dastardly(BaseModule):
|
|
6
6
|
watched_events = ["HTTP_RESPONSE"]
|
|
7
7
|
produced_events = ["FINDING", "VULNERABILITY"]
|
|
8
|
-
flags = ["active", "aggressive", "slow", "web-thorough"]
|
|
8
|
+
flags = ["active", "aggressive", "slow", "web-thorough", "deadly"]
|
|
9
9
|
meta = {
|
|
10
10
|
"description": "Lightweight web application security scanner",
|
|
11
11
|
"created_date": "2023-12-11",
|
|
@@ -9,7 +9,7 @@ import base64
|
|
|
9
9
|
class ffuf(BaseModule):
|
|
10
10
|
watched_events = ["URL"]
|
|
11
11
|
produced_events = ["URL_UNVERIFIED"]
|
|
12
|
-
flags = ["aggressive", "active"]
|
|
12
|
+
flags = ["aggressive", "active", "deadly"]
|
|
13
13
|
meta = {"description": "A fast web fuzzer written in Go", "created_date": "2022-04-10", "author": "@liquidsec"}
|
|
14
14
|
|
|
15
15
|
options = {
|
bbot/modules/ffuf_shortnames.py
CHANGED
bbot/modules/httpx.py
CHANGED
|
@@ -3,6 +3,8 @@ import orjson
|
|
|
3
3
|
import tempfile
|
|
4
4
|
import subprocess
|
|
5
5
|
from pathlib import Path
|
|
6
|
+
from http.cookies import SimpleCookie
|
|
7
|
+
|
|
6
8
|
from bbot.modules.base import BaseModule
|
|
7
9
|
|
|
8
10
|
|
|
@@ -137,8 +139,20 @@ class httpx(BaseModule):
|
|
|
137
139
|
if self.probe_all_ips:
|
|
138
140
|
command += ["-probe-all-ips"]
|
|
139
141
|
|
|
142
|
+
# Add custom HTTP headers
|
|
140
143
|
for hk, hv in self.scan.custom_http_headers.items():
|
|
141
144
|
command += ["-header", f"{hk}: {hv}"]
|
|
145
|
+
|
|
146
|
+
# Add custom HTTP cookies as a single header
|
|
147
|
+
if self.scan.custom_http_cookies:
|
|
148
|
+
cookie = SimpleCookie()
|
|
149
|
+
for ck, cv in self.scan.custom_http_cookies.items():
|
|
150
|
+
cookie[ck] = cv
|
|
151
|
+
|
|
152
|
+
# Build the cookie header
|
|
153
|
+
cookie_header = f"Cookie: {cookie.output(header='', sep='; ').strip()}"
|
|
154
|
+
command += ["-header", cookie_header]
|
|
155
|
+
|
|
142
156
|
proxy = self.scan.http_proxy
|
|
143
157
|
if proxy:
|
|
144
158
|
command += ["-http-proxy", proxy]
|
bbot/modules/hunt.py
CHANGED
|
@@ -284,11 +284,29 @@ class hunt(BaseModule):
|
|
|
284
284
|
|
|
285
285
|
async def handle_event(self, event):
|
|
286
286
|
p = event.data["name"]
|
|
287
|
+
matching_categories = []
|
|
288
|
+
|
|
289
|
+
# Collect all matching categories
|
|
287
290
|
for k in hunt_param_dict.keys():
|
|
288
291
|
if p.lower() in hunt_param_dict[k]:
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
292
|
+
matching_categories.append(k)
|
|
293
|
+
|
|
294
|
+
if matching_categories:
|
|
295
|
+
# Create a comma-separated string of categories
|
|
296
|
+
category_str = ", ".join(matching_categories)
|
|
297
|
+
description = f"Found potentially interesting parameter. Name: [{p}] Parameter Type: [{event.data['type']}] Categories: [{category_str}]"
|
|
298
|
+
|
|
299
|
+
if (
|
|
300
|
+
"original_value" in event.data.keys()
|
|
301
|
+
and event.data["original_value"] != ""
|
|
302
|
+
and event.data["original_value"] is not None
|
|
303
|
+
):
|
|
304
|
+
description += (
|
|
305
|
+
f" Original Value: [{self.helpers.truncate_string(str(event.data['original_value']), 200)}]"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
data = {"host": str(event.host), "description": description}
|
|
309
|
+
url = event.data.get("url", "")
|
|
310
|
+
if url:
|
|
311
|
+
data["url"] = url
|
|
312
|
+
await self.emit_event(data, "FINDING", event)
|