From 92234641bca3f8c40caeba430cf87d1e9e969757 Mon Sep 17 00:00:00 2001 From: NtAlexio2 Date: Fri, 13 Sep 2024 16:12:01 -0400 Subject: [PATCH 1/5] modernize enumuser_domain in smb scanner --- db/modules_metadata_base.json | 3 +- lib/msf/core/exploit/remote/ms_wkst.rb | 76 ++++++ .../scanner/smb/smb_enumusers_domain.rb | 235 ++++++------------ 3 files changed, 155 insertions(+), 159 deletions(-) create mode 100644 lib/msf/core/exploit/remote/ms_wkst.rb diff --git a/db/modules_metadata_base.json b/db/modules_metadata_base.json index ed5b2a3b90b1..3303ad7fb7cb 100644 --- a/db/modules_metadata_base.json +++ b/db/modules_metadata_base.json @@ -54959,7 +54959,8 @@ "type": "auxiliary", "author": [ "natron ", - "Joshua D. Abraham " + "Joshua D. Abraham ", + "NtAlexio2 " ], "description": "Determine what domain users are logged into a remote system via a DCERPC to NetWkstaUserEnum.", "references": [ diff --git a/lib/msf/core/exploit/remote/ms_wkst.rb b/lib/msf/core/exploit/remote/ms_wkst.rb new file mode 100644 index 000000000000..f5b061bee2b4 --- /dev/null +++ b/lib/msf/core/exploit/remote/ms_wkst.rb @@ -0,0 +1,76 @@ +### +# +# The Workstation Service Remote Protocol is used to perform tasks on a computer remotely on a +# network, including: +# +# - Configuring properties and behavior of a Server Message Block network +# redirector (SMB network redirector). +# - Managing domain membership and computer names. +# - Gathering information, such as the number of enabled transport protocols and the number of +# currently logged-on users. +# +# -*- coding: binary -*- + +module Msf + + module Exploit::Remote::MsWkst + + include Msf::Exploit::Remote::SMB::Client::Ipc + + class MsWkstError < StandardError; end + class MsWkstConnectionError < MsWkstError; end + class MsWkstAuthenticationError < MsWkstError; end + class MsWkstUnexpectedReplyError < MsWkstError; end + + WKS_UUID = '6bffd098-a112-3610-9833-46c3f87e345a'.freeze + WKS_VERS = '1.0'.freeze + WKSSVC_ENDPOINT = RubySMB::Dcerpc::Wkssvc.freeze + + # The currently connected WKSSVC pipe + attr_reader :wkssvc_pipe + + def user_enum(level) + self.wkssvc_pipe.netr_wksta_user_enum( + level: level + ) if (self.wkssvc_pipe) + end + + def get_info() + self.wkssvc_pipe.netr_wksta_get_info if (self.wkssvc_pipe) + end + + def disconnect_wkssvc + begin + self.wkssvc_pipe.close if self.wkssvc_pipe&.is_connected? + rescue RubySMB::Error::UnexpectedStatusCode, RubySMB::Error::CommunicationError => e + wlog e + end + end + + module_function + + def connect_wkssvc(tree) + begin + vprint_status('Connecting to Workstation Service Remote Protocol') + self.wkssvc_pipe = tree.open_file(filename: 'wkssvc', write: true, read: true) + + raise MsWkstConnectionError.new('Could not open wkssvc pipe on remote SMB server.') unless wkssvc_pipe + + vprint_status('Binding to \\wkssvc...') + self.wkssvc_pipe.bind(endpoint: WKSSVC_ENDPOINT) + vprint_good('Bound to \\wkssvc') + + self.wkssvc_pipe + rescue RubySMB::Dcerpc::Error::FaultError => e + elog(e.message, error: e) + raise MsWkstUnexpectedReplyError, "Connection failed (DCERPC fault: #{e.status_name})" + end + end + + protected + + attr_writer :wkssvc_pipe + + end + +end diff --git a/modules/auxiliary/scanner/smb/smb_enumusers_domain.rb b/modules/auxiliary/scanner/smb/smb_enumusers_domain.rb index 86376844cadd..a08d6b36fcbf 100644 --- a/modules/auxiliary/scanner/smb/smb_enumusers_domain.rb +++ b/modules/auxiliary/scanner/smb/smb_enumusers_domain.rb @@ -6,6 +6,7 @@ class MetasploitModule < Msf::Auxiliary # Exploit mixins should be called first + include Msf::Exploit::Remote::MsWkst include Msf::Exploit::Remote::SMB::Client include Msf::Exploit::Remote::SMB::Client::Authenticated include Msf::Exploit::Remote::DCERPC @@ -24,6 +25,7 @@ def initialize [ 'natron', # original module 'Joshua D. Abraham ', # database storage + 'NtAlexio2 ', # refactor ], 'References' => [ @@ -31,184 +33,101 @@ def initialize ], 'License' => MSF_LICENSE, ) - - deregister_options('RPORT') - end - def parse_value(resp, idx) - #val_length = resp[idx,4].unpack("V")[0] - idx += 4 - #val_offset = resp[idx,4].unpack("V")[0] - idx += 4 - val_actual = resp[idx,4].unpack("V")[0] - idx += 4 - value = resp[idx,val_actual*2] - idx += val_actual * 2 - - idx += val_actual % 2 * 2 # alignment - - return value,idx + def rport + @rport end - def parse_net_wksta_enum_users_info(resp) - accounts = [ Hash.new() ] - - idx = 20 - count = resp[idx,4].unpack("V")[0] # wkssvc_NetWkstaEnumUsersInfo -> Info -> PtrCt0 -> User() -> Ptr -> Max Count - idx += 4 - - 1.upto(count) do - # wkssvc_NetWkstaEnumUsersInfo -> Info -> PtrCt0 -> User() -> Ptr -> Ref ID - idx += 4 # ref id name - idx += 4 # ref id logon domain - idx += 4 # ref id other domains - idx += 4 # ref id logon server - end + def smb_direct + @smb_direct + end - 1.upto(count) do - # wkssvc_NetWkstaEnumUsersInfo -> Info -> PtrCt0 -> User() -> Ptr -> ID1 max count + def connect(*args, **kwargs) + super(*args, **kwargs, direct: @smb_direct) + end - account_name,idx = parse_value(resp, idx) - logon_domain,idx = parse_value(resp, idx) - other_domains,idx = parse_value(resp, idx) - logon_server,idx = parse_value(resp, idx) + 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 - accounts << { - :account_name => account_name, - :logon_domain => logon_domain, - :other_domains => other_domains, - :logon_server => logon_server - } + 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 - accounts + smb_services.map { |smb_service| run_service(smb_service[:port], smb_service[:direct]) } end - def rport - @rport || datastore['RPORT'] + def run_service(port, direct) + @rport = port + @smb_direct = direct + + ipc_tree = connect_ipc + wkssvc_pipe = connect_wkssvc(ipc_tree) + endpoint = RubySMB::Dcerpc::Wkssvc.freeze + + user_info = user_enum(endpoint::WKSTA_USER_INFO_1) + user_info.wkui1_buffer + + rescue Msf::Exploit::Remote::SMB::Client::Ipc::SmbIpcAuthenticationError => e + print_warning(e.message) + nil + rescue RubySMB::Error::RubySMBError => e + print_error("Error: #{e.message}") + nil + rescue ::Timeout::Error + rescue ::Exception => e + print_error("Error: #{e.class} #{e}") + ensure + disconnect_wkssvc end - def smb_direct - @smbdirect || datastore['SMBDirect'] - end + def format_results(results) + users_table = Rex::Text::Table.new( + 'Indent' => 4, + 'Header' => "Logged-on Users", + 'Columns' => + [ + 'Name', + 'Domain', + 'Other Domains', + 'Logon Server' + ], + 'SortIndex' => 0, + ) - def store_username(username, res, ip, rport) - service_data = { - address: ip, - port: rport, - service_name: 'smb', - protocol: 'tcp', - workspace_id: myworkspace_id, - proof: res - } - - credential_data = { - origin_type: :service, - module_fullname: fullname, - username: username - } - - credential_data.merge!(service_data) - - credential_core = create_credential(credential_data) - - login_data = { - core: credential_core, - status: Metasploit::Model::Login::Status::UNTRIED - } - - login_data.merge!(service_data) - create_credential_login(login_data) - end + results.compact.each do |result_set| + result_set.each { |result| users_table << [result.wkui1_username, result.wkui1_logon_domain, result.wkui1_oth_domains, result.wkui1_logon_server] } + end - def run_host(ip) + users_table - ports = [139, 445] + end + def run_host(_ip) 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 + self.simple = session.simple_client + results = run_session + else + results = run_rhost end - ports.each do |port| - - @rport = port - begin - unless session - connect() - smb_login() - end - - uuid = [ '6bffd098-a112-3610-9833-46c3f87e345a', '1.0' ] - - handle = dcerpc_handle_target( - uuid[0], uuid[1], 'ncacn_np', ["\\wkssvc"], simple.address - ) - begin - dcerpc_bind(handle) - stub = - NDR.uwstring("\\\\" + simple.address) + # Server Name - NDR.long(1) + # Level - NDR.long(1) + # Ctr - NDR.long(rand(0xffffffff)) + # ref id - NDR.long(0) + # entries read - NDR.long(0) + # null ptr to user0 - - NDR.long(0xffffffff) + # Prefmaxlen - NDR.long(rand(0xffffffff)) + # ref id - NDR.long(0) # null ptr to resume handle - - dcerpc.call(2,stub) - - resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil - - accounts = parse_net_wksta_enum_users_info(resp) - accounts.shift - - if datastore['VERBOSE'] - accounts.each do |x| - print_status x[:logon_domain] + "\\" + x[:account_name] + - "\t(logon_server: #{x[:logon_server]}, other_domains: #{x[:other_domains]})" - end - else - print_status "#{accounts.collect{|x| x[:logon_domain] + "\\" + x[:account_name]}.join(", ")}" - end - - found_accounts = [] - accounts.each do |x| - comp_user = x[:logon_domain] + "\\" + x[:account_name] - found_accounts.push(comp_user.scan(/[[:print:]]/).join) unless found_accounts.include?(comp_user.scan(/[[:print:]]/).join) - end - - found_accounts.each do |comp_user| - if comp_user.to_s =~ /\$$/ - next - end - - print_good("Found user: #{comp_user}") - store_username(comp_user, resp, simple.address, rport) - end - - rescue ::Rex::Proto::SMB::Exceptions::ErrorCode => e - print_error("UUID #{uuid[0]} #{uuid[1]} ERROR 0x%.8x" % e.error_code) - #puts e - #return - rescue ::Exception => e - print_error("UUID #{uuid[0]} #{uuid[1]} ERROR #{$!}") - #puts e - #return - end - - disconnect() - return - rescue ::Exception - print_line($!.to_s) - end + unless results.to_s.empty? + 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 + end end From 9fac88f709b639add42578d32388fbc2d4ad4735 Mon Sep 17 00:00:00 2001 From: Alex Romero Date: Tue, 17 Sep 2024 00:32:34 +0330 Subject: [PATCH 2/5] Update lib/msf/core/exploit/remote/ms_wkst.rb Co-authored-by: Spencer McIntyre <58950994+smcintyre-r7@users.noreply.github.com> --- lib/msf/core/exploit/remote/ms_wkst.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/msf/core/exploit/remote/ms_wkst.rb b/lib/msf/core/exploit/remote/ms_wkst.rb index f5b061bee2b4..44037303cc6a 100644 --- a/lib/msf/core/exploit/remote/ms_wkst.rb +++ b/lib/msf/core/exploit/remote/ms_wkst.rb @@ -32,11 +32,11 @@ class MsWkstUnexpectedReplyError < MsWkstError; end def user_enum(level) self.wkssvc_pipe.netr_wksta_user_enum( level: level - ) if (self.wkssvc_pipe) + ) end def get_info() - self.wkssvc_pipe.netr_wksta_get_info if (self.wkssvc_pipe) + self.wkssvc_pipe.netr_wksta_get_info end def disconnect_wkssvc From a93e00883642072e198afa4e3f7d0d1a503ee8f4 Mon Sep 17 00:00:00 2001 From: NtAlexio2 Date: Mon, 16 Sep 2024 17:55:58 -0400 Subject: [PATCH 3/5] update ruby_smb version --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 539a8d643104..12247023041a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -495,7 +495,7 @@ GEM ruby-progressbar (1.13.0) ruby-rc4 (0.1.5) ruby2_keywords (0.0.5) - ruby_smb (3.3.9) + ruby_smb (3.3.10) bindata (= 2.4.15) openssl-ccm openssl-cmac From d4378d6c828e038295c2774656a51a4994c1b8f8 Mon Sep 17 00:00:00 2001 From: NtAlexio2 Date: Mon, 16 Sep 2024 18:28:01 -0400 Subject: [PATCH 4/5] change output format to old style --- .../scanner/smb/smb_enumusers_domain.rb | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/modules/auxiliary/scanner/smb/smb_enumusers_domain.rb b/modules/auxiliary/scanner/smb/smb_enumusers_domain.rb index a08d6b36fcbf..516bab830cd3 100644 --- a/modules/auxiliary/scanner/smb/smb_enumusers_domain.rb +++ b/modules/auxiliary/scanner/smb/smb_enumusers_domain.rb @@ -91,28 +91,6 @@ def run_service(port, direct) disconnect_wkssvc end - def format_results(results) - users_table = Rex::Text::Table.new( - 'Indent' => 4, - 'Header' => "Logged-on Users", - 'Columns' => - [ - 'Name', - 'Domain', - 'Other Domains', - 'Logon Server' - ], - 'SortIndex' => 0, - ) - - results.compact.each do |result_set| - result_set.each { |result| users_table << [result.wkui1_username, result.wkui1_logon_domain, result.wkui1_oth_domains, result.wkui1_logon_server] } - end - - users_table - - end - def run_host(_ip) if session self.simple = session.simple_client @@ -122,11 +100,40 @@ def run_host(_ip) end unless results.to_s.empty? - 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 + + accounts = [ Hash.new() ] + results.compact.each do |result_set| + result_set.each { |result| accounts << { + :account_name => result.wkui1_username.encode('UTF-8'), + :logon_domain => result.wkui1_logon_domain.encode('UTF-8'), + :other_domains => result.wkui1_oth_domains.encode('UTF-8'), + :logon_server => result.wkui1_logon_server.encode('UTF-8')} } + end + accounts.shift + + if datastore['VERBOSE'] + accounts.each do |x| + print_status x[:logon_domain] + "\\" + x[:account_name] + + "\t(logon_server: #{x[:logon_server]}, other_domains: #{x[:other_domains]})" + end + else + print_status "#{accounts.collect{|x| x[:logon_domain] + "\\" + x[:account_name]}.join(", ")}" + end + + found_accounts = [] + accounts.each do |x| + comp_user = x[:logon_domain] + "\\" + x[:account_name] + found_accounts.push(comp_user.scan(/[[:print:]]/).join) unless found_accounts.include?(comp_user.scan(/[[:print:]]/).join) + end + + found_accounts.each do |comp_user| + if comp_user.to_s =~ /\$$/ + next + end + + print_good("Found user: #{comp_user}") + end + end end From 7abfb6c205fd60ed95a22edc466137a3c37ab9ed Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 17 Sep 2024 09:59:42 -0400 Subject: [PATCH 5/5] Return nil on error to avoid another exception --- modules/auxiliary/scanner/smb/smb_enumusers_domain.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/auxiliary/scanner/smb/smb_enumusers_domain.rb b/modules/auxiliary/scanner/smb/smb_enumusers_domain.rb index 516bab830cd3..7fbd92ad4a6b 100644 --- a/modules/auxiliary/scanner/smb/smb_enumusers_domain.rb +++ b/modules/auxiliary/scanner/smb/smb_enumusers_domain.rb @@ -87,6 +87,7 @@ def run_service(port, direct) rescue ::Timeout::Error rescue ::Exception => e print_error("Error: #{e.class} #{e}") + nil ensure disconnect_wkssvc end @@ -100,7 +101,6 @@ def run_host(_ip) end unless results.to_s.empty? - accounts = [ Hash.new() ] results.compact.each do |result_set| result_set.each { |result| accounts << {