379 lines
9.9 KiB
Ruby
379 lines
9.9 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
|
|
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
|
|
},
|
|
)
|
|
|
|
register_options(
|
|
[
|
|
OptBool.new('DB_ALL_USERS', [ false, "Add all enumerated usernames to the database", 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 = session ? true : false
|
|
begin
|
|
unless connected
|
|
connect
|
|
smb_login
|
|
connected = true
|
|
end
|
|
|
|
handle = dcerpc_handle_target(
|
|
uuid, vers,
|
|
'ncacn_np', ["\\#{pipe_name}"], simple.address
|
|
)
|
|
|
|
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)
|
|
ports = [139, 445]
|
|
|
|
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)
|
|
ports = [simple.port]
|
|
self.simple.connect("\\\\#{simple.address}\\IPC$") # smb_login connects to this share for some reason and it doesn't work unless we do too
|
|
end
|
|
|
|
ports.each do |port|
|
|
|
|
@rport = port
|
|
|
|
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("\\\\" + simple.address) +
|
|
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 => simple.address,
|
|
: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}")
|
|
if datastore['DB_ALL_USERS']
|
|
users.each { |user|
|
|
store_username(user, domain, simple.address, rport, resp)
|
|
}
|
|
end
|
|
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: #{simple.address} #{e.class} #{e}")
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
def store_username(username, domain, ip, rport, resp)
|
|
service_data = {
|
|
address: ip,
|
|
port: rport,
|
|
service_name: 'smb',
|
|
protocol: 'tcp',
|
|
workspace_id: myworkspace_id,
|
|
proof: resp
|
|
}
|
|
|
|
credential_data = {
|
|
origin_type: :service,
|
|
module_fullname: fullname,
|
|
username: username[1],
|
|
realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
|
|
realm_value: domain,
|
|
}.merge(service_data)
|
|
|
|
login_data = {
|
|
core: create_credential(credential_data),
|
|
status: Metasploit::Model::Login::Status::UNTRIED
|
|
}.merge(service_data)
|
|
|
|
create_credential_login(login_data)
|
|
end
|
|
|
|
end
|