From b3799673eaac0ec2611040ebf9a228bf4216ec9a Mon Sep 17 00:00:00 2001 From: sjanusz-r7 Date: Fri, 17 May 2024 10:59:18 +0100 Subject: [PATCH 1/5] Bump ruby_smb to 3.3.8 --- Gemfile.lock | 2 +- LICENSE_GEMS | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0ccc9f7d2193..707405add13d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -475,7 +475,7 @@ GEM ruby-progressbar (1.13.0) ruby-rc4 (0.1.5) ruby2_keywords (0.0.5) - ruby_smb (3.3.7) + ruby_smb (3.3.8) bindata (= 2.4.15) openssl-ccm openssl-cmac diff --git a/LICENSE_GEMS b/LICENSE_GEMS index 962802e5aa53..5f149bd43bcf 100644 --- a/LICENSE_GEMS +++ b/LICENSE_GEMS @@ -172,7 +172,7 @@ ruby-prof, 1.4.2, "Simplified BSD" ruby-progressbar, 1.13.0, MIT ruby-rc4, 0.1.5, MIT ruby2_keywords, 0.0.5, "ruby, Simplified BSD" -ruby_smb, 3.3.7, "New BSD" +ruby_smb, 3.3.8, "New BSD" rubyntlm, 0.6.3, MIT rubyzip, 2.3.2, "Simplified BSD" sawyer, 0.9.2, MIT From d56907756430939d378add9edd189718191ecb49 Mon Sep 17 00:00:00 2001 From: sjanusz-r7 Date: Wed, 8 May 2024 10:05:38 +0100 Subject: [PATCH 2/5] Refactor smb_lookupsid module to use RubySMB --- lib/msf/core/exploit/remote/ms_icpr.rb | 24 +- lib/msf/core/exploit/remote/ms_lsarpc.rb | 124 +++++++ lib/msf/core/exploit/remote/ms_samr.rb | 30 +- lib/msf/core/exploit/remote/smb/client.rb | 4 +- lib/msf/core/exploit/remote/smb/client/ipc.rb | 54 +++ .../auxiliary/scanner/smb/smb_lookupsid.rb | 337 +++++------------- spec/acceptance/smb_spec.rb | 32 +- 7 files changed, 280 insertions(+), 325 deletions(-) create mode 100644 lib/msf/core/exploit/remote/ms_lsarpc.rb create mode 100644 lib/msf/core/exploit/remote/smb/client/ipc.rb diff --git a/lib/msf/core/exploit/remote/ms_icpr.rb b/lib/msf/core/exploit/remote/ms_icpr.rb index d074214e39ce..339e8d6e1a30 100644 --- a/lib/msf/core/exploit/remote/ms_icpr.rb +++ b/lib/msf/core/exploit/remote/ms_icpr.rb @@ -11,9 +11,8 @@ module Msf module Exploit::Remote::MsIcpr - include Msf::Exploit::Remote::SMB::Client::Authenticated + include Msf::Exploit::Remote::MsIpc include Msf::Exploit::Remote::DCERPC - include Msf::Auxiliary::Report # [2.2.2.7.7.4 szOID_NTDS_CA_SECURITY_EXT](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/e563cff8-1af6-4e6f-a655-7571ca482e71) OID_NTDS_CA_SECURITY_EXT = '1.3.6.1.4.1.311.25.2'.freeze @@ -109,27 +108,6 @@ def request_certificate(opts = {}) module_function - def connect_ipc - begin - connect - rescue Rex::ConnectionError => e - raise MsIcprConnectionError, e.message - end - - begin - smb_login - rescue Rex::Proto::SMB::Exceptions::Error, RubySMB::Error::RubySMBError => e - raise MsIcprAuthenticationError, "Unable to authenticate ([#{e.class}] #{e})." - end - report_service(icpr_service_data) - - begin - simple.client.tree_connect("\\\\#{sock.peerhost}\\IPC$") - rescue RubySMB::Error::RubySMBError => e - raise MsIcprConnectionError, "Unable to connect to the remote IPC$ share ([#{e.class}] #{e})." - end - end - def connect_icpr(tree) vprint_status('Connecting to ICertPassage (ICPR) Remote Protocol') icpr = tree.open_file(filename: 'cert', write: true, read: true) diff --git a/lib/msf/core/exploit/remote/ms_lsarpc.rb b/lib/msf/core/exploit/remote/ms_lsarpc.rb new file mode 100644 index 000000000000..d791e9d515b7 --- /dev/null +++ b/lib/msf/core/exploit/remote/ms_lsarpc.rb @@ -0,0 +1,124 @@ +### +# +# This mixin provides methods to open, and close policy handles, and to query policy info on the remote SMB server. +# +# -*- coding: binary -*- + +module Msf + + module Exploit::Remote::MsLsarpc + + include Msf::Exploit::Remote::SMB::Client::Ipc + + class MsLsarpcError < StandardError; end + class MsLsarpcConnectionError < MsLsarpcError; end + class MsLsarpcAuthenticationError < MsLsarpcError; end + class MsLsarpcUnexpectedReplyError < MsLsarpcError; end + + LSA_UUID = '12345778-1234-abcd-ef00-0123456789ab'.freeze + LSA_VERS = '0.0'.freeze + LSARPC_ENDPOINT = RubySMB::Dcerpc::Lsarpc.freeze + + # The currently connected LSARPC pipe + attr_reader :lsarpc_pipe + + def map_security_principal_to_string(security_principal) + case security_principal + when 1 + 'User' + when 2 + 'Group' + when 3 + 'Domain' + when 4 + 'Alias' + when 5 + 'Well-Known Group' + when 6 + 'Deleted Account' + when 7 + 'Invalid' + when 8 + 'Unknown' + when '9' + 'Computer' + when 10 + 'Label' + else + 'Unknown - Not a valid Security Principal' + end + end + + def open_policy2(impersonation_level, security_context_tracking_mode, access_mask) + self.lsarpc_pipe.lsar_open_policy2( + system_name: simple.peerhost, + object_attributes: { + security_quality_of_service: { + impersonation_level: impersonation_level, + security_context_tracking_mode: security_context_tracking_mode + } + }, + access_mask: access_mask + ) + end + + def query_information_policy(policy_handle, information_class) + self.lsarpc_pipe.lsar_query_information_policy( + policy_handle: policy_handle, + information_class: information_class + ) + end + + def lookup_sids(policy_handle, sids, lookup_level) + sids = [sids] unless sids.is_a?(Array) + + self.lsarpc_pipe.lsar_lookup_sids( + policy_handle: policy_handle, + sids: sids, + lookup_level: lookup_level + ) + end + + def close_policy(policy_handle) + self.lsarpc_pipe.lsar_close_handle( + policy_handle: policy_handle + ) if (self.lsarpc_pipe && policy_handle) + end + + def disconnect_lsarpc + begin + self.lsarpc_pipe.close if self.lsarpc_pipe&.is_connected? + rescue RubySMB::Error::UnexpectedStatusCode => e + wlog e + end + end + + module_function + + def connect_lsarpc(tree) + begin + vprint_status('Connecting to LSARPC') + self.lsarpc_pipe = tree.open_file(filename: 'LSARPC', write: true, read: true) + + raise MsLsarpcConnectionError.new('Could not open LSARPC pipe on remote SMB server.') unless lsarpc_pipe + + self.lsarpc_pipe.extend(LSARPC_ENDPOINT) unless lsarpc_pipe.is_a?(LSARPC_ENDPOINT) + + vprint_status('Binding to \\LSARPC...') + self.lsarpc_pipe.bind(endpoint: LSARPC_ENDPOINT) + vprint_good('Bound to \\LSARPC') + + self.lsarpc_pipe + rescue RubySMB::Dcerpc::Error::FaultError => e + elog(e.message, error: e) + raise MsLsarpcUnexpectedReplyError, "Connection failed (DCERPC fault: #{e.status_name})" + end + end + + protected + + attr_writer :lsarpc_pipe + + end + +end diff --git a/lib/msf/core/exploit/remote/ms_samr.rb b/lib/msf/core/exploit/remote/ms_samr.rb index 69c602bcd419..966f8fa77131 100644 --- a/lib/msf/core/exploit/remote/ms_samr.rb +++ b/lib/msf/core/exploit/remote/ms_samr.rb @@ -8,7 +8,7 @@ module Msf module Exploit::Remote::MsSamr - include Msf::Exploit::Remote::SMB::Client::Authenticated + include Msf::Exploit::Remote::MsIpc class MsSamrError < StandardError; end class MsSamrConnectionError < MsSamrError; end @@ -22,34 +22,6 @@ class MsSamrBadConfigError < MsSamrError; end module_function - def connect_ipc - begin - connect - rescue Rex::ConnectionError => e - raise MsSamrConnectionError, e.message - end - - begin - smb_login - rescue Rex::Proto::SMB::Exceptions::Error, RubySMB::Error::RubySMBError => e - raise MsSamrAuthenticationError, "Unable to authenticate ([#{e.class}] #{e})." - end - report_service( - host: rhost, - port: rport, - host_name: simple.client.default_name, - proto: 'tcp', - name: 'smb', - info: "Module: #{fullname}, last negotiated version: SMBv#{simple.client.negotiated_smb_version} (dialect = #{simple.client.dialect})" - ) - - begin - simple.client.tree_connect("\\\\#{sock.peerhost}\\IPC$") - rescue RubySMB::Error::RubySMBError => e - raise MsSamrConnectionError, "Unable to connect to the remote IPC$ share ([#{e.class}] #{e})." - end - end - def connect_samr(tree) begin vprint_status('Connecting to Security Account Manager (SAM) Remote Protocol') diff --git a/lib/msf/core/exploit/remote/smb/client.rb b/lib/msf/core/exploit/remote/smb/client.rb index 3724f39184bc..25f41b2abcb6 100644 --- a/lib/msf/core/exploit/remote/smb/client.rb +++ b/lib/msf/core/exploit/remote/smb/client.rb @@ -189,10 +189,10 @@ def smb_login(simple_client = self.simple) datastore['NTLM::SendNTLM'], datastore['SMB::Native_OS'], datastore['SMB::Native_LM'], - {:use_spn => datastore['NTLM::SendSPN'], :name => self.rhost} + { :use_spn => datastore['NTLM::SendSPN'], :name => simple_client.peerhost } ) # XXX: Any reason to connect to the IPC$ share in this method? - simple_client.connect("\\\\#{datastore['RHOST']}\\IPC$") + simple_client.client.tree_connect("\\\\#{simple_client.peerhost}\\IPC$") end # This method returns the native operating system of the peer diff --git a/lib/msf/core/exploit/remote/smb/client/ipc.rb b/lib/msf/core/exploit/remote/smb/client/ipc.rb new file mode 100644 index 000000000000..774f555958fb --- /dev/null +++ b/lib/msf/core/exploit/remote/smb/client/ipc.rb @@ -0,0 +1,54 @@ +### +# +# This mixin provides a method to connect to an IPC share on the remote SMB server. +# +# -*- coding: binary -*- + +module Msf + + module Exploit::Remote::SMB::Client::Ipc + + include Msf::Exploit::Remote::SMB::Client::Authenticated + include Msf::Auxiliary::Report + + class SmbIpcError < StandardError; end + class SmbIpcConnectionError < SmbIpcError; end + class SmbIpcAuthenticationError < SmbIpcError; end + + module_function + + def connect_ipc + begin + if session + self.simple = session.simple_client + ipc_tree = simple.client.tree_connect("\\\\#{simple.peerhost}\\IPC$") + else + connect + # smb_login does a tree_connect to the IPC share already. + ipc_tree = smb_login + end + rescue Rex::ConnectionError => e + raise SmbIpcConnectionError, e.message + rescue Rex::Proto::SMB::Exceptions::Error, RubySMB::Error::RubySMBError => e + raise SmbIpcAuthenticationError, "Unable to authenticate ([#{e.class}] #{e})." + end + + report_service( + host: simple.peerhost, + port: simple.peerport, + host_name: simple.client.default_name, + proto: 'tcp', + name: 'smb', + info: "Module: #{fullname}, last negotiated version: SMBv#{simple.client.negotiated_smb_version} (dialect = #{simple.client.dialect})" + ) + + ipc_tree + end + + def disconnect_ipc(ipc_tree) + ipc_tree.disconnect! if ipc_tree + end + + end + +end diff --git a/modules/auxiliary/scanner/smb/smb_lookupsid.rb b/modules/auxiliary/scanner/smb/smb_lookupsid.rb index 4637532bc32c..e3eefaad7fd7 100644 --- a/modules/auxiliary/scanner/smb/smb_lookupsid.rb +++ b/modules/auxiliary/scanner/smb/smb_lookupsid.rb @@ -3,12 +3,11 @@ # Current source: https://github.com/rapid7/metasploit-framework ## + class MetasploitModule < Msf::Auxiliary # Exploit mixins should be called first - include Msf::Exploit::Remote::SMB::Client - include Msf::Exploit::Remote::SMB::Client::Authenticated - + include Msf::Exploit::Remote::MsLsarpc include Msf::Exploit::Remote::DCERPC # Scanner mixin should be near last @@ -43,270 +42,100 @@ def initialize [ OptInt.new('MinRID', [ false, "Starting RID to check", 500 ]), OptInt.new('MaxRID', [ false, "Maximum RID to check", 4000 ]) - ], - self.class + ] ) - - deregister_options('RPORT') - end - - # Constants used by this module - LSA_UUID = '12345778-1234-abcd-ef00-0123456789ab' - LSA_VERS = '0.0' - LSA_PIPES = %W{ LSARPC NETLOGON SAMR BROWSER SRVSVC } - - def rport - @rport || datastore['RPORT'] - end - - def smb_direct - @smbdirect || datastore['SMBDirect'] - end - - # Locate an available SMB PIPE for the specified service - def smb_find_dcerpc_pipe(uuid, vers, pipes) - found_pipe = nil - found_handle = nil - pipes.each do |pipe_name| - connected = session ? true : false - begin - unless connected - connect - smb_login - connected = true - end - - handle = dcerpc_handle_target( - uuid, vers, - 'ncacn_np', ["\\#{pipe_name}"], simple.address - ) - - dcerpc_bind(handle) - return pipe_name - - rescue ::Interrupt => e - raise e - rescue ::Exception => e - raise e if not connected - end - disconnect - end - nil - end - - def smb_parse_sid(data) - fields = data.unpack('VvvvvVVVVV') - domain = data[32, fields[3]] - domain.gsub!("\x00", '') - - if(fields[6] == 0) - return [nil, domain] - end - - while(fields[3] % 4 != 0) - fields[3] += 1 - end - - buff = data[32 + fields[3], data.length].unpack('VCCvNVVVVV') - sid = buff[4..8].map{|x| x.to_s }.join("-") - return [sid, domain] - end - - def smb_pack_sid(str) - [1,5,0].pack('CCv') + str.split('-').map{|x| x.to_i}.pack('NVVVV') - end - - def smb_parse_sid_lookup(data) - - fields = data.unpack('VVVVVvvVVVVV') - if(fields[0] == 0) - return nil - end - - domain = data[44, fields[5]] - domain.gsub!("\x00", '') - - while(fields[5] % 4 != 0) - fields[5] += 1 - end - - ginfo = data[44 + fields[5], data.length].unpack('VCCvNVVVV') - uinfo = data[72 + fields[5], data.length].unpack('VVVVvvVVVVV') - - if(uinfo[3] == 8) - return [8, nil] - end - - name = data[112 + fields[5], uinfo[4]] - name.gsub!("\x00", '') - - [ uinfo[3], name ] end # Fingerprint a single host def run_host(ip) - ports = [139, 445] - - if session - print_status("Using existing session #{session.sid}") - client = session.client - self.simple = ::Rex::Proto::SMB::SimpleClient.new(client.dispatcher.tcp_socket, client: client) - ports = [simple.port] - self.simple.connect("\\\\#{simple.address}\\IPC$") # smb_login connects to this share for some reason and it doesn't work unless we do too - end - - ports.each do |port| - - @rport = port - - lsa_pipe = nil - lsa_handle = nil - begin - # find the lsarpc pipe - lsa_pipe = smb_find_dcerpc_pipe(LSA_UUID, LSA_VERS, LSA_PIPES) - break if not lsa_pipe - - # OpenPolicy2() - stub = - NDR.uwstring(ip) + - NDR.long(24) + - NDR.long(0) + - NDR.long(0) + - NDR.long(0) + - NDR.long(0) + - NDR.long(rand(0x10000000)) + - NDR.long(12) + + sids_table = Rex::Text::Table.new( + 'Indent' => 4, + 'Header' => "SMB Lookup SIDs Output", + 'Columns' => [ - 2, # Impersonation - 1, # Context - 0 # Effective - ].pack('vCC') + - NDR.long(0x02000000) - - dcerpc.call(44, stub) - resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil - - if ! (resp and resp.length == 24) - print_error("Invalid response from the OpenPolicy request") - disconnect - return - end - - phandle = resp[0,20] - perror = resp[20,4].unpack("V")[0] - - # Recent versions of Windows restrict this by default - if(perror == 0xc0000022) - disconnect - return - end - - if(perror != 0) - print_error("Received error #{"0x%.8x" % perror} from the OpenPolicy2 request") - disconnect - return - end - - # QueryInfoPolicy(Local) - stub = phandle + NDR.long(5) - dcerpc.call(7, stub) - resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil - host_sid, host_name = smb_parse_sid(resp) - - # QueryInfoPolicy(Domain) - stub = phandle + NDR.long(3) - dcerpc.call(7, stub) - resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil - domain_sid, domain_name = smb_parse_sid(resp) - - # Store SID, local domain name, joined domain name - print_status("PIPE(#{lsa_pipe}) LOCAL(#{host_name} - #{host_sid}) DOMAIN(#{domain_name} - #{domain_sid})") + 'Type', + 'Name', + 'RID' + ], + 'SortIndex' => 2, # Sort by RID + ) - domain = { - :name => host_name, - :txt_sid => host_sid, - :users => {}, - :groups => {} + ipc_tree = connect_ipc + lsarpc_pipe = connect_lsarpc(ipc_tree) + endpoint = RubySMB::Dcerpc::Lsarpc.freeze + policy_handle = open_policy2(endpoint::SECURITY_IMPERSONATION, endpoint::SECURITY_CONTEXT_CONTINUOUS_UPDATES, endpoint::MAXIMUM_ALLOWED) + + account_policy = query_information_policy(policy_handle, endpoint::POLICY_ACCOUNT_DOMAIN_INFORMATION) + primary_policy = query_information_policy(policy_handle, endpoint::POLICY_PRIMARY_DOMAIN_INFORMATION) + + info = { + local: { + name: primary_policy[:policy_information][:name].encode('ASCII-8BIT'), + sid: primary_policy[:policy_information][:sid].to_s.encode('ASCII-8BIT') + }, + domain: { + name: account_policy[:policy_information][:domain_name].encode('ASCII-8BIT'), + sid: account_policy[:policy_information][:domain_sid].to_s.encode('ASCII-8BIT') } + } + + # Store the domain information + report_note( + :host => self.simple.peerhost, + :proto => 'tcp', + :port => self.simple.peerport, + :type => 'smb.domain.lookupsid', + :data => info[:domain] + ) - target_sid = case action.name.upcase - when 'LOCAL' - host_sid - when 'DOMAIN' - # Fallthrough to the host SID if no domain SID was returned - unless domain_sid - print_error("No domain SID identified, falling back to the local SID...") - end - domain_sid || host_sid - end - - min_rid = datastore['MinRID'] - # Brute force through a common RID range - - min_rid.upto(datastore['MaxRID']) do |rid| - - stub = - phandle + - NDR.long(1) + - NDR.long(rand(0x10000000)) + - NDR.long(1) + - NDR.long(rand(0x10000000)) + - NDR.long(5) + - smb_pack_sid(target_sid) + - NDR.long(rid) + - NDR.long(0) + - NDR.long(0) + - NDR.long(1) + - NDR.long(0) - - dcerpc.call(15, stub) - resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil - - # Skip the "not mapped" error message - if(resp and resp[-4,4].unpack("V")[0] == 0xc0000073) - next - end - - # Stop if we are seeing access denied - if(resp and resp[-4,4].unpack("V")[0] == 0xc0000022) - break + pipe_info = "PIPE(#{lsarpc_pipe.name})" + local_info = "LOCAL(#{info[:local][:name]} - #{info[:local][:sid]})" + domain_info = "DOMAIN(#{info[:domain][:name]} - #{info[:domain][:sid]})" + all_info = "#{pipe_info} #{local_info} #{domain_info}" + print_status(all_info) + + target_sid = case action.name.upcase + when 'LOCAL' + info[:local][:sid] == 'null' ? info[:domain][:sid] : info[:local][:sid] + when 'DOMAIN' + # Fallthrough to the host SID if no domain SID was returned + if info[:domain][:sid] == 'null' + print_error 'No domain SID identified, falling back to the local SID...' + info[:local][:sid] + else + info[:domain][:sid] + end + end + + min_rid = datastore['MinRID'] + max_rid = datastore['MaxRID'] + + # Brute force through a common RID range + min_rid.upto(max_rid) do |rid| + print "%bld%blu[*]%clr Trying RID #{rid} / #{max_rid}\r" + begin + sid = "#{target_sid}-#{rid}" + sids = lookup_sids(policy_handle, sid, endpoint::LSAP_LOOKUP_WKSTA) + sids.each do |sid| + sids_table << [ map_security_principal_to_string(sid[:type]), sid[:name], rid ] end - - utype,uname = smb_parse_sid_lookup(resp) - case utype - when 1 - print_status("USER=#{uname} RID=#{rid}") - domain[:users][rid] = uname - when 2 - domain[:groups][rid] = uname - print_status("GROUP=#{uname} RID=#{rid}") - else - print_status("TYPE=#{utype} NAME=#{uname} rid=#{rid}") + rescue RubySMB::Dcerpc::Error::LsarpcError => e + # Ignore unmapped RIDs + unless e.message.match?(/STATUS_NONE_MAPPED/) || e.message.match?(/STATUS_SOME_MAPPED/) + wlog e end end + end - # Store the domain information - report_note( - :host => ip, - :proto => 'tcp', - :port => rport, - :type => 'smb.domain.lookupsid', - :data => domain - ) - - print_status("#{domain[:name].upcase} [#{domain[:users].keys.map{|k| domain[:users][k]}.join(", ")} ]") - disconnect - return + print_line + print_status sids_table.to_s - rescue ::Timeout::Error - rescue ::Interrupt - raise $! - rescue ::Rex::ConnectionError - rescue ::Rex::Proto::SMB::Exceptions::LoginError - next - rescue ::Exception => e - print_line("Error: #{e.class} #{e}") - end - end + rescue ::Timeout::Error + rescue ::Exception => e + print_error("Error: #{e.class} #{e}") + ensure + close_policy(policy_handle) + disconnect_lsarpc + disconnect_ipc(ipc_tree) end end diff --git a/spec/acceptance/smb_spec.rb b/spec/acceptance/smb_spec.rb index 8fa770c636f9..2d5ab53d79f2 100644 --- a/spec/acceptance/smb_spec.rb +++ b/spec/acceptance/smb_spec.rb @@ -28,23 +28,21 @@ targets: [:session], skipped: false, }, - # Flaky: - # Error: RubySMB::Error::UnexpectedStatusCode The server responded with an unexpected status code: STATUS_PIPE_BROKEN - # { - # name: "auxiliary/scanner/smb/smb_lookupsid", - # platforms: [:linux, :osx, :windows], - # targets: [:session, :rhost], - # skipped: false, - # lines: { - # all: { - # required: [ - # "GROUP=None", - # "USER=nobody", - # "PIPE(LSARPC) LOCAL", - # ], - # }, - # } - # }, + { + name: "auxiliary/scanner/smb/smb_lookupsid", + platforms: [:linux, :osx, :windows], + targets: [:session, :rhost], + skipped: false, + lines: { + all: { + required: [ + "PIPE(LSARPC) LOCAL", + /User( *)(Administrator|nobody)/, + /Group( *)(None|Domain (Admins|Users|Guests|Computers))/, + ], + }, + } + }, { name: "auxiliary/scanner/smb/smb_enumusers", platforms: [:linux, :osx, :windows], From 138a553b36c604f920c19fd91ef1f44be8b6d168 Mon Sep 17 00:00:00 2001 From: sjanusz-r7 Date: Mon, 13 May 2024 18:13:07 +0100 Subject: [PATCH 3/5] Add support for configurable RPORT, session & default rports to lookupsid --- lib/msf/core/exploit/remote/ms_icpr.rb | 2 +- lib/msf/core/exploit/remote/ms_lsarpc.rb | 14 +-- lib/msf/core/exploit/remote/ms_samr.rb | 2 +- .../auxiliary/scanner/smb/smb_lookupsid.rb | 117 ++++++++++++++---- spec/acceptance/smb_spec.rb | 2 +- 5 files changed, 99 insertions(+), 38 deletions(-) diff --git a/lib/msf/core/exploit/remote/ms_icpr.rb b/lib/msf/core/exploit/remote/ms_icpr.rb index 339e8d6e1a30..e85877feff73 100644 --- a/lib/msf/core/exploit/remote/ms_icpr.rb +++ b/lib/msf/core/exploit/remote/ms_icpr.rb @@ -11,7 +11,7 @@ module Msf module Exploit::Remote::MsIcpr - include Msf::Exploit::Remote::MsIpc + include Msf::Exploit::Remote::SMB::Client::Ipc include Msf::Exploit::Remote::DCERPC # [2.2.2.7.7.4 szOID_NTDS_CA_SECURITY_EXT](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-wcce/e563cff8-1af6-4e6f-a655-7571ca482e71) diff --git a/lib/msf/core/exploit/remote/ms_lsarpc.rb b/lib/msf/core/exploit/remote/ms_lsarpc.rb index d791e9d515b7..42c70d3cc827 100644 --- a/lib/msf/core/exploit/remote/ms_lsarpc.rb +++ b/lib/msf/core/exploit/remote/ms_lsarpc.rb @@ -88,7 +88,7 @@ def close_policy(policy_handle) def disconnect_lsarpc begin self.lsarpc_pipe.close if self.lsarpc_pipe&.is_connected? - rescue RubySMB::Error::UnexpectedStatusCode => e + rescue RubySMB::Error::UnexpectedStatusCode, RubySMB::Error::CommunicationError => e wlog e end end @@ -97,16 +97,14 @@ def disconnect_lsarpc def connect_lsarpc(tree) begin - vprint_status('Connecting to LSARPC') - self.lsarpc_pipe = tree.open_file(filename: 'LSARPC', write: true, read: true) + vprint_status('Connecting to Local Security Authority Remote Protocol') + self.lsarpc_pipe = tree.open_file(filename: 'lsarpc', write: true, read: true) - raise MsLsarpcConnectionError.new('Could not open LSARPC pipe on remote SMB server.') unless lsarpc_pipe + raise MsLsarpcConnectionError.new('Could not open lsarpc pipe on remote SMB server.') unless lsarpc_pipe - self.lsarpc_pipe.extend(LSARPC_ENDPOINT) unless lsarpc_pipe.is_a?(LSARPC_ENDPOINT) - - vprint_status('Binding to \\LSARPC...') + vprint_status('Binding to \\lsarpc...') self.lsarpc_pipe.bind(endpoint: LSARPC_ENDPOINT) - vprint_good('Bound to \\LSARPC') + vprint_good('Bound to \\lsarpc') self.lsarpc_pipe rescue RubySMB::Dcerpc::Error::FaultError => e diff --git a/lib/msf/core/exploit/remote/ms_samr.rb b/lib/msf/core/exploit/remote/ms_samr.rb index 966f8fa77131..13923ff556bd 100644 --- a/lib/msf/core/exploit/remote/ms_samr.rb +++ b/lib/msf/core/exploit/remote/ms_samr.rb @@ -8,7 +8,7 @@ module Msf module Exploit::Remote::MsSamr - include Msf::Exploit::Remote::MsIpc + include Msf::Exploit::Remote::SMB::Client::Ipc class MsSamrError < StandardError; end class MsSamrConnectionError < MsSamrError; end diff --git a/modules/auxiliary/scanner/smb/smb_lookupsid.rb b/modules/auxiliary/scanner/smb/smb_lookupsid.rb index e3eefaad7fd7..c10e2aee2ba4 100644 --- a/modules/auxiliary/scanner/smb/smb_lookupsid.rb +++ b/modules/auxiliary/scanner/smb/smb_lookupsid.rb @@ -46,19 +46,41 @@ def initialize ) end - # Fingerprint a single host - def run_host(ip) - sids_table = Rex::Text::Table.new( - 'Indent' => 4, - 'Header' => "SMB Lookup SIDs Output", - 'Columns' => - [ - 'Type', - 'Name', - 'RID' - ], - 'SortIndex' => 2, # Sort by RID - ) + def rport + @rport + end + + def smb_direct + @smb_direct + end + + def connect(*args, **kwargs) + super(*args, **kwargs, direct: @smb_direct) + end + + def run_session + smb_services = [{ port: self.simple.peerport, direct: self.simple.direct }] + smb_services.map { |smb_service| run_service(smb_service[:port], smb_service[:direct]) } + end + + def run_rhost + if datastore['RPORT'].blank? || datastore['RPORT'] == 0 + smb_services = [ + { port: 445, direct: true }, + { port: 139, direct: false } + ] + else + smb_services = [ + { port: datastore['RPORT'], direct: datastore['SMBDirect'] } + ] + end + + smb_services.map { |smb_service| run_service(smb_service[:port], smb_service[:direct]) } + end + + def run_service(port, direct) + @rport = port + @smb_direct = direct ipc_tree = connect_ipc lsarpc_pipe = connect_lsarpc(ipc_tree) @@ -95,21 +117,23 @@ def run_host(ip) print_status(all_info) target_sid = case action.name.upcase - when 'LOCAL' - info[:local][:sid] == 'null' ? info[:domain][:sid] : info[:local][:sid] - when 'DOMAIN' - # Fallthrough to the host SID if no domain SID was returned - if info[:domain][:sid] == 'null' - print_error 'No domain SID identified, falling back to the local SID...' - info[:local][:sid] - else - info[:domain][:sid] - end - end + when 'LOCAL' + info[:local][:sid] == 'null' ? info[:domain][:sid] : info[:local][:sid] + when 'DOMAIN' + # Fallthrough to the host SID if no domain SID was returned + if info[:domain][:sid] == 'null' + print_error 'No domain SID identified, falling back to the local SID...' + info[:local][:sid] + else + info[:domain][:sid] + end + end min_rid = datastore['MinRID'] max_rid = datastore['MaxRID'] + output = [] + # Brute force through a common RID range min_rid.upto(max_rid) do |rid| print "%bld%blu[*]%clr Trying RID #{rid} / #{max_rid}\r" @@ -117,7 +141,7 @@ def run_host(ip) sid = "#{target_sid}-#{rid}" sids = lookup_sids(policy_handle, sid, endpoint::LSAP_LOOKUP_WKSTA) sids.each do |sid| - sids_table << [ map_security_principal_to_string(sid[:type]), sid[:name], rid ] + output << [ map_security_principal_to_string(sid[:type]), sid[:name], rid ] end rescue RubySMB::Dcerpc::Error::LsarpcError => e # Ignore unmapped RIDs @@ -127,9 +151,11 @@ def run_host(ip) end end - print_line - print_status sids_table.to_s + output + rescue Msf::Exploit::Remote::SMB::Client::Ipc::SmbIpcAuthenticationError => e + print_warning e.message + nil rescue ::Timeout::Error rescue ::Exception => e print_error("Error: #{e.class} #{e}") @@ -138,4 +164,41 @@ def run_host(ip) disconnect_lsarpc disconnect_ipc(ipc_tree) end + + def format_results(results) + sids_table = Rex::Text::Table.new( + 'Indent' => 4, + 'Header' => "SMB Lookup SIDs Output", + 'Columns' => + [ + 'Type', + 'Name', + 'RID' + ], + 'SortIndex' => 2, # Sort by RID + ) + + # Each result contains 0 or more arrays containing: SID Type, Name, RID + results.compact.each do |result_set| + result_set.each { |result| sids_table << result } + end + + sids_table + end + + # Fingerprint a single host + def run_host(_ip) + if session + self.simple = session.simple_client + results = run_session + else + results = run_rhost + end + + results_table = format_results(results) + results_table.rows = results_table.rows.uniq # Remove potentially duplicate entries from port 139 & 445 + + print_line + print_line results_table.to_s + end end diff --git a/spec/acceptance/smb_spec.rb b/spec/acceptance/smb_spec.rb index 2d5ab53d79f2..6c5078f40a13 100644 --- a/spec/acceptance/smb_spec.rb +++ b/spec/acceptance/smb_spec.rb @@ -36,7 +36,7 @@ lines: { all: { required: [ - "PIPE(LSARPC) LOCAL", + "PIPE(lsarpc) LOCAL", /User( *)(Administrator|nobody)/, /Group( *)(None|Domain (Admins|Users|Guests|Computers))/, ], From 34ab7d97b2777c24ce4f476233fb85ee363b4cf3 Mon Sep 17 00:00:00 2001 From: sjanusz-r7 Date: Thu, 16 May 2024 10:45:25 +0100 Subject: [PATCH 4/5] Follow MS-LSAD and MS-LSAT spec for LSARPC & LookupSids --- .../remote/{ms_lsarpc.rb => ms_lsad.rb} | 12 +--------- lib/msf/core/exploit/remote/ms_lsat.rb | 22 +++++++++++++++++++ .../auxiliary/scanner/smb/smb_lookupsid.rb | 3 ++- 3 files changed, 25 insertions(+), 12 deletions(-) rename lib/msf/core/exploit/remote/{ms_lsarpc.rb => ms_lsad.rb} (91%) create mode 100644 lib/msf/core/exploit/remote/ms_lsat.rb diff --git a/lib/msf/core/exploit/remote/ms_lsarpc.rb b/lib/msf/core/exploit/remote/ms_lsad.rb similarity index 91% rename from lib/msf/core/exploit/remote/ms_lsarpc.rb rename to lib/msf/core/exploit/remote/ms_lsad.rb index 42c70d3cc827..e481c9b9ffb4 100644 --- a/lib/msf/core/exploit/remote/ms_lsarpc.rb +++ b/lib/msf/core/exploit/remote/ms_lsad.rb @@ -6,7 +6,7 @@ module Msf - module Exploit::Remote::MsLsarpc + module Exploit::Remote::MsLsad include Msf::Exploit::Remote::SMB::Client::Ipc @@ -69,16 +69,6 @@ def query_information_policy(policy_handle, information_class) ) end - def lookup_sids(policy_handle, sids, lookup_level) - sids = [sids] unless sids.is_a?(Array) - - self.lsarpc_pipe.lsar_lookup_sids( - policy_handle: policy_handle, - sids: sids, - lookup_level: lookup_level - ) - end - def close_policy(policy_handle) self.lsarpc_pipe.lsar_close_handle( policy_handle: policy_handle diff --git a/lib/msf/core/exploit/remote/ms_lsat.rb b/lib/msf/core/exploit/remote/ms_lsat.rb new file mode 100644 index 000000000000..1b979f0c74b0 --- /dev/null +++ b/lib/msf/core/exploit/remote/ms_lsat.rb @@ -0,0 +1,22 @@ +### +# +# This mixin provides methods to look-up security identifiers on the remote SMB server. +# +# -*- coding: binary -*- + +module Msf + + module Exploit::Remote::MsLsat + + def lookup_sids(policy_handle, sids, lookup_level) + sids = [sids] unless sids.is_a?(Array) + + self.lsarpc_pipe.lsar_lookup_sids( + policy_handle: policy_handle, + sids: sids, + lookup_level: lookup_level + ) + end + + end +end diff --git a/modules/auxiliary/scanner/smb/smb_lookupsid.rb b/modules/auxiliary/scanner/smb/smb_lookupsid.rb index c10e2aee2ba4..90bca3a7b913 100644 --- a/modules/auxiliary/scanner/smb/smb_lookupsid.rb +++ b/modules/auxiliary/scanner/smb/smb_lookupsid.rb @@ -7,7 +7,8 @@ class MetasploitModule < Msf::Auxiliary # Exploit mixins should be called first - include Msf::Exploit::Remote::MsLsarpc + include Msf::Exploit::Remote::MsLsad + include Msf::Exploit::Remote::MsLsat include Msf::Exploit::Remote::DCERPC # Scanner mixin should be near last From 638ad36b1279f7e34e85d90655ad0967cc86f02f Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 16 May 2024 17:08:07 -0400 Subject: [PATCH 5/5] Fixed names that were missed while refactoring --- lib/msf/core/exploit/remote/ms_lsad.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/msf/core/exploit/remote/ms_lsad.rb b/lib/msf/core/exploit/remote/ms_lsad.rb index e481c9b9ffb4..cdd16e326236 100644 --- a/lib/msf/core/exploit/remote/ms_lsad.rb +++ b/lib/msf/core/exploit/remote/ms_lsad.rb @@ -10,10 +10,10 @@ module Exploit::Remote::MsLsad include Msf::Exploit::Remote::SMB::Client::Ipc - class MsLsarpcError < StandardError; end - class MsLsarpcConnectionError < MsLsarpcError; end - class MsLsarpcAuthenticationError < MsLsarpcError; end - class MsLsarpcUnexpectedReplyError < MsLsarpcError; end + class MsLsadError < StandardError; end + class MsLsadConnectionError < MsLsadError; end + class MsLsadAuthenticationError < MsLsadError; end + class MsLsadUnexpectedReplyError < MsLsadError; end LSA_UUID = '12345778-1234-abcd-ef00-0123456789ab'.freeze LSA_VERS = '0.0'.freeze @@ -87,10 +87,10 @@ def disconnect_lsarpc def connect_lsarpc(tree) begin - vprint_status('Connecting to Local Security Authority Remote Protocol') + vprint_status('Connecting to Local Security Authority (LSA) Remote Protocol') self.lsarpc_pipe = tree.open_file(filename: 'lsarpc', write: true, read: true) - raise MsLsarpcConnectionError.new('Could not open lsarpc pipe on remote SMB server.') unless lsarpc_pipe + raise MsLsadConnectionError.new('Could not open lsarpc pipe on remote SMB server.') unless lsarpc_pipe vprint_status('Binding to \\lsarpc...') self.lsarpc_pipe.bind(endpoint: LSARPC_ENDPOINT) @@ -99,7 +99,7 @@ def connect_lsarpc(tree) self.lsarpc_pipe rescue RubySMB::Dcerpc::Error::FaultError => e elog(e.message, error: e) - raise MsLsarpcUnexpectedReplyError, "Connection failed (DCERPC fault: #{e.status_name})" + raise MsLsadUnexpectedReplyError, "Connection failed (DCERPC fault: #{e.status_name})" end end