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

414 lines
13 KiB
Ruby

##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# Framework web site for more information on licensing and terms of use.
# http://metasploit.com/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. When SRVSVC is used, please
note the module will not attempt to enumerate more info like netshareenum.
},
'Author' =>
[
'hdm',
'nebulus',
'sinn3r'
],
'License' => MSF_LICENSE,
'DefaultOptions' =>
{
'DCERPC::fake_bind_multi' => false
}
))
register_options(
[
OptBool.new('DIR_SHARE', [true, 'Show all the folders and files', false ]),
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)
read = write = false
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("\\")
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 get_files_info(ip, rport, shares, info)
read = false
write = false
list = shares.collect {|e| e[0]}
list.each do |x|
read,write,type,files = eval_host(ip, x)
if files and (read or write)
header = "#{ip}:#{rport}"
if simple.client.default_domain and simple.client.default_name
header << " \\\\#{simple.client.default_domain}"
end
header << "\\#{simple.client.default_name}\\#{x}" if simple.client.default_name
header << " (#{type})" if type
header << " Readable" if read
header << " Writable" if write
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
tbl << [fa || 'Unknown', fname, tcr, tac, twr, tch, sz]
end
end
print_good(tbl.to_s)
unless tbl.rows.empty?
p = store_loot('smb.shares', 'text/csv', ip, tbl.to_csv)
print_good("#{x} info saved in: #{p.to_s}")
end
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| "#{x[0]} - #{x[2]} (#{x[1]})" }.join(", ")
print_status("#{ip}:#{rport} - #{shares_info}")
report_note(
:host => ip,
:proto => 'tcp',
:port => rport,
:type => 'smb.shares',
:data => { :shares => shares.inspect },
:update => :unique_data
)
if datastore['DIR_SHARE'] and not datastore['USE_SRVSVC_ONLY']
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