From 44d3ad85865d99a539ae6d8029e5a6e1c1c002f6 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 25 Nov 2021 17:14:24 +0100 Subject: [PATCH 01/17] Agent: Add realistic puppet exploit telemetry info and attempts Fix logging consistency in mock master. --- monkey/infection_monkey/master/mock_master.py | 26 +++++----- monkey/infection_monkey/puppet/mock_puppet.py | 51 +++++++++++++++---- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index 4cf6dc176..41d478b93 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -33,7 +33,7 @@ class MockMaster(IMaster): self._run_payload() def _run_sys_info_collectors(self): - logging.info("Running system info collectors") + logger.info("Running system info collectors") system_info_telemetry = {} system_info_telemetry["ProcessListCollector"] = self._puppet.run_sys_info_collector( "ProcessListCollector" @@ -43,10 +43,10 @@ class MockMaster(IMaster): ) system_info = self._puppet.run_sys_info_collector("LinuxInfoCollector") self._telemetry_messenger.send_telemetry(SystemInfoTelem(system_info)) - logging.info("Finished running system info collectors") + logger.info("Finished running system info collectors") def _run_pbas(self): - logging.info("Running post breach actions") + logger.info("Running post breach actions") name = "AccountDiscovery" command, result = self._puppet.run_pba(name, {}) self._telemetry_messenger.send_telemetry(PostBreachTelem(name, command, result)) @@ -54,10 +54,10 @@ class MockMaster(IMaster): name = "CommunicateAsBackdoorUser" command, result = self._puppet.run_pba(name, {}) self._telemetry_messenger.send_telemetry(PostBreachTelem(name, command, result)) - logging.info("Finished running post breach actions") + logger.info("Finished running post breach actions") def _scan_victims(self): - logging.info("Scanning network for potential victims") + logger.info("Scanning network for potential victims") ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3"] ports = [22, 445, 3389, 8008] for ip in ips: @@ -78,10 +78,10 @@ class MockMaster(IMaster): h.services[port_scan_data.service]["banner"] = port_scan_data.banner self._telemetry_messenger.send_telemetry(ScanTelem(h)) - logging.info("Finished scanning network for potential victims") + logger.info("Finished scanning network for potential victims") def _fingerprint(self): - logging.info("Running fingerprinters on potential victims") + logger.info("Running fingerprinters on potential victims") machine_1 = self._hosts["10.0.0.1"] machine_3 = self._hosts["10.0.0.3"] @@ -93,29 +93,31 @@ class MockMaster(IMaster): self._puppet.fingerprint("HTTPFinger", machine_3) self._telemetry_messenger.send_telemetry(ScanTelem(machine_3)) - logging.info("Finished running fingerprinters on potential victims") + logger.info("Finished running fingerprinters on potential victims") def _exploit(self): - logging.info("Exploiting victims") + logger.info("Exploiting victims") result, info, attempts = self._puppet.exploit_host( "PowerShellExploiter", "10.0.0.1", {}, None ) + logger.info(f"Attempts for exploiting {attempts}") self._telemetry_messenger.send_telemetry( ExploitTelem("PowerShellExploiter", self._hosts["10.0.0.1"], result, info, attempts) ) result, info, attempts = self._puppet.exploit_host("SSHExploiter", "10.0.0.3", {}, None) + logger.info(f"Attempts for exploiting {attempts}") self._telemetry_messenger.send_telemetry( ExploitTelem("SSHExploiter", self._hosts["10.0.0.3"], result, info, attempts) ) - logging.info("Finished exploiting victims") + logger.info("Finished exploiting victims") def _run_payload(self): - logging.info("Running payloads") + logger.info("Running payloads") # TODO: modify what FileEncryptionTelem gets path, success, error = self._puppet.run_payload("RansomwarePayload", {}, None) self._telemetry_messenger.send_telemetry(FileEncryptionTelem(path, success, error)) - logging.info("Finished running payloads") + logger.info("Finished running payloads") def terminate(self) -> None: logger.info("Terminating MockMaster") diff --git a/monkey/infection_monkey/puppet/mock_puppet.py b/monkey/infection_monkey/puppet/mock_puppet.py index 6996d4d7c..3a32f3718 100644 --- a/monkey/infection_monkey/puppet/mock_puppet.py +++ b/monkey/infection_monkey/puppet/mock_puppet.py @@ -220,17 +220,48 @@ class MockPuppet(IPuppet): self, name: str, host: str, options: Dict, interrupt: threading.Event ) -> ExploiterResultData: logger.debug(f"exploit_hosts({name}, {host}, {options})") + attempts = [ + { + "result": False, + "user": "Administrator", + "password": "", + "lm_hash": "", + "ntlm_hash": "", + "ssh_key": host, + }, + { + "result": False, + "user": "root", + "password": "", + "lm_hash": "", + "ntlm_hash": "", + "ssh_key": host, + }, + ] + info_powershell = { + "display_name": "PowerShell", + "started": "2021-11-25T15:57:06.307696", + "finished": "2021-11-25T15:58:33.788238", + "vulnerable_urls": [], + "vulnerable_ports": [], + "executed_cmds": [ + { + "cmd": "/tmp/monkey m0nk3y -s 10.10.10.10:5000 -d 1 >git s /dev/null 2>&1 &", + "powershell": True, + } + ], + } + info_ssh = { + "display_name": "SSH", + "started": "2021-11-25T15:57:06.307696", + "finished": "2021-11-25T15:58:33.788238", + "vulnerable_urls": [], + "vulnerable_ports": [22], + "executed_cmds": [], + } successful_exploiters = { - DOT_1: { - "PowerShellExploiter": ExploiterResultData( - True, {"info": "important success stuff"}, ["attempt 1"] - ) - }, - DOT_3: { - "SSHExploiter": ExploiterResultData( - False, {"info": "important failure stuff"}, ["attempt 2"] - ) - }, + DOT_1: {"PowerShellExploiter": ExploiterResultData(True, info_powershell, attempts)}, + DOT_3: {"SSHExploiter": ExploiterResultData(False, info_ssh, attempts)}, } return successful_exploiters[host][name] From fb007e9cc835e04035e6d5be8dffcfb2af4bc79d Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Thu, 25 Nov 2021 17:17:23 +0100 Subject: [PATCH 02/17] Agent: Initial refactoring of monkey including mocked puppet and a master --- monkey/infection_monkey/monkey.py | 170 ++++++++++++++++++------------ 1 file changed, 102 insertions(+), 68 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 76bcbdf02..ccf268ecf 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -13,18 +13,23 @@ from common.version import get_version from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient from infection_monkey.exploit.HostExploiter import HostExploiter +from infection_monkey.master.mock_master import MockMaster from infection_monkey.model import DELAY_DELETE_CMD from infection_monkey.network.firewall import app as firewall from infection_monkey.network.HostFinger import HostFinger from infection_monkey.network.network_scanner import NetworkScanner from infection_monkey.network.tools import get_interface_to_target, is_running_on_island from infection_monkey.post_breach.post_breach_handler import PostBreach +from infection_monkey.puppet.mock_puppet import MockPuppet from infection_monkey.ransomware.ransomware_payload_builder import build_ransomware_payload from infection_monkey.system_info import SystemInfoCollector from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from infection_monkey.telemetry.attack.t1107_telem import T1107Telem from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem +from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( + LegacyTelemetryMessengerAdapter, +) from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem @@ -38,6 +43,7 @@ from infection_monkey.utils.monkey_dir import ( remove_monkey_dir, ) from infection_monkey.utils.monkey_log_path import get_monkey_log_path +from infection_monkey.utils.signal_handler import register_signal_handlers from infection_monkey.windows_upgrader import WindowsUpgrader MAX_DEPTH_REACHED_MESSAGE = "Reached max depth, skipping propagation phase." @@ -48,6 +54,7 @@ logger = logging.getLogger(__name__) class InfectionMonkey(object): def __init__(self, args): + self.master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) self._keep_running = False self._exploited_machines = set() self._fail_exploitation_machines = set() @@ -106,71 +113,14 @@ class InfectionMonkey(object): try: logger.info("Monkey is starting...") - logger.debug("Starting the setup phase.") + # Sets the monkey up + self.setup() - # Sets island's IP and port for monkey to communicate to - self.set_default_server() - self.set_default_port() + # Start post breach phase + self.start_post_breach() - # Create a dir for monkey files if there isn't one - create_monkey_dir() - - self.upgrade_to_64_if_needed() - - ControlClient.wakeup(parent=self._parent) - ControlClient.load_control_config() - - if is_windows_os(): - T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() - - self.shutdown_by_not_alive_config() - - if is_running_on_island(): - WormConfiguration.started_on_island = True - ControlClient.report_start_on_island() - - if not ControlClient.should_monkey_run(self._opts.vulnerable_port): - raise PlannedShutdownException( - "Monkey shouldn't run on current machine " - "(it will be exploited later with more depth)." - ) - - if firewall.is_enabled(): - firewall.add_firewall_rule() - - self._monkey_tunnel = ControlClient.create_control_tunnel() - if self._monkey_tunnel: - self._monkey_tunnel.start() - - StateTelem(is_done=False, version=get_version()).send() - TunnelTelem().send() - - logger.debug("Starting the post-breach phase asynchronously.") - self._post_breach_phase = Thread(target=self.start_post_breach_phase) - self._post_breach_phase.start() - - if not InfectionMonkey.max_propagation_depth_reached(): - logger.info("Starting the propagation phase.") - logger.debug("Running with depth: %d" % WormConfiguration.depth) - self.propagate() - else: - logger.info( - "Maximum propagation depth has been reached; monkey will not propagate." - ) - TraceTelem(MAX_DEPTH_REACHED_MESSAGE).send() - - if self._keep_running and WormConfiguration.alive: - InfectionMonkey.run_ransomware() - - # if host was exploited, before continue to closing the tunnel ensure the exploited - # host had its chance to - # connect to the tunnel - if len(self._exploited_machines) > 0: - time_to_sleep = WormConfiguration.keep_tunnel_open_time - logger.info( - "Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep - ) - time.sleep(time_to_sleep) + # Start propagation phase + self.start_propagation() except PlannedShutdownException: logger.info( @@ -180,12 +130,96 @@ class InfectionMonkey(object): logger.exception("Planned shutdown, reason:") finally: - if self._monkey_tunnel: - self._monkey_tunnel.stop() - self._monkey_tunnel.join() + self.teardown() - if self._post_breach_phase: - self._post_breach_phase.join() + def setup(self): + logger.debug("Starting the setup phase.") + + self.shutdown_by_not_alive_config() + + # Sets island's IP and port for monkey to communicate to + self.set_default_server() + self.set_default_port() + + # Create a dir for monkey files if there isn't one + create_monkey_dir() + + self.upgrade_to_64_if_needed() + + ControlClient.wakeup(parent=self._parent) + ControlClient.load_control_config() + + if ControlClient.check_for_stop(): + raise PlannedShutdownException("Monkey has been marked for shutdown.") + + if not ControlClient.should_monkey_run(self._opts.vulnerable_port): + raise PlannedShutdownException( + "Monkey shouldn't run on current machine " + "(it will be exploited later with more depth)." + ) + + if is_windows_os(): + T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() + + if is_running_on_island(): + WormConfiguration.started_on_island = True + ControlClient.report_start_on_island() + + if firewall.is_enabled(): + firewall.add_firewall_rule() + + self._monkey_tunnel = ControlClient.create_control_tunnel() + if self._monkey_tunnel: + self._monkey_tunnel.start() + + StateTelem(is_done=False, version=get_version()).send() + TunnelTelem().send() + + self.master.start() + + register_signal_handlers(self.master) + + def start_propagation(self): + if not InfectionMonkey.max_propagation_depth_reached(): + logger.info("Starting the propagation phase.") + logger.debug("Running with depth: %d" % WormConfiguration.depth) + self.propagate() + else: + logger.info("Maximum propagation depth has been reached; monkey will not propagate.") + TraceTelem(MAX_DEPTH_REACHED_MESSAGE).send() + + if self._keep_running and WormConfiguration.alive: + InfectionMonkey.run_ransomware() + + # if host was exploited, before continue to closing the tunnel ensure the exploited + # host had its chance to + # connect to the tunnel + if len(self._exploited_machines) > 0: + time_to_sleep = WormConfiguration.keep_tunnel_open_time + logger.info( + "Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep + ) + time.sleep(time_to_sleep) + + def start_post_breach(self): + logger.debug("Starting the post-breach phase asynchronously.") + self._post_breach_phase = Thread(target=self.start_post_breach_phase) + self._post_breach_phase.start() + + def teardown(self): + if self._monkey_tunnel: + self._monkey_tunnel.stop() + self._monkey_tunnel.join() + + if self._post_breach_phase: + self._post_breach_phase.join() + + if firewall.is_enabled(): + firewall.remove_firewall_rule() + firewall.close() + + self.master.terminate() + self.master.cleanup() def start_post_breach_phase(self): self.collect_system_info_if_configured() From 3c13324e8a0394f90dec40dee2a1da03987a0411 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 26 Nov 2021 13:32:41 +0100 Subject: [PATCH 03/17] Agent: Change send_exploit_telemetry for host exploiter --- monkey/infection_monkey/exploit/HostExploiter.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 3a5abf4c5..34fd674ff 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -66,10 +66,16 @@ class HostExploiter(Plugin): def is_os_supported(self): return self.host.os.get("type") in self._TARGET_OS_TYPE - def send_exploit_telemetry(self, result): + def send_exploit_telemetry(self, name: str, result: bool): from infection_monkey.telemetry.exploit_telem import ExploitTelem - ExploitTelem(self, result).send() + ExploitTelem( + name=name, + host=self.host, + result=result, + info=self.exploit_info, + attempts=self.exploit_attempts, + ).send() def report_login_attempt(self, result, user, password="", lm_hash="", ntlm_hash="", ssh_key=""): self.exploit_attempts.append( From 1ee6d10b4c018a6a5f4a23b9db8ec9cd57761d7c Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Fri, 26 Nov 2021 13:34:06 +0100 Subject: [PATCH 04/17] Agent: Refactor agent startup Reorder and rename functions. --- monkey/infection_monkey/monkey.py | 384 +++++++++++++++--------------- 1 file changed, 195 insertions(+), 189 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index ccf268ecf..88ca20b98 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -75,8 +75,7 @@ class InfectionMonkey(object): def initialize(self): logger.info("Monkey is initializing...") - if not self._singleton.try_lock(): - raise Exception("Another instance of the monkey is already running") + self._check_for_running_monkey() arg_parser = argparse.ArgumentParser() arg_parser.add_argument("-p", "--parent") @@ -85,7 +84,7 @@ class InfectionMonkey(object): arg_parser.add_argument("-d", "--depth", type=int) arg_parser.add_argument("-vp", "--vulnerable-port") self._opts, self._args = arg_parser.parse_known_args(self._args) - self.log_arguments() + self._log_arguments() self._parent = self._opts.parent self._default_tunnel = self._opts.tunnel @@ -109,18 +108,25 @@ class InfectionMonkey(object): "Default server: %s is already in command servers list" % self._default_server ) + def _check_for_running_monkey(self): + if not self._singleton.try_lock(): + raise Exception("Another instance of the monkey is already running") + + def _log_arguments(self): + arg_string = " ".join([f"{key}: {value}" for key, value in vars(self._opts).items()]) + logger.info(f"Monkey started with arguments: {arg_string}") + def start(self): try: logger.info("Monkey is starting...") - # Sets the monkey up - self.setup() + self._setup() # Start post breach phase - self.start_post_breach() + self._start_post_breach_async() # Start propagation phase - self.start_propagation() + self._start_propagation() except PlannedShutdownException: logger.info( @@ -130,28 +136,28 @@ class InfectionMonkey(object): logger.exception("Planned shutdown, reason:") finally: - self.teardown() + self._teardown() - def setup(self): + def _setup(self): logger.debug("Starting the setup phase.") - self.shutdown_by_not_alive_config() + InfectionMonkey._shutdown_by_not_alive_config() # Sets island's IP and port for monkey to communicate to - self.set_default_server() - self.set_default_port() + self._set_default_server() + self._set_default_port() # Create a dir for monkey files if there isn't one create_monkey_dir() - self.upgrade_to_64_if_needed() - ControlClient.wakeup(parent=self._parent) ControlClient.load_control_config() if ControlClient.check_for_stop(): raise PlannedShutdownException("Monkey has been marked for shutdown.") + self._upgrade_to_64_if_needed() + if not ControlClient.should_monkey_run(self._opts.vulnerable_port): raise PlannedShutdownException( "Monkey shouldn't run on current machine " @@ -175,61 +181,53 @@ class InfectionMonkey(object): StateTelem(is_done=False, version=get_version()).send() TunnelTelem().send() - self.master.start() - register_signal_handlers(self.master) - def start_propagation(self): - if not InfectionMonkey.max_propagation_depth_reached(): - logger.info("Starting the propagation phase.") - logger.debug("Running with depth: %d" % WormConfiguration.depth) - self.propagate() - else: - logger.info("Maximum propagation depth has been reached; monkey will not propagate.") - TraceTelem(MAX_DEPTH_REACHED_MESSAGE).send() + self.master.start() - if self._keep_running and WormConfiguration.alive: - InfectionMonkey.run_ransomware() + @staticmethod + def _shutdown_by_not_alive_config(): + if not WormConfiguration.alive: + raise PlannedShutdownException("Marked 'not alive' from configuration.") - # if host was exploited, before continue to closing the tunnel ensure the exploited - # host had its chance to - # connect to the tunnel - if len(self._exploited_machines) > 0: - time_to_sleep = WormConfiguration.keep_tunnel_open_time - logger.info( - "Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep + def _set_default_server(self): + """ + Sets the default server for the Monkey to communicate back to. + :raises PlannedShutdownException if couldn't find the server. + """ + if not ControlClient.find_server(default_tunnel=self._default_tunnel): + raise PlannedShutdownException( + "Monkey couldn't find server with {} default tunnel.".format(self._default_tunnel) ) - time.sleep(time_to_sleep) + self._default_server = WormConfiguration.current_server + logger.debug("default server set to: %s" % self._default_server) - def start_post_breach(self): + def _set_default_port(self): + try: + self._default_server_port = self._default_server.split(":")[1] + except KeyError: + self._default_server_port = "" + + def _upgrade_to_64_if_needed(self): + if WindowsUpgrader.should_upgrade(): + self._upgrading_to_64 = True + self._singleton.unlock() + logger.info("32bit monkey running on 64bit Windows. Upgrading.") + WindowsUpgrader.upgrade(self._opts) + raise PlannedShutdownException("Finished upgrading from 32bit to 64bit.") + + def _start_post_breach_async(self): logger.debug("Starting the post-breach phase asynchronously.") - self._post_breach_phase = Thread(target=self.start_post_breach_phase) + self._post_breach_phase = Thread(target=InfectionMonkey._start_post_breach_phase) self._post_breach_phase.start() - def teardown(self): - if self._monkey_tunnel: - self._monkey_tunnel.stop() - self._monkey_tunnel.join() - - if self._post_breach_phase: - self._post_breach_phase.join() - - if firewall.is_enabled(): - firewall.remove_firewall_rule() - firewall.close() - - self.master.terminate() - self.master.cleanup() - - def start_post_breach_phase(self): - self.collect_system_info_if_configured() + @staticmethod + def _start_post_breach_phase(): + InfectionMonkey._collect_system_info_if_configured() PostBreach().execute_all_configured() @staticmethod - def max_propagation_depth_reached(): - return 0 == WormConfiguration.depth - - def collect_system_info_if_configured(self): + def _collect_system_info_if_configured(): logger.debug("Calling for system info collection") try: system_info_collector = SystemInfoCollector() @@ -238,11 +236,32 @@ class InfectionMonkey(object): except Exception as e: logger.exception(f"Exception encountered during system info collection: {str(e)}") - def shutdown_by_not_alive_config(self): - if not WormConfiguration.alive: - raise PlannedShutdownException("Marked 'not alive' from configuration.") + def _start_propagation(self): + if not InfectionMonkey._max_propagation_depth_reached(): + logger.info("Starting the propagation phase.") + logger.debug("Running with depth: %d" % WormConfiguration.depth) + self._propagate() + else: + logger.info("Maximum propagation depth has been reached; monkey will not propagate.") + TraceTelem(MAX_DEPTH_REACHED_MESSAGE).send() - def propagate(self): + if self._keep_running and WormConfiguration.alive: + InfectionMonkey._run_ransomware() + + # if host was exploited, before continue to closing the tunnel ensure the exploited + # host had its chance to connect to the tunnel + if len(self._exploited_machines) > 0: + time_to_sleep = WormConfiguration.keep_tunnel_open_time + logger.info( + "Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep + ) + time.sleep(time_to_sleep) + + @staticmethod + def _max_propagation_depth_reached(): + return 0 == WormConfiguration.depth + + def _propagate(self): ControlClient.keepalive() ControlClient.load_control_config() @@ -304,7 +323,7 @@ class InfectionMonkey(object): ) host_exploited = False for exploiter in [exploiter(machine) for exploiter in self._exploiters]: - if self.try_exploiting(machine, exploiter): + if self._try_exploiting(machine, exploiter): host_exploited = True VictimHostTelem("T1210", ScanStatus.USED, machine=machine).send() if exploiter.RUNS_AGENT_ON_SUCCESS: @@ -319,35 +338,125 @@ class InfectionMonkey(object): if not WormConfiguration.alive: logger.info("Marked not alive from configuration") - def upgrade_to_64_if_needed(self): - if WindowsUpgrader.should_upgrade(): - self._upgrading_to_64 = True - self._singleton.unlock() - logger.info("32bit monkey running on 64bit Windows. Upgrading.") - WindowsUpgrader.upgrade(self._opts) - raise PlannedShutdownException("Finished upgrading from 32bit to 64bit.") + def _try_exploiting(self, machine, exploiter): + """ + Workflow of exploiting one machine with one exploiter + :param machine: Machine monkey tries to exploit + :param exploiter: Exploiter to use on that machine + :return: True if successfully exploited, False otherwise + """ + if not exploiter.is_os_supported(): + logger.info( + "Skipping exploiter %s host:%r, os %s is not supported", + exploiter.__class__.__name__, + machine, + machine.os, + ) + return False + + logger.info( + "Trying to exploit %r with exploiter %s...", machine, exploiter.__class__.__name__ + ) + + result = False + try: + result = exploiter.exploit_host() + if result: + self._successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS) + return True + else: + logger.info( + "Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__ + ) + except ExploitingVulnerableMachineError as exc: + logger.error( + "Exception while attacking %s using %s: %s", + machine, + exploiter.__class__.__name__, + exc, + ) + self._successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS) + return True + except FailedExploitationError as e: + logger.info( + "Failed exploiting %r with exploiter %s, %s", + machine, + exploiter.__class__.__name__, + e, + ) + except Exception as exc: + logger.exception( + "Exception while attacking %s using %s: %s", + machine, + exploiter.__class__.__name__, + exc, + ) + finally: + exploiter.send_exploit_telemetry(exploiter.__class__.__name__, result) + return False + + def _successfully_exploited(self, machine, exploiter, RUNS_AGENT_ON_SUCCESS=True): + """ + Workflow of registering successfully exploited machine + :param machine: machine that was exploited + :param exploiter: exploiter that succeeded + """ + if RUNS_AGENT_ON_SUCCESS: + self._exploited_machines.add(machine) + + logger.info("Successfully propagated to %s using %s", machine, exploiter.__class__.__name__) + + # check if max-exploitation limit is reached + if WormConfiguration.victims_max_exploit <= len(self._exploited_machines): + self._keep_running = False + + logger.info("Max exploited victims reached (%d)", WormConfiguration.victims_max_exploit) + + @staticmethod + def _run_ransomware(): + try: + ransomware_payload = build_ransomware_payload(WormConfiguration.ransomware) + ransomware_payload.run_payload() + except Exception as ex: + logger.error(f"An unexpected error occurred while running the ransomware payload: {ex}") + + def _teardown(self): + logger.info("Monkey teardown started") + if self._monkey_tunnel: + self._monkey_tunnel.stop() + self._monkey_tunnel.join() + + if self._post_breach_phase: + self._post_breach_phase.join() + + if firewall.is_enabled(): + firewall.remove_firewall_rule() + firewall.close() + + self.master.terminate() + self.master.cleanup() def cleanup(self): logger.info("Monkey cleanup started") self._keep_running = False if self._upgrading_to_64: - InfectionMonkey.close_tunnel() + InfectionMonkey._close_tunnel() firewall.close() else: StateTelem( is_done=True, version=get_version() ).send() # Signal the server (before closing the tunnel) - InfectionMonkey.close_tunnel() + InfectionMonkey._close_tunnel() firewall.close() - self.send_log() + InfectionMonkey._send_log() self._singleton.unlock() - InfectionMonkey.self_delete() + InfectionMonkey._self_delete() logger.info("Monkey is shutting down") @staticmethod - def close_tunnel(): + def _close_tunnel(): tunnel_address = ( ControlClient.proxies.get("https", "").replace("https://", "").split(":")[0] ) @@ -356,7 +465,18 @@ class InfectionMonkey(object): tunnel.quit_tunnel(tunnel_address) @staticmethod - def self_delete(): + def _send_log(): + monkey_log_path = get_monkey_log_path() + if os.path.exists(monkey_log_path): + with open(monkey_log_path, "r") as f: + log = f.read() + else: + log = "" + + ControlClient.send_log(log) + + @staticmethod + def _self_delete(): status = ScanStatus.USED if remove_monkey_dir() else ScanStatus.SCANNED T1107Telem(status, get_monkey_dir_path()).send() @@ -385,117 +505,3 @@ class InfectionMonkey(object): status = ScanStatus.SCANNED if status: T1107Telem(status, sys.executable).send() - - def send_log(self): - monkey_log_path = get_monkey_log_path() - if os.path.exists(monkey_log_path): - with open(monkey_log_path, "r") as f: - log = f.read() - else: - log = "" - - ControlClient.send_log(log) - - def try_exploiting(self, machine, exploiter): - """ - Workflow of exploiting one machine with one exploiter - :param machine: Machine monkey tries to exploit - :param exploiter: Exploiter to use on that machine - :return: True if successfully exploited, False otherwise - """ - if not exploiter.is_os_supported(): - logger.info( - "Skipping exploiter %s host:%r, os %s is not supported", - exploiter.__class__.__name__, - machine, - machine.os, - ) - return False - - logger.info( - "Trying to exploit %r with exploiter %s...", machine, exploiter.__class__.__name__ - ) - - result = False - try: - result = exploiter.exploit_host() - if result: - self.successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS) - return True - else: - logger.info( - "Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__ - ) - except ExploitingVulnerableMachineError as exc: - logger.error( - "Exception while attacking %s using %s: %s", - machine, - exploiter.__class__.__name__, - exc, - ) - self.successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS) - return True - except FailedExploitationError as e: - logger.info( - "Failed exploiting %r with exploiter %s, %s", - machine, - exploiter.__class__.__name__, - e, - ) - except Exception as exc: - logger.exception( - "Exception while attacking %s using %s: %s", - machine, - exploiter.__class__.__name__, - exc, - ) - finally: - exploiter.send_exploit_telemetry(result) - return False - - def successfully_exploited(self, machine, exploiter, RUNS_AGENT_ON_SUCCESS=True): - """ - Workflow of registering successfully exploited machine - :param machine: machine that was exploited - :param exploiter: exploiter that succeeded - """ - if RUNS_AGENT_ON_SUCCESS: - self._exploited_machines.add(machine) - - logger.info("Successfully propagated to %s using %s", machine, exploiter.__class__.__name__) - - # check if max-exploitation limit is reached - if WormConfiguration.victims_max_exploit <= len(self._exploited_machines): - self._keep_running = False - - logger.info("Max exploited victims reached (%d)", WormConfiguration.victims_max_exploit) - - def set_default_port(self): - try: - self._default_server_port = self._default_server.split(":")[1] - except KeyError: - self._default_server_port = "" - - def set_default_server(self): - """ - Sets the default server for the Monkey to communicate back to. - :raises PlannedShutdownException if couldn't find the server. - """ - if not ControlClient.find_server(default_tunnel=self._default_tunnel): - raise PlannedShutdownException( - "Monkey couldn't find server with {} default tunnel.".format(self._default_tunnel) - ) - self._default_server = WormConfiguration.current_server - logger.debug("default server set to: %s" % self._default_server) - - def log_arguments(self): - arg_string = " ".join([f"{key}: {value}" for key, value in vars(self._opts).items()]) - logger.info(f"Monkey started with arguments: {arg_string}") - - @staticmethod - def run_ransomware(): - try: - ransomware_payload = build_ransomware_payload(WormConfiguration.ransomware) - ransomware_payload.run_payload() - except Exception as ex: - logger.error(f"An unexpected error occurred while running the ransomware payload: {ex}") From 75226bdf6edf650cd374f89a9f6685fbd4371deb Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Fri, 26 Nov 2021 18:34:19 +0530 Subject: [PATCH 05/17] Agent: Comment out mock master things in monkey.py So that both 'masters' don't run at the same time. To test the mock master, un-comment the lines in this commit and comment the lines `self._start_post_breach_async()` and `self._start_propagation()` in `start()`. --- monkey/infection_monkey/monkey.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 88ca20b98..184a940bf 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -13,23 +13,26 @@ from common.version import get_version from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.master.mock_master import MockMaster + +# from infection_monkey.master.mock_master import MockMaster from infection_monkey.model import DELAY_DELETE_CMD from infection_monkey.network.firewall import app as firewall from infection_monkey.network.HostFinger import HostFinger from infection_monkey.network.network_scanner import NetworkScanner from infection_monkey.network.tools import get_interface_to_target, is_running_on_island from infection_monkey.post_breach.post_breach_handler import PostBreach -from infection_monkey.puppet.mock_puppet import MockPuppet + +# from infection_monkey.puppet.mock_puppet import MockPuppet from infection_monkey.ransomware.ransomware_payload_builder import build_ransomware_payload from infection_monkey.system_info import SystemInfoCollector from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from infection_monkey.telemetry.attack.t1107_telem import T1107Telem from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem -from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( - LegacyTelemetryMessengerAdapter, -) + +# from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( +# LegacyTelemetryMessengerAdapter, +# ) from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem @@ -43,7 +46,8 @@ from infection_monkey.utils.monkey_dir import ( remove_monkey_dir, ) from infection_monkey.utils.monkey_log_path import get_monkey_log_path -from infection_monkey.utils.signal_handler import register_signal_handlers + +# from infection_monkey.utils.signal_handler import register_signal_handlers from infection_monkey.windows_upgrader import WindowsUpgrader MAX_DEPTH_REACHED_MESSAGE = "Reached max depth, skipping propagation phase." @@ -54,7 +58,7 @@ logger = logging.getLogger(__name__) class InfectionMonkey(object): def __init__(self, args): - self.master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) + # self.master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) self._keep_running = False self._exploited_machines = set() self._fail_exploitation_machines = set() @@ -128,6 +132,8 @@ class InfectionMonkey(object): # Start propagation phase self._start_propagation() + # self.master.start() + except PlannedShutdownException: logger.info( "A planned shutdown of the Monkey occurred. Logging the reason and finishing " @@ -181,9 +187,7 @@ class InfectionMonkey(object): StateTelem(is_done=False, version=get_version()).send() TunnelTelem().send() - register_signal_handlers(self.master) - - self.master.start() + # register_signal_handlers(self.master) @staticmethod def _shutdown_by_not_alive_config(): @@ -433,8 +437,8 @@ class InfectionMonkey(object): firewall.remove_firewall_rule() firewall.close() - self.master.terminate() - self.master.cleanup() + # self.master.terminate() + # self.master.cleanup() def cleanup(self): logger.info("Monkey cleanup started") From 72f4fc1ef6aa7d5ce2f9df45efb786a2c52f1759 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 29 Nov 2021 18:34:25 +0100 Subject: [PATCH 06/17] Agent: Remove intialize both from monkey and dropper Add legacy start and cleanup to the agent which are the same code reformated in the previous commits. Reformat start function. --- monkey/infection_monkey/dropper.py | 5 +- monkey/infection_monkey/main.py | 8 +- monkey/infection_monkey/master/mock_master.py | 4 + monkey/infection_monkey/monkey.py | 263 ++++++++++++------ 4 files changed, 192 insertions(+), 88 deletions(-) diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py index f74767cef..30be3798e 100644 --- a/monkey/infection_monkey/dropper.py +++ b/monkey/infection_monkey/dropper.py @@ -55,10 +55,9 @@ class MonkeyDrops(object): "destination_path": self.opts.location, } - def initialize(self): logger.debug("Dropper is running with config:\n%s", pprint.pformat(self._config)) - def start(self): + def legacy_start(self): if self._config["destination_path"] is None: logger.error("No destination path specified") return False @@ -183,7 +182,7 @@ class MonkeyDrops(object): if monkey_process.poll() is not None: logger.warning("Seems like monkey died too soon") - def cleanup(self): + def legacy_cleanup(self): logger.info("Cleaning up the dropper") try: diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index bb08f4b4f..fac31bf78 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -118,16 +118,16 @@ def main(): logger.info(f"version: {get_version()}") monkey = monkey_cls(monkey_args) - monkey.initialize() try: - monkey.start() - + monkey.legacy_start() + # monkey.start() return True except Exception as e: logger.exception("Exception thrown from monkey's start function. More info: {}".format(e)) finally: - monkey.cleanup() + monkey.legacy_cleanup() + # monkey.cleanup() if "__main__" == __name__: diff --git a/monkey/infection_monkey/master/mock_master.py b/monkey/infection_monkey/master/mock_master.py index 41d478b93..e78519a43 100644 --- a/monkey/infection_monkey/master/mock_master.py +++ b/monkey/infection_monkey/master/mock_master.py @@ -46,6 +46,9 @@ class MockMaster(IMaster): logger.info("Finished running system info collectors") def _run_pbas(self): + + # TODO: Create monkey_dir and revise setup in monkey.py + logger.info("Running post breach actions") name = "AccountDiscovery" command, result = self._puppet.run_pba(name, {}) @@ -123,4 +126,5 @@ class MockMaster(IMaster): logger.info("Terminating MockMaster") def cleanup(self) -> None: + # TODO: Cleanup monkey_dir and send telemetry pass diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 184a940bf..16a948d75 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -13,26 +13,23 @@ from common.version import get_version from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient from infection_monkey.exploit.HostExploiter import HostExploiter - -# from infection_monkey.master.mock_master import MockMaster +from infection_monkey.master.mock_master import MockMaster from infection_monkey.model import DELAY_DELETE_CMD from infection_monkey.network.firewall import app as firewall from infection_monkey.network.HostFinger import HostFinger from infection_monkey.network.network_scanner import NetworkScanner from infection_monkey.network.tools import get_interface_to_target, is_running_on_island from infection_monkey.post_breach.post_breach_handler import PostBreach - -# from infection_monkey.puppet.mock_puppet import MockPuppet +from infection_monkey.puppet.mock_puppet import MockPuppet from infection_monkey.ransomware.ransomware_payload_builder import build_ransomware_payload from infection_monkey.system_info import SystemInfoCollector from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from infection_monkey.telemetry.attack.t1107_telem import T1107Telem from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem - -# from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( -# LegacyTelemetryMessengerAdapter, -# ) +from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( + LegacyTelemetryMessengerAdapter, +) from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.system_info_telem import SystemInfoTelem @@ -46,8 +43,7 @@ from infection_monkey.utils.monkey_dir import ( remove_monkey_dir, ) from infection_monkey.utils.monkey_log_path import get_monkey_log_path - -# from infection_monkey.utils.signal_handler import register_signal_handlers +from infection_monkey.utils.signal_handler import register_signal_handlers from infection_monkey.windows_upgrader import WindowsUpgrader MAX_DEPTH_REACHED_MESSAGE = "Reached max depth, skipping propagation phase." @@ -58,51 +54,49 @@ logger = logging.getLogger(__name__) class InfectionMonkey(object): def __init__(self, args): - # self.master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) + logger.info("Monkey is initializing...") + self.master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) self._keep_running = False self._exploited_machines = set() self._fail_exploitation_machines = set() self._singleton = SystemSingleton() - self._parent = None - self._default_tunnel = None - self._args = args - self._network = None + self._opts = None + self._set_arguments(args) + self._parent = self._opts.parent + self._default_tunnel = self._opts.tunnel + self._default_server = self._opts.server + self._set_propagation_depth() + self._add_default_server_to_config() + self._network = NetworkScanner() self._exploiters = None self._fingerprint = None - self._default_server = None self._default_server_port = None - self._opts = None self._upgrading_to_64 = False self._monkey_tunnel = None self._post_breach_phase = None - def initialize(self): - logger.info("Monkey is initializing...") - - self._check_for_running_monkey() - + def _set_arguments(self, args): arg_parser = argparse.ArgumentParser() arg_parser.add_argument("-p", "--parent") arg_parser.add_argument("-t", "--tunnel") arg_parser.add_argument("-s", "--server") arg_parser.add_argument("-d", "--depth", type=int) arg_parser.add_argument("-vp", "--vulnerable-port") - self._opts, self._args = arg_parser.parse_known_args(self._args) + self._opts, _ = arg_parser.parse_known_args(args) self._log_arguments() - self._parent = self._opts.parent - self._default_tunnel = self._opts.tunnel - self._default_server = self._opts.server + def _log_arguments(self): + arg_string = " ".join([f"{key}: {value}" for key, value in vars(self._opts).items()]) + logger.info(f"Monkey started with arguments: {arg_string}") + def _set_propagation_depth(self): if self._opts.depth is not None: WormConfiguration._depth_from_commandline = True WormConfiguration.depth = self._opts.depth logger.debug("Setting propagation depth from command line") logger.debug(f"Set propagation depth to {WormConfiguration.depth}") - self._keep_running = True - self._network = NetworkScanner() - + def _add_default_server_to_config(self): if self._default_server: if self._default_server not in WormConfiguration.command_servers: logger.debug("Added default server: %s" % self._default_server) @@ -112,28 +106,38 @@ class InfectionMonkey(object): "Default server: %s is already in command servers list" % self._default_server ) - def _check_for_running_monkey(self): - if not self._singleton.try_lock(): - raise Exception("Another instance of the monkey is already running") - - def _log_arguments(self): - arg_string = " ".join([f"{key}: {value}" for key, value in vars(self._opts).items()]) - logger.info(f"Monkey started with arguments: {arg_string}") - def start(self): + if not self._is_another_monkey_running(): + + logger.info("Monkey is starting...") + + self._connect_to_island() + + if InfectionMonkey._is_monkey_alive_by_config(): + logger.error("Monkey marked 'not alive' from configuration.") + return + + if InfectionMonkey._is_upgrade_to_64_needed(): + self._upgrade_to_64() + return + + self._setup() + self.master.start() + else: + logger.info("Another instance of the monkey is already running") + + def legacy_start(self): + if self._is_another_monkey_running(): + raise Exception("Another instance of the monkey is already running") try: logger.info("Monkey is starting...") - # Sets the monkey up - self._setup() - # Start post breach phase + self._legacy_setup() + self._start_post_breach_async() - # Start propagation phase self._start_propagation() - # self.master.start() - except PlannedShutdownException: logger.info( "A planned shutdown of the Monkey occurred. Logging the reason and finishing " @@ -141,28 +145,103 @@ class InfectionMonkey(object): ) logger.exception("Planned shutdown, reason:") - finally: - self._teardown() - - def _setup(self): - logger.debug("Starting the setup phase.") - - InfectionMonkey._shutdown_by_not_alive_config() - + def _connect_to_island(self): # Sets island's IP and port for monkey to communicate to - self._set_default_server() + if not self._is_default_server_set(): + raise Exception( + "Monkey couldn't find server with {} default tunnel.".format(self._default_tunnel) + ) self._set_default_port() - # Create a dir for monkey files if there isn't one - create_monkey_dir() - ControlClient.wakeup(parent=self._parent) ControlClient.load_control_config() - if ControlClient.check_for_stop(): - raise PlannedShutdownException("Monkey has been marked for shutdown.") + def _is_default_server_set(self) -> bool: + """ + Sets the default server for the Monkey to communicate back to. + :return + """ + if not ControlClient.find_server(default_tunnel=self._default_tunnel): + return False + self._default_server = WormConfiguration.current_server + logger.debug("default server set to: %s" % self._default_server) + return True - self._upgrade_to_64_if_needed() + @staticmethod + def _is_monkey_alive_by_config(): + return not WormConfiguration.alive + + @staticmethod + def _is_upgrade_to_64_needed(): + return WindowsUpgrader.should_upgrade() + + def _upgrade_to_64(self): + self._upgrading_to_64 = True + self._singleton.unlock() + logger.info("32bit monkey running on 64bit Windows. Upgrading.") + WindowsUpgrader.upgrade(self._opts) + logger.info("Finished upgrading from 32bit to 64bit.") + + def _legacy_upgrade_to_64_if_needed(self): + if WindowsUpgrader.should_upgrade(): + self._upgrading_to_64 = True + self._singleton.unlock() + logger.info("32bit monkey running on 64bit Windows. Upgrading.") + WindowsUpgrader.upgrade(self._opts) + raise PlannedShutdownException("Finished upgrading from 32bit to 64bit.") + + def _setup(self): + logger.debug("Starting the setup phase.") + + # Create a dir for monkey files if there isn't one + create_monkey_dir() + + # TODO: Evaluate should we run this check + # if not ControlClient.should_monkey_run(self._opts.vulnerable_port): + # logger.error("Monkey shouldn't run on current machine " + # "(it will be exploited later with more depth).") + # return False + + # Singleton should handle sending this information + if is_windows_os(): + T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() + + if is_running_on_island(): + WormConfiguration.started_on_island = True + ControlClient.report_start_on_island() + + if firewall.is_enabled(): + firewall.add_firewall_rule() + + self._monkey_tunnel = ControlClient.create_control_tunnel() + if self._monkey_tunnel: + self._monkey_tunnel.start() + + StateTelem(is_done=False, version=get_version()).send() + TunnelTelem().send() + + register_signal_handlers(self.master) + + return True + + def _legacy_setup(self): + logger.debug("Starting the setup phase.") + + self._keep_running = True + + # Create a dir for monkey files if there isn't one + create_monkey_dir() + + # Sets island's IP and port for monkey to communicate to + self._legacy_set_default_server() + self._set_default_port() + + ControlClient.wakeup(parent=self._parent) + ControlClient.load_control_config() + + InfectionMonkey._legacy_shutdown_by_not_alive_config() + + self._legacy_upgrade_to_64_if_needed() if not ControlClient.should_monkey_run(self._opts.vulnerable_port): raise PlannedShutdownException( @@ -187,14 +266,10 @@ class InfectionMonkey(object): StateTelem(is_done=False, version=get_version()).send() TunnelTelem().send() - # register_signal_handlers(self.master) + def _is_another_monkey_running(self): + return not self._singleton.try_lock() - @staticmethod - def _shutdown_by_not_alive_config(): - if not WormConfiguration.alive: - raise PlannedShutdownException("Marked 'not alive' from configuration.") - - def _set_default_server(self): + def _legacy_set_default_server(self): """ Sets the default server for the Monkey to communicate back to. :raises PlannedShutdownException if couldn't find the server. @@ -212,13 +287,10 @@ class InfectionMonkey(object): except KeyError: self._default_server_port = "" - def _upgrade_to_64_if_needed(self): - if WindowsUpgrader.should_upgrade(): - self._upgrading_to_64 = True - self._singleton.unlock() - logger.info("32bit monkey running on 64bit Windows. Upgrading.") - WindowsUpgrader.upgrade(self._opts) - raise PlannedShutdownException("Finished upgrading from 32bit to 64bit.") + @staticmethod + def _legacy_shutdown_by_not_alive_config(): + if not WormConfiguration.alive: + raise PlannedShutdownException("Marked 'not alive' from configuration.") def _start_post_breach_async(self): logger.debug("Starting the post-breach phase asynchronously.") @@ -424,8 +496,9 @@ class InfectionMonkey(object): except Exception as ex: logger.error(f"An unexpected error occurred while running the ransomware payload: {ex}") - def _teardown(self): - logger.info("Monkey teardown started") + def legacy_cleanup(self): + logger.info("Monkey cleanup started") + self._keep_running = False if self._monkey_tunnel: self._monkey_tunnel.stop() self._monkey_tunnel.join() @@ -437,13 +510,6 @@ class InfectionMonkey(object): firewall.remove_firewall_rule() firewall.close() - # self.master.terminate() - # self.master.cleanup() - - def cleanup(self): - logger.info("Monkey cleanup started") - self._keep_running = False - if self._upgrading_to_64: InfectionMonkey._close_tunnel() firewall.close() @@ -456,6 +522,41 @@ class InfectionMonkey(object): InfectionMonkey._send_log() self._singleton.unlock() + # self.master.terminate() + # self.master.cleanup() + + InfectionMonkey._self_delete() + logger.info("Monkey is shutting down") + + def cleanup(self): + logger.info("Monkey cleanup started") + self._keep_running = False + if self._monkey_tunnel: + self._monkey_tunnel.stop() + self._monkey_tunnel.join() + + if self._post_breach_phase: + self._post_breach_phase.join() + + if firewall.is_enabled(): + firewall.remove_firewall_rule() + firewall.close() + + if self._upgrading_to_64: + InfectionMonkey._close_tunnel() + firewall.close() + else: + StateTelem( + is_done=True, version=get_version() + ).send() # Signal the server (before closing the tunnel) + InfectionMonkey._close_tunnel() + firewall.close() + InfectionMonkey._send_log() + self._singleton.unlock() + + self.master.terminate() + self.master.cleanup() + InfectionMonkey._self_delete() logger.info("Monkey is shutting down") From f8441f2d7f6e37775dbe200994c525954f703fb3 Mon Sep 17 00:00:00 2001 From: Ilija Lazoroski Date: Mon, 29 Nov 2021 19:57:25 +0100 Subject: [PATCH 07/17] Agent: Refactor the new start and cleanup function --- monkey/infection_monkey/monkey.py | 117 +++++++++++++++--------------- 1 file changed, 57 insertions(+), 60 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 16a948d75..bfbf270dc 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -55,7 +55,7 @@ logger = logging.getLogger(__name__) class InfectionMonkey(object): def __init__(self, args): logger.info("Monkey is initializing...") - self.master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) + self._master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) self._keep_running = False self._exploited_machines = set() self._fail_exploitation_machines = set() @@ -107,24 +107,28 @@ class InfectionMonkey(object): ) def start(self): - if not self._is_another_monkey_running(): - - logger.info("Monkey is starting...") - - self._connect_to_island() - - if InfectionMonkey._is_monkey_alive_by_config(): - logger.error("Monkey marked 'not alive' from configuration.") - return - - if InfectionMonkey._is_upgrade_to_64_needed(): - self._upgrade_to_64() - return - - self._setup() - self.master.start() - else: + if self._is_another_monkey_running(): logger.info("Another instance of the monkey is already running") + return + + logger.info("Monkey is starting...") + + self._connect_to_island() + + # TODO: Reevaluate who is responsible to send this information + if is_windows_os(): + T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() + + if InfectionMonkey._is_monkey_alive_by_config(): + logger.error("Monkey marked 'not alive' from configuration.") + return + + if InfectionMonkey._is_upgrade_to_64_needed(): + self._upgrade_to_64() + return + + self._setup() + self._master.start() def legacy_start(self): if self._is_another_monkey_running(): @@ -193,8 +197,10 @@ class InfectionMonkey(object): def _setup(self): logger.debug("Starting the setup phase.") - # Create a dir for monkey files if there isn't one - create_monkey_dir() + if is_running_on_island(): + # TODO: Evaluate also this with ControlClient.should_monkey_run + # WormConfiguration.started_on_island = True + ControlClient.report_start_on_island() # TODO: Evaluate should we run this check # if not ControlClient.should_monkey_run(self._opts.vulnerable_port): @@ -202,14 +208,6 @@ class InfectionMonkey(object): # "(it will be exploited later with more depth).") # return False - # Singleton should handle sending this information - if is_windows_os(): - T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() - - if is_running_on_island(): - WormConfiguration.started_on_island = True - ControlClient.report_start_on_island() - if firewall.is_enabled(): firewall.add_firewall_rule() @@ -220,9 +218,7 @@ class InfectionMonkey(object): StateTelem(is_done=False, version=get_version()).send() TunnelTelem().send() - register_signal_handlers(self.master) - - return True + register_signal_handlers(self._master) def _legacy_setup(self): logger.debug("Starting the setup phase.") @@ -239,23 +235,23 @@ class InfectionMonkey(object): ControlClient.wakeup(parent=self._parent) ControlClient.load_control_config() + if is_windows_os(): + T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() + InfectionMonkey._legacy_shutdown_by_not_alive_config() self._legacy_upgrade_to_64_if_needed() + if is_running_on_island(): + WormConfiguration.started_on_island = True + ControlClient.report_start_on_island() + if not ControlClient.should_monkey_run(self._opts.vulnerable_port): raise PlannedShutdownException( "Monkey shouldn't run on current machine " "(it will be exploited later with more depth)." ) - if is_windows_os(): - T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() - - if is_running_on_island(): - WormConfiguration.started_on_island = True - ControlClient.report_start_on_island() - if firewall.is_enabled(): firewall.add_firewall_rule() @@ -522,42 +518,43 @@ class InfectionMonkey(object): InfectionMonkey._send_log() self._singleton.unlock() - # self.master.terminate() - # self.master.cleanup() - InfectionMonkey._self_delete() logger.info("Monkey is shutting down") def cleanup(self): logger.info("Monkey cleanup started") - self._keep_running = False - if self._monkey_tunnel: - self._monkey_tunnel.stop() - self._monkey_tunnel.join() + try: + if self._is_upgrade_to_64_needed(): + logger.debug("Detected upgrade to 64bit") + return - if self._post_breach_phase: - self._post_breach_phase.join() + if self._master: + self._master.cleanup() - if firewall.is_enabled(): - firewall.remove_firewall_rule() - firewall.close() + if self._monkey_tunnel: + self._monkey_tunnel.stop() + self._monkey_tunnel.join() + + if firewall.is_enabled(): + firewall.remove_firewall_rule() + firewall.close() + + InfectionMonkey._self_delete() + + InfectionMonkey._send_log() - if self._upgrading_to_64: - InfectionMonkey._close_tunnel() - firewall.close() - else: StateTelem( is_done=True, version=get_version() ).send() # Signal the server (before closing the tunnel) + + # TODO: Determine how long between when we + # send telemetry and the monkey actually exits InfectionMonkey._close_tunnel() - firewall.close() - InfectionMonkey._send_log() self._singleton.unlock() + except Exception as e: + logger.error(f"An error occurred while cleaning up the monkey agent: {e}") + InfectionMonkey._self_delete() - self.master.terminate() - self.master.cleanup() - - InfectionMonkey._self_delete() logger.info("Monkey is shutting down") @staticmethod From 89436a4cd9d89355ccf685371ed1f3e57c14c63d Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 1 Dec 2021 12:11:38 +0200 Subject: [PATCH 08/17] Agent: remove behavioral methods from monkey.py and leave only setup/teardown related code Behavior is handled by master, monkey.py should only setup/teardown the agent --- monkey/infection_monkey/monkey.py | 381 +----------------- .../exceptions/planned_shutdown_exception.py | 2 - 2 files changed, 21 insertions(+), 362 deletions(-) delete mode 100644 monkey/infection_monkey/utils/exceptions/planned_shutdown_exception.py diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index bfbf270dc..9b36ba59e 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -3,77 +3,53 @@ import logging import os import subprocess import sys -import time -from threading import Thread import infection_monkey.tunnel as tunnel from common.utils.attack_utils import ScanStatus, UsageEnum -from common.utils.exceptions import ExploitingVulnerableMachineError, FailedExploitationError from common.version import get_version from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient -from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.master.mock_master import MockMaster from infection_monkey.model import DELAY_DELETE_CMD from infection_monkey.network.firewall import app as firewall -from infection_monkey.network.HostFinger import HostFinger -from infection_monkey.network.network_scanner import NetworkScanner -from infection_monkey.network.tools import get_interface_to_target, is_running_on_island -from infection_monkey.post_breach.post_breach_handler import PostBreach +from infection_monkey.network.tools import is_running_on_island from infection_monkey.puppet.mock_puppet import MockPuppet -from infection_monkey.ransomware.ransomware_payload_builder import build_ransomware_payload -from infection_monkey.system_info import SystemInfoCollector from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from infection_monkey.telemetry.attack.t1107_telem import T1107Telem -from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( LegacyTelemetryMessengerAdapter, ) -from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.telemetry.state_telem import StateTelem -from infection_monkey.telemetry.system_info_telem import SystemInfoTelem -from infection_monkey.telemetry.trace_telem import TraceTelem from infection_monkey.telemetry.tunnel_telem import TunnelTelem from infection_monkey.utils.environment import is_windows_os -from infection_monkey.utils.exceptions.planned_shutdown_exception import PlannedShutdownException -from infection_monkey.utils.monkey_dir import ( - create_monkey_dir, - get_monkey_dir_path, - remove_monkey_dir, -) +from infection_monkey.utils.monkey_dir import get_monkey_dir_path, remove_monkey_dir from infection_monkey.utils.monkey_log_path import get_monkey_log_path from infection_monkey.utils.signal_handler import register_signal_handlers from infection_monkey.windows_upgrader import WindowsUpgrader -MAX_DEPTH_REACHED_MESSAGE = "Reached max depth, skipping propagation phase." - - logger = logging.getLogger(__name__) -class InfectionMonkey(object): +class PlannedShutdownError(Exception): + # Raise when we deliberately want to shut down the agent + pass + + +class InfectionMonkey: def __init__(self, args): logger.info("Monkey is initializing...") self._master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) - self._keep_running = False - self._exploited_machines = set() - self._fail_exploitation_machines = set() self._singleton = SystemSingleton() self._opts = None self._set_arguments(args) self._parent = self._opts.parent self._default_tunnel = self._opts.tunnel self._default_server = self._opts.server + self._default_server_port = None self._set_propagation_depth() self._add_default_server_to_config() - self._network = NetworkScanner() - self._exploiters = None - self._fingerprint = None - self._default_server_port = None - self._upgrading_to_64 = False self._monkey_tunnel = None - self._post_breach_phase = None def _set_arguments(self, args): arg_parser = argparse.ArgumentParser() @@ -108,8 +84,7 @@ class InfectionMonkey(object): def start(self): if self._is_another_monkey_running(): - logger.info("Another instance of the monkey is already running") - return + raise PlannedShutdownError("Another instance of the monkey is already running.") logger.info("Monkey is starting...") @@ -120,35 +95,15 @@ class InfectionMonkey(object): T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() if InfectionMonkey._is_monkey_alive_by_config(): - logger.error("Monkey marked 'not alive' from configuration.") - return + raise PlannedShutdownError("Monkey marked 'not alive' from configuration.") if InfectionMonkey._is_upgrade_to_64_needed(): self._upgrade_to_64() - return + raise PlannedShutdownError("32 bit Agent can't run on 64 bit system.") self._setup() self._master.start() - def legacy_start(self): - if self._is_another_monkey_running(): - raise Exception("Another instance of the monkey is already running") - try: - logger.info("Monkey is starting...") - - self._legacy_setup() - - self._start_post_breach_async() - - self._start_propagation() - - except PlannedShutdownException: - logger.info( - "A planned shutdown of the Monkey occurred. Logging the reason and finishing " - "execution." - ) - logger.exception("Planned shutdown, reason:") - def _connect_to_island(self): # Sets island's IP and port for monkey to communicate to if not self._is_default_server_set(): @@ -180,33 +135,15 @@ class InfectionMonkey(object): return WindowsUpgrader.should_upgrade() def _upgrade_to_64(self): - self._upgrading_to_64 = True self._singleton.unlock() logger.info("32bit monkey running on 64bit Windows. Upgrading.") WindowsUpgrader.upgrade(self._opts) logger.info("Finished upgrading from 32bit to 64bit.") - def _legacy_upgrade_to_64_if_needed(self): - if WindowsUpgrader.should_upgrade(): - self._upgrading_to_64 = True - self._singleton.unlock() - logger.info("32bit monkey running on 64bit Windows. Upgrading.") - WindowsUpgrader.upgrade(self._opts) - raise PlannedShutdownException("Finished upgrading from 32bit to 64bit.") - def _setup(self): logger.debug("Starting the setup phase.") - if is_running_on_island(): - # TODO: Evaluate also this with ControlClient.should_monkey_run - # WormConfiguration.started_on_island = True - ControlClient.report_start_on_island() - - # TODO: Evaluate should we run this check - # if not ControlClient.should_monkey_run(self._opts.vulnerable_port): - # logger.error("Monkey shouldn't run on current machine " - # "(it will be exploited later with more depth).") - # return False + self._should_run_check_for_performance() if firewall.is_enabled(): firewall.add_firewall_rule() @@ -220,307 +157,31 @@ class InfectionMonkey(object): register_signal_handlers(self._master) - def _legacy_setup(self): - logger.debug("Starting the setup phase.") - - self._keep_running = True - - # Create a dir for monkey files if there isn't one - create_monkey_dir() - - # Sets island's IP and port for monkey to communicate to - self._legacy_set_default_server() - self._set_default_port() - - ControlClient.wakeup(parent=self._parent) - ControlClient.load_control_config() - - if is_windows_os(): - T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() - - InfectionMonkey._legacy_shutdown_by_not_alive_config() - - self._legacy_upgrade_to_64_if_needed() - + def _should_run_check_for_performance(self): + """ + This method implements propagation performance enhancing algorithm that + kicks in if the run was started from the Island. + Should get replaced by other, better performance enhancement solutions + """ if is_running_on_island(): WormConfiguration.started_on_island = True ControlClient.report_start_on_island() if not ControlClient.should_monkey_run(self._opts.vulnerable_port): - raise PlannedShutdownException( - "Monkey shouldn't run on current machine " + raise PlannedShutdownError( + "Monkey shouldn't run on current machine to improve perfomance" "(it will be exploited later with more depth)." ) - if firewall.is_enabled(): - firewall.add_firewall_rule() - - self._monkey_tunnel = ControlClient.create_control_tunnel() - if self._monkey_tunnel: - self._monkey_tunnel.start() - - StateTelem(is_done=False, version=get_version()).send() - TunnelTelem().send() - def _is_another_monkey_running(self): return not self._singleton.try_lock() - def _legacy_set_default_server(self): - """ - Sets the default server for the Monkey to communicate back to. - :raises PlannedShutdownException if couldn't find the server. - """ - if not ControlClient.find_server(default_tunnel=self._default_tunnel): - raise PlannedShutdownException( - "Monkey couldn't find server with {} default tunnel.".format(self._default_tunnel) - ) - self._default_server = WormConfiguration.current_server - logger.debug("default server set to: %s" % self._default_server) - def _set_default_port(self): try: self._default_server_port = self._default_server.split(":")[1] except KeyError: self._default_server_port = "" - @staticmethod - def _legacy_shutdown_by_not_alive_config(): - if not WormConfiguration.alive: - raise PlannedShutdownException("Marked 'not alive' from configuration.") - - def _start_post_breach_async(self): - logger.debug("Starting the post-breach phase asynchronously.") - self._post_breach_phase = Thread(target=InfectionMonkey._start_post_breach_phase) - self._post_breach_phase.start() - - @staticmethod - def _start_post_breach_phase(): - InfectionMonkey._collect_system_info_if_configured() - PostBreach().execute_all_configured() - - @staticmethod - def _collect_system_info_if_configured(): - logger.debug("Calling for system info collection") - try: - system_info_collector = SystemInfoCollector() - system_info = system_info_collector.get_info() - SystemInfoTelem(system_info).send() - except Exception as e: - logger.exception(f"Exception encountered during system info collection: {str(e)}") - - def _start_propagation(self): - if not InfectionMonkey._max_propagation_depth_reached(): - logger.info("Starting the propagation phase.") - logger.debug("Running with depth: %d" % WormConfiguration.depth) - self._propagate() - else: - logger.info("Maximum propagation depth has been reached; monkey will not propagate.") - TraceTelem(MAX_DEPTH_REACHED_MESSAGE).send() - - if self._keep_running and WormConfiguration.alive: - InfectionMonkey._run_ransomware() - - # if host was exploited, before continue to closing the tunnel ensure the exploited - # host had its chance to connect to the tunnel - if len(self._exploited_machines) > 0: - time_to_sleep = WormConfiguration.keep_tunnel_open_time - logger.info( - "Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep - ) - time.sleep(time_to_sleep) - - @staticmethod - def _max_propagation_depth_reached(): - return 0 == WormConfiguration.depth - - def _propagate(self): - ControlClient.keepalive() - ControlClient.load_control_config() - - self._network.initialize() - - self._fingerprint = HostFinger.get_instances() - - self._exploiters = HostExploiter.get_classes() - - if not WormConfiguration.alive: - logger.info("Marked not alive from configuration") - - machines = self._network.get_victim_machines( - max_find=WormConfiguration.victims_max_find, - stop_callback=ControlClient.check_for_stop, - ) - for machine in machines: - if ControlClient.check_for_stop(): - break - - for finger in self._fingerprint: - logger.info( - "Trying to get OS fingerprint from %r with module %s", - machine, - finger.__class__.__name__, - ) - try: - finger.get_host_fingerprint(machine) - except BaseException as exc: - logger.error( - "Failed to run fingerprinter %s, exception %s" % finger.__class__.__name__, - str(exc), - ) - - ScanTelem(machine).send() - - # skip machines that we've already exploited - if machine in self._exploited_machines: - logger.debug("Skipping %r - already exploited", machine) - continue - - if self._monkey_tunnel: - self._monkey_tunnel.set_tunnel_for_host(machine) - if self._default_server: - if self._network.on_island(self._default_server): - machine.set_default_server( - get_interface_to_target(machine.ip_addr) - + (":" + self._default_server_port if self._default_server_port else "") - ) - else: - machine.set_default_server(self._default_server) - logger.debug( - "Default server for machine: %r set to %s" % (machine, machine.default_server) - ) - - # Order exploits according to their type - self._exploiters = sorted( - self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value - ) - host_exploited = False - for exploiter in [exploiter(machine) for exploiter in self._exploiters]: - if self._try_exploiting(machine, exploiter): - host_exploited = True - VictimHostTelem("T1210", ScanStatus.USED, machine=machine).send() - if exploiter.RUNS_AGENT_ON_SUCCESS: - break # if adding machine to exploited, won't try other exploits - # on it - if not host_exploited: - self._fail_exploitation_machines.add(machine) - VictimHostTelem("T1210", ScanStatus.SCANNED, machine=machine).send() - if not self._keep_running: - break - - if not WormConfiguration.alive: - logger.info("Marked not alive from configuration") - - def _try_exploiting(self, machine, exploiter): - """ - Workflow of exploiting one machine with one exploiter - :param machine: Machine monkey tries to exploit - :param exploiter: Exploiter to use on that machine - :return: True if successfully exploited, False otherwise - """ - if not exploiter.is_os_supported(): - logger.info( - "Skipping exploiter %s host:%r, os %s is not supported", - exploiter.__class__.__name__, - machine, - machine.os, - ) - return False - - logger.info( - "Trying to exploit %r with exploiter %s...", machine, exploiter.__class__.__name__ - ) - - result = False - try: - result = exploiter.exploit_host() - if result: - self._successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS) - return True - else: - logger.info( - "Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__ - ) - except ExploitingVulnerableMachineError as exc: - logger.error( - "Exception while attacking %s using %s: %s", - machine, - exploiter.__class__.__name__, - exc, - ) - self._successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS) - return True - except FailedExploitationError as e: - logger.info( - "Failed exploiting %r with exploiter %s, %s", - machine, - exploiter.__class__.__name__, - e, - ) - except Exception as exc: - logger.exception( - "Exception while attacking %s using %s: %s", - machine, - exploiter.__class__.__name__, - exc, - ) - finally: - exploiter.send_exploit_telemetry(exploiter.__class__.__name__, result) - return False - - def _successfully_exploited(self, machine, exploiter, RUNS_AGENT_ON_SUCCESS=True): - """ - Workflow of registering successfully exploited machine - :param machine: machine that was exploited - :param exploiter: exploiter that succeeded - """ - if RUNS_AGENT_ON_SUCCESS: - self._exploited_machines.add(machine) - - logger.info("Successfully propagated to %s using %s", machine, exploiter.__class__.__name__) - - # check if max-exploitation limit is reached - if WormConfiguration.victims_max_exploit <= len(self._exploited_machines): - self._keep_running = False - - logger.info("Max exploited victims reached (%d)", WormConfiguration.victims_max_exploit) - - @staticmethod - def _run_ransomware(): - try: - ransomware_payload = build_ransomware_payload(WormConfiguration.ransomware) - ransomware_payload.run_payload() - except Exception as ex: - logger.error(f"An unexpected error occurred while running the ransomware payload: {ex}") - - def legacy_cleanup(self): - logger.info("Monkey cleanup started") - self._keep_running = False - if self._monkey_tunnel: - self._monkey_tunnel.stop() - self._monkey_tunnel.join() - - if self._post_breach_phase: - self._post_breach_phase.join() - - if firewall.is_enabled(): - firewall.remove_firewall_rule() - firewall.close() - - if self._upgrading_to_64: - InfectionMonkey._close_tunnel() - firewall.close() - else: - StateTelem( - is_done=True, version=get_version() - ).send() # Signal the server (before closing the tunnel) - InfectionMonkey._close_tunnel() - firewall.close() - InfectionMonkey._send_log() - self._singleton.unlock() - - InfectionMonkey._self_delete() - logger.info("Monkey is shutting down") - def cleanup(self): logger.info("Monkey cleanup started") try: diff --git a/monkey/infection_monkey/utils/exceptions/planned_shutdown_exception.py b/monkey/infection_monkey/utils/exceptions/planned_shutdown_exception.py deleted file mode 100644 index f0147e1e5..000000000 --- a/monkey/infection_monkey/utils/exceptions/planned_shutdown_exception.py +++ /dev/null @@ -1,2 +0,0 @@ -class PlannedShutdownException(Exception): - pass From 0806afed1ad6bfb7d4b451a1c9cf67bb4272b270 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 1 Dec 2021 12:49:11 +0200 Subject: [PATCH 09/17] Agent: rename PlannedShutdownException to PlannedShutdownError This will stay consistent with python and our own codebase --- monkey/infection_monkey/monkey.py | 1 + .../utils/exceptions/planned_shutdown_error.py | 2 ++ monkey/infection_monkey/utils/signal_handler.py | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 monkey/infection_monkey/utils/exceptions/planned_shutdown_error.py diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 9b36ba59e..294381696 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -23,6 +23,7 @@ from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter im from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.tunnel_telem import TunnelTelem from infection_monkey.utils.environment import is_windows_os +from infection_monkey.utils.exceptions.planned_shutdown_error import PlannedShutdownError from infection_monkey.utils.monkey_dir import get_monkey_dir_path, remove_monkey_dir from infection_monkey.utils.monkey_log_path import get_monkey_log_path from infection_monkey.utils.signal_handler import register_signal_handlers diff --git a/monkey/infection_monkey/utils/exceptions/planned_shutdown_error.py b/monkey/infection_monkey/utils/exceptions/planned_shutdown_error.py new file mode 100644 index 000000000..885340c23 --- /dev/null +++ b/monkey/infection_monkey/utils/exceptions/planned_shutdown_error.py @@ -0,0 +1,2 @@ +class PlannedShutdownError(Exception): + pass diff --git a/monkey/infection_monkey/utils/signal_handler.py b/monkey/infection_monkey/utils/signal_handler.py index 87d965398..6fda3bc12 100644 --- a/monkey/infection_monkey/utils/signal_handler.py +++ b/monkey/infection_monkey/utils/signal_handler.py @@ -3,7 +3,7 @@ import signal from infection_monkey.i_master import IMaster from infection_monkey.utils.environment import is_windows_os -from infection_monkey.utils.exceptions.planned_shutdown_exception import PlannedShutdownException +from infection_monkey.utils.exceptions.planned_shutdown_error import PlannedShutdownError logger = logging.getLogger(__name__) @@ -16,7 +16,7 @@ class StopSignalHandler: self._handle_signal(signum) # Windows signal handlers must return boolean. Only raising this exception for POSIX # signals. - raise PlannedShutdownException("Monkey Agent got an interrupt signal") + raise PlannedShutdownError("Monkey Agent got an interrupt signal") def handle_windows_signals(self, signum): import win32con From 793bb33c8cd780ed39c38d05ad8d6a68be41d794 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 1 Dec 2021 17:04:45 +0200 Subject: [PATCH 10/17] Agent: use the refactored startup instead of legacy methods (monkey.start() instead of monkey.legacy_start(), etc.) --- monkey/infection_monkey/main.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index fac31bf78..d6edfaec2 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -120,14 +120,12 @@ def main(): monkey = monkey_cls(monkey_args) try: - monkey.legacy_start() - # monkey.start() + monkey.start() return True except Exception as e: logger.exception("Exception thrown from monkey's start function. More info: {}".format(e)) finally: - monkey.legacy_cleanup() - # monkey.cleanup() + monkey.cleanup() if "__main__" == __name__: From 81e61dcea52e27342117643a39dd2372ea12125e Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 1 Dec 2021 17:08:32 +0200 Subject: [PATCH 11/17] Agent: improve the readability of InfectionMonkey constructor by decoupling cmd argument parsing from object parameter setting --- monkey/infection_monkey/monkey.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 294381696..e6733b4a5 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -42,8 +42,7 @@ class InfectionMonkey: logger.info("Monkey is initializing...") self._master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) self._singleton = SystemSingleton() - self._opts = None - self._set_arguments(args) + self._opts = self._get_arguments(args) self._parent = self._opts.parent self._default_tunnel = self._opts.tunnel self._default_server = self._opts.server @@ -52,18 +51,21 @@ class InfectionMonkey: self._add_default_server_to_config() self._monkey_tunnel = None - def _set_arguments(self, args): + @staticmethod + def _get_arguments(args): arg_parser = argparse.ArgumentParser() arg_parser.add_argument("-p", "--parent") arg_parser.add_argument("-t", "--tunnel") arg_parser.add_argument("-s", "--server") arg_parser.add_argument("-d", "--depth", type=int) arg_parser.add_argument("-vp", "--vulnerable-port") - self._opts, _ = arg_parser.parse_known_args(args) - self._log_arguments() + opts, _ = arg_parser.parse_known_args(args) + InfectionMonkey._log_arguments(opts) + return opts - def _log_arguments(self): - arg_string = " ".join([f"{key}: {value}" for key, value in vars(self._opts).items()]) + @staticmethod + def _log_arguments(args): + arg_string = " ".join([f"{key}: {value}" for key, value in vars(args).items()]) logger.info(f"Monkey started with arguments: {arg_string}") def _set_propagation_depth(self): From ad6b3095237fca8427638a24b23958102fa4ad42 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 1 Dec 2021 18:13:27 +0200 Subject: [PATCH 12/17] Agent: readability and style changes in monkey.py: refactored back from raising exceptions to logging and returning, not storing part of island config options as separate parameters, etc. --- monkey/infection_monkey/monkey.py | 57 +++++++++++++++---------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index e6733b4a5..a3a0a4f28 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -23,7 +23,6 @@ from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter im from infection_monkey.telemetry.state_telem import StateTelem from infection_monkey.telemetry.tunnel_telem import TunnelTelem from infection_monkey.utils.environment import is_windows_os -from infection_monkey.utils.exceptions.planned_shutdown_error import PlannedShutdownError from infection_monkey.utils.monkey_dir import get_monkey_dir_path, remove_monkey_dir from infection_monkey.utils.monkey_log_path import get_monkey_log_path from infection_monkey.utils.signal_handler import register_signal_handlers @@ -32,24 +31,18 @@ from infection_monkey.windows_upgrader import WindowsUpgrader logger = logging.getLogger(__name__) -class PlannedShutdownError(Exception): - # Raise when we deliberately want to shut down the agent - pass - - class InfectionMonkey: def __init__(self, args): logger.info("Monkey is initializing...") self._master = MockMaster(MockPuppet(), LegacyTelemetryMessengerAdapter()) self._singleton = SystemSingleton() self._opts = self._get_arguments(args) - self._parent = self._opts.parent - self._default_tunnel = self._opts.tunnel - self._default_server = self._opts.server + # TODO Used in propagation phase to set the default server for the victim self._default_server_port = None self._set_propagation_depth() self._add_default_server_to_config() - self._monkey_tunnel = None + # TODO used in propogation phase + self._monkey_inbound_tunnel = None @staticmethod def _get_arguments(args): @@ -87,7 +80,8 @@ class InfectionMonkey: def start(self): if self._is_another_monkey_running(): - raise PlannedShutdownError("Another instance of the monkey is already running.") + logger.info("Another instance of the monkey is already running") + return logger.info("Monkey is starting...") @@ -98,11 +92,13 @@ class InfectionMonkey: T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() if InfectionMonkey._is_monkey_alive_by_config(): - raise PlannedShutdownError("Monkey marked 'not alive' from configuration.") + logger.info("Monkey marked 'not alive' from configuration.") + return if InfectionMonkey._is_upgrade_to_64_needed(): self._upgrade_to_64() - raise PlannedShutdownError("32 bit Agent can't run on 64 bit system.") + logger.info("32 bit Agent can't run on 64 bit system.") + return self._setup() self._master.start() @@ -111,11 +107,13 @@ class InfectionMonkey: # Sets island's IP and port for monkey to communicate to if not self._is_default_server_set(): raise Exception( - "Monkey couldn't find server with {} default tunnel.".format(self._default_tunnel) + "Monkey couldn't find server with {} default tunnel.".format( + self._opts._default_tunnel + ) ) self._set_default_port() - ControlClient.wakeup(parent=self._parent) + ControlClient.wakeup(parent=self._opts._parent) ControlClient.load_control_config() def _is_default_server_set(self) -> bool: @@ -123,7 +121,7 @@ class InfectionMonkey: Sets the default server for the Monkey to communicate back to. :return """ - if not ControlClient.find_server(default_tunnel=self._default_tunnel): + if not ControlClient.find_server(default_tunnel=self._opts._default_tunnel): return False self._default_server = WormConfiguration.current_server logger.debug("default server set to: %s" % self._default_server) @@ -146,21 +144,26 @@ class InfectionMonkey: def _setup(self): logger.debug("Starting the setup phase.") - self._should_run_check_for_performance() + if self._should_exit_for_performance(): + logger.info( + "Monkey shouldn't run on current machine to improve perfomance" + "(it will be exploited later with more depth)." + ) + return if firewall.is_enabled(): firewall.add_firewall_rule() - self._monkey_tunnel = ControlClient.create_control_tunnel() - if self._monkey_tunnel: - self._monkey_tunnel.start() + self._monkey_inbound_tunnel = ControlClient.create_control_tunnel() + if self._monkey_inbound_tunnel: + self._monkey_inbound_tunnel.start() StateTelem(is_done=False, version=get_version()).send() TunnelTelem().send() register_signal_handlers(self._master) - def _should_run_check_for_performance(self): + def _should_exit_for_performance(self): """ This method implements propagation performance enhancing algorithm that kicks in if the run was started from the Island. @@ -170,11 +173,7 @@ class InfectionMonkey: WormConfiguration.started_on_island = True ControlClient.report_start_on_island() - if not ControlClient.should_monkey_run(self._opts.vulnerable_port): - raise PlannedShutdownError( - "Monkey shouldn't run on current machine to improve perfomance" - "(it will be exploited later with more depth)." - ) + return not ControlClient.should_monkey_run(self._opts.vulnerable_port) def _is_another_monkey_running(self): return not self._singleton.try_lock() @@ -195,9 +194,9 @@ class InfectionMonkey: if self._master: self._master.cleanup() - if self._monkey_tunnel: - self._monkey_tunnel.stop() - self._monkey_tunnel.join() + if self._monkey_inbound_tunnel: + self._monkey_inbound_tunnel.stop() + self._monkey_inbound_tunnel.join() if firewall.is_enabled(): firewall.remove_firewall_rule() From 48782e79d458121f2439fb9d28ae05a9ec68775e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Dec 2021 11:23:45 -0500 Subject: [PATCH 13/17] =?UTF-8?q?Swimm:=20update=20exercise=20Add=20a=20ne?= =?UTF-8?q?w=20configuration=20setting=20to=20the=20Agent=20=E2=9A=99=20Az?= =?UTF-8?q?D8XysWg1BBXCjCDkfq?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .swm/AzD8XysWg1BBXCjCDkfq.swm | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.swm/AzD8XysWg1BBXCjCDkfq.swm b/.swm/AzD8XysWg1BBXCjCDkfq.swm index 708d8e8c5..3339f5178 100644 --- a/.swm/AzD8XysWg1BBXCjCDkfq.swm +++ b/.swm/AzD8XysWg1BBXCjCDkfq.swm @@ -29,24 +29,6 @@ " victims_max_exploit = 100" ] }, - { - "type": "snippet", - "path": "monkey/infection_monkey/monkey.py", - "comments": [], - "firstLineNumber": 220, - "lines": [ - " if not WormConfiguration.alive:", - " logger.info(\"Marked not alive from configuration\")", - " ", - "* machines = self._network.get_victim_machines(", - "* max_find=WormConfiguration.victims_max_find,", - "* stop_callback=ControlClient.check_for_stop,", - "* )", - " for machine in machines:", - " if ControlClient.check_for_stop():", - " break" - ] - }, { "type": "snippet", "path": "monkey/monkey_island/cc/services/config_schema/internal.py", @@ -79,7 +61,6 @@ "app_version": "0.6.6-2", "file_blobs": { "monkey/infection_monkey/config.py": "8f4984ba6563564343282765ab498efca5d89ba8", - "monkey/infection_monkey/monkey.py": "4160a36e0e624404d77526472d51dd07bba49e5a", "monkey/monkey_island/cc/services/config_schema/internal.py": "86318eaf19b9991a8af5de861a3eb085238e17a4" } } From 13e16b9dead5f4a2c04bc6001c5ec6fad53e3fd4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 1 Dec 2021 11:24:54 -0500 Subject: [PATCH 14/17] Agent: Revert "legacy" in dropper start() and cleanup() functions --- monkey/infection_monkey/dropper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py index 30be3798e..e2f59e601 100644 --- a/monkey/infection_monkey/dropper.py +++ b/monkey/infection_monkey/dropper.py @@ -57,7 +57,7 @@ class MonkeyDrops(object): logger.debug("Dropper is running with config:\n%s", pprint.pformat(self._config)) - def legacy_start(self): + def start(self): if self._config["destination_path"] is None: logger.error("No destination path specified") return False @@ -182,7 +182,7 @@ class MonkeyDrops(object): if monkey_process.poll() is not None: logger.warning("Seems like monkey died too soon") - def legacy_cleanup(self): + def cleanup(self): logger.info("Cleaning up the dropper") try: From 1e9c9ab823aa50e8bc009c18cf1756c5024547a5 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 2 Dec 2021 10:55:46 +0200 Subject: [PATCH 15/17] Agent: move _set_propagation_depth and _add_default_server_to_config from constructor to start Moved because these methods don't initialize the parameters, they change the global WormConfiguration object which is logic/behavior --- monkey/infection_monkey/monkey.py | 52 ++++++++++++++++--------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index a3a0a4f28..771ed00b5 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -39,8 +39,6 @@ class InfectionMonkey: self._opts = self._get_arguments(args) # TODO Used in propagation phase to set the default server for the victim self._default_server_port = None - self._set_propagation_depth() - self._add_default_server_to_config() # TODO used in propogation phase self._monkey_inbound_tunnel = None @@ -61,23 +59,6 @@ class InfectionMonkey: arg_string = " ".join([f"{key}: {value}" for key, value in vars(args).items()]) logger.info(f"Monkey started with arguments: {arg_string}") - def _set_propagation_depth(self): - if self._opts.depth is not None: - WormConfiguration._depth_from_commandline = True - WormConfiguration.depth = self._opts.depth - logger.debug("Setting propagation depth from command line") - logger.debug(f"Set propagation depth to {WormConfiguration.depth}") - - def _add_default_server_to_config(self): - if self._default_server: - if self._default_server not in WormConfiguration.command_servers: - logger.debug("Added default server: %s" % self._default_server) - WormConfiguration.command_servers.insert(0, self._default_server) - else: - logger.debug( - "Default server: %s is already in command servers list" % self._default_server - ) - def start(self): if self._is_another_monkey_running(): logger.info("Another instance of the monkey is already running") @@ -85,6 +66,8 @@ class InfectionMonkey: logger.info("Monkey is starting...") + self._set_propagation_depth(self._opts) + self._add_default_server_to_config(self._opts.server) self._connect_to_island() # TODO: Reevaluate who is responsible to send this information @@ -103,17 +86,36 @@ class InfectionMonkey: self._setup() self._master.start() + @staticmethod + def _set_propagation_depth(options): + if options.depth is not None: + WormConfiguration._depth_from_commandline = True + WormConfiguration.depth = options.depth + logger.debug("Setting propagation depth from command line") + logger.debug(f"Set propagation depth to {WormConfiguration.depth}") + + @staticmethod + def _add_default_server_to_config(default_server: str): + if default_server: + if default_server not in WormConfiguration.command_servers: + logger.debug("Added default server: %s" % default_server) + WormConfiguration.command_servers.insert(0, default_server) + else: + logger.debug( + "Default server: %s is already in command servers list" % default_server + ) + def _connect_to_island(self): # Sets island's IP and port for monkey to communicate to if not self._is_default_server_set(): raise Exception( "Monkey couldn't find server with {} default tunnel.".format( - self._opts._default_tunnel + self._opts.tunnel ) ) self._set_default_port() - ControlClient.wakeup(parent=self._opts._parent) + ControlClient.wakeup(parent=self._opts.parent) ControlClient.load_control_config() def _is_default_server_set(self) -> bool: @@ -121,10 +123,10 @@ class InfectionMonkey: Sets the default server for the Monkey to communicate back to. :return """ - if not ControlClient.find_server(default_tunnel=self._opts._default_tunnel): + if not ControlClient.find_server(default_tunnel=self._opts.tunnel): return False - self._default_server = WormConfiguration.current_server - logger.debug("default server set to: %s" % self._default_server) + self._opts.server = WormConfiguration.current_server + logger.debug("default server set to: %s" % self._opts.server) return True @staticmethod @@ -180,7 +182,7 @@ class InfectionMonkey: def _set_default_port(self): try: - self._default_server_port = self._default_server.split(":")[1] + self._default_server_port = self._opts.server.split(":")[1] except KeyError: self._default_server_port = "" From e4bdc964103de75d04a23fcd65014556da90fafa Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 2 Dec 2021 11:51:14 +0200 Subject: [PATCH 16/17] Agent: move _set_propagation_depth and _add_default_server_to_config from constructor to start Moved because these methods don't initialize the parameters, they change the global WormConfiguration object which is logic/behavior --- monkey/infection_monkey/monkey.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 771ed00b5..cc3c5156e 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -109,9 +109,7 @@ class InfectionMonkey: # Sets island's IP and port for monkey to communicate to if not self._is_default_server_set(): raise Exception( - "Monkey couldn't find server with {} default tunnel.".format( - self._opts.tunnel - ) + "Monkey couldn't find server with {} default tunnel.".format(self._opts.tunnel) ) self._set_default_port() @@ -190,7 +188,7 @@ class InfectionMonkey: logger.info("Monkey cleanup started") try: if self._is_upgrade_to_64_needed(): - logger.debug("Detected upgrade to 64bit") + logger.debug("Cleanup not needed for 32 bit agent on 64 bit system(it didn't run)") return if self._master: From ce7362e27881a0c3043c34c02ea909d1d39cea7f Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 2 Dec 2021 14:26:10 +0200 Subject: [PATCH 17/17] Agent: add a waiting timer to allow exploited machines to connect to the tunnel (in agent cleanup) --- monkey/infection_monkey/monkey.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index cc3c5156e..4eb959129 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -3,6 +3,7 @@ import logging import os import subprocess import sys +import time import infection_monkey.tunnel as tunnel from common.utils.attack_utils import ScanStatus, UsageEnum @@ -186,6 +187,7 @@ class InfectionMonkey: def cleanup(self): logger.info("Monkey cleanup started") + self._wait_for_exploited_machine_connection() try: if self._is_upgrade_to_64_needed(): logger.debug("Cleanup not needed for 32 bit agent on 64 bit system(it didn't run)") @@ -220,6 +222,19 @@ class InfectionMonkey: logger.info("Monkey is shutting down") + def _wait_for_exploited_machine_connection(self): + # TODO check for actual exploitation + machines_exploited = False + # if host was exploited, before continue to closing the tunnel ensure the exploited + # host had its chance to + # connect to the tunnel + if machines_exploited: + time_to_sleep = WormConfiguration.keep_tunnel_open_time + logger.info( + "Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep + ) + time.sleep(time_to_sleep) + @staticmethod def _close_tunnel(): tunnel_address = (