metasploit-framework/modules/auxiliary/scanner/smb/smb_enumshares.rb

516 lines
17 KiB
Ruby

##
# This module requires Metasploit: http//metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
require 'msf/core/auxiliary/report'
class Metasploit3 < Msf::Auxiliary
# Exploit mixins should be called first
include Msf::Exploit::Remote::SMB
include Msf::Exploit::Remote::SMB::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('VERBOSE', [true, 'Show detailed information when spidering', true]),
OptBool.new('SpiderProfiles', [false, 'Spider only user profiles when share = C$', true]),
OptInt.new('LogSpider', [false, '1 = CSV, 2 = table (txt), 3 = one liner (txt)', 3]),
OptInt.new('MaxDepth', [true, 'Max number of subdirectories to spider', 999]),
OptBool.new('USE_SRVSVC_ONLY', [true, 'List shares only with SRVSVC', false ])
], self.class)
deregister_options('RPORT', 'RHOST')
end
def share_type(val)
[ 'DISK', 'PRINTER', 'DEVICE', 'IPC', 'SPECIAL', 'TEMPORARY' ][val]
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 lanman_netshareenum(ip, rport, info)
shares = []
begin
res = self.simple.client.trans(
"\\PIPE\\LANMAN",
(
[0x00].pack('v') +
"WrLeh\x00" +
"B13BWz\x00" +
[0x01, 65406].pack("vv")
))
rescue ::Rex::Proto::SMB::Exceptions::ErrorCode => e
if e.error_code == 0xC00000BB
vprint_error("#{ip}:#{rport} - Got 0xC00000BB while enumerating shares, switching to srvsvc...")
datastore['USE_SRVSVC_ONLY'] = true # Make sure the module is aware of this state
return srvsvc_netshareenum(ip)
end
end
return [] if res.nil?
lerror, lconv, lentries, lcount = res['Payload'].to_s[
res['Payload'].v['ParamOffset'],
res['Payload'].v['ParamCount']
].unpack("v4")
data = res['Payload'].to_s[
res['Payload'].v['DataOffset'],
res['Payload'].v['DataCount']
]
0.upto(lentries - 1) do |i|
sname,tmp = data[(i * 20) + 0, 14].split("\x00")
stype = data[(i * 20) + 14, 2].unpack('v')[0]
scoff = data[(i * 20) + 16, 2].unpack('v')[0]
scoff -= lconv if lconv != 0
scomm,tmp = data[scoff, data.length - scoff].split("\x00")
shares << [ sname, share_type(stype), scomm]
end
shares
end
def srvsvc_netshareenum(ip)
shares = []
simple.connect("\\\\#{ip}\\IPC$")
handle = dcerpc_handle('4b324fc8-1670-01d3-1278-5a47bf6ee188', '3.0', 'ncacn_np', ["\\srvsvc"])
begin
dcerpc_bind(handle)
rescue Rex::Proto::SMB::Exceptions::ErrorCode => e
vprint_error("#{ip} : #{e.message}")
return []
end
stubdata =
NDR.uwstring("\\\\#{ip}") +
NDR.long(1) #level
ref_id = stubdata[0,4].unpack("V")[0]
ctr = [1, ref_id + 4 , 0, 0].pack("VVVV")
stubdata << ctr
stubdata << NDR.align(ctr)
stubdata << ["FFFFFFFF"].pack("H*")
stubdata << [ref_id + 8, 0].pack("VV")
response = dcerpc.call(0x0f, stubdata)
res = response.dup
win_error = res.slice!(-4, 4).unpack("V")[0]
if win_error != 0
raise "DCE/RPC error : Win_error = #{win_error + 0}"
end
#remove some uneeded data
res.slice!(0,12) # level, CTR header, Reference ID of CTR
share_count = res.slice!(0, 4).unpack("V")[0]
res.slice!(0,4) # Reference ID of CTR1
share_max_count = res.slice!(0, 4).unpack("V")[0]
raise "Dce/RPC error : Unknow situation encountered count != count max (#{share_count}/#{share_max_count})" if share_max_count != share_count
# RerenceID / Type / ReferenceID of Comment
types = res.slice!(0, share_count * 12).scan(/.{12}/n).map{|a| a[4,2].unpack("v")[0]}
share_count.times do |t|
length, offset, max_length = res.slice!(0, 12).unpack("VVV")
raise "Dce/RPC error : Unknow situation encountered offset != 0 (#{offset})" if offset != 0
raise "Dce/RPC error : Unknow situation encountered length !=max_length (#{length}/#{max_length})" if length != max_length
name = res.slice!(0, 2 * length).gsub('\x00','')
res.slice!(0,2) if length % 2 == 1 # pad
comment_length, comment_offset, comment_max_length = res.slice!(0, 12).unpack("VVV")
raise "Dce/RPC error : Unknow situation encountered comment_offset != 0 (#{comment_offset})" if comment_offset != 0
if comment_length != comment_max_length
raise "Dce/RPC error : Unknow situation encountered comment_length != comment_max_length (#{comment_length}/#{comment_max_length})"
end
comment = res.slice!(0, 2 * comment_length).gsub('\x00','')
res.slice!(0,2) if comment_length % 2 == 1 # pad
name = Rex::Text.to_ascii(name)
s_type = Rex::Text.to_ascii(share_type(types[t]))
comment = Rex::Text.to_ascii(comment)
shares << [ name, s_type, comment ]
end
shares
end
def profile_options(ip, share)
usernames = []
old_dirs = ['My Documents','Desktop']
new_dirs = ['Desktop','Documents','Downloads','Music','Pictures','Videos']
subdirs = []
begin
read,write,type,files = eval_host(ip, share, "Documents and Settings")
files.each do |f|
if f[0] != "." and f[0] != ".."
usernames.push(f[0])
end
end
# Return usernames along with their profile directories.
usernames.each do |username|
old_dirs.each do |dir|
subdirs.push("Documents and Settings\\#{username}\\#{dir}")
end
end
rescue
read,write,type,files = eval_host(ip, share, "Users")
files.each do |f|
if f[0] != "." and f[0] != ".."
usernames.push(f[0])
end
end
# Return usernames along with their profile directories.
usernames.each do |username|
new_dirs.each do |dir|
subdirs.push("Users\\#{username}\\#{dir}")
end
end
end
return subdirs
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::Ui::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|
if x.strip == "ADMIN$"
next
end
if not datastore['VERBOSE']
print_status("#{ip}:#{rport} - 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 = "#{ip}:#{rport}"
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]
header << " (#{type})" if type
header << " - Readable" if read
header << " - Writable" if write
pretty_tbl = Rex::Ui::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
vprint_good(pretty_tbl.to_s)
end
subdirs.shift
end
print_status("#{ip}:#{rport} - Spider #{x} complete.") unless datastore['VERBOSE'] == true
end
unless detailed_tbl.rows.empty?
if datastore['LogSpider'] == 1
p = store_loot('smb.enumshares', 'text/csv', ip, detailed_tbl.to_csv)
print_good("#{ip} - info saved in: #{p.to_s}")
elsif datastore['LogSpider'] == 2
p = store_loot('smb.enumshares', 'text', ip, detailed_tbl)
print_good("#{ip} - info saved in: #{p.to_s}")
elsif datastore['LogSpider'] == 3
p = store_loot('smb.enumshares', 'text', ip, logdata)
print_good("#{ip} - info saved in: #{p.to_s}")
end
end
end
def cleanup
datastore['RPORT'] = @rport
datastore['SMBDirect'] = @smb_redirect
datastore['USE_SRVSVC_ONLY'] = @srvsvc
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|
datastore['RPORT'] = info[0]
datastore['SMBDirect'] = info[1]
begin
connect
smb_login
if datastore['USE_SRVSVC_ONLY']
shares = srvsvc_netshareenum(ip)
else
shares = lanman_netshareenum(ip, rport, info)
end
os_info = get_os_info(ip, rport)
print_status("#{ip}:#{rport} - #{os_info}") if os_info
if shares.empty?
print_status("#{ip}:#{rport} - No shares collected")
else
shares_info = shares.map{|x| "#{ip}:#{rport} - #{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("#{ip}:#{rport} - #{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("#{ip}:#{rport} - #{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("#{ip}:#{rport} - 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