diff --git a/modules/auxiliary/scanner/smb/smb_enumshares.rb b/modules/auxiliary/scanner/smb/smb_enumshares.rb index f4a951e178..9909391e94 100644 --- a/modules/auxiliary/scanner/smb/smb_enumshares.rb +++ b/modules/auxiliary/scanner/smb/smb_enumshares.rb @@ -23,16 +23,14 @@ class MetasploitModule < Msf::Auxiliary This module determines what shares are provided by the SMB service and which ones are readable/writable. It also collects additional information such as share types, directories, files, time stamps, etc. - - By default, a netshareenum request is done in order to retrieve share information, - but if this fails, you may also fall back to SRVSVC. }, 'Author' => [ 'hdm', 'nebulus', 'sinn3r', 'r3dy', - 'altonjx' + 'altonjx', + 'sjanusz-r7' ], 'License' => MSF_LICENSE, 'DefaultOptions' => { @@ -48,113 +46,66 @@ class MetasploitModule < Msf::Auxiliary OptBool.new('SpiderProfiles', [false, 'Spider only user profiles when share = C$', true]), OptEnum.new('LogSpider', [false, '0 = disabled, 1 = CSV, 2 = table (txt), 3 = one liner (txt)', 3, [0, 1, 2, 3]]), OptInt.new('MaxDepth', [true, 'Max number of subdirectories to spider', 999]), + OptInt.new('RPORT', [true, 'Which port to connect to', 445]) ] ) - - deregister_options('RPORT') end - def device_type_int_to_text(device_type) - types = [ - 'UNSET', 'BEEP', 'CDROM', 'CDROM FILE SYSTEM', 'CONTROLLER', 'DATALINK', - 'DFS', 'DISK', 'DISK FILE SYSTEM', 'FILE SYSTEM', 'INPORT PORT', 'KEYBOARD', - 'MAILSLOT', 'MIDI IN', 'MIDI OUT', 'MOUSE', 'UNC PROVIDER', 'NAMED PIPE', - 'NETWORK', 'NETWORK BROWSER', 'NETWORK FILE SYSTEM', 'NULL', 'PARALLEL PORT', - 'PHYSICAL NETCARD', 'PRINTER', 'SCANNER', 'SERIAL MOUSE PORT', 'SERIAL PORT', - 'SCREEN', 'SOUND', 'STREAMS', 'TAPE', 'TAPE FILE SYSTEM', 'TRANSPORT', 'UNKNOWN', - 'VIDEO', 'VIRTUAL DISK', 'WAVE IN', 'WAVE OUT', '8042 PORT', 'NETWORK REDIRECTOR', - 'BATTERY', 'BUS EXTENDER', 'MODEM', 'VDM' - ] + # Updated types for RubySMB. These are all the types we can ever receive from calling net_share_enum_all + ENUMERABLE_TYPES = ['DISK', 'TEMPORARY'].freeze + SKIPPABLE_TYPES = ['PRINTER', 'IPC', 'DEVICE', 'SPECIAL'].freeze - types[device_type] + def rport + @rport || datastore['RPORT'] end - def to_unix_time(thi, tlo) - t = ::Time.at(::Rex::Proto::SMB::Utils.time_smb_to_unix(thi, tlo)) - t.strftime('%m-%d-%Y %H:%M:%S') - end - - def eval_host(ip, share, subdir = '') - read = write = false - - # srvsvc adds a null byte that needs to be removed - share = share.chomp("\x00") - - return false, false, nil, nil if share == 'IPC$' - - simple.connect("\\\\#{ip}\\#{share}") - - begin - # XXX: not implemented with RubySMB client, should I implement it? - device_type = simple.client.queryfs_fs_device['device_type'] - unless device_type - vprint_error("\\\\#{ip}\\#{share}: Error querying filesystem device type") - return false, false, nil, nil - end - rescue ::Rex::Proto::SMB::Exceptions::ErrorCode => e - err = e.to_s.scan(/The server responded with error: (\w+)/i).flatten[0] - case err - when /0xffff0002/ - # 0xffff0002 means that the server can't handle the request for device type - device_type = -1 - when /STATUS_INVALID_DEVICE_REQUEST/ - return false, false, 'Invalid device request' - when /0x00040002/ - # Samba may throw this error too - return false, false, 'Mac/Apple Clipboard?' - when /STATUS_NETWORK_ACCESS_DENIED/, /0x00030001/, /0x00060002/ - # 0x0006002 = bad network name, 0x0030001 Directory not found - return false, false, nil, nil - else - vprint_error("\\\\#{ip}\\#{share}: Error querying filesystem device type") - return false, false, nil, nil - end - end + def eval_tree(tree, share_type, subdir = '') + subdir = subdir[1..subdir.length] if subdir.starts_with?('\\') + read = tree.permissions.read_ea == 1 + write = tree.permissions.write_ea == 1 + share = tree.share.split('\\').last skip = false - msg = '' - case device_type - when -1 - msg = 'Unable to determine device' - when 1, 21..29, 34..35, 37..44 + + if ENUMERABLE_TYPES.include? share_type + msg = share_type + elsif SKIPPABLE_TYPES.include? share_type + msg = share_type skip = true - msg = "Unhandled Device Type (#{device_type})" - when 2..16, 18..20, 30..33, 36 - msg = device_type_int_to_text(device_type) - when 17 - skip = true - msg = device_type_int_to_text(device_type) else - msg = 'Unknown Device Type' - msg << " (#{device_type})" if device_type + msg = "Unhandled Device Type (#{share_type})" + skip = true end + print_status "Skipping share: #{share}" if skip return read, write, msg, nil if skip - rfd = simple.client.find_first("#{subdir}\\*") - read = true if !rfd.nil? + # Create list after possibly skipping a share we wouldn't be able to access. + begin + list = tree.list(directory: subdir) + rescue RubySMB::Error::UnexpectedStatusCode => e + print_error e.to_s + return read, write, msg, nil + end - # Test writable - filename = Rex::Text.rand_text_alpha(rand(8)) - wfd = simple.open("\\#{filename}", 'rwct') - wfd << Rex::Text.rand_text_alpha(rand(1024)) - wfd.close - simple.delete("\\#{filename}") - simple.disconnect("\\\\#{ip}\\#{share}") + rfd = [] + list.entries.each do |file| + file_name = file.file_name.strip.encode('UTF-8') + next if file_name == '.' || file_name == '..' - # Operating under assumption STATUS_ACCESS_DENIED or the like will get - # thrown before write=true - write = true + rfd.push(file) + end return read, write, msg, rfd - rescue ::Rex::Proto::SMB::Exceptions::NoReply, ::Rex::Proto::SMB::Exceptions::InvalidType, - ::Rex::Proto::SMB::Exceptions::ReadPacket, ::Rex::Proto::SMB::Exceptions::ErrorCode - return read, false, msg, rfd + rescue RubySMB::Error::UnexpectedStatusCode => e + print_error e.to_s end - def get_os_info(ip, rport) + def get_os_info(ip) os = smb_fingerprint - os_info = "#{os['os']} #{os['sp']} (#{os['lang']})" if os['os'] != 'Unknown' + if os['os'] != 'Unknown' + os_info = "#{os['os']} #{os['sp']} (#{os['lang']})" + end if os_info report_service( host: ip, @@ -168,19 +119,16 @@ class MetasploitModule < Msf::Auxiliary os_info end - def get_user_dirs(ip, share, base, sub_dirs) + def get_user_dirs(tree, share_type, base, sub_dirs) dirs = [] usernames = [] begin - read, write, type, files = eval_host(ip, share, base) + _read, _write, _type, files = eval_tree(tree, share_type, base) # files or type could return nil due to various conditions return dirs if files.nil? - files.each do |f| - if (f[0] != '.') && (f[0] != '..') - usernames.push(f[0]) - end + usernames.push(f) end usernames.each do |username| sub_dirs.each do |sub_dir| @@ -189,25 +137,24 @@ class MetasploitModule < Msf::Auxiliary end return dirs rescue StandardError + print_status "Error when trying to access: #{base}" return dirs end end - def profile_options(ip, share) + def profile_options(tree, share_type) old_dirs = ['My Documents', 'Desktop'] new_dirs = ['Desktop', 'Documents', 'Downloads', 'Music', 'Pictures', 'Videos'] - dirs = get_user_dirs(ip, share, 'Documents and Settings', old_dirs) + dirs = get_user_dirs(tree, share_type, 'Documents and Settings', old_dirs) if dirs.blank? - dirs = get_user_dirs(ip, share, 'Users', new_dirs) + dirs = get_user_dirs(tree, share_type, 'Users', new_dirs) end - return dirs + + dirs end - def get_files_info(ip, _rport, shares, info) - read = false - write = false - + def get_files_info(ip, shares) # Creating a separate file for each IP address's results. detailed_tbl = Rex::Text::Table.new( 'Header' => "Spidered results for #{ip}.", @@ -217,84 +164,103 @@ class MetasploitModule < Msf::Auxiliary logdata = '' - list = shares.collect { |e| e[0] } - list.each do |x| - x = x.strip - if (x == 'ADMIN$') || (x == 'IPC$') + shares.each do |share| + share_name = share[:name].strip + if (share_name == 'ADMIN$') || (share_name == 'IPC$') + next + end + + if (share_name == 'Users') && !datastore['SpiderProfiles'] next end if !datastore['ShowFiles'] - print_status("Spidering #{x}.") + print_status("Spidering #{share_name}.") end + + begin + tree = simple.client.tree_connect("\\\\#{ip}\\#{share_name}") + rescue RubySMB::Error::UnexpectedStatusCode => e + print_error "Error when trying to connect to share #{share_name} - #{e.status_code.name}" + print_status "Spider #{share_name} complete." + next + end + subdirs = [''] - if (x.strip == 'C$') && datastore['SpiderProfiles'] - subdirs = profile_options(ip, x) + if (share_name == 'C$') && datastore['SpiderProfiles'] + subdirs = profile_options(tree, share[:type]) end until subdirs.empty? - depth = subdirs[0].count('\\') - if datastore['SpiderProfiles'] && (x == 'C$') - if depth - 2 > datastore['MaxDepth'] + depth = subdirs.first.count('\\') + + if share_name == 'C$' + if datastore['SpiderProfiles'] + if (depth - 2) > datastore['MaxDepth'] + subdirs.shift + next + end + else subdirs.shift next end - elsif depth > datastore['MaxDepth'] + end + + if depth > datastore['MaxDepth'] subdirs.shift next end - read, write, type, files = eval_host(ip, x, subdirs[0]) + + read, write, _type, files = eval_tree(tree, share[:type], subdirs.first) + if files && (read || write) - if files.length < 3 + if files.empty? subdirs.shift next end - header = '' - if simple.client.default_domain && simple.client.default_name - header << " \\\\#{simple.client.default_domain}" - end - header << "\\#{x.sub('C$', 'C$\\')}" if simple.client.default_name - header << subdirs[0] + header = '' pretty_tbl = Rex::Text::Table.new( 'Header' => header, 'Indent' => 1, 'Columns' => [ 'Type', 'Name', 'Created', 'Accessed', 'Written', 'Changed', 'Size' ] ) - f_types = { - 1 => 'RO', 2 => 'HIDDEN', 4 => 'SYS', 8 => 'VOL', - 16 => 'DIR', 32 => 'ARC', 64 => 'DEV', 128 => 'FILE' - } + if simple.client.default_domain && simple.client.default_name + header << " \\\\#{simple.client.default_domain}" + end + header << "\\#{share_name.sub('C$', 'C$\\')}" if simple.client.default_name + header << subdirs.first files.each do |file| - next unless file[0] && (file[0] != '.') && (file[0] != '..') + fname = file.file_name.encode('UTF-8') + tcr = file.create_time.to_datetime + tac = file.last_access.to_datetime + twr = file.last_write.to_datetime + tch = file.last_change.to_datetime - info = file[1]['info'] - fa = f_types[file[1]['attr']] # Item type - fname = file[0] # Filename - tcr = to_unix_time(info[3], info[2]) # Created - tac = to_unix_time(info[5], info[4]) # Accessed - twr = to_unix_time(info[7], info[6]) # Written - tch = to_unix_time(info[9], info[8]) # Changed - sz = info[12] + info[13] # Size + # Add subdirectories to list to use if SpiderShare is enabled. + if file.file_attributes.directory == 1 + fa = 'DIR' + subdirs.push(subdirs.first + '\\' + fname) + else + fa = 'FILE' + sz = file.end_of_file + end # Filename is too long for the UI table, cut it. fname = "#{fname[0, 35]}..." if fname.length > 35 - # Add subdirectories to list to use if SpiderShare is enabled. - if (fa == 'DIR') || (fa.nil? && (sz == 0)) - subdirs.push(subdirs[0] + '\\' + fname) - end - pretty_tbl << [fa || 'Unknown', fname, tcr, tac, twr, tch, sz] - detailed_tbl << [ip.to_s, fa || 'Unknown', x.to_s, subdirs[0] + '\\', fname, tcr, tac, twr, tch, sz] - logdata << "#{ip}\\#{x.sub('C$', 'C$\\')}#{subdirs[0]}\\#{fname}\n" + detailed_tbl << [ip.to_s, fa || 'Unknown', share.to_s, subdirs.first + '\\', fname, tcr, tac, twr, tch, sz] + logdata << "#{ip}\\#{share_name.sub('C$', 'C$\\')}#{subdirs.first}\\#{fname.encode}\n" end print_good(pretty_tbl.to_s) if datastore['ShowFiles'] end subdirs.shift end - print_status("Spider #{x} complete.") unless datastore['ShowFiles'] + + tree.disconnect! # simple.client.tree_disconnect is the same. Which is preferred? + print_status("Spider #{share[:name]} complete.") unless datastore['ShowFiles'] end unless detailed_tbl.rows.empty? if datastore['LogSpider'] == '1' @@ -310,94 +276,58 @@ class MetasploitModule < Msf::Auxiliary end end - def rport - @rport || datastore['RPORT'] - end - - # Overrides the one in smb.rb - def smb_direct - @smb_redirect || datastore['SMBDirect'] - end - def run_host(ip) - @rport = datastore['RPORT'] - @smb_redirect = datastore['SMBDirect'] - @srvsvc = datastore['USE_SRVSVC_ONLY'] shares = [] + begin + print_status 'Starting module' + connect(versions: [1, 2, 3]) + smb_login + shares = simple.client.net_share_enum_all(ip) + os_info = get_os_info(ip) + print_status(os_info) if os_info - [[139, false], [445, true]].each do |info| - @rport = info[0] - @smb_redirect = info[1] - - begin - connect - smb_login - shares = smb_netshareenumall - - os_info = get_os_info(ip, rport) - print_status(os_info) if os_info - - if shares.empty? - print_status('No shares collected') - else - shares_info = shares.map { |x| "#{x[0]} - (#{x[1]}) #{x[2]}" }.join(', ') - shares_info.split(', ').each do |share| - print_good share - end - report_note( - host: ip, - proto: 'tcp', - port: rport, - type: 'smb.shares', - data: { shares: shares }, - update: :unique_data - ) - - if datastore['SpiderShares'] - begin - connect(versions: [1]) - smb_login - get_files_info(ip, rport, shares, info) - rescue ::Rex::Proto::SMB::Exceptions::Error, Errno::ECONNRESET => e - print_error( - "Error when Spidering shares recursively (#{e}). This feature "\ - 'is only available with Rex client (SMB1 only) and the host '\ - "probably doesn't support SMB1." - ) - end - end - - break if rport == 139 + if shares.empty? + print_status('No shares collected') + else + shares.each do |share| + print_good("#{share[:name]} - (#{share[:type]}) #{share[:comment]}") end - rescue ::Interrupt - raise $ERROR_INFO - rescue ::Rex::Proto::SMB::Exceptions::LoginError, - ::Rex::Proto::SMB::Exceptions::ErrorCode => e - print_error(e.message) - return if e.message =~ /STATUS_ACCESS_DENIED/ - rescue Errno::ECONNRESET, - ::Rex::Proto::SMB::Exceptions::InvalidType, - ::Rex::Proto::SMB::Exceptions::ReadPacket, - ::Rex::Proto::SMB::Exceptions::InvalidCommand, - ::Rex::Proto::SMB::Exceptions::InvalidWordCount, - ::Rex::Proto::SMB::Exceptions::NoReply => e - vprint_error(e.message) - next if !shares.empty? && (rport == 139) # no results, try again - rescue Errno::ENOPROTOOPT - print_status('Wait 5 seconds before retrying...') - select(nil, nil, nil, 5) - retry - rescue ::Exception => e - next if e.to_s =~ /execution expired/ - next if !shares.empty? && (rport == 139) - vprint_error("Error: '#{ip}' '#{e.class}' '#{e}'") - ensure - disconnect + report_note( + host: ip, + proto: 'tcp', + port: rport, + type: 'smb.shares', + data: { shares: shares }, # We are now storing an array of hashes here, rather than an array of arrays. Will this cause issues further down the line? + update: :unique_data + ) + + if datastore['SpiderShares'] + get_files_info(ip, shares) + end end - - # if we already got results, not need to try on another port - return unless shares.empty? + rescue ::Interrupt + raise $ERROR_INFO + rescue Errno::ECONNRESET => e + vprint_error(e.message) + rescue Errno::ENOPROTOOPT + print_status('Wait 5 seconds before retrying...') + select(nil, nil, nil, 5) + retry + rescue Rex::ConnectionTimeout => e + print_error e.to_s + return + rescue StandardError => e + vprint_error("Error: '#{ip}' '#{e.class}' '#{e}'") + ensure + # Calling simple.client.disconnect! might not be needed here as we also call disconnect. + if simple && simple.client + simple.client.disconnect! + end + disconnect end + + # if we already got results, not need to try on another port + return unless shares.empty? end end