Fix smb enumshare to work with SMB1/2/3

This commit is contained in:
sjanusz
2021-11-08 17:15:37 +00:00
parent 163dbf2cc1
commit eba2df0a8a
+155 -225
View File
@@ -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