From ce492d25f4a6ca598ca51d78c0d65d832d07be49 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 30 Jul 2021 04:22:21 -0400 Subject: [PATCH 1/6] Agent: Return boolean from "should_monkey_run()" A function named "should_monkey_run()" should let you know whether or not monkey should run. Before this commit, the function was responsible for flow control, as it raised a PlannedShutdownException, resulting in the shutdown of the monkey agent. --- monkey/infection_monkey/control.py | 7 ++----- monkey/infection_monkey/monkey.py | 7 ++++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index b71e27a5e..109110498 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -22,7 +22,6 @@ from infection_monkey.config import GUID, WormConfiguration from infection_monkey.network.info import check_internet_access, local_ips from infection_monkey.transport.http import HTTPConnectProxy from infection_monkey.transport.tcp import TcpProxy -from infection_monkey.utils.exceptions.planned_shutdown_exception import PlannedShutdownException requests.packages.urllib3.disable_warnings() @@ -401,10 +400,8 @@ class ControlClient(object): and ControlClient.can_island_see_port(vulnerable_port) and WormConfiguration.started_on_island ): - raise PlannedShutdownException( - "Monkey shouldn't run on current machine " - "(it will be exploited later with more depth)." - ) + return False + return True @staticmethod diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 6059c291a..0c0cca9cc 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -125,7 +125,12 @@ class InfectionMonkey(object): if is_running_on_island(): WormConfiguration.started_on_island = True ControlClient.report_start_on_island() - ControlClient.should_monkey_run(self._opts.vulnerable_port) + + 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() From 2543e5b2a514648827e99bc5f5ce1a0e1c132930 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 30 Jul 2021 04:32:50 -0400 Subject: [PATCH 2/6] Agent: Only run ransomware payload once --- monkey/infection_monkey/monkey.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 0c0cca9cc..c720fe46a 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -237,13 +237,13 @@ class InfectionMonkey(object): if not self._keep_running: break - InfectionMonkey.run_ransomware() - if (not is_empty) and (WormConfiguration.max_iterations > iteration_index + 1): time_to_sleep = WormConfiguration.timeout_between_iterations LOG.info("Sleeping %d seconds before next life cycle iteration", time_to_sleep) time.sleep(time_to_sleep) + InfectionMonkey.run_ransomware() + if self._keep_running and WormConfiguration.alive: LOG.info("Reached max iterations (%d)", WormConfiguration.max_iterations) elif not WormConfiguration.alive: From 017e37deb0e82bbda7771e675ed955413b0609d5 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 30 Jul 2021 04:42:31 -0400 Subject: [PATCH 3/6] Agent: Extract propagation loop into a function --- monkey/infection_monkey/monkey.py | 206 +++++++++++++++--------------- 1 file changed, 103 insertions(+), 103 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index c720fe46a..5b0b1bafd 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -62,6 +62,7 @@ class InfectionMonkey(object): self._default_server_port = None self._opts = None self._upgrading_to_64 = False + self._monkey_tunnel = None def initialize(self): LOG.info("Monkey is initializing...") @@ -135,9 +136,9 @@ class InfectionMonkey(object): if firewall.is_enabled(): firewall.add_firewall_rule() - monkey_tunnel = ControlClient.create_control_tunnel() - if monkey_tunnel: - monkey_tunnel.start() + 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() @@ -149,106 +150,10 @@ class InfectionMonkey(object): LOG.debug("Starting the propagation phase.") self.shutdown_by_max_depth_reached() - for iteration_index in range(WormConfiguration.max_iterations): - ControlClient.keepalive() - ControlClient.load_control_config() - - self._network.initialize() - - self._fingerprint = HostFinger.get_instances() - - self._exploiters = HostExploiter.get_classes() - - if not self._keep_running or not WormConfiguration.alive: - break - - machines = self._network.get_victim_machines( - max_find=WormConfiguration.victims_max_find, - stop_callback=ControlClient.check_for_stop, - ) - is_empty = True - for machine in machines: - if ControlClient.check_for_stop(): - break - - is_empty = False - for finger in self._fingerprint: - LOG.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: - LOG.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: - LOG.debug("Skipping %r - already exploited", machine) - continue - elif machine in self._fail_exploitation_machines: - if WormConfiguration.retry_failed_explotation: - LOG.debug("%r - exploitation failed before, trying again", machine) - else: - LOG.debug("Skipping %r - exploitation failed before", machine) - continue - - if monkey_tunnel: - 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) - LOG.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 is_empty) and (WormConfiguration.max_iterations > iteration_index + 1): - time_to_sleep = WormConfiguration.timeout_between_iterations - LOG.info("Sleeping %d seconds before next life cycle iteration", time_to_sleep) - time.sleep(time_to_sleep) + self.propagate() InfectionMonkey.run_ransomware() - if self._keep_running and WormConfiguration.alive: - LOG.info("Reached max iterations (%d)", WormConfiguration.max_iterations) - elif not WormConfiguration.alive: - LOG.info("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 @@ -259,9 +164,9 @@ class InfectionMonkey(object): ) time.sleep(time_to_sleep) - if monkey_tunnel: - monkey_tunnel.stop() - monkey_tunnel.join() + if self._monkey_tunnel: + self._monkey_tunnel.stop() + self._monkey_tunnel.join() post_breach_phase.join() @@ -296,6 +201,101 @@ class InfectionMonkey(object): if not WormConfiguration.alive: raise PlannedShutdownException("Marked 'not alive' from configuration.") + def propagate(self): + for iteration_index in range(WormConfiguration.max_iterations): + ControlClient.keepalive() + ControlClient.load_control_config() + + self._network.initialize() + + self._fingerprint = HostFinger.get_instances() + + self._exploiters = HostExploiter.get_classes() + + if not self._keep_running or not WormConfiguration.alive: + break + + machines = self._network.get_victim_machines( + max_find=WormConfiguration.victims_max_find, + stop_callback=ControlClient.check_for_stop, + ) + is_empty = True + for machine in machines: + if ControlClient.check_for_stop(): + break + + is_empty = False + for finger in self._fingerprint: + LOG.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: + LOG.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: + LOG.debug("Skipping %r - already exploited", machine) + continue + elif machine in self._fail_exploitation_machines: + if WormConfiguration.retry_failed_explotation: + LOG.debug("%r - exploitation failed before, trying again", machine) + else: + LOG.debug("Skipping %r - exploitation failed before", 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) + LOG.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 is_empty) and (WormConfiguration.max_iterations > iteration_index + 1): + time_to_sleep = WormConfiguration.timeout_between_iterations + LOG.info("Sleeping %d seconds before next life cycle iteration", time_to_sleep) + time.sleep(time_to_sleep) + + if self._keep_running and WormConfiguration.alive: + LOG.info("Reached max iterations (%d)", WormConfiguration.max_iterations) + elif not WormConfiguration.alive: + LOG.info("Marked not alive from configuration") + def upgrade_to_64_if_needed(self): if WindowsUpgrader.should_upgrade(): self._upgrading_to_64 = True From e7d694380dbd0d4314c29230598881a2e24cc0cc Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 30 Jul 2021 04:55:45 -0400 Subject: [PATCH 4/6] Agent: Skip propagation if max depth is reached Fixes #1374 When monkey starts, it launches a thread for executing post breach actions. It then executes its propagation loop on the main thread. If the maximum propagation depth has been reached, a PlannedShutdownException is raised on the main thread. This results in InfectionMonkey.cleanup() being called, which then calls InfectionMonkey.self_delete(). self_delete() deletes the monkey binary, but the post breach actions thread hasn't been stopped, resulting in a trace when the thread attempts an import. We don't need to shutdown if the maximum propagation depth is reached. We only need to skip the propagation phase so that monkey does not propagate further. PBAs and payloads can still be allowed to run. --- .swm/AzD8XysWg1BBXCjCDkfq.swm | 31 +++++++++++++++---------------- monkey/infection_monkey/monkey.py | 22 +++++++++++----------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/.swm/AzD8XysWg1BBXCjCDkfq.swm b/.swm/AzD8XysWg1BBXCjCDkfq.swm index 5383db0b7..29ad78526 100644 --- a/.swm/AzD8XysWg1BBXCjCDkfq.swm +++ b/.swm/AzD8XysWg1BBXCjCDkfq.swm @@ -17,7 +17,7 @@ "type": "snippet", "path": "monkey/infection_monkey/config.py", "comments": [], - "firstLineNumber": 126, + "firstLineNumber": 124, "lines": [ " exploiter_classes = []", " system_info_collector_classes = []", @@ -33,19 +33,18 @@ "type": "snippet", "path": "monkey/infection_monkey/monkey.py", "comments": [], - "firstLineNumber": 159, + "firstLineNumber": 220, "lines": [ + " if not self._keep_running or not WormConfiguration.alive:", + " break", " ", - " if not self._keep_running or not WormConfiguration.alive:", - " break", - "*", - "* machines = self._network.get_victim_machines(", - "* max_find=WormConfiguration.victims_max_find,", - "* stop_callback=ControlClient.check_for_stop,", - "* )", - " is_empty = True", - " for machine in machines:", - " if ControlClient.check_for_stop():" + "* machines = self._network.get_victim_machines(", + "* max_find=WormConfiguration.victims_max_find,", + "* stop_callback=ControlClient.check_for_stop,", + "* )", + " is_empty = True", + " for machine in machines:", + " if ControlClient.check_for_stop():" ] }, { @@ -77,11 +76,11 @@ "symbols": {}, "file_version": "2.0.1", "meta": { - "app_version": "0.4.1-1", + "app_version": "0.4.9-1", "file_blobs": { - "monkey/infection_monkey/config.py": "ffdea551eb1ae2b65d4700db896c746771e7954c", - "monkey/infection_monkey/monkey.py": "c81a6251746e3af4e93eaa7d50af44d33debe05c", - "monkey/monkey_island/cc/services/config_schema/internal.py": "d03527b89c21dfb832a15e4f7d55f4027d83b453" + "monkey/infection_monkey/config.py": "0bede1c57949987f5c8025bd9b8f7aa29d02a6af", + "monkey/infection_monkey/monkey.py": "89d2fa8452dee70f6d2985a9bb452f0159ea8219", + "monkey/monkey_island/cc/services/config_schema/internal.py": "1ce1c864b1df332b65e16b4ce9ed533affd73f9c" } } } diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 5b0b1bafd..5c2cdf5eb 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -40,7 +40,7 @@ from infection_monkey.utils.monkey_dir import ( from infection_monkey.utils.monkey_log_path import get_monkey_log_path from infection_monkey.windows_upgrader import WindowsUpgrader -MAX_DEPTH_REACHED_MESSAGE = "Reached max depth, shutting down" +MAX_DEPTH_REACHED_MESSAGE = "Reached max depth, skipping propagation phase." LOG = logging.getLogger(__name__) @@ -147,10 +147,13 @@ class InfectionMonkey(object): post_breach_phase = Thread(target=self.start_post_breach_phase) post_breach_phase.start() - LOG.debug("Starting the propagation phase.") - self.shutdown_by_max_depth_reached() - - self.propagate() + if not InfectionMonkey.max_propagation_depth_reached(): + LOG.info("Starting the propagation phase.") + LOG.debug("Running with depth: %d" % WormConfiguration.depth) + self.propagate() + else: + LOG.info("Maximum propagation depth has been reached; monkey will not propagate.") + TraceTelem(MAX_DEPTH_REACHED_MESSAGE).send() InfectionMonkey.run_ransomware() @@ -181,12 +184,9 @@ class InfectionMonkey(object): self.collect_system_info_if_configured() PostBreach().execute_all_configured() - def shutdown_by_max_depth_reached(self): - if 0 == WormConfiguration.depth: - TraceTelem(MAX_DEPTH_REACHED_MESSAGE).send() - raise PlannedShutdownException(MAX_DEPTH_REACHED_MESSAGE) - else: - LOG.debug("Running with depth: %d" % WormConfiguration.depth) + @staticmethod + def max_propagation_depth_reached(): + return 0 == WormConfiguration.depth def collect_system_info_if_configured(self): LOG.debug("Calling for system info collection") From e97943a90577f5ff5649dc6757034eb83004e211 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 30 Jul 2021 05:07:50 -0400 Subject: [PATCH 5/6] Agent: Always join threads in main loop before exit --- monkey/infection_monkey/monkey.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 5c2cdf5eb..741635c6e 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -63,6 +63,7 @@ class InfectionMonkey(object): self._opts = None self._upgrading_to_64 = False self._monkey_tunnel = None + self._post_breach_phase = None def initialize(self): LOG.info("Monkey is initializing...") @@ -144,8 +145,8 @@ class InfectionMonkey(object): TunnelTelem().send() LOG.debug("Starting the post-breach phase asynchronously.") - post_breach_phase = Thread(target=self.start_post_breach_phase) - post_breach_phase.start() + self._post_breach_phase = Thread(target=self.start_post_breach_phase) + self._post_breach_phase.start() if not InfectionMonkey.max_propagation_depth_reached(): LOG.info("Starting the propagation phase.") @@ -167,12 +168,6 @@ class InfectionMonkey(object): ) time.sleep(time_to_sleep) - if self._monkey_tunnel: - self._monkey_tunnel.stop() - self._monkey_tunnel.join() - - post_breach_phase.join() - except PlannedShutdownException: LOG.info( "A planned shutdown of the Monkey occurred. Logging the reason and finishing " @@ -180,6 +175,14 @@ class InfectionMonkey(object): ) LOG.exception("Planned shutdown, reason:") + finally: + if self._monkey_tunnel: + self._monkey_tunnel.stop() + self._monkey_tunnel.join() + + if self._post_breach_phase: + self._post_breach_phase.join() + def start_post_breach_phase(self): self.collect_system_info_if_configured() PostBreach().execute_all_configured() From b11277a340e9b7cbf095cb7451b2dfd166426e46 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 30 Jul 2021 05:49:24 -0400 Subject: [PATCH 6/6] Update changelog for #1374 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf3507068..f3f39d0ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Attempted to delete a directory when monkey config reset was called. #1054 - An errant space in the windows commands to run monkey manually. #1153 - gevent tracebacks in console output. #859 +- Crash and failure to run PBAs if max depth reached. #1374 ### Security - Address minor issues discovered by Dlint. #1075