From 3c6ec91bb8d6ef74d229d27255866df82b59f9d6 Mon Sep 17 00:00:00 2001 From: bretfourbe Date: Fri, 17 May 2024 10:17:00 +0200 Subject: [PATCH] Improve mod exec detection Signed-off-by: bretfourbe --- tests/attack/test_mod_exec.py | 26 ++++++++-- wapitiCore/attack/attack.py | 28 ++++++++++- wapitiCore/attack/mod_exec.py | 9 +++- wapitiCore/data/attacks/execPayloads.ini | 63 ++++++++++++++++-------- 4 files changed, 98 insertions(+), 28 deletions(-) diff --git a/tests/attack/test_mod_exec.py b/tests/attack/test_mod_exec.py index 8df572e8e..8e2540226 100644 --- a/tests/attack/test_mod_exec.py +++ b/tests/attack/test_mod_exec.py @@ -80,22 +80,42 @@ async def test_detection(): return_value=httpx.Response(200, text="Hello there") ) + respx.post(url__regex=r"http://perdu\.com/\?env=foo").mock( + return_value=httpx.Response(200, text="PATH=/bin:/usr/bin;PWD=/") + ) + + respx.post(url__regex=r"http://perdu\.com/.*").mock(httpx.Response(200, text="Hello there")) + persister = AsyncMock() + all_requests = [] request = Request("http://perdu.com/?vuln=hello") request.path_id = 1 + all_requests.append(request) + + request = Request( + "http://perdu.com/", + post_params=[["foo", "bar"]] + ) + request.path_id = 2 + all_requests.append(request) crawler_configuration = CrawlerConfiguration(Request("http://perdu.com/"), timeout=1) async with AsyncCrawler.with_configuration(crawler_configuration) as crawler: options = {"timeout": 10, "level": 1} module = ModuleExec(crawler, persister, options, Event(), crawler_configuration) - await module.attack(request) + module.do_post = True + for request in all_requests: + await module.attack(request) - assert persister.add_payload.call_count == 1 + assert persister.add_payload.call_count == 2 assert persister.add_payload.call_args_list[0][1]["module"] == "exec" assert persister.add_payload.call_args_list[0][1]["category"] == "Command execution" assert persister.add_payload.call_args_list[0][1]["request"].get_params == [["vuln", ";env;"]] + assert persister.add_payload.call_args_list[1][1]["module"] == "exec" + assert persister.add_payload.call_args_list[1][1]["category"] == "Command execution" + assert persister.add_payload.call_args_list[1][1]["request"].post_params == [["foo", 'env'],["env", "foo"]] @pytest.mark.asyncio @@ -128,7 +148,7 @@ def timeout_callback(http_request): ): if "sleep" in payload_info.payload: break - payloads_until_sleep += 1 + payloads_until_sleep += 2 # paylaod + reversed_payload await module.attack(request) diff --git a/wapitiCore/attack/attack.py b/wapitiCore/attack/attack.py index 174767e5f..e78c1226d 100644 --- a/wapitiCore/attack/attack.py +++ b/wapitiCore/attack/attack.py @@ -137,6 +137,7 @@ class ParameterSituation(Flag): class Parameter: name: str situation: ParameterSituation + reversed_parameter: bool = False @property def is_qs_injection(self) -> bool: @@ -341,7 +342,8 @@ def get_mutator(self): return Mutator( methods=methods, qs_inject=self.must_attack_query_string, - skip=self.options.get("skipped_parameters") + skip=self.options.get("skipped_parameters"), + module=self.name ) async def does_timeout(self, request, timeout: float = None): @@ -358,7 +360,8 @@ class Mutator: def __init__( self, methods="FGP", qs_inject=False, max_queries_per_pattern: int = 1000, parameters=None, # Restrict attack to a whitelist of parameters - skip=None # Must not attack those parameters (blacklist) + skip=None, # Must not attack those parameters (blacklist) + module=None ): self._mutate_get = "G" in methods.upper() self._mutate_file = "F" in methods.upper() @@ -371,6 +374,7 @@ def __init__( self._attack_hashes = set() self._json_attack_hashes = set() self._skip_list.update(COMMON_ANNOYING_PARAMETERS) + self._module = module def _mutate_urlencoded_multipart( self, @@ -430,6 +434,7 @@ def _mutate_urlencoded_multipart( if hash(attack_pattern) not in self._attack_hashes: self._attack_hashes.add(hash(attack_pattern)) parameter = Parameter(name=param_name, situation=parameter_situation) + reverse_parameter = None iterator = payloads if isinstance(payloads, list) else payloads(request, parameter) for payload_info in iterator: @@ -485,6 +490,25 @@ def _mutate_urlencoded_multipart( payload_info.payload = raw_payload yield evil_req, parameter, payload_info + if self._module == "exec": + reverse_parameter = Parameter(name=payload_info.payload, + situation=parameter_situation, reversed_parameter=True) + reverse_payload_info = payload_info + reverse_payload_info.payload = param_name + + reverse_evil_req = Request( + request.path, + method=request.method, + get_params=get_params+[[reverse_parameter.name, reverse_payload_info.payload]], + post_params=post_params+[[reverse_parameter.name, reverse_payload_info.payload]], + file_params=file_params, + referer=referer, + link_depth=request.link_depth, + enctype=request.enctype, + ) + yield reverse_evil_req, reverse_parameter, reverse_payload_info + + params_list[i][1] = saved_value def _mutate_query_string( diff --git a/wapitiCore/attack/mod_exec.py b/wapitiCore/attack/mod_exec.py index 8e9bb7bf8..e37cf377a 100644 --- a/wapitiCore/attack/mod_exec.py +++ b/wapitiCore/attack/mod_exec.py @@ -75,16 +75,19 @@ async def attack(self, request: Request, response: Optional[Response] = None): saw_internal_error = False current_parameter = None vulnerable_parameter = False + vulnerable_reversed_parameter = False for mutated_request, parameter, payload_info in self.mutator.mutate(request, self.get_payloads): - if current_parameter != parameter: + if current_parameter != parameter and not parameter.reversed_parameter: # Forget what we know about current parameter current_parameter = parameter vulnerable_parameter = False elif vulnerable_parameter: # If parameter is vulnerable, just skip till next parameter continue + if vulnerable_reversed_parameter and parameter.reversed_parameter: + continue if payload_info.type == "time" and request.path_id in self.false_positive_timeouts: # If the original request is known to gives timeout and payload is time-based, just skip @@ -167,10 +170,12 @@ async def attack(self, request: Request, response: Optional[Response] = None): vuln_info = None # No timeout raised, check for patterns in response - if all(rule in response.content for rule in payload_info.rules): + if any(rule in response.content for rule in payload_info.rules): vuln_info = payload_info.description # We reached maximum exploitation for this parameter, don't send more payloads vulnerable_parameter = True + if parameter.reversed_parameter: + vulnerable_reversed_parameter = True elif not warned: vuln_info = self._find_warning_in_response(response.content) warned = True diff --git a/wapitiCore/data/attacks/execPayloads.ini b/wapitiCore/data/attacks/execPayloads.ini index 91a54696a..dcc605f88 100644 --- a/wapitiCore/data/attacks/execPayloads.ini +++ b/wapitiCore/data/attacks/execPayloads.ini @@ -6,73 +6,94 @@ description = None status = vulnerability type = pattern +[no_escape] +payload = id +rules = uid + gid +description = Command execution +status = vulnerability + [semicolon_escape] payload = ;env; -rules = PATH= - PWD= +rules = PATH + PWD description = Command execution status = vulnerability [pipe_escape] -payload = |env| -rules = PATH= - PWD= +payload = |env +rules = PATH + PWD +description = Command execution +status = vulnerability + +[double_ampersand_escape] +payload = &&env +rules = PWD + PATH +description = Command execution +status = vulnerability + +[pipe_escape_known_prefix] +payload = [VALUE]|env +rules = PWD + PATH description = Command execution status = vulnerability [semicolon_escape_prefix] payload = a;env; -rules = PATH= - PWD= +rules = PATH + PWD description = Command execution status = vulnerability [semicolon_escape_prefix_and_parenthesis] payload = a;env; -rules = PATH= - PWD= +rules = PATH + PWD description = Command execution status = vulnerability [dir_up_perl_exec] payload = ../../../../../../../../../../../../../../../usr/bin/env| -rules = PATH= - PWD= +rules = PATH + PWD description = Command execution status = vulnerability [semicolon_escape_known_prefix] payload = [VALUE];env; -rules = PATH= - PWD= +rules = PATH + PWD description = Command execution status = vulnerability [new_line_escape] payload = [VALUE][LF]env; -rules = PATH= - PWD= +rules = PATH + PWD description = Command execution status = vulnerability [ampersand_escape] payload = &set& -rules = PATH= - PWD= +rules = PATH + PWD description = Command execution status = vulnerability [simple_set_execution] payload = set -rules = PATH= - PWD= +rules = PATH + PWD description = Command execution status = vulnerability [simple_env_execution] payload = env -rules = PATH= - PWD= +rules = PATH + PWD description = Command execution status = vulnerability