Fix smb enumshare to work with SMB1/2/3
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user