From 02154e38fd820f9bd705e74602cda183f62a8b21 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Fri, 18 Mar 2022 15:05:30 +0000 Subject: [PATCH 1/5] Agent: Make powershell exploiter interruptable --- monkey/infection_monkey/exploit/HostExploiter.py | 4 ++++ monkey/infection_monkey/exploit/powershell.py | 13 ++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 2e198ac4c..b1e2c72d3 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -103,6 +103,10 @@ class HostExploiter: self.exploit_result.error_message = "Exploiter has been interrupted" return self.interrupt.is_set() + class InterruptError(Exception): + # Raise when exploiter gets interrupted + pass + def post_exploit(self): self.set_finish_time() diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 026ffb17d..ede63daaf 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -67,7 +67,11 @@ class PowerShellExploiter(HostExploiter): auth_options = [get_auth_options(creds, use_ssl) for creds in credentials] - self._client = self._authenticate_via_brute_force(credentials, auth_options) + try: + self._client = self._authenticate_via_brute_force(credentials, auth_options) + except self.InterruptError: + return self.exploit_result + if not self._client: self.exploit_result.error_message = ( "Unable to authenticate to the remote host using any of the available credentials" @@ -79,6 +83,8 @@ class PowerShellExploiter(HostExploiter): try: self._execute_monkey_agent_on_victim() self.exploit_result.propagation_success = True + except self.InterruptError: + return self.exploit_result except Exception as ex: logger.error(f"Failed to propagate to the remote host: {ex}") self.exploit_result.error_message = str(ex) @@ -134,6 +140,8 @@ class PowerShellExploiter(HostExploiter): self, credentials: List[Credentials], auth_options: List[AuthOptions] ) -> Optional[IPowerShellClient]: for (creds, opts) in zip(credentials, auth_options): + if self.is_interrupted(): + raise self.InterruptError try: client = PowerShellClient(self.host.ip_addr, creds, opts) client.connect() @@ -166,6 +174,9 @@ class PowerShellExploiter(HostExploiter): def _execute_monkey_agent_on_victim(self): monkey_path_on_victim = self.options["dropper_target_path_win_64"] + if self.is_interrupted(): + raise self.InterruptError() + self._copy_monkey_binary_to_victim(monkey_path_on_victim) logger.info("Successfully copied the monkey binary to the victim.") From f50f4cf71caa621af1b2532aba1ab11c95a678ed Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Fri, 18 Mar 2022 15:19:14 +0000 Subject: [PATCH 2/5] Agent: Add interrupt error message to powershell results --- monkey/infection_monkey/exploit/powershell.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index ede63daaf..9840ac7a7 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -70,6 +70,7 @@ class PowerShellExploiter(HostExploiter): try: self._client = self._authenticate_via_brute_force(credentials, auth_options) except self.InterruptError: + self.exploit_result.error_message = "Exploiter has been interrupted" return self.exploit_result if not self._client: @@ -84,6 +85,7 @@ class PowerShellExploiter(HostExploiter): self._execute_monkey_agent_on_victim() self.exploit_result.propagation_success = True except self.InterruptError: + self.exploit_result.error_message = "Exploiter has been interrupted" return self.exploit_result except Exception as ex: logger.error(f"Failed to propagate to the remote host: {ex}") From 83b18debc0e4ac5c1e831cb90ebf38ba9fe98ef1 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 21 Mar 2022 08:39:49 -0400 Subject: [PATCH 3/5] Agent: Remove InterruptError and use `if` instead --- .../infection_monkey/exploit/HostExploiter.py | 4 ---- monkey/infection_monkey/exploit/powershell.py | 22 ++++++------------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index b1e2c72d3..2e198ac4c 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -103,10 +103,6 @@ class HostExploiter: self.exploit_result.error_message = "Exploiter has been interrupted" return self.interrupt.is_set() - class InterruptError(Exception): - # Raise when exploiter gets interrupted - pass - def post_exploit(self): self.set_finish_time() diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index 9840ac7a7..b097630d7 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -23,6 +23,7 @@ from infection_monkey.exploit.tools.helpers import get_random_file_suffix from infection_monkey.model import DROPPER_ARG, RUN_MONKEY, VictimHost from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.environment import is_windows_os +from infection_monkey.utils.threading import interruptable_iter logger = logging.getLogger(__name__) @@ -67,10 +68,9 @@ class PowerShellExploiter(HostExploiter): auth_options = [get_auth_options(creds, use_ssl) for creds in credentials] - try: - self._client = self._authenticate_via_brute_force(credentials, auth_options) - except self.InterruptError: - self.exploit_result.error_message = "Exploiter has been interrupted" + self._client = self._authenticate_via_brute_force(credentials, auth_options) + + if self.is_interrupted(): return self.exploit_result if not self._client: @@ -79,14 +79,9 @@ class PowerShellExploiter(HostExploiter): ) return self.exploit_result - self.exploit_result.exploitation_success = True - try: self._execute_monkey_agent_on_victim() self.exploit_result.propagation_success = True - except self.InterruptError: - self.exploit_result.error_message = "Exploiter has been interrupted" - return self.exploit_result except Exception as ex: logger.error(f"Failed to propagate to the remote host: {ex}") self.exploit_result.error_message = str(ex) @@ -141,9 +136,7 @@ class PowerShellExploiter(HostExploiter): def _authenticate_via_brute_force( self, credentials: List[Credentials], auth_options: List[AuthOptions] ) -> Optional[IPowerShellClient]: - for (creds, opts) in zip(credentials, auth_options): - if self.is_interrupted(): - raise self.InterruptError + for (creds, opts) in interruptable_iter(zip(credentials, auth_options), self.interrupt): try: client = PowerShellClient(self.host.ip_addr, creds, opts) client.connect() @@ -152,7 +145,9 @@ class PowerShellExploiter(HostExploiter): f"{creds.username}, Secret Type: {creds.secret_type.name}" ) + self.exploit_result.exploitation_success = True self._report_login_attempt(True, creds) + return client except Exception as ex: logger.debug( @@ -176,9 +171,6 @@ class PowerShellExploiter(HostExploiter): def _execute_monkey_agent_on_victim(self): monkey_path_on_victim = self.options["dropper_target_path_win_64"] - if self.is_interrupted(): - raise self.InterruptError() - self._copy_monkey_binary_to_victim(monkey_path_on_victim) logger.info("Successfully copied the monkey binary to the victim.") From b0f03179c1058797b85f9a3f5ea9e32f9fe61482 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 21 Mar 2022 08:59:17 -0400 Subject: [PATCH 4/5] Agent: Add `interrupted` boolean to ExploiterResultData Setting an interrupted flag on the ExploiterResultData is a more useful way to present the information to anything that uses it. If decisions need to be made based on whether or not something was interrupted, a flag can be checked instead of parsing an error message. --- monkey/infection_monkey/exploit/HostExploiter.py | 2 +- monkey/infection_monkey/i_puppet/i_puppet.py | 1 + monkey/infection_monkey/puppet/mock_puppet.py | 15 +++++++++------ .../infection_monkey/telemetry/exploit_telem.py | 4 +++- .../infection_monkey/master/test_propagator.py | 12 ++++++------ .../telemetry/test_exploit_telem.py | 3 ++- 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 2e198ac4c..f791e7a9c 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -100,7 +100,7 @@ class HostExploiter: # Ideally the user should only do "check_for_interrupt()" if self.interrupt.is_set(): logger.info("Exploiter has been interrupted") - self.exploit_result.error_message = "Exploiter has been interrupted" + self.exploit_result.interrupted = True return self.interrupt.is_set() def post_exploit(self): diff --git a/monkey/infection_monkey/i_puppet/i_puppet.py b/monkey/infection_monkey/i_puppet/i_puppet.py index 0db62bae2..d68e42049 100644 --- a/monkey/infection_monkey/i_puppet/i_puppet.py +++ b/monkey/infection_monkey/i_puppet/i_puppet.py @@ -24,6 +24,7 @@ class UnknownPluginError(Exception): class ExploiterResultData: exploitation_success: bool = False propagation_success: bool = False + interrupted: bool = False os: str = "" info: Mapping = None attempts: Iterable = None diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index bd00c3acb..4ac6f3c2f 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -190,17 +190,18 @@ class MockPuppet(IPuppet): successful_exploiters = { DOT_1: { "PowerShellExploiter": ExploiterResultData( - True, True, os_windows, info_powershell, attempts, None + True, True, False, os_windows, info_powershell, attempts, None ), "ZerologonExploiter": ExploiterResultData( - False, False, os_windows, {}, [], "Zerologon failed" + False, False, False, os_windows, {}, [], "Zerologon failed" ), "SSHExploiter": ExploiterResultData( - False, False, os_linux, info_ssh, attempts, "Failed exploiting" + False, False, False, os_linux, info_ssh, attempts, "Failed exploiting" ), }, DOT_3: { "PowerShellExploiter": ExploiterResultData( + False, False, False, os_windows, @@ -209,9 +210,11 @@ class MockPuppet(IPuppet): "PowerShell Exploiter Failed", ), "SSHExploiter": ExploiterResultData( - False, False, os_linux, info_ssh, attempts, "Failed exploiting" + False, False, False, os_linux, info_ssh, attempts, "Failed exploiting" + ), + "ZerologonExploiter": ExploiterResultData( + True, False, False, os_windows, {}, [], None ), - "ZerologonExploiter": ExploiterResultData(True, False, os_windows, {}, [], None), }, } @@ -219,7 +222,7 @@ class MockPuppet(IPuppet): return successful_exploiters[host.ip_addr][name] except KeyError: return ExploiterResultData( - False, False, os_linux, {}, [], f"{name} failed for host {host}" + False, False, False, os_linux, {}, [], f"{name} failed for host {host}" ) def run_payload(self, name: str, options: Dict, interrupt: threading.Event): diff --git a/monkey/infection_monkey/telemetry/exploit_telem.py b/monkey/infection_monkey/telemetry/exploit_telem.py index c276e1b8f..62f5d728f 100644 --- a/monkey/infection_monkey/telemetry/exploit_telem.py +++ b/monkey/infection_monkey/telemetry/exploit_telem.py @@ -1,9 +1,9 @@ from typing import Dict from common.common_consts.telem_categories import TelemCategoryEnum +from infection_monkey.i_puppet.i_puppet import ExploiterResultData from infection_monkey.model.host import VictimHost from infection_monkey.telemetry.base_telem import BaseTelem -from infection_monkey.i_puppet.i_puppet import ExploiterResultData class ExploitTelem(BaseTelem): @@ -25,6 +25,7 @@ class ExploitTelem(BaseTelem): self.host = host.__dict__ self.exploitation_result = result.exploitation_success self.propagation_result = result.propagation_success + self.interrupted = result.interrupted self.info = result.info self.attempts = result.attempts @@ -34,6 +35,7 @@ class ExploitTelem(BaseTelem): return { "exploitation_result": self.exploitation_result, "propagation_result": self.propagation_result, + "interrupted": self.interrupted, "machine": self.host, "exploiter": self.name, "info": self.info, diff --git a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py index 49cdd103a..3746e65eb 100644 --- a/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py +++ b/monkey/tests/unit_tests/infection_monkey/master/test_propagator.py @@ -201,38 +201,38 @@ class MockExploiter: results_callback( "PowerShellExploiter", host, - ExploiterResultData(True, True, os_windows, {}, {}, None), + ExploiterResultData(True, True, False, os_windows, {}, {}, None), ) results_callback( "SSHExploiter", host, - ExploiterResultData(False, False, os_linux, {}, {}, "SSH FAILED for .1"), + ExploiterResultData(False, False, False, os_linux, {}, {}, "SSH FAILED for .1"), ) elif host.ip_addr.endswith(".2"): results_callback( "PowerShellExploiter", host, ExploiterResultData( - False, False, os_windows, {}, {}, "POWERSHELL FAILED for .2" + False, False, False, os_windows, {}, {}, "POWERSHELL FAILED for .2" ), ) results_callback( "SSHExploiter", host, - ExploiterResultData(False, False, os_linux, {}, {}, "SSH FAILED for .2"), + ExploiterResultData(False, False, False, os_linux, {}, {}, "SSH FAILED for .2"), ) elif host.ip_addr.endswith(".3"): results_callback( "PowerShellExploiter", host, ExploiterResultData( - False, False, os_windows, {}, {}, "POWERSHELL FAILED for .3" + False, False, False, os_windows, {}, {}, "POWERSHELL FAILED for .3" ), ) results_callback( "SSHExploiter", host, - ExploiterResultData(True, True, os_linux, {}, {}, None), + ExploiterResultData(True, True, False, os_linux, {}, {}, None), ) diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py index 600e1db20..3255cc7b7 100644 --- a/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py @@ -40,7 +40,7 @@ def exploit_telem_test_instance(): EXPLOITER_NAME, HOST, ExploiterResultData( - RESULT, RESULT, OS_LINUX, EXPLOITER_INFO, EXPLOITER_ATTEMPTS, ERROR_MSG + RESULT, RESULT, False, OS_LINUX, EXPLOITER_INFO, EXPLOITER_ATTEMPTS, ERROR_MSG ), ) @@ -50,6 +50,7 @@ def test_exploit_telem_send(exploit_telem_test_instance, spy_send_telemetry): expected_data = { "exploitation_result": RESULT, "propagation_result": RESULT, + "interrupted": False, "machine": HOST_AS_DICT, "exploiter": EXPLOITER_NAME, "info": EXPLOITER_INFO, From 7a1fcced2ffa3b7d12c23da2a02d428775ed0edb Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 21 Mar 2022 09:09:15 -0400 Subject: [PATCH 5/5] Agent: Extract method _set_interrupted() from is_interrupted() --- monkey/infection_monkey/exploit/HostExploiter.py | 11 ++++++----- monkey/infection_monkey/exploit/mssqlexec.py | 3 ++- monkey/infection_monkey/exploit/powershell.py | 3 ++- monkey/infection_monkey/exploit/wmiexec.py | 3 ++- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index f791e7a9c..17bbee2a3 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -94,14 +94,15 @@ class HostExploiter: ) self.set_start_time() - def is_interrupted(self): + def _is_interrupted(self): + return self.interrupt.is_set() + + def _set_interrupted(self): # This method should be refactored to raise an exception to reduce duplication in the # "if is_interrupted: return self.exploitation_results" # Ideally the user should only do "check_for_interrupt()" - if self.interrupt.is_set(): - logger.info("Exploiter has been interrupted") - self.exploit_result.interrupted = True - return self.interrupt.is_set() + logger.info("Exploiter has been interrupted") + self.exploit_result.interrupted = True def post_exploit(self): self.set_finish_time() diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 9a9bfef7a..eae4f33dd 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -73,7 +73,8 @@ class MSSQLExploiter(HostExploiter): ) return self.exploit_result - if self.is_interrupted(): + if self._is_interrupted(): + self._set_interrupted() return self.exploit_result try: diff --git a/monkey/infection_monkey/exploit/powershell.py b/monkey/infection_monkey/exploit/powershell.py index b097630d7..868c31c97 100644 --- a/monkey/infection_monkey/exploit/powershell.py +++ b/monkey/infection_monkey/exploit/powershell.py @@ -70,7 +70,8 @@ class PowerShellExploiter(HostExploiter): self._client = self._authenticate_via_brute_force(credentials, auth_options) - if self.is_interrupted(): + if self._is_interrupted(): + self._set_interrupted() return self.exploit_result if not self._client: diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 7b7c9ad1e..9bfcb3d14 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -70,7 +70,8 @@ class WmiExploiter(HostExploiter): downloaded_agent = self.agent_repository.get_agent_binary(self.host.os["type"]) - if self.is_interrupted(): + if self._is_interrupted(): + self._set_interrupted() return self.exploit_result remote_full_path = SmbTools.copy_file(