## # This module requires Metasploit: http://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', 'RHOST') 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