From eec72b8f546554db98168ef09471e8a0a84bf2ba Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 15 Apr 2024 17:56:06 -0400 Subject: [PATCH 1/6] Start refactoring smb_enumusers to use RubySMB --- Gemfile | 2 + Gemfile.lock | 19 +- .../auxiliary/scanner/smb/smb_enumusers.rb | 319 ++---------------- 3 files changed, 35 insertions(+), 305 deletions(-) diff --git a/Gemfile b/Gemfile index 83b7b2811fbd..84f8040c27e6 100644 --- a/Gemfile +++ b/Gemfile @@ -53,3 +53,5 @@ group :test do gem 'timecop' end +# remove after https://github.com/rapid7/ruby_smb/pull/266 is landed +gem 'ruby_smb', git: 'https://github.com/zeroSteiner/ruby_smb', branch: 'feat/dcerpc/samr/2' diff --git a/Gemfile.lock b/Gemfile.lock index 5515d3d4de0d..2499a477d6ef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,15 @@ +GIT + remote: https://github.com/zeroSteiner/ruby_smb + revision: 7e8c8c89b71674399e16242326532e319c5db6f5 + branch: feat/dcerpc/samr/2 + specs: + ruby_smb (3.3.6) + bindata (= 2.4.15) + openssl-ccm + openssl-cmac + rubyntlm + windows_error (>= 0.1.4) + PATH remote: . specs: @@ -475,12 +487,6 @@ GEM ruby-progressbar (1.13.0) ruby-rc4 (0.1.5) ruby2_keywords (0.0.5) - ruby_smb (3.3.5) - bindata (= 2.4.15) - openssl-ccm - openssl-cmac - rubyntlm - windows_error (>= 0.1.4) rubyntlm (0.6.3) rubyzip (2.3.2) sawyer (0.9.2) @@ -568,6 +574,7 @@ DEPENDENCIES rspec-rerun rubocop ruby-prof (= 1.4.2) + ruby_smb! simplecov (= 0.18.2) test-prof timecop diff --git a/modules/auxiliary/scanner/smb/smb_enumusers.rb b/modules/auxiliary/scanner/smb/smb_enumusers.rb index 13b55fcd6940..84588a7592f6 100644 --- a/modules/auxiliary/scanner/smb/smb_enumusers.rb +++ b/modules/auxiliary/scanner/smb/smb_enumusers.rb @@ -10,6 +10,7 @@ class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::SMB::Client::Authenticated include Msf::Exploit::Remote::DCERPC + include Msf::Exploit::Remote::MsSamr # Scanner mixin should be near last include Msf::Auxiliary::Report @@ -32,8 +33,6 @@ def initialize [ OptBool.new('DB_ALL_USERS', [ false, "Add all enumerated usernames to the database", false ]), ]) - - deregister_options('RPORT') end def rport @@ -44,311 +43,33 @@ def smb_direct @smbdirect || super 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_pack_sid(str) - [1,5,0].pack('CCv') + str.split('-').map{|x| x.to_i}.pack('NVVVV') - end - - def smb_parse_sam_domains(data) - ret = [] - idx = 0 - - cnt = data[8, 4].unpack("V")[0] - return ret if cnt == 0 - idx += 20 - idx += 12 * cnt - - 1.upto(cnt) do - v = data[idx,data.length].unpack('V*') - l = v[2] * 2 - - while(l % 4 != 0) - l += 1 - end - - idx += 12 - ret << data[idx, v[2] * 2].gsub("\x00", '') - idx += l - end - ret - end - - def smb_parse_sam_users(data) - ret = {} - rid = [] - idx = 0 - - cnt = data[8, 4].unpack("V")[0] - return ret if cnt == 0 - idx += 20 - - 1.upto(cnt) do - v = data[idx,12].unpack('V3') - rid << v[0] - idx += 12 - end - - 1.upto(cnt) do - v = data[idx,32].unpack('V*') - l = v[2] * 2 - - while(l % 4 != 0) - l += 1 - end - - uid = rid.shift - - idx += 12 - ret[uid] = data[idx, v[2] * 2].gsub("\x00", '') - idx += l - end - - ret - end - - @@sam_uuid = '12345778-1234-abcd-ef00-0123456789ac' - @@sam_vers = '1.0' - @@sam_pipes = %W{ SAMR LSARPC NETLOGON BROWSER SRVSVC } - # 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 - - sam_pipe = nil - sam_handle = nil - begin - # Find the SAM pipe - sam_pipe = smb_find_dcerpc_pipe(@@sam_uuid, @@sam_vers, @@sam_pipes) - break if not sam_pipe - - # Connect4 - stub = - NDR.uwstring("\\\\" + simple.address) + - NDR.long(2) + - NDR.long(0x30) - - dcerpc.call(62, stub) - resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil + tree = connect_ipc - if ! (resp and resp.length == 24) - print_error("Invalid response from the Connect5 request") - disconnect - return - end + samr_con = connect_samr(tree) - phandle = resp[0,20] - perror = resp[20,4].unpack("V")[0] - - if(perror == 0xc0000022) - disconnect - return - end - - if(perror != 0) - print_error("Received error #{"0x%.8x" % perror} from the OpenPolicy2 request") - disconnect - return - end - - # EnumDomains - stub = phandle + NDR.long(0) + NDR.long(8192) - dcerpc.call(6, stub) - resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil - domlist = smb_parse_sam_domains(resp) - domains = {} - - # LookupDomain - domlist.each do |domain| - next if domain == 'Builtin' - - # Round up the name to match NDR.uwstring() behavior - dlen = (domain.length + 1) * 2 - - # The SAM functions are picky on Windows 2000 - stub = - phandle + - [(domain.length + 0) * 2].pack("v") + # NameSize - [(domain.length + 1) * 2].pack("v") + # NameLen (includes null) - NDR.long(rand(0x100000000)) + - [domain.length + 1].pack("V") + # MaxCount (includes null) - NDR.long(0) + - [domain.length + 0].pack("V") + # ActualCount (ignores null) - Rex::Text.to_unicode(domain) # No null appended - - dcerpc.call(5, stub) - resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil - raw_sid = resp[12, 20] - txt_sid = raw_sid.unpack("NVVVV").join("-") - - domains[domain] = { - :sid_raw => raw_sid, - :sid_txt => txt_sid - } - end - - - # OpenDomain, QueryDomainInfo, CloseDomain - domains.each_key do |domain| - - # Open - stub = - phandle + - NDR.long(0x00000305) + - NDR.long(4) + - [1,4,0].pack('CvC') + - domains[domain][:sid_raw] - - dcerpc.call(7, stub) - resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil - dhandle = resp[0,20] - derror = resp[20,4].unpack("V")[0] - - # Catch access denied replies to OpenDomain - if(derror != 0) - next - end - - # Password information - stub = dhandle + [0x01].pack('v') - dcerpc.call(8, stub) - resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil - if(resp and resp[-4,4].unpack('V')[0] == 0) - mlen,hlen = resp[8,4].unpack('vv') - domains[domain][:pass_min] = mlen - domains[domain][:pass_min_history] = hlen - end - - # Server Role - stub = dhandle + [0x07].pack('v') - dcerpc.call(8, stub) - if(resp and resp[-4,4].unpack('V')[0] == 0) - resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil - domains[domain][:server_role] = resp[8,2].unpack('v')[0] - end - - # Lockout Threshold - stub = dhandle + [12].pack('v') - dcerpc.call(8, stub) - resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil - - if(resp and resp[-4,4].unpack('V')[0] == 0) - lduration = resp[8,8] - lwindow = resp[16,8] - lthresh = resp[24, 2].unpack('v')[0] - - domains[domain][:lockout_threshold] = lthresh - domains[domain][:lockout_duration] = Rex::Proto::SMB::Utils.time_smb_to_unix(*(lduration.unpack('V2'))) - domains[domain][:lockout_window] = Rex::Proto::SMB::Utils.time_smb_to_unix(*(lwindow.unpack('V2'))) - end - - # Users - stub = dhandle + NDR.long(0) + NDR.long(0x10) + NDR.long(1024*1024) - dcerpc.call(13, stub) - resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil - if(resp and resp[-4,4].unpack('V')[0] == 0) - domains[domain][:users] = smb_parse_sam_users(resp) - end - - - # Close Domain - dcerpc.call(1, dhandle) - end - - # Close Policy - dcerpc.call(1, phandle) - - - domains.each_key do |domain| - - # Delete the no longer used raw SID value - domains[domain].delete(:sid_raw) - - # Store the domain name itself - domains[domain][:name] = domain + lockout_info = samr_con.samr.samr_query_information_domain( + domain_handle: samr_con.domain_handle, + info_class: RubySMB::Dcerpc::Samr::DOMAIN_LOCKOUT_INFORMATION + ) - # Store the domain information - report_note( - :host => simple.address, - :proto => 'tcp', - :port => rport, - :type => 'smb.domain.enumusers', - :data => domains[domain] - ) + password_info = samr_con.samr.samr_query_information_domain( + domain_handle: samr_con.domain_handle, + info_class: RubySMB::Dcerpc::Samr::DOMAIN_PASSWORD_INFORMATION + ) - users = domains[domain][:users] || {} - extra = "" - if (domains[domain][:lockout_threshold]) - extra = "( " - extra << "LockoutTries=#{domains[domain][:lockout_threshold]} " - extra << "PasswordMin=#{domains[domain][:pass_min]} " - extra << ")" - end - print_good("#{domain.upcase} [ #{users.keys.map{|k| users[k]}.join(", ")} ] #{extra}") - if datastore['DB_ALL_USERS'] - users.each { |user| - store_username(user, domain, simple.address, rport, resp) - } - end - end + users = samr_con.samr.samr_enumerate_users_in_domain( + domain_handle: samr_con.domain_handle, + user_account_control: RubySMB::Dcerpc::Samr::USER_NORMAL_ACCOUNT + ) - # cleanup - disconnect - return - rescue ::Timeout::Error - rescue ::Interrupt - raise $! - rescue ::Rex::ConnectionError - rescue ::Rex::Proto::SMB::Exceptions::LoginError - next - rescue ::Exception => e - print_line("Error: #{simple.address} #{e.class} #{e}") - end - end + print_good("#{samr_con.domain_name} [ #{users.values.map { |name| name.encode('UTF-8') }.join(', ') } ] ( LockoutTries=#{lockout_info.lockout_threshold} PasswordMin=#{password_info.min_password_length} )") + ensure + samr_con.samr.close_handle(samr_con.domain_handle) if samr_con.domain_handle + samr_con.samr.close_handle(samr_con.server_handle) if samr_con.server_handle end - def store_username(username, domain, ip, rport, resp) service_data = { address: ip, @@ -362,7 +83,7 @@ def store_username(username, domain, ip, rport, resp) credential_data = { origin_type: :service, module_fullname: fullname, - username: username[1], + username: username, realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, realm_value: domain, }.merge(service_data) From a008288e05f8a1cd2d5a5666c81c31a931ce3285 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 16 Apr 2024 15:17:23 -0400 Subject: [PATCH 2/6] Readd support for multiple ports --- lib/msf/core/exploit/remote/ms_samr.rb | 4 +- lib/msf/core/exploit/remote/smb/client.rb | 10 +-- .../auxiliary/scanner/smb/smb_enumusers.rb | 61 ++++++++++++++----- 3 files changed, 55 insertions(+), 20 deletions(-) diff --git a/lib/msf/core/exploit/remote/ms_samr.rb b/lib/msf/core/exploit/remote/ms_samr.rb index caae602c879c..7afa3ad0c26a 100644 --- a/lib/msf/core/exploit/remote/ms_samr.rb +++ b/lib/msf/core/exploit/remote/ms_samr.rb @@ -204,7 +204,7 @@ def connect_samr(tree) raise MsSamrUnexpectedReplyError, "Connection failed (DCERPC fault: #{e.status_name})" end - if datastore['SMBDomain'].blank? || datastore['SMBDomain'] == '.' + if domain.blank? || domain == '.' all_domains = samr.samr_enumerate_domains_in_sam_server(server_handle: server_handle).map(&:to_s).map(&:encode) all_domains.delete('Builtin') if all_domains.empty? @@ -217,7 +217,7 @@ def connect_samr(tree) domain_name = all_domains.first print_status("Using automatically identified domain: #{domain_name}") else - domain_name = datastore['SMBDomain'] + domain_name = domain end domain_sid = samr.samr_lookup_domain(server_handle: server_handle, name: domain_name) diff --git a/lib/msf/core/exploit/remote/smb/client.rb b/lib/msf/core/exploit/remote/smb/client.rb index 786154205ebb..3724f39184bc 100644 --- a/lib/msf/core/exploit/remote/smb/client.rb +++ b/lib/msf/core/exploit/remote/smb/client.rb @@ -94,7 +94,7 @@ def initialize(info = {}) # # @param (see Exploit::Remote::Tcp#connect) # @return (see Exploit::Remote::Tcp#connect) - def connect(global=true, versions: [], backend: nil) + def connect(global=true, versions: [], backend: nil, direct: nil) if versions.nil? || versions.empty? versions = datastore['SMB::ProtocolVersion'].split(',').map(&:strip).reject(&:blank?).map(&:to_i) # if the user explicitly set the protocol version to 1, still use ruby_smb @@ -108,9 +108,11 @@ def connect(global=true, versions: [], backend: nil) # Disable direct SMB when SMBDirect has not been set # and the destination port is configured as 139 - direct = smb_direct - if(datastore.default?('SMBDirect') and rport.to_i == 139) - direct = false + if direct.nil? + direct = smb_direct + if datastore.default?('SMBDirect') and rport.to_i == 139 + direct = false + end end c = Rex::Proto::SMB::SimpleClient.new(s, direct, versions, always_encrypt: datastore['SMB::AlwaysEncrypt'], backend: backend) diff --git a/modules/auxiliary/scanner/smb/smb_enumusers.rb b/modules/auxiliary/scanner/smb/smb_enumusers.rb index 84588a7592f6..fe35f2e020bb 100644 --- a/modules/auxiliary/scanner/smb/smb_enumusers.rb +++ b/modules/auxiliary/scanner/smb/smb_enumusers.rb @@ -4,15 +4,7 @@ ## 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::DCERPC include Msf::Exploit::Remote::MsSamr - - # Scanner mixin should be near last include Msf::Auxiliary::Report include Msf::Auxiliary::Scanner @@ -21,7 +13,7 @@ class MetasploitModule < Msf::Auxiliary def initialize super( 'Name' => 'SMB User Enumeration (SAM EnumUsers)', - 'Description' => 'Determine what local users exist via the SAM RPC service', + 'Description' => 'Determine what users exist via the SAM RPC service', 'Author' => 'hdm', 'License' => MSF_LICENSE, 'DefaultOptions' => { @@ -39,14 +31,56 @@ def rport @rport || super end - def smb_direct - @smbdirect || super + def domain + @smb_domain || super end - # Fingerprint a single host - def run_host(ip) + def connect(*args, **kwargs) + super(*args, **kwargs, direct: @smb_direct) + end + + def run_host(_ip) + if datastore['RPORT'].blank? || datastore['RPORT'] == 0 + smb_services = [ + { port: 139, direct: false }, + { port: 445, direct: true } + ] + else + smb_services = [ + { port: datastore['RPORT'], direct: datastore['SMBDirect'] } + ] + end + + smb_services.each do |smb_service| + run_service(smb_service[:port], smb_service[:direct]) + end + end + + def run_service(port, direct) + @rport = port + @smb_direct = direct + tree = connect_ipc + run_service_domain(tree) + run_service_domain(tree, smb_domain: 'Builtin') + rescue ::Timeout::Error + rescue ::Interrupt + raise $! + rescue ::Rex::ConnectionError + rescue ::Rex::Proto::SMB::Exceptions::LoginError + return + rescue ::Exception => e + print_error("Error: #{e.class} #{e}") + ensure + tree.disconnect! if tree + disconnect + end + + # Fingerprint a single host + def run_service_domain(tree, smb_domain: nil) + @smb_domain = smb_domain + samr_con = connect_samr(tree) lockout_info = samr_con.samr.samr_query_information_domain( @@ -95,5 +129,4 @@ def store_username(username, domain, ip, rport, resp) create_credential_login(login_data) end - end From 837e503170a17f581dee235bff385e33691ad2a5 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 16 Apr 2024 16:43:30 -0400 Subject: [PATCH 3/6] Refactor the MsSamr mixin to split it out --- lib/msf/core/exploit/remote/ms_samr.rb | 188 ---------------- .../core/exploit/remote/ms_samr/computer.rb | 203 ++++++++++++++++++ .../admin/dcerpc/cve_2022_26923_certifried.rb | 2 +- .../auxiliary/admin/dcerpc/samr_computer.rb | 2 +- 4 files changed, 205 insertions(+), 190 deletions(-) create mode 100644 lib/msf/core/exploit/remote/ms_samr/computer.rb diff --git a/lib/msf/core/exploit/remote/ms_samr.rb b/lib/msf/core/exploit/remote/ms_samr.rb index 7afa3ad0c26a..69c602bcd419 100644 --- a/lib/msf/core/exploit/remote/ms_samr.rb +++ b/lib/msf/core/exploit/remote/ms_samr.rb @@ -9,7 +9,6 @@ module Msf module Exploit::Remote::MsSamr include Msf::Exploit::Remote::SMB::Client::Authenticated - include Msf::Auxiliary::Report class MsSamrError < StandardError; end class MsSamrConnectionError < MsSamrError; end @@ -19,147 +18,8 @@ class MsSamrUnexpectedReplyError < MsSamrError; end class MsSamrUnknownError < MsSamrError; end class MsSamrBadConfigError < MsSamrError; end - ComputerInfo = Struct.new(:name, :password) SamrConnection = Struct.new(:samr, :server_handle, :domain_handle, :domain_name) - def initialize(info = {}) - super - - register_options([ - OptString.new('COMPUTER_NAME', [ false, 'The computer name' ]), - OptString.new('COMPUTER_PASSWORD', [ false, 'The password for the new computer' ]), - ], Msf::Exploit::Remote::MsSamr) - end - - def add_computer(opts = {}) - tree = opts[:tree] || connect_ipc - - samr_con = connect_samr(tree) - - computer_name = opts[:computer_name] || datastore['COMPUTER_NAME'] - if computer_name.blank? - computer_name = random_hostname - 4.downto(0) do |attempt| - break if samr_con.samr.samr_lookup_names_in_domain( - domain_handle: samr_con.domain_handle, - names: [ computer_name ] - ).nil? - - computer_name = random_hostname - raise MsSamrBadConfigError, 'Could not find an unused computer name.' if attempt == 0 - end - else - if samr_con.samr.samr_lookup_names_in_domain(domain_handle: samr_con.domain_handle, names: [ computer_name ]) - raise MsSamrBadConfigError, 'The specified computer name already exists.' - end - end - - result = samr_con.samr.samr_create_user2_in_domain( - domain_handle: samr_con.domain_handle, - name: computer_name, - account_type: RubySMB::Dcerpc::Samr::USER_WORKSTATION_TRUST_ACCOUNT, - desired_access: RubySMB::Dcerpc::Samr::USER_FORCE_PASSWORD_CHANGE | RubySMB::Dcerpc::Samr::MAXIMUM_ALLOWED - ) - - user_handle = result[:user_handle] - if datastore['COMPUTER_PASSWORD'].blank? - computer_password = Rex::Text.rand_text_alphanumeric(32) - else - computer_password = datastore['COMPUTER_PASSWORD'] - end - - user_info = RubySMB::Dcerpc::Samr::SamprUserInfoBuffer.new( - tag: RubySMB::Dcerpc::Samr::USER_INTERNAL4_INFORMATION_NEW, - member: RubySMB::Dcerpc::Samr::SamprUserInternal4InformationNew.new( - i1: { - password_expired: 1, - which_fields: RubySMB::Dcerpc::Samr::USER_ALL_NTPASSWORDPRESENT | RubySMB::Dcerpc::Samr::USER_ALL_PASSWORDEXPIRED - }, - user_password: { - buffer: RubySMB::Dcerpc::Samr::SamprEncryptedUserPasswordNew.encrypt_password( - computer_password, - @simple.client.application_key - ) - } - ) - ) - samr_con[:samr].samr_set_information_user2( - user_handle: user_handle, - user_info: user_info - ) - - user_info = RubySMB::Dcerpc::Samr::SamprUserInfoBuffer.new( - tag: RubySMB::Dcerpc::Samr::USER_CONTROL_INFORMATION, - member: RubySMB::Dcerpc::Samr::UserControlInformation.new( - user_account_control: RubySMB::Dcerpc::Samr::USER_WORKSTATION_TRUST_ACCOUNT - ) - ) - samr_con.samr.samr_set_information_user2( - user_handle: user_handle, - user_info: user_info - ) - print_good("Successfully created #{samr_con.domain_name}\\#{computer_name}") - print_good(" Password: #{computer_password}") - print_good(" SID: #{get_computer_sid(samr_con, computer_name)}") - report_creds(samr_con.domain_name, computer_name, computer_password) - - ComputerInfo.new(computer_name, computer_password) - - rescue RubySMB::Dcerpc::Error::SamrError => e - raise MsSamrUnknownError, "A DCERPC SAMR error occurred: #{e.message}" - ensure - if samr_con - samr_con.samr.close_handle(user_handle) if user_handle - samr_con.samr.close_handle(samr_con.domain_handle) if samr_con.domain_handle - samr_con.samr.close_handle(samr_con.server_handle) if samr_con.server_handle - end - end - - def delete_computer(opts = {}) - tree = opts[:tree] || connect_ipc - - samr_con = connect_samr(tree) - - computer_name = opts[:computer_name] || datastore['COMPUTER_NAME'] - if computer_name.blank? - raise MsSamrBadConfigError, 'Unable to delete the computer account since its name is unknown' - end - - details = samr_con.samr.samr_lookup_names_in_domain(domain_handle: samr_con.domain_handle, names: [ computer_name ]) - raise MsSamrBadConfigError, 'The specified computer was not found.' if details.nil? - details = details[computer_name] - - user_handle = samr_con.samr.samr_open_user(domain_handle: samr_con.domain_handle, user_id: details[:rid]) - samr_con.samr.samr_delete_user(user_handle: user_handle) - print_good('The specified computer has been deleted.') - rescue RubySMB::Dcerpc::Error::SamrError => e - # `user_handle` only needs to be closed if an error occurs in `samr_delete_user` - # If this method succeed, the server took care of closing the handle - samr_con.samr.close_handle(user_handle) if user_handle - raise MsSamrUnknownError, "Could not delete the computer #{computer_name}: #{e.message}" - ensure - samr_con.samr.close_handle(samr_con.domain_handle) if samr_con.domain_handle - samr_con.samr.close_handle(samr_con.server_handle) if samr_con.server_handle - end - - def lookup_computer(opts = {}) - tree = opts[:tree] || connect_ipc - - samr_con = connect_samr(tree) - - computer_name = opts[:computer_name] || datastore['COMPUTER_NAME'] - if computer_name.blank? - raise MsSamrBadConfigError, 'Unable to lookup the computer account since its name is unknown' - end - - sid = get_computer_sid(samr_con, computer_name) - print_good("Found #{samr_con.domain_name}\\#{computer_name} (SID: #{sid})") - ensure - samr_con.samr.close_handle(samr_con.domain_handle) if samr_con.domain_handle - samr_con.samr.close_handle(samr_con.server_handle) if samr_con.server_handle - end - - module_function def connect_ipc @@ -232,53 +92,5 @@ def connect_samr(tree) elog(e.message, error: e) raise MsSamrUnknownError, e.message end - - def random_hostname(prefix: 'DESKTOP') - "#{prefix}-#{Rex::Text.rand_base(8, '', ('A'..'Z').to_a + ('0'..'9').to_a)}$" - end - - def report_creds(domain, username, password) - service_data = { - address: datastore['RHOST'], - port: datastore['RPORT'], - service_name: 'smb', - protocol: 'tcp', - workspace_id: myworkspace_id - } - - credential_data = { - module_fullname: fullname, - origin_type: :service, - private_data: password, - private_type: :password, - username: username, - realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, - realm_value: domain - }.merge(service_data) - - credential_core = create_credential(credential_data) - - login_data = { - core: credential_core, - status: Metasploit::Model::Login::Status::UNTRIED - }.merge(service_data) - - create_credential_login(login_data) - end - - def get_computer_sid(samr_con, computer_name) - details = samr_con.samr.samr_lookup_names_in_domain( - domain_handle: samr_con.domain_handle, - names: [ computer_name ] - ) - raise MsSamrNotFoundError, 'The computer was not found.' if details.nil? - - details = details[computer_name] - samr_con.samr.samr_rid_to_sid( - object_handle: samr_con.domain_handle, - rid: details[:rid] - ).to_s - end - end end diff --git a/lib/msf/core/exploit/remote/ms_samr/computer.rb b/lib/msf/core/exploit/remote/ms_samr/computer.rb new file mode 100644 index 000000000000..a278bbd5f421 --- /dev/null +++ b/lib/msf/core/exploit/remote/ms_samr/computer.rb @@ -0,0 +1,203 @@ +### +# +# This mixin provides methods to add, delete and lookup computer accounts via MS-SAMR +# +# -*- coding: binary -*- + +module Msf + +module Exploit::Remote::MsSamr::Computer + + include Msf::Auxiliary::Report + include Msf::Exploit::Remote::MsSamr + + ComputerInfo = Struct.new(:name, :password) + + def initialize(info = {}) + super + + register_options([ + OptString.new('COMPUTER_NAME', [ false, 'The computer name' ]), + OptString.new('COMPUTER_PASSWORD', [ false, 'The password for the new computer' ]), + ], Msf::Exploit::Remote::MsSamr) + end + + def add_computer(opts = {}) + tree = opts[:tree] || connect_ipc + + samr_con = connect_samr(tree) + + computer_name = opts[:computer_name] || datastore['COMPUTER_NAME'] + if computer_name.blank? + computer_name = random_hostname + 4.downto(0) do |attempt| + break if samr_con.samr.samr_lookup_names_in_domain( + domain_handle: samr_con.domain_handle, + names: [ computer_name ] + ).nil? + + computer_name = random_hostname + raise MsSamrBadConfigError, 'Could not find an unused computer name.' if attempt == 0 + end + else + if samr_con.samr.samr_lookup_names_in_domain(domain_handle: samr_con.domain_handle, names: [ computer_name ]) + raise MsSamrBadConfigError, 'The specified computer name already exists.' + end + end + + result = samr_con.samr.samr_create_user2_in_domain( + domain_handle: samr_con.domain_handle, + name: computer_name, + account_type: RubySMB::Dcerpc::Samr::USER_WORKSTATION_TRUST_ACCOUNT, + desired_access: RubySMB::Dcerpc::Samr::USER_FORCE_PASSWORD_CHANGE | RubySMB::Dcerpc::Samr::MAXIMUM_ALLOWED + ) + + user_handle = result[:user_handle] + computer_password = opts[:computer_password] || datastore['COMPUTER_PASSWORD'] + if computer_password.blank? + computer_password = Rex::Text.rand_text_alphanumeric(32) + else + computer_password = datastore['COMPUTER_PASSWORD'] + end + + user_info = RubySMB::Dcerpc::Samr::SamprUserInfoBuffer.new( + tag: RubySMB::Dcerpc::Samr::USER_INTERNAL4_INFORMATION_NEW, + member: RubySMB::Dcerpc::Samr::SamprUserInternal4InformationNew.new( + i1: { + password_expired: 1, + which_fields: RubySMB::Dcerpc::Samr::USER_ALL_NTPASSWORDPRESENT | RubySMB::Dcerpc::Samr::USER_ALL_PASSWORDEXPIRED + }, + user_password: { + buffer: RubySMB::Dcerpc::Samr::SamprEncryptedUserPasswordNew.encrypt_password( + computer_password, + @simple.client.application_key + ) + } + ) + ) + samr_con.samr.samr_set_information_user2( + user_handle: user_handle, + user_info: user_info + ) + + user_info = RubySMB::Dcerpc::Samr::SamprUserInfoBuffer.new( + tag: RubySMB::Dcerpc::Samr::USER_CONTROL_INFORMATION, + member: RubySMB::Dcerpc::Samr::UserControlInformation.new( + user_account_control: RubySMB::Dcerpc::Samr::USER_WORKSTATION_TRUST_ACCOUNT + ) + ) + samr_con.samr.samr_set_information_user2( + user_handle: user_handle, + user_info: user_info + ) + print_good("Successfully created #{samr_con.domain_name}\\#{computer_name}") + print_good(" Password: #{computer_password}") + print_good(" SID: #{get_computer_sid(samr_con, computer_name)}") + report_creds(samr_con.domain_name, computer_name, computer_password) + + ComputerInfo.new(computer_name, computer_password) + + rescue RubySMB::Dcerpc::Error::SamrError => e + raise MsSamrUnknownError, "A DCERPC SAMR error occurred: #{e.message}" + ensure + if samr_con + samr_con.samr.close_handle(user_handle) if user_handle + samr_con.samr.close_handle(samr_con.domain_handle) if samr_con.domain_handle + samr_con.samr.close_handle(samr_con.server_handle) if samr_con.server_handle + end + end + + def delete_computer(opts = {}) + tree = opts[:tree] || connect_ipc + + samr_con = connect_samr(tree) + + computer_name = opts[:computer_name] || datastore['COMPUTER_NAME'] + if computer_name.blank? + raise MsSamrBadConfigError, 'Unable to delete the computer account since its name is unknown' + end + + details = samr_con.samr.samr_lookup_names_in_domain(domain_handle: samr_con.domain_handle, names: [ computer_name ]) + raise MsSamrBadConfigError, 'The specified computer was not found.' if details.nil? + details = details[computer_name] + + user_handle = samr_con.samr.samr_open_user(domain_handle: samr_con.domain_handle, user_id: details[:rid]) + samr_con.samr.samr_delete_user(user_handle: user_handle) + print_good('The specified computer has been deleted.') + rescue RubySMB::Dcerpc::Error::SamrError => e + # `user_handle` only needs to be closed if an error occurs in `samr_delete_user` + # If this method succeed, the server took care of closing the handle + samr_con.samr.close_handle(user_handle) if user_handle + raise MsSamrUnknownError, "Could not delete the computer #{computer_name}: #{e.message}" + ensure + samr_con.samr.close_handle(samr_con.domain_handle) if samr_con.domain_handle + samr_con.samr.close_handle(samr_con.server_handle) if samr_con.server_handle + end + + def lookup_computer(opts = {}) + tree = opts[:tree] || connect_ipc + + samr_con = connect_samr(tree) + + computer_name = opts[:computer_name] || datastore['COMPUTER_NAME'] + if computer_name.blank? + raise MsSamrBadConfigError, 'Unable to lookup the computer account since its name is unknown' + end + + sid = get_computer_sid(samr_con, computer_name) + print_good("Found #{samr_con.domain_name}\\#{computer_name} (SID: #{sid})") + ensure + samr_con.samr.close_handle(samr_con.domain_handle) if samr_con.domain_handle + samr_con.samr.close_handle(samr_con.server_handle) if samr_con.server_handle + end + + module_function + + def random_hostname(prefix: 'DESKTOP') + "#{prefix}-#{Rex::Text.rand_base(8, '', ('A'..'Z').to_a + ('0'..'9').to_a)}$" + end + + def get_computer_sid(samr_con, computer_name) + details = samr_con.samr.samr_lookup_names_in_domain( + domain_handle: samr_con.domain_handle, + names: [ computer_name ] + ) + raise MsSamrNotFoundError, 'The computer was not found.' if details.nil? + + details = details[computer_name] + samr_con.samr.samr_rid_to_sid( + object_handle: samr_con.domain_handle, + rid: details[:rid] + ).to_s + end + + def report_creds(domain, username, password) + service_data = { + address: rhost, + port: rport, + service_name: 'smb', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + module_fullname: fullname, + origin_type: :service, + private_data: password, + private_type: :password, + username: username, + realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, + realm_value: domain + }.merge(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED + }.merge(service_data) + + create_credential_login(login_data) + end +end +end diff --git a/modules/auxiliary/admin/dcerpc/cve_2022_26923_certifried.rb b/modules/auxiliary/admin/dcerpc/cve_2022_26923_certifried.rb index 89aebed7eb86..0db7c34f8655 100644 --- a/modules/auxiliary/admin/dcerpc/cve_2022_26923_certifried.rb +++ b/modules/auxiliary/admin/dcerpc/cve_2022_26923_certifried.rb @@ -12,7 +12,7 @@ class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::LDAP include Msf::Auxiliary::Report include Msf::Exploit::Remote::MsIcpr - include Msf::Exploit::Remote::MsSamr + include Msf::Exploit::Remote::MsSamr::Computer def initialize(info = {}) super( diff --git a/modules/auxiliary/admin/dcerpc/samr_computer.rb b/modules/auxiliary/admin/dcerpc/samr_computer.rb index 544e8683c867..8c3e878ac8b9 100644 --- a/modules/auxiliary/admin/dcerpc/samr_computer.rb +++ b/modules/auxiliary/admin/dcerpc/samr_computer.rb @@ -9,7 +9,7 @@ class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::SMB::Client::Authenticated include Msf::Exploit::Remote::DCERPC include Msf::Auxiliary::Report - include Msf::Exploit::Remote::MsSamr + include Msf::Exploit::Remote::MsSamr::Computer include Msf::OptionalSession::SMB def initialize(info = {}) From eefa762c1594836684a7b8a385bd415e26d64704 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 16 Apr 2024 17:11:27 -0400 Subject: [PATCH 4/6] Put username reporting back in --- modules/auxiliary/scanner/smb/smb_enumusers.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/auxiliary/scanner/smb/smb_enumusers.rb b/modules/auxiliary/scanner/smb/smb_enumusers.rb index fe35f2e020bb..dcbe58e6aa93 100644 --- a/modules/auxiliary/scanner/smb/smb_enumusers.rb +++ b/modules/auxiliary/scanner/smb/smb_enumusers.rb @@ -99,19 +99,23 @@ def run_service_domain(tree, smb_domain: nil) ) print_good("#{samr_con.domain_name} [ #{users.values.map { |name| name.encode('UTF-8') }.join(', ') } ] ( LockoutTries=#{lockout_info.lockout_threshold} PasswordMin=#{password_info.min_password_length} )") + if datastore['DB_ALL_USERS'] + users.values.each do |username| + report_username(samr_con.domain_name, username.encode('UTF-8')) + end + end ensure samr_con.samr.close_handle(samr_con.domain_handle) if samr_con.domain_handle samr_con.samr.close_handle(samr_con.server_handle) if samr_con.server_handle end - def store_username(username, domain, ip, rport, resp) + def report_username(domain, username) service_data = { - address: ip, + address: rhost, port: rport, service_name: 'smb', protocol: 'tcp', - workspace_id: myworkspace_id, - proof: resp + workspace_id: myworkspace_id } credential_data = { From a3e3eb9e44cdad2f52ee0da797842395d188c4fd Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 22 Apr 2024 10:44:57 -0400 Subject: [PATCH 5/6] Re-add session support to the smb_enumusers module --- .../auxiliary/scanner/smb/smb_enumusers.rb | 27 ++++++++++++++--- spec/acceptance/smb_spec.rb | 30 ++++++++----------- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/modules/auxiliary/scanner/smb/smb_enumusers.rb b/modules/auxiliary/scanner/smb/smb_enumusers.rb index dcbe58e6aa93..e674e08b5234 100644 --- a/modules/auxiliary/scanner/smb/smb_enumusers.rb +++ b/modules/auxiliary/scanner/smb/smb_enumusers.rb @@ -27,10 +27,6 @@ def initialize ]) end - def rport - @rport || super - end - def domain @smb_domain || super end @@ -40,6 +36,11 @@ def connect(*args, **kwargs) end def run_host(_ip) + if session + run_session + return + end + if datastore['RPORT'].blank? || datastore['RPORT'] == 0 smb_services = [ { port: 139, direct: false }, @@ -56,6 +57,24 @@ def run_host(_ip) end end + def run_session + simple = session.simple_client + @rhost = simple.peerhost + @rport = simple.peerport + ipc_connect_result = simple.connect("\\\\#{simple.address}\\IPC$") + unless ipc_connect_result + print_error "Failed to connect to IPC in session #{session.sid}" + return + end + tree = simple.client.tree_connects.last + + run_service_domain(tree) + run_service_domain(tree, smb_domain: 'Builtin') + rescue ::Timeout::Error + rescue ::Exception => e + print_error("Error: #{e.class} #{e}") + end + def run_service(port, direct) @rport = port @smb_direct = direct diff --git a/spec/acceptance/smb_spec.rb b/spec/acceptance/smb_spec.rb index 51d689412d25..8fa770c636f9 100644 --- a/spec/acceptance/smb_spec.rb +++ b/spec/acceptance/smb_spec.rb @@ -45,23 +45,19 @@ # }, # } # }, - # Flaky: - # RubySMB::Error::CommunicationError Communication error with the remote host: Read timeout expired when reading from the Socket (timeout=30). - # The server supports encryption and this error may have been caused by encryption issues, but not always. - # Fixed here: https://github.com/rapid7/metasploit-framework/pull/19095 - # { - # name: "auxiliary/scanner/smb/smb_enumusers", - # platforms: [:linux, :osx, :windows], - # targets: [:session, :rhost], - # skipped: false, - # lines: { - # all: { - # required: [ - # "acceptance_tests_user", - # ], - # }, - # } - # }, + { + name: "auxiliary/scanner/smb/smb_enumusers", + platforms: [:linux, :osx, :windows], + targets: [:session, :rhost], + skipped: false, + lines: { + all: { + required: [ + "acceptance_tests_user", + ], + }, + } + }, { name: "auxiliary/scanner/smb/pipe_auditor", platforms: [:linux, :osx, :windows], From d6317923f6d14e8c9fdff8fa5a06b6f02438461d Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 25 Apr 2024 09:41:48 -0400 Subject: [PATCH 6/6] Bump ruby_smb to 3.3.6 This pulls in the changes from rapid7/ruby_smb#266 which adds SamrQueryInformationDomain support. --- Gemfile | 2 -- Gemfile.lock | 19 ++++++------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/Gemfile b/Gemfile index 84f8040c27e6..83b7b2811fbd 100644 --- a/Gemfile +++ b/Gemfile @@ -53,5 +53,3 @@ group :test do gem 'timecop' end -# remove after https://github.com/rapid7/ruby_smb/pull/266 is landed -gem 'ruby_smb', git: 'https://github.com/zeroSteiner/ruby_smb', branch: 'feat/dcerpc/samr/2' diff --git a/Gemfile.lock b/Gemfile.lock index 2499a477d6ef..b93ab871cb58 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,15 +1,3 @@ -GIT - remote: https://github.com/zeroSteiner/ruby_smb - revision: 7e8c8c89b71674399e16242326532e319c5db6f5 - branch: feat/dcerpc/samr/2 - specs: - ruby_smb (3.3.6) - bindata (= 2.4.15) - openssl-ccm - openssl-cmac - rubyntlm - windows_error (>= 0.1.4) - PATH remote: . specs: @@ -487,6 +475,12 @@ GEM ruby-progressbar (1.13.0) ruby-rc4 (0.1.5) ruby2_keywords (0.0.5) + ruby_smb (3.3.6) + bindata (= 2.4.15) + openssl-ccm + openssl-cmac + rubyntlm + windows_error (>= 0.1.4) rubyntlm (0.6.3) rubyzip (2.3.2) sawyer (0.9.2) @@ -574,7 +568,6 @@ DEPENDENCIES rspec-rerun rubocop ruby-prof (= 1.4.2) - ruby_smb! simplecov (= 0.18.2) test-prof timecop