Skip to content

Commit 837359c

Browse files
authored
Merge pull request #3067 from blacklanternsecurity/paramminer-wordlist-cleanup
Paramminer Cleanup / Update
2 parents 83a809a + 6f774c1 commit 837359c

9 files changed

Lines changed: 913 additions & 2162 deletions

bbot/defaults.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,25 @@ parameter_blacklist:
263263
- ASP.NET_SessionId
264264
- .AspNetCore.Session
265265
- PHPSESSID
266+
- sessionid
267+
- csrftoken
266268
- __cf_bm
269+
- cf_clearance
270+
- _abck
271+
- bm_sz
272+
- ak_bmsc
267273
- f5_cspm
274+
- _ga
275+
- _gid
276+
- _gat
277+
- _gcl_au
278+
- _fbp
279+
- _fbc
280+
- __utma
281+
- __utmb
282+
- __utmc
283+
- __utmz
284+
- _hjid
268285

269286
parameter_blacklist_prefixes:
270287
- TS01
@@ -277,6 +294,9 @@ parameter_blacklist_prefixes:
277294
- ApplicationGatewayAffinity
278295
- JSESSIONID
279296
- ARRAffinity
297+
- _hjSession
298+
- _gat_
299+
- intercom-
280300

281301
# Don't output these types of events (they are still distributed to modules)
282302
omit_event_types:

bbot/modules/paramminer_cookies.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,15 @@ class paramminer_cookies(paramminer_headers):
2020
"skip_boring_words": True,
2121
}
2222
options_desc = {
23-
"wordlist": "Define the wordlist to be used to derive headers",
23+
"wordlist": "Define the wordlist to be used to derive cookies",
2424
"recycle_words": "Attempt to use words found during the scan on all other endpoints",
2525
"skip_boring_words": "Remove commonly uninteresting words from the wordlist",
2626
}
27-
options_desc = {"wordlist": "Define the wordlist to be used to derive cookies"}
2827
scanned_hosts = []
29-
boring_words = set()
3028
_module_threads = 12
3129
in_scope_only = True
3230
compare_mode = "cookie"
33-
default_wordlist = "paramminer_parameters.txt"
31+
default_wordlist = "paramminer_cookies.txt"
3432

3533
async def check_batch(self, compare_helper, url, cookie_list):
3634
cookies = {p: self.rand_string(14) for p in cookie_list}

bbot/modules/paramminer_getparams.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
from .paramminer_headers import paramminer_headers
1+
import itertools
2+
import string
3+
from urllib.parse import urlparse
4+
5+
from .paramminer_headers import paramminer_headers, _mutate_case
26

37

48
class paramminer_getparams(paramminer_headers):
@@ -19,17 +23,54 @@ class paramminer_getparams(paramminer_headers):
1923
"wordlist": "", # default is defined within setup function
2024
"recycle_words": False,
2125
"skip_boring_words": True,
26+
"mutate_case": False,
27+
"brute_short": False,
2228
}
2329
options_desc = {
2430
"wordlist": "Define the wordlist to be used to derive headers",
2531
"recycle_words": "Attempt to use words found during the scan on all other endpoints",
2632
"skip_boring_words": "Remove commonly uninteresting words from the wordlist",
33+
"mutate_case": (
34+
"Also test case-mutated variants of each entry "
35+
"(camelCase for snake_case/kebab-case, Title case for single words). "
36+
"Skipped on URLs with case-insensitive backend extensions like .aspx/.cfm."
37+
),
38+
"brute_short": (
39+
"Generate every 1-, 2-, and 3-letter [a-z] combination and add to the wordlist. "
40+
"Costs ~18,278 extra requests per host — opt-in for thorough scans."
41+
),
2742
}
2843
boring_words = {"utm_source", "utm_campaign", "utm_medium", "utm_term", "utm_content"}
2944
in_scope_only = True
3045
compare_mode = "getparam"
3146
default_wordlist = "paramminer_parameters.txt"
3247

