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

337 lines
11 KiB
Ruby

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'recog'
class MetasploitModule < Msf::Auxiliary
# Exploit mixins should be called first
include Msf::Exploit::Remote::DCERPC
include Msf::Exploit::Remote::SMB::Client
include Msf::Exploit::Remote::Kerberos::Ticket::Storage
include Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Options
# Scanner mixin should be near last
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::Report
SMB2_DIALECT_STRINGS = {
'0x0202' => 'SMB 2.0.2',
'0x0210' => 'SMB 2.1',
'0x0300' => 'SMB 3.0',
'0x0302' => 'SMB 3.0.2',
'0x0311' => 'SMB 3.1.1',
'0x02ff' => 'SMB 2.???'
}.freeze
def initialize
super(
'Name' => 'SMB Version Detection',
'Description' => %q{
Fingerprint and display version information about SMB servers. Protocol
information and host operating system (if available) will be reported.
Host operating system detection requires the remote server to support
version 1 of the SMB protocol. Compression and encryption capability
negotiation is only present in version 3.1.1.
},
'Author' => ['hdm', 'Spencer McIntyre', 'Christophe De La Fuente'],
'License' => MSF_LICENSE
)
register_advanced_options(
[
*kerberos_storage_options(protocol: 'SMB'),
*kerberos_auth_options(protocol: 'SMB', auth_methods: Msf::Exploit::Remote::AuthOption::SMB_OPTIONS),
]
)
deregister_options('RPORT', 'SMBDIRECT', 'SMB::ProtocolVersion')
end
def rport
@smb_port
end
def smb_direct
(@smb_port == 445)
end
def seconds_to_timespan(seconds)
timespan = []
[
['w', 60 * 60 * 24 * 7], # weeks
['d', 60 * 60 * 24], # days
['h', 60 * 60], # hours
['m', 60], # minutes
['s', 1] # seconds
].each do |spec, span|
if seconds > span || !timespan.empty?
timespan << "#{(seconds / span).floor}#{spec}"
seconds %= span
end
end
timespan.join(' ')
end
def smb_proto_info
info = {
capabilities: {},
versions: []
}
versions = [1, 2, 3]
while !versions.empty?
begin
simple = connect(false, versions: versions)
protocol = simple.client.negotiate
rescue Rex::Proto::SMB::Exceptions::Error, RubySMB::Error::RubySMBError
break
rescue Errno::ECONNRESET
break
rescue ::Exception => e # rubocop:disable Lint/RescueException
vprint_error("#{rhost}: #{e.class} #{e}")
break
end
break if protocol.nil?
version = { 'SMB2' => 2, 'SMB3' => 3 }.fetch(protocol, 1)
versions.select! { |v| v < version }
dialect = simple.client.dialect
if simple.client.is_a? RubySMB::Client
info[:signing_required] = simple.client.signing_required
if dialect == '0x0311'
info[:capabilities][:compression] = simple.client.server_compression_algorithms.map do |algorithm|
RubySMB::SMB2::CompressionCapabilities::COMPRESSION_ALGORITHM_MAP[algorithm]
end
info[:capabilities][:encryption] = simple.client.server_encryption_algorithms.map do |algorithm|
RubySMB::SMB2::EncryptionCapabilities::ENCRYPTION_ALGORITHM_MAP[algorithm]
end
end
# assume that if the server supports multiple versions, the preferred
# dialect will correspond to the latest version
dialect = SMB2_DIALECT_STRINGS[dialect]
if simple.client.server_start_time && simple.client.server_system_time
uptime = simple.client.server_system_time - simple.client.server_start_time
info[:uptime] = seconds_to_timespan(uptime)
end
info[:server_guid] = simple.client.server_guid
unless info.key? :auth_domain
begin
simple.client.authenticate
rescue RubySMB::Error::RubySMBError
info[:auth_domain] = nil
else
info[:auth_domain] = simple.client.default_domain
end
end
else
info[:signing_required] = simple.client.peer_require_signing
end
info[:preferred_dialect] = dialect unless info.key? :preferred_dialect
info[:versions] << version
end
info[:versions].reverse!
info
end
def smb_os_description(res, nd_smb_fingerprint)
#
# Create the note hash for fingerprint.match
#
nd_fingerprint_match = {}
#
# Create a descriptive string for service.info
#
desc = res['os'].dup
if !res['edition'].to_s.empty?
desc << " #{res['edition']}"
nd_smb_fingerprint[:os_edition] = res['edition']
nd_fingerprint_match['os.edition'] = res['edition']
end
if !res['sp'].to_s.empty?
desc << " #{res['sp'].downcase.gsub('service pack ', 'SP')}"
nd_smb_fingerprint[:os_sp] = res['sp']
nd_fingerprint_match['os.version'] = res['sp']
end
if !res['build'].to_s.empty?
desc << " (build:#{res['build']})"
nd_smb_fingerprint[:os_build] = res['build']
nd_fingerprint_match['os.build'] = res['build']
end
if !res['lang'].to_s.empty? && res['lang'] != 'Unknown'
desc << " (language:#{res['lang']})"
nd_smb_fingerprint[:os_lang] = res['lang']
nd_fingerprint_match['os.language'] = nd_smb_fingerprint[:os_lang]
end
if simple.client.default_name
desc << " (name:#{simple.client.default_name})"
nd_smb_fingerprint[:SMBName] = simple.client.default_name
nd_fingerprint_match['host.name'] = nd_smb_fingerprint[:SMBName]
end
{ text: desc, fingerprint_match: nd_fingerprint_match, smb_fingerprint: nd_smb_fingerprint }
end
#
# Fingerprint a single host
#
def run_host(ip)
smb_ports = [445, 139]
lines = [] # defer status output to the very end to group lines together by host
smb_ports.each do |pnum|
@smb_port = pnum
self.simple = nil
begin
res = smb_fingerprint
info = smb_proto_info
desc = "SMB Detected (versions:#{info[:versions].join(', ')}) (preferred dialect:#{info[:preferred_dialect]})"
info[:capabilities].each do |name, values|
desc << " (#{name} capabilities:#{values.join(', ')})"
end
if info[:signing_required]
desc << ' (signatures:required)'
else
desc << ' (signatures:optional)'
report_vuln({
host: ip,
port: rport,
proto: 'tcp',
name: 'SMB Signing Is Not Required',
refs: [
SiteReference.new('URL', 'https://support.microsoft.com/en-us/help/161372/how-to-enable-smb-signing-in-windows-nt'),
SiteReference.new('URL', 'https://support.microsoft.com/en-us/help/887429/overview-of-server-message-block-signing'),
]
})
end
desc << " (uptime:#{info[:uptime]})" if info[:uptime]
desc << " (guid:#{Rex::Text.to_guid(info[:server_guid])})" if info[:server_guid]
desc << " (authentication domain:#{info[:auth_domain]})" if info[:auth_domain]
lines << { type: :status, message: desc }
#
# Create the note hash for smb.fingerprint
#
nd_smb_fingerprint = {
native_os: res['native_os'],
native_lm: res['native_lm']
}
if res['os'] && res['os'] != 'Unknown'
description = smb_os_description(res, nd_smb_fingerprint)
desc << description[:text]
nd_fingerprint_match = description[:fingerprint_match]
nd_smb_fingerprint = description[:smb_fingerprint]
if simple.client.default_domain
if simple.client.default_domain.encoding.name == 'UTF-8'
desc << " (domain:#{simple.client.default_domain})"
else
# Workgroup names are in ANSI, but may contain invalid characters
# Go through each char and convert/check
temp_workgroup = simple.client.default_domain.dup
desc << ' (workgroup:'
temp_workgroup.each_char do |i|
begin
desc << i.encode('UTF-8')
rescue ::Encoding::UndefinedConversionError # rubocop:disable Metrics/BlockNesting
desc << '?'
end
end
desc << ')'
end
nd_smb_fingerprint[:SMBDomain] = simple.client.default_domain
nd_fingerprint_match['host.domain'] = nd_smb_fingerprint[:SMBDomain]
end
lines << { type: :good, message: " Host is running #{desc}" }
# Report the service with a friendly banner
report_service(
host: ip,
port: rport,
proto: 'tcp',
name: 'smb',
info: desc
)
# Report a fingerprint.match hash for name, domain, and language
# Ignore OS fields, as those are handled via smb.fingerprint
report_note(
host: ip,
port: rport,
proto: 'tcp',
ntype: 'fingerprint.match',
data: nd_fingerprint_match
)
elsif res['native_os'] || res['native_lm']
desc = "#{res['native_os']} (#{res['native_lm']})"
report_service(host: ip, port: rport, name: 'smb', info: desc)
lines << { type: :status, message: " Host could not be identified: #{desc}" }
else
lines << { type: :status, message: ' Host could not be identified', verbose: true }
end
report_service(
host: ip,
port: rport,
proto: 'tcp',
name: 'smb',
info: desc
)
# Report a smb.fingerprint hash of attributes for OS fingerprinting
report_note(
host: ip,
port: rport,
proto: 'tcp',
ntype: 'smb.fingerprint',
data: nd_smb_fingerprint
)
disconnect
break
rescue ::Rex::Proto::SMB::Exceptions::NoReply
next
rescue ::Rex::Proto::SMB::Exceptions::ErrorCode
next
rescue ::Rex::Proto::SMB::Exceptions::LoginError => e
# Vista has 139 open but doesnt like *SMBSERVER
next if e.to_s =~ /server refused our NetBIOS/
break
rescue ::Timeout::Error
next
rescue ::Rex::ConnectionError
next
rescue ::Exception => e # rubocop:disable Lint/RescueException
print_error("#{rhost}: #{e.class} #{e}")
ensure
disconnect
lines.each do |line|
send "#{ line[:verbose] ? 'v' : '' }print_#{line[:type]}", line[:message]
end
lines = []
end
end
end
end