Skip to content

Commit

Permalink
Improve mod exec detection
Browse files Browse the repository at this point in the history
Signed-off-by: bretfourbe <[email protected]>
  • Loading branch information
bretfourbe committed May 17, 2024
1 parent e39487c commit 3c6ec91
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 28 deletions.
26 changes: 23 additions & 3 deletions tests/attack/test_mod_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
28 changes: 26 additions & 2 deletions wapitiCore/attack/attack.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ class ParameterSituation(Flag):
class Parameter:
name: str
situation: ParameterSituation
reversed_parameter: bool = False

@property
def is_qs_injection(self) -> bool:
Expand Down Expand Up @@ -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):
Expand All @@ -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()
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down
9 changes: 7 additions & 2 deletions wapitiCore/attack/mod_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
63 changes: 42 additions & 21 deletions wapitiCore/data/attacks/execPayloads.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 3c6ec91

Please sign in to comment.