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

386 lines
12 KiB
Ruby

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
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
include Msf::OptionalSession::SMB
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 RubySMB net_share_enum_all request is done in order to retrieve share information,
which uses SRVSVC.
},
'Author' => [
'hdm',
'nebulus',
'sinn3r',
'r3dy',
'altonjx',
'sjanusz-r7'
],
'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]),
OptString.new('Share', [ false, 'Show only the specified share']),
OptRegexp.new('HIGHLIGHT_NAME_PATTERN', [true, 'PCRE regex of resource names to highlight', 'username|password|user|pass|Groups.xml']),
OptBool.new('SpiderProfiles', [false, 'Spider only user profiles when share is a disk share', 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
# Updated types for RubySMB. These are all the types we can ever receive from calling net_share_enum_all
ENUMERABLE_SHARE_TYPES = ['DISK', 'TEMPORARY'].freeze
SKIPPABLE_SHARE_TYPES = ['PRINTER', 'IPC', 'DEVICE', 'SPECIAL'].freeze
SKIPPABLE_SHARES = ['ADMIN$', 'IPC$'].freeze
# By default all of the drives connected to the server can be seen
DEFAULT_SHARES = [
'C$', 'D$', 'E$', 'F$', 'G$', 'H$', 'I$', 'J$', 'K$', 'L$', 'M$', 'N$',
'O$', 'P$', 'Q$', 'R$', 'S$', 'T$', 'U$', 'V$', 'W$', 'X$', 'Y$', 'Z$'
].freeze
USERS_SHARE = 'Users'.freeze # Where the users are stored in Windows 7
USERS_DIR = '\Users'.freeze # Windows 7 & Windows 10 user directory
DOCUMENTS_DIR = '\Documents and Settings'.freeze # Windows XP user directory
SMB1_PORT = 139
SMB2_3_PORT = 445
def rport
@rport || datastore['RPORT']
end
def enum_tree(tree, share, subdir = '')
subdir = subdir[1..subdir.length] if subdir.starts_with?('\\')
read = tree.permissions.read_ea == 1
write = tree.permissions.write_ea == 1
skip = false
if ENUMERABLE_SHARE_TYPES.include?(share[:type])
msg = share[:type]
elsif SKIPPABLE_SHARE_TYPES.include?(share[:type])
msg = share[:type]
skip = true
else
msg = "Unhandled Device Type (#{share[:type]})"
skip = true
end
print_status("Skipping share #{share[:name].strip} as it is of type #{share[:type]}") if skip
return read, write, msg, nil if skip
# 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
vprint_error("Error when trying to list tree contents in #{share[:name]}\\#{subdir} - #{e.status_code.name}")
return read, write, msg, nil
end
rfd = []
unless list.nil? || list.empty?
list.entries.each do |file|
file_name = file.file_name.strip.encode('UTF-8')
next if file_name == '.' || file_name == '..'
rfd.push(file)
end
end
return read, write, msg, rfd
end
def get_os_info(ip)
os = smb_fingerprint
if os['os'] != 'Unknown'
os_info = "#{os['os']} #{os['sp']} (#{os['lang']})"
end
if os_info
report_service(
host: ip,
port: rport,
proto: 'tcp',
name: 'smb',
info: os_info
)
end
os_info
end
def get_user_dirs(tree, share, base)
dirs = []
read, _write, _type, files = enum_tree(tree, share, base)
return dirs if files.nil? || !read
files.each do |f|
dirs.push("\\#{base}\\#{f[:file_name].encode('UTF-8')}")
end
dirs
end
def profile_options(tree, share)
dirs = get_user_dirs(tree, share, 'Documents and Settings')
if dirs.blank?
dirs = get_user_dirs(tree, share, 'Users')
end
dirs
end
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}.",
'Indent' => 1,
'Columns' => [ 'IP Address', 'Type', 'Share', 'Path', 'Name', 'Created', 'Accessed', 'Written', 'Changed', 'Size' ]
)
logdata = ''
shares.each do |share|
share_name = share[:name].strip
if SKIPPABLE_SHARES.include?(share_name) || (share_name == USERS_SHARE && !datastore['SpiderProfiles'])
print_status("Skipping #{share_name}")
next
end
if !datastore['ShowFiles']
print_status("Spidering #{share_name}")
end
begin
tree = simple.client.tree_connect("\\\\#{ip}\\#{share_name}")
rescue RubySMB::Error::UnexpectedStatusCode, RubySMB::Error::InvalidPacket => e
if datastore['Share'].nil?
vprint_error("Error when trying to connect to share #{share_name} - #{e.status_code.name}")
else
print_error("Error when trying to connect to share #{share_name} - #{e.status_code.name}")
end
print_status("Spidering #{share_name} complete") unless datastore['ShowFiles']
next
end
subdirs = ['']
if DEFAULT_SHARES.include?(share_name) && datastore['SpiderProfiles']
subdirs = profile_options(tree, share)
end
until subdirs.empty?
# Skip user directories if we do not want to spider them
if (subdirs.first == USERS_DIR || subdirs.first == DOCUMENTS_DIR) && !datastore['SpiderProfiles']
subdirs.shift
next
end
depth = subdirs.first.count('\\')
if datastore['SpiderProfiles'] && DEFAULT_SHARES.include?(share_name)
if (depth - 2) > datastore['MaxDepth']
subdirs.shift
next
end
elsif depth > datastore['MaxDepth']
subdirs.shift
next
end
read, _write, _type, files = enum_tree(tree, share, subdirs.first)
if files.nil? || files.empty? || !read
subdirs.shift
next
end
header = ''
if simple.client.default_domain && simple.client.default_name
header << " \\\\#{simple.client.default_domain}"
end
header << "\\#{share_name}" if simple.client.default_name
header << subdirs.first
pretty_tbl = Rex::Text::Table.new(
'Header' => header,
'Indent' => 1,
'Columns' => [ 'Type', 'Name', 'Created', 'Accessed', 'Written', 'Changed', 'Size' ],
'ColProps' => {
'Name' => {
'Stylers' => [Msf::Ui::Console::TablePrint::HighlightSubstringStyler.new([datastore['HIGHLIGHT_NAME_PATTERN']])]
}
}
)
files.each do |file|
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
# Add subdirectories to list to use if SpiderShare is enabled.
if (file[:file_attributes]&.directory == 1) || (file[:ext_file_attributes]&.directory == 1)
fa = 'DIR'
subdirs.push(subdirs.first + '\\' + fname)
else
fa = 'FILE'
sz = file.end_of_file
end
# Logging of the obtained data.
logdata << "#{ip}\\#{share_name}#{subdirs.first}\\#{fname.encode}\n"
detailed_tbl << [ip.to_s, fa || 'Unknown', share_name, subdirs.first + '\\', fname, tcr, tac, twr, tch, sz]
# Filename is too long for the UI table, cut it.
fname = "#{fname[0, 35]}..." if fname.length > 35
pretty_tbl << [fa || 'Unknown', fname, tcr, tac, twr, tch, sz]
end
print_good(pretty_tbl.to_s) if datastore['ShowFiles']
subdirs.shift
end
print_status("Spidering #{share_name} 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}")
elsif datastore['LogSpider'] == '2'
p = store_loot('smb.enumshares', 'text/plain', ip, detailed_tbl)
print_good("info saved in: #{p}")
elsif datastore['LogSpider'] == '3'
p = store_loot('smb.enumshares', 'text/plain', ip, logdata)
print_good("info saved in: #{p}")
end
end
end
def run_host(ip)
if session
print_status("Using existing session #{session.sid}")
client = session.client
self.simple = ::Rex::Proto::SMB::SimpleClient.new(client.dispatcher.tcp_socket, client: client)
enum_shares(session.address)
else
[{ port: SMB1_PORT }, { port: SMB2_3_PORT } ].each do |info|
vprint_status("Connecting to the server...")
# Assign @rport so that it is accessible via the rport method in this module,
# as well as making it accessible to the module mixins
@rport = info[:port]
if rport == SMB1_PORT
# force library in smb1 mode otherwise simple.client is a
# `Rex::Proto::SMB::Client` that does not supply `net_share_enum_all`
connect(versions: [1], backend: :ruby_smb)
else
connect(versions: [1, 2, 3])
end
smb_login
break unless enum_shares(ip).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)
rescue Rex::Proto::SMB::Exceptions::LoginError => e
print_error(e.to_s)
rescue RubySMB::Error::RubySMBError => e
print_error("RubySMB encountered an error: #{e}")
rescue RuntimeError => e
print_error e.to_s
rescue StandardError => e
vprint_error("Error: '#{ip}' '#{e.class}' '#{e}'")
ensure
disconnect
end
end
end
def enum_shares(ip)
shares = []
begin
# Return all shares if `Shares` option has not been set
if datastore['Share'].nil?
shares = simple.client.net_share_enum_all(ip)
else
# Return specific share if the `Share` option has been set
simple.client.net_share_enum_all(ip).each { |share| shares = [share] if share[:name] == datastore['Share'] }
# Return an error if `Share` option has been set but no matches were found
if shares.empty?
print_error("No shares match #{datastore['Share']}")
end
end
rescue RubySMB::Error::UnexpectedStatusCode => e
print_error("Error when trying to enumerate shares - #{e.status_code.name}")
return
rescue RubySMB::Error::InvalidPacket => e
print_error("Invalid packet received when trying to enumerate shares - #{e}")
return
end
os_info = get_os_info(ip)
print_status(os_info) if os_info
if shares.empty?
print_status('No shares available')
else
shares.each do |share|
print_good("#{share[:name]} - (#{share[:type]}) #{share[:comment]}")
end
# Map RubySMB shares to the same data format as it was with Rex SMB
report_shares = shares.map { |share| [share[:name], share[:type], share[:comment]] }
report_note(
host: ip,
proto: 'tcp',
port: rport,
type: 'smb.shares',
data: { shares: report_shares },
update: :unique_data
)
if datastore['SpiderShares']
get_files_info(ip, shares)
end
end
shares
end
end