Files
metasploit-gs/modules/auxiliary/scanner/smb/smb_enumusers.rb
T
Brent Cook ddef5b4961 MSF5: Remove unneeded RHOST deregister in scanners
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.
2019-03-05 13:04:49 -06:00

331 lines
8.2 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
def initialize
super(
'Name' => 'SMB User Enumeration (SAM EnumUsers)',
'Description' => 'Determine what local users exist via the SAM RPC service',
'Author' => 'hdm',
'License' => MSF_LICENSE,
'DefaultOptions' => {
'DCERPC::fake_bind_multi' => false
}
)
deregister_options('RPORT')
end
def rport
@rport || super
end
def smb_direct
@smbdirect || super
end
# Locate an available SMB PIPE for the specified service
def smb_find_dcerpc_pipe(uuid, vers, pipes)
found_pipe = nil
found_handle = nil
pipes.each do |pipe_name|
connected = false
begin
connect
smb_login
connected = true
handle = dcerpc_handle(
uuid, vers,
'ncacn_np', ["\\#{pipe_name}"]
)
dcerpc_bind(handle)
return pipe_name
rescue ::Interrupt => e
raise e
rescue ::Exception => e
raise e if not connected
end
disconnect
end
nil
end
def smb_pack_sid(str)
[1,5,0].pack('CCv') + str.split('-').map{|x| x.to_i}.pack('NVVVV')
end
def smb_parse_sam_domains(data)
ret = []
idx = 0
cnt = data[8, 4].unpack("V")[0]
return ret if cnt == 0
idx += 20
idx += 12 * cnt
1.upto(cnt) do
v = data[idx,data.length].unpack('V*')
l = v[2] * 2
while(l % 4 != 0)
l += 1
end
idx += 12
ret << data[idx, v[2] * 2].gsub("\x00", '')
idx += l
end
ret
end
def smb_parse_sam_users(data)
ret = {}
rid = []
idx = 0
cnt = data[8, 4].unpack("V")[0]
return ret if cnt == 0
idx += 20
1.upto(cnt) do
v = data[idx,12].unpack('V3')
rid << v[0]
idx += 12
end
1.upto(cnt) do
v = data[idx,32].unpack('V*')
l = v[2] * 2
while(l % 4 != 0)
l += 1
end
uid = rid.shift
idx += 12
ret[uid] = data[idx, v[2] * 2].gsub("\x00", '')
idx += l
end
ret
end
@@sam_uuid = '12345778-1234-abcd-ef00-0123456789ac'
@@sam_vers = '1.0'
@@sam_pipes = %W{ SAMR LSARPC NETLOGON BROWSER SRVSVC }
# Fingerprint a single host
def run_host(ip)
[[139, false], [445, true]].each do |info|
@rport = info[0]
@smbdirect = info[1]
sam_pipe = nil
sam_handle = nil
begin
# Find the SAM pipe
sam_pipe = smb_find_dcerpc_pipe(@@sam_uuid, @@sam_vers, @@sam_pipes)
break if not sam_pipe
# Connect4
stub =
NDR.uwstring("\\\\" + ip) +
NDR.long(2) +
NDR.long(0x30)
dcerpc.call(62, stub)
resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
if ! (resp and resp.length == 24)
print_error("Invalid response from the Connect5 request")
disconnect
return
end
phandle = resp[0,20]
perror = resp[20,4].unpack("V")[0]
if(perror == 0xc0000022)
disconnect
return
end
if(perror != 0)
print_error("Received error #{"0x%.8x" % perror} from the OpenPolicy2 request")
disconnect
return
end
# EnumDomains
stub = phandle + NDR.long(0) + NDR.long(8192)
dcerpc.call(6, stub)
resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
domlist = smb_parse_sam_domains(resp)
domains = {}
# LookupDomain
domlist.each do |domain|
next if domain == 'Builtin'
# Round up the name to match NDR.uwstring() behavior
dlen = (domain.length + 1) * 2
# The SAM functions are picky on Windows 2000
stub =
phandle +
[(domain.length + 0) * 2].pack("v") + # NameSize
[(domain.length + 1) * 2].pack("v") + # NameLen (includes null)
NDR.long(rand(0x100000000)) +
[domain.length + 1].pack("V") + # MaxCount (includes null)
NDR.long(0) +
[domain.length + 0].pack("V") + # ActualCount (ignores null)
Rex::Text.to_unicode(domain) # No null appended
dcerpc.call(5, stub)
resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
raw_sid = resp[12, 20]
txt_sid = raw_sid.unpack("NVVVV").join("-")
domains[domain] = {
:sid_raw => raw_sid,
:sid_txt => txt_sid
}
end
# OpenDomain, QueryDomainInfo, CloseDomain
domains.each_key do |domain|
# Open
stub =
phandle +
NDR.long(0x00000305) +
NDR.long(4) +
[1,4,0].pack('CvC') +
domains[domain][:sid_raw]
dcerpc.call(7, stub)
resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
dhandle = resp[0,20]
derror = resp[20,4].unpack("V")[0]
# Catch access denied replies to OpenDomain
if(derror != 0)
next
end
# Password information
stub = dhandle + [0x01].pack('v')
dcerpc.call(8, stub)
resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
if(resp and resp[-4,4].unpack('V')[0] == 0)
mlen,hlen = resp[8,4].unpack('vv')
domains[domain][:pass_min] = mlen
domains[domain][:pass_min_history] = hlen
end
# Server Role
stub = dhandle + [0x07].pack('v')
dcerpc.call(8, stub)
if(resp and resp[-4,4].unpack('V')[0] == 0)
resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
domains[domain][:server_role] = resp[8,2].unpack('v')[0]
end
# Lockout Threshold
stub = dhandle + [12].pack('v')
dcerpc.call(8, stub)
resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
if(resp and resp[-4,4].unpack('V')[0] == 0)
lduration = resp[8,8]
lwindow = resp[16,8]
lthresh = resp[24, 2].unpack('v')[0]
domains[domain][:lockout_threshold] = lthresh
domains[domain][:lockout_duration] = Rex::Proto::SMB::Utils.time_smb_to_unix(*(lduration.unpack('V2')))
domains[domain][:lockout_window] = Rex::Proto::SMB::Utils.time_smb_to_unix(*(lwindow.unpack('V2')))
end
# Users
stub = dhandle + NDR.long(0) + NDR.long(0x10) + NDR.long(1024*1024)
dcerpc.call(13, stub)
resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
if(resp and resp[-4,4].unpack('V')[0] == 0)
domains[domain][:users] = smb_parse_sam_users(resp)
end
# Close Domain
dcerpc.call(1, dhandle)
end
# Close Policy
dcerpc.call(1, phandle)
domains.each_key do |domain|
# Delete the no longer used raw SID value
domains[domain].delete(:sid_raw)
# Store the domain name itself
domains[domain][:name] = domain
# Store the domain information
report_note(
:host => ip,
:proto => 'tcp',
:port => rport,
:type => 'smb.domain.enumusers',
:data => domains[domain]
)
users = domains[domain][:users] || {}
extra = ""
if (domains[domain][:lockout_threshold])
extra = "( "
extra << "LockoutTries=#{domains[domain][:lockout_threshold]} "
extra << "PasswordMin=#{domains[domain][:pass_min]} "
extra << ")"
end
print_good("#{domain.upcase} [ #{users.keys.map{|k| users[k]}.join(", ")} ] #{extra}")
end
# cleanup
disconnect
return
rescue ::Timeout::Error
rescue ::Interrupt
raise $!
rescue ::Rex::ConnectionError
rescue ::Rex::Proto::SMB::Exceptions::LoginError
next
rescue ::Exception => e
print_line("Error: #{ip} #{e.class} #{e}")
end
end
end
end