Skip to content

Commit a62462b

Browse files
authored
Merge pull request #2914 from blacklanternsecurity/lightfuzz-try-post-as-get
Lightfuzz - Try POST as Get feature
2 parents 052e88e + 7c02e70 commit a62462b

File tree

7 files changed

+197
-7
lines changed

7 files changed

+197
-7
lines changed

bbot/modules/lightfuzz/lightfuzz.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ class lightfuzz(BaseModule):
1313
"force_common_headers": False,
1414
"enabled_submodules": ["sqli", "cmdi", "xss", "path", "ssti", "crypto", "serial", "esi"],
1515
"disable_post": False,
16+
"try_post_as_get": False,
17+
"try_get_as_post": False,
1618
"avoid_wafs": True,
1719
}
1820
options_desc = {
1921
"force_common_headers": "Force emit commonly exploitable parameters that may be difficult to detect",
2022
"enabled_submodules": "A list of submodules to enable. Empty list enabled all modules.",
2123
"disable_post": "Disable processing of POST parameters, avoiding form submissions.",
24+
"try_post_as_get": "For each POSTPARAM, also fuzz it as a GETPARAM (in addition to normal POST fuzzing).",
25+
"try_get_as_post": "For each GETPARAM, also fuzz it as a POSTPARAM (in addition to normal GET fuzzing).",
2226
"avoid_wafs": "Avoid running against confirmed WAFs, which are likely to block lightfuzz requests",
2327
}
2428

@@ -38,6 +42,8 @@ async def setup(self):
3842
self.interactsh_instance = None
3943
self.interactsh_domain = None
4044
self.disable_post = self.config.get("disable_post", False)
45+
self.try_post_as_get = self.config.get("try_post_as_get", False)
46+
self.try_get_as_post = self.config.get("try_get_as_post", False)
4147
self.enabled_submodules = self.config.get("enabled_submodules")
4248
self.interactsh_disable = self.scan.config.get("interactsh_disable", False)
4349
self.avoid_wafs = self.scan.config.get("avoid_wafs", True)
@@ -145,9 +151,29 @@ async def handle_event(self, event):
145151
connectivity_test = await self.helpers.request(event.data["url"], timeout=10)
146152

147153
if connectivity_test:
148-
for submodule_name, submodule in self.submodules.items():
149-
self.debug(f"Starting {submodule_name} fuzz()")
150-
await self.run_submodule(submodule, event)
154+
original_type = event.data["type"]
155+
156+
# Normal fuzzing pass (skipped for POSTPARAM if disable_post is True)
157+
if not (self.disable_post and original_type == "POSTPARAM"):
158+
for submodule_name, submodule in self.submodules.items():
159+
self.debug(f"Starting {submodule_name} fuzz()")
160+
await self.run_submodule(submodule, event)
161+
162+
# Additional pass: try POSTPARAM as GETPARAM
163+
if self.try_post_as_get and original_type == "POSTPARAM":
164+
event.data["type"] = "GETPARAM"
165+
event.data["converted_from_post"] = True
166+
for submodule_name, submodule in self.submodules.items():
167+
self.debug(f"Starting {submodule_name} fuzz() (try_post_as_get)")
168+
await self.run_submodule(submodule, event)
169+
170+
# Additional pass: try GETPARAM as POSTPARAM
171+
if self.try_get_as_post and original_type == "GETPARAM":
172+
event.data["type"] = "POSTPARAM"
173+
event.data["converted_from_get"] = True
174+
for submodule_name, submodule in self.submodules.items():
175+
self.debug(f"Starting {submodule_name} fuzz() (try_get_as_post)")
176+
await self.run_submodule(submodule, event)
151177
else:
152178
self.debug(f"WEB_PARAMETER URL {event.data['url']} failed connectivity test, aborting")
153179

@@ -181,7 +207,8 @@ async def filter_event(self, event):
181207

182208
# If we've disabled fuzzing POST parameters, back out of POSTPARAM WEB_PARAMETER events as quickly as possible
183209
if event.type == "WEB_PARAMETER" and self.disable_post and event.data["type"] == "POSTPARAM":
184-
return False, "POST parameter disabled in lightfuzz module"
210+
if not self.try_post_as_get:
211+
return False, "POST parameter disabled in lightfuzz module"
185212
return True
186213

187214
@classmethod

