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,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)
@@ -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[hk] = hv
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
- if "templates" in file.parts:
108
- return False
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
- config = {}
335
- options_desc = {}
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
- await self.queue_outgoing_event(event, **emit_kwargs)
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 = {
@@ -3,7 +3,7 @@ import re
3
3
  import random
4
4
  import string
5
5
 
6
- from bbot.modules.deadly.ffuf import ffuf
6
+ from bbot.modules.ffuf import ffuf
7
7
 
8
8
 
9
9
  class ffuf_shortnames(ffuf):
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
- description = f"Found potential {k.upper()} parameter [{p}]"
290
- data = {"host": str(event.host), "description": description}
291
- url = event.data.get("url", "")
292
- if url:
293
- data["url"] = url
294
- await self.emit_event(data, "FINDING", event)
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)
@@ -2,6 +2,7 @@ from bbot.modules.report.base import BaseReportModule
2
2
 
3
3
 
4
4
  class aggregate(BaseReportModule):
5
+ watched_events = []
5
6
  flags = ["passive", "safe"]
6
7
  meta = {
7
8
  "description": "Summarize statistics at the end of a scan",