ddef5b4961
With Metasploit 5, RHOST and RHOSTS are aliases, so no need to deregister one or the other, as they are the same option. Deregistering one deregisters both.
395 lines
12 KiB
Ruby
395 lines
12 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
require 'msf/core/auxiliary/report'
|
|
|
|
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
|
|
|
|
# Scanner mixin should be near last
|
|
include Msf::Auxiliary::Report
|
|
include Msf::Auxiliary::Scanner
|
|
|
|
def initialize(info={})
|
|
super(update_info(info,
|
|
'Name' => 'SMB Share Enumeration',
|
|
'Description' => %q{
|
|
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'
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'DefaultOptions' =>
|
|
{
|
|
'DCERPC::fake_bind_multi' => false
|
|
}
|
|
))
|
|
|
|
register_options(
|
|
[
|
|
OptBool.new('SpiderShares', [false, 'Spider shares recursively', false]),
|
|
OptBool.new('ShowFiles', [true, 'Show detailed information when spidering', false]),
|
|
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]),
|
|
])
|
|
|
|
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"
|
|
]
|
|
|
|
types[device_type]
|
|
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$'
|
|
|
|
self.simple.connect("\\\\#{ip}\\#{share}")
|
|
|
|
begin
|
|
device_type = self.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
|
|
|
|
skip = false
|
|
msg = ''
|
|
case device_type
|
|
when -1
|
|
msg = "Unable to determine device"
|
|
when 1, 21 .. 29, 34 .. 35, 37 .. 44
|
|
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
|
|
end
|
|
|
|
return read,write,msg,nil if skip
|
|
|
|
rfd = self.simple.client.find_first("#{subdir}\\*")
|
|
read = true if rfd != nil
|
|
|
|
# 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}")
|
|
|
|
# Operating under assumption STATUS_ACCESS_DENIED or the like will get
|
|
# thrown before write=true
|
|
write = true
|
|
|
|
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
|
|
end
|
|
|
|
def get_os_info(ip, rport)
|
|
os = smb_fingerprint
|
|
os_info = "#{os['os']} #{os['sp']} (#{os['lang']})" if os['os'] != "Unknown"
|
|
report_service(
|
|
:host => ip,
|
|
:port => rport,
|
|
:proto => 'tcp',
|
|
:name => 'smb',
|
|
:info => os_info
|
|
) if os_info
|
|
|
|
os_info
|
|
end
|
|
|
|
def get_user_dirs(ip, share, base, sub_dirs)
|
|
dirs = []
|
|
usernames = []
|
|
|
|
begin
|
|
read,write,type,files = eval_host(ip, share, base)
|
|
# files or type could return nil due to various conditions
|
|
return dirs if files.nil?
|
|
files.each do |f|
|
|
if f[0] != "." and f[0] != ".."
|
|
usernames.push(f[0])
|
|
end
|
|
end
|
|
usernames.each do |username|
|
|
sub_dirs.each do |sub_dir|
|
|
dirs.push("#{base}\\#{username}\\#{sub_dir}")
|
|
end
|
|
end
|
|
return dirs
|
|
rescue
|
|
return dirs
|
|
end
|
|
end
|
|
|
|
def profile_options(ip, share)
|
|
old_dirs = ['My Documents','Desktop']
|
|
new_dirs = ['Desktop','Documents','Downloads','Music','Pictures','Videos']
|
|
|
|
dirs = get_user_dirs(ip, share, "Documents and Settings", old_dirs)
|
|
if dirs.blank?
|
|
dirs = get_user_dirs(ip, share, "Users", new_dirs)
|
|
end
|
|
return dirs
|
|
end
|
|
|
|
def get_files_info(ip, rport, shares, info)
|
|
read = false
|
|
write = false
|
|
|
|
# Creating a separate file for each IP address's results.
|
|
detailed_tbl = Rex::Text::Table.new(
|
|
'Header' => "Spidered results for #{ip}.",
|
|
'Indent' => 1,
|
|
'Columns' => [ 'IP Address', 'Type', 'Share', 'Path', 'Name', 'Created', 'Accessed', 'Written', 'Changed', 'Size' ]
|
|
)
|
|
|
|
logdata = ""
|
|
|
|
list = shares.collect {|e| e[0]}
|
|
list.each do |x|
|
|
x = x.strip
|
|
if x == "ADMIN$" or x == "IPC$"
|
|
next
|
|
end
|
|
if not datastore['ShowFiles']
|
|
print_status("Spidering #{x}.")
|
|
end
|
|
subdirs = [""]
|
|
if x.strip() == "C$" and datastore['SpiderProfiles']
|
|
subdirs = profile_options(ip, x)
|
|
end
|
|
while subdirs.length > 0
|
|
depth = subdirs[0].count("\\")
|
|
if datastore['SpiderProfiles'] and x == "C$"
|
|
if depth-2 > datastore['MaxDepth']
|
|
subdirs.shift
|
|
next
|
|
end
|
|
else
|
|
if depth > datastore['MaxDepth']
|
|
subdirs.shift
|
|
next
|
|
end
|
|
end
|
|
read,write,type,files = eval_host(ip, x, subdirs[0])
|
|
if files and (read or write)
|
|
if files.length < 3
|
|
subdirs.shift
|
|
next
|
|
end
|
|
header = ""
|
|
if simple.client.default_domain and simple.client.default_name
|
|
header << " \\\\#{simple.client.default_domain}"
|
|
end
|
|
header << "\\#{x.sub("C$","C$\\")}" if simple.client.default_name
|
|
header << subdirs[0]
|
|
|
|
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'
|
|
}
|
|
|
|
files.each do |file|
|
|
if file[0] and file[0] != '.' and file[0] != '..'
|
|
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
|
|
|
|
# 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" or (fa == nil and sz == 0)
|
|
subdirs.push(subdirs[0] + "\\" + fname)
|
|
end
|
|
|
|
pretty_tbl << [fa || 'Unknown', fname, tcr, tac, twr, tch, sz]
|
|
detailed_tbl << ["#{ip}", fa || 'Unknown', "#{x}", subdirs[0] + "\\", fname, tcr, tac, twr, tch, sz]
|
|
logdata << "#{ip}\\#{x.sub("C$","C$\\")}#{subdirs[0]}\\#{fname}\n"
|
|
|
|
end
|
|
end
|
|
print_good(pretty_tbl.to_s) if datastore['ShowFiles']
|
|
end
|
|
subdirs.shift
|
|
end
|
|
print_status("Spider #{x} complete.") unless datastore['ShowFiles']
|
|
end
|
|
unless detailed_tbl.rows.empty?
|
|
if datastore['LogSpider'] == '1'
|
|
p = store_loot('smb.enumshares', 'text/csv', ip, detailed_tbl.to_csv)
|
|
print_good("info saved in: #{p.to_s}")
|
|
elsif datastore['LogSpider'] == '2'
|
|
p = store_loot('smb.enumshares', 'text/plain', ip, detailed_tbl)
|
|
print_good("info saved in: #{p.to_s}")
|
|
elsif datastore['LogSpider'] == '3'
|
|
p = store_loot('smb.enumshares', 'text/plain', ip, logdata)
|
|
print_good("info saved in: #{p.to_s}")
|
|
end
|
|
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 = []
|
|
|
|
[[139, false], [445, true]].each do |info|
|
|
@rport = info[0]
|
|
@smb_redirect = info[1]
|
|
|
|
begin
|
|
connect(versions: [2,1])
|
|
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 { |share|
|
|
print_good share
|
|
}
|
|
report_note(
|
|
:host => ip,
|
|
:proto => 'tcp',
|
|
:port => rport,
|
|
:type => 'smb.shares',
|
|
:data => { :shares => shares },
|
|
:update => :unique_data
|
|
)
|
|
|
|
if datastore['SpiderShares']
|
|
get_files_info(ip, rport, shares, info)
|
|
end
|
|
|
|
break if rport == 139
|
|
end
|
|
|
|
rescue ::Interrupt
|
|
raise $!
|
|
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 not shares.empty? and 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 not shares.empty? and rport == 139
|
|
vprint_error("Error: '#{ip}' '#{e.class}' '#{e.to_s}'")
|
|
ensure
|
|
disconnect
|
|
end
|
|
|
|
# if we already got results, not need to try on another port
|
|
return unless shares.empty?
|
|
end
|
|
end
|
|
end
|
|
|