bbot/modules/lightfuzz/submodules/base.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,15 @@ async def standard_probe(
238238
self.debug(f"standard_probe requested URL: [{request_params['url']}]")
239239
return await self.lightfuzz.helpers.request(**request_params)
240240

241+
def conversion_note(self):
242+
if self.event.data.get("converted_from_post", False):
243+
return " (converted from POSTPARAM)"
244+
elif self.event.data.get("converted_from_get", False):
245+
return " (converted from GETPARAM)"
246+
return ""
247+
241248
def metadata(self):
242-
metadata_string = f"Parameter: [{self.event.data['name']}] Parameter Type: [{self.event.data['type']}]"
249+
metadata_string = f"Parameter: [{self.event.data['name']}] Parameter Type: [{self.event.data['type']}]{self.conversion_note()}"
243250
if self.event.data["original_value"] != "" and self.event.data["original_value"] is not None:
244251
metadata_string += (
245252
f" Original Value: [{self.lightfuzz.helpers.truncate_string(self.event.data['original_value'], 200)}]"

bbot/modules/lightfuzz/submodules/esi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ async def check_probe(self, cookies, probe, match):
2222
self.results.append(
2323
{
2424
"type": "FINDING",
25-
"description": f"Edge Side Include. Parameter: [{self.event.data['name']}] Parameter Type: [{self.event.data['type']}]",
25+
"description": f"Edge Side Include. Parameter: [{self.event.data['name']}] Parameter Type: [{self.event.data['type']}]{self.conversion_note()}",
2626
}
2727
)
2828
return True

bbot/modules/lightfuzz/submodules/xss.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ async def check_probe(self, cookies, probe, match, context):
9191
self.results.append(
9292
{
9393
"type": "FINDING",
94-
"description": f"Possible Reflected XSS. Parameter: [{self.event.data['name']}] Context: [{context}] Parameter Type: [{self.event.data['type']}]",
94+
"description": f"Possible Reflected XSS. Parameter: [{self.event.data['name']}] Context: [{context}] Parameter Type: [{self.event.data['type']}]{self.conversion_note()}",
9595
}
9696
)
9797
return True

bbot/presets/web/lightfuzz-heavy.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ config:
1414
lightfuzz:
1515
enabled_submodules: [cmdi,crypto,path,serial,sqli,ssti,xss,esi]
1616
disable_post: False
17+
try_post_as_get: True
18+
try_get_as_post: True

bbot/presets/web/lightfuzz-medium.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ config:
1212
modules:
1313
lightfuzz:
1414
enabled_submodules: [cmdi,crypto,path,serial,sqli,ssti,xss,esi]
15+
try_post_as_get: True

bbot/test/test_step_2/module_tests/test_module_lightfuzz.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2146,3 +2146,156 @@ async def test_filter_event(self, module_test):
21462146
def check(self, module_test, events):
21472147
# This test doesn't need to check events since it's testing the filter method directly
21482148
pass
2149+
2150+
2151+
# try_post_as_get: fuzz POST parameters as GET parameters
2152+
class Test_Lightfuzz_try_post_as_get(ModuleTestBase):
2153+
targets = ["http://127.0.0.1:8888"]
2154+
modules_overrides = ["httpx", "lightfuzz", "excavate"]
2155+
config_overrides = {
2156+
"interactsh_disable": True,
2157+
"modules": {
2158+
"lightfuzz": {
2159+
"enabled_submodules": ["sqli"],
2160+
"disable_post": True,
2161+
"try_post_as_get": True,
2162+
}
2163+
},
2164+
}
2165+
2166+
def request_handler(self, request):
2167+
qs = str(request.query_string.decode())
2168+
2169+
parameter_block = """
2170+
<section class=search>
2171+
<form action=/ method=POST>
2172+
<input type=text placeholder='Search the blog...' name=search>
2173+
<button type=submit class=button>Search</button>
2174+
</form>
2175+
</section>
2176+
"""
2177+
2178+
if "search=" in qs:
2179+
value = qs.split("=")[1]
2180+
if "&" in value:
2181+
value = value.split("&")[0]
2182+
2183+
sql_block_normal = f"""
2184+
<section class=blog-header>
2185+
<h1>0 search results for '{unquote(value)}'</h1>
2186+
<hr>
2187+
</section>
2188+
"""
2189+
2190+
sql_block_error = """
2191+
<section class=error>
2192+
<h1>Found error in SQL query</h1>
2193+
<hr>
2194+
</section>
2195+
"""
2196+
if value.endswith("'"):
2197+
if value.endswith("''"):
2198+
return Response(sql_block_normal, status=200)
2199+
return Response(sql_block_error, status=500)
2200+
return Response(parameter_block, status=200)
2201+
2202+
async def setup_after_prep(self, module_test):
2203+
module_test.scan.modules["lightfuzz"].helpers.rand_string = lambda *args, **kwargs: "AAAAAAAAAAAAAA"
2204+
expect_args = re.compile("/")
2205+
module_test.set_expect_requests_handler(expect_args=expect_args, request_handler=self.request_handler)
2206+
2207+
def check(self, module_test, events):
2208+
web_parameter_emitted = False
2209+
sqli_getparam_finding_emitted = False
2210+
sqli_postparam_finding_emitted = False
2211+
for e in events:
2212+
if e.type == "WEB_PARAMETER":
2213+
if "HTTP Extracted Parameter [search]" in e.data["description"]:
2214+
web_parameter_emitted = True
2215+
2216+
if e.type == "FINDING":
2217+
if (
2218+
"Possible SQL Injection. Parameter: [search] Parameter Type: [GETPARAM] (converted from POSTPARAM) Detection Method: [Single Quote/Two Single Quote, Code Change (200->500->200)]"
2219+
in e.data["description"]
2220+
):
2221+
sqli_getparam_finding_emitted = True
2222+
if "Possible SQL Injection. Parameter: [search] Parameter Type: [POSTPARAM]" in e.data["description"]:
2223+
sqli_postparam_finding_emitted = True
2224+
2225+
assert web_parameter_emitted, "WEB_PARAMETER was not emitted"
2226+
assert sqli_getparam_finding_emitted, (
2227+
"SQLi GETPARAM (converted from POSTPARAM) FINDING not emitted (try_post_as_get failed)"
2228+
)
2229+
assert not sqli_postparam_finding_emitted, "POSTPARAM FINDING emitted despite disable_post=True"
2230+
2231+
2232+
# try_get_as_post: fuzz GET parameters as POST parameters
2233+
class Test_Lightfuzz_try_get_as_post(ModuleTestBase):
2234+
targets = ["http://127.0.0.1:8888"]
2235+
modules_overrides = ["httpx", "lightfuzz", "excavate"]
2236+
config_overrides = {
2237+
"interactsh_disable": True,
2238+
"modules": {
2239+
"lightfuzz": {
2240+
"enabled_submodules": ["sqli"],
2241+
"try_get_as_post": True,
2242+
}
2243+
},
2244+
}
2245+
2246+
def request_handler(self, request):
2247+
parameter_block = """
2248+
<section class=search>
2249+
<form action=/ method=GET>
2250+
<input type=text placeholder='Search the blog...' name=search>
2251+
<button type=submit class=button>Search</button>
2252+
</form>
2253+
</section>
2254+
"""
2255+
2256+
if request.method == "POST" and "search" in request.form.keys():
2257+
value = request.form["search"]
2258+
2259+
sql_block_normal = f"""
2260+
<section class=blog-header>
2261+
<h1>0 search results for '{unquote(value)}'</h1>
2262+
<hr>
2263+
</section>
2264+
"""
2265+
2266+
sql_block_error = """
2267+
<section class=error>
2268+
<h1>Found error in SQL query</h1>
2269+
<hr>
2270+
</section>
2271+
"""
2272+
if value.endswith("'"):
2273+
if value.endswith("''"):
2274+
return Response(sql_block_normal, status=200)
2275+
return Response(sql_block_error, status=500)
2276+
return Response(parameter_block, status=200)
2277+
2278+
async def setup_after_prep(self, module_test):
2279+
module_test.scan.modules["lightfuzz"].helpers.rand_string = lambda *args, **kwargs: "AAAAAAAAAAAAAA"
2280+
expect_args = re.compile("/")
2281+
module_test.set_expect_requests_handler(expect_args=expect_args, request_handler=self.request_handler)
2282+
2283+
def check(self, module_test, events):
2284+
web_parameter_emitted = False
2285+
sqli_postparam_converted_finding_emitted = False
2286+
for e in events:
2287+
if e.type == "WEB_PARAMETER":
2288+
if "HTTP Extracted Parameter [search]" in e.data["description"]:
2289+
web_parameter_emitted = True
2290+
2291+
if e.type == "FINDING":
2292+
if (
2293+
"Possible SQL Injection. Parameter: [search] Parameter Type: [POSTPARAM] (converted from GETPARAM) Detection Method: [Single Quote/Two Single Quote, Code Change (200->500->200)]"
2294+
in e.data["description"]
2295+
):
2296+
sqli_postparam_converted_finding_emitted = True
2297+
2298+
assert web_parameter_emitted, "WEB_PARAMETER was not emitted"
2299+
assert sqli_postparam_converted_finding_emitted, (
2300+
"SQLi POSTPARAM (converted from GETPARAM) FINDING not emitted (try_get_as_post failed)"
2301+
)

0 commit comments

Comments
 (0)