48+
async def setup(self):
49+
result = await super().setup()
50+
if self.config.get("brute_short", False):
51+
chars = string.ascii_lowercase
52+
extra = set()
53+
for length in (1, 2, 3):
54+
extra |= {"".join(c) for c in itertools.product(chars, repeat=length)}
55+
# respect global blacklist + boring words on generated combos
56+
extra -= self.boring_words
57+
extra -= self.global_blacklist
58+
if self.global_blacklist_prefixes:
59+
extra = {w for w in extra if not w.startswith(self.global_blacklist_prefixes)}
60+
self.wl |= extra
61+
self.debug(f"brute_short: added {len(extra)} 1-3 letter combinations")
62+
return result
63+
64+
def _mutate_for_url(self, url, words):
65+
if not self.config.get("mutate_case", False):
66+
return words
67+
path = urlparse(url).path.lower()
68+
for ext in self.case_insensitive_extensions:
69+
if path.endswith(ext):
70+
return words
71+
mutations = {m for m in (_mutate_case(w) for w in words) if m}
72+
return words | mutations
73+
3374
async def check_batch(self, compare_helper, url, getparam_list):
3475
test_getparams = {p: self.rand_string(14) for p in getparam_list}
3576
return await compare_helper.compare(

bbot/modules/paramminer_headers.py

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,28 @@
33
from bbot.errors import HttpCompareError
44
from bbot.modules.base import BaseModule
55

6+
_case_split = re.compile(r"[-_]+")
7+
8+
9+
def _mutate_case(word):
10+
"""
11+
Multi-word (snake_case/kebab-case) → camelCase: ``user_id`` → ``userId``.
12+
Single word → Title case: ``admin`` → ``Admin``.
13+
Returns None if no useful mutation exists.
14+
"""
15+
parts = _case_split.split(word)
16+
if len(parts) >= 2:
17+
head = parts[0]
18+
tail = "".join(p[:1].upper() + p[1:] for p in parts[1:] if p)
19+
if not tail:
20+
return None
21+
result = head + tail
22+
else:
23+
if not word or not word[0].islower():
24+
return None
25+
result = word[:1].upper() + word[1:]
26+
return result if result != word else None
27+
628

729
class paramminer_headers(BaseModule):
830
"""
@@ -27,6 +49,21 @@ class paramminer_headers(BaseModule):
2749
"recycle_words": "Attempt to use words found during the scan on all other endpoints",
2850
"skip_boring_words": "Remove commonly uninteresting words from the wordlist",
2951
}
52+
# URLs ending with these extensions are known to be case-insensitive — skip case mutation.
53+
# (Used by paramminer_getparams and paramminer_cookies; HTTP headers are inherently
54+
# case-insensitive per RFC 7230 so this isn't relevant to paramminer_headers itself.)
55+
case_insensitive_extensions = {
56+
".aspx",
57+
".ashx",
58+
".ascx",
59+
".asmx",
60+
".axd",
61+
".cshtml",
62+
".vbhtml",
63+
".razor",
64+
".cfm",
65+
".cfc",
66+
}
3067
scanned_hosts = []
3168
boring_words = {
3269
"accept",
@@ -95,17 +132,32 @@ async def setup(self):
95132
self.event_dict = {}
96133
self.already_checked = set()
97134

135+
# global parameter blacklist (shared with excavate) — known framework/CDN/tracker names
136+
self.global_blacklist = {p.lower() for p in self.scan.config.get("parameter_blacklist", [])}
137+
self.global_blacklist_prefixes = tuple(
138+
p.lower() for p in self.scan.config.get("parameter_blacklist_prefixes", [])
139+
)
140+
98141
self.wl = {
99142
h.strip().lower() for h in self.helpers.read_file(self.wordlist_file) if len(h) > 0 and "%" not in h
100143
}
101144

102145
# check against the boring list (if the option is set)
103146
if self.config.get("skip_boring_words", True):
104147
self.wl -= self.boring_words
148+
self.wl -= self.global_blacklist
149+
if self.global_blacklist_prefixes:
150+
self.wl = {w for w in self.wl if not w.startswith(self.global_blacklist_prefixes)}
151+
105152
self.extracted_words_master = set()
106153

107154
return True
108155

156+
def _mutate_for_url(self, url, words):
157+
"""Hook for subclasses to expand a word set with URL-aware mutations
158+
(e.g. paramminer_getparams adds case mutations on case-sensitive backends)."""
159+
return words
160+
109161
def rand_string(self, *args, **kwargs):
110162
return self.helpers.rand_string(*args, **kwargs)
111163

@@ -166,8 +218,14 @@ async def handle_event(self, event):
166218
if event.type == "WEB_PARAMETER":
167219
parameter_name = event.data.get("name")
168220
if self.recycle_words or (event.data.get("type") == "SPECULATIVE"):
169-
if self.config.get("skip_boring_words", True) and parameter_name in self.boring_words:
170-
return
221+
if self.config.get("skip_boring_words", True):
222+
if parameter_name in self.boring_words:
223+
return
224+
lower_name = parameter_name.lower()
225+
if lower_name in self.global_blacklist:
226+
return
227+
if self.global_blacklist_prefixes and lower_name.startswith(self.global_blacklist_prefixes):
228+
return
171229
if parameter_name not in self.wl: # Ensure it's not already in the wordlist
172230
self.debug(f"Adding {parameter_name} to wordlist")
173231
self.extracted_words_master.add(parameter_name)
@@ -194,7 +252,7 @@ async def handle_event(self, event):
194252
return
195253

196254
try:
197-
results = await self.do_mining(self.wl, url, batch_size, compare_helper)
255+
results = await self.do_mining(self._mutate_for_url(url, self.wl), url, batch_size, compare_helper)
198256
except HttpCompareError as e:
199257
self.debug(f"Encountered HttpCompareError: [{e}] for URL [{event.url}]")
200258
await self.process_results(event, results)
@@ -253,7 +311,9 @@ async def finish(self):
253311
self.debug(f"Error initializing compare helper: {e}")
254312
continue
255313
words_to_process = {
256-
i for i in self.extracted_words_master.copy() if hash(i + url) not in self.already_checked
314+
i
315+
for i in self._mutate_for_url(url, self.extracted_words_master)
316+
if hash(i + url) not in self.already_checked
257317
}
258318
try:
259319
results = await self.do_mining(words_to_process, url, batch_size, compare_helper)

bbot/presets/web/lightfuzz-max.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
description: "Maximum fuzzing: everything in lightfuzz-heavy, plus WAF targets are no longer skipped, each unique parameter-value pair is fuzzed individually (no collapsing), common headers like X-Forwarded-For are fuzzed even if not observed, and potential parameters are speculated from JSON/XML response bodies. Significantly increases scan time."
1+
description: "Maximum fuzzing: everything in lightfuzz-heavy, plus the heavy paramminer variant (1-3 letter brute-force on GET params, case mutation on case-sensitive backends, recycle_words on all paramminer modules), WAF targets are no longer skipped, each unique parameter-value pair is fuzzed individually (no collapsing), common headers like X-Forwarded-For are fuzzed even if not observed, and potential parameters are speculated from JSON/XML response bodies. Significantly increases scan time."
22

33
include:
44
- lightfuzz-heavy
5+
- paramminer-heavy
56

67
config:
78
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
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
description: "Aggressive paramminer brute-force: enables 1-3 letter combination brute-force on GET parameters and case mutation (camelCase / Title-case variants) on case-sensitive backends. Significantly increases scan time."
2+
3+
include:
4+
- paramminer
5+
6+
config:
7+
modules:
8+
paramminer_getparams:
9+
brute_short: True
10+
mutate_case: True
11+
recycle_words: True
12+
paramminer_headers:
13+
recycle_words: True
14+
paramminer_cookies:
15+
recycle_words: True

0 commit comments

Comments
 (0)