rapid7/metasploit-framework

View on GitHub
lib/rex/proto/ldap/auth.rb

Summary

Maintainability
D
2 days
Test Coverage
require 'net/ldap'
require 'net/ldap/dn'

module Rex
  module Proto
    module LDAP
      class Auth
        SUPPORTS_SASL = %w[GSS-SPNEGO NTLM]
        NTLM_CONST = Rex::Proto::NTLM::Constants
        NTLM_CRYPT = Rex::Proto::NTLM::Crypt
        MESSAGE = Rex::Proto::NTLM::Message

        #
        # Initialize the required variables
        #
        # @param challenge [String] NTLM Server Challenge
        # @param domain    [String] Domain value used in NTLM
        # @param server    [String] Server value used in NTLM
        # @param dnsname   [String] DNS Name value used in NTLM
        # @param dnsdomain [String] DNS Domain value used in NTLM
        def initialize(challenge, domain, server, dnsname, dnsdomain)
          @domain = domain.nil? ? 'DOMAIN' : domain
          @server = server.nil? ? 'SERVER' : server
          @dnsname = dnsname.nil? ? 'server' : dnsname
          @dnsdomain = dnsdomain.nil? ? 'example.com' : dnsdomain
          @challenge = [challenge.nil? ? Rex::Text.rand_text_alphanumeric(16) : challenge].pack('H*')
        end

        #
        # Process the incoming LDAP login requests from clients
        #
        # @param user_login [OpenStruct] User login information
        #
        # @return auth_info [Hash] Processed authentication information
        def process_login_request(user_login)
          auth_info = {}

          if user_login.name.empty? && user_login.authentication.empty? # Anonymous
            auth_info = handle_anonymous_request(user_login, auth_info)
          elsif !user_login.name.empty? # Simple
            auth_info = handle_simple_request(user_login, auth_info)
          elsif sasl?(user_login)
            auth_info = handle_sasl_request(user_login, auth_info)
          else
            auth_info = handle_unknown_request(user_login, auth_info)
          end

          auth_info
        end

        #
        # Handle Anonymous authentication requests
        #
        # @param user_login [OpenStruct] User login information
        # @param auth_info [Hash] Processed authentication information
        #
        # @return auth_info [Hash] Processed authentication information
        def handle_anonymous_request(user_login, auth_info = {})
          if user_login.name.empty? && user_login.authentication.empty?
            auth_info[:user] = user_login.name
            auth_info[:pass] = user_login.authentication
            auth_info[:domain] = nil
            auth_info[:result_code] = Net::LDAP::ResultCodeSuccess
            auth_info[:auth_type] = 'Anonymous'
          end
          auth_info
        end

        #
        # Handle Unknown authentication requests
        #
        # @param user_login [OpenStruct] User login information
        # @param auth_info [Hash] Processed authentication information
        #
        # @return auth_info [Hash] Processed authentication information
        def handle_unknown_request(user_login, auth_info = {})
          auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported
          auth_info[:error_msg] = 'Invalid LDAP Login Attempt => Unknown Authentication Format'
          auth_info
        end

        #
        # Handle Simple authentication requests
        #
        # @param user_login [OpenStruct] User login information
        # @param auth_info [Hash] Processed authentication information
        #
        # @return auth_info [Hash] Processed authentication information
        def handle_simple_request(user_login, auth_info = {})
          domains = []
          names = []
          if !user_login.name.empty?
            if user_login.name =~ /@/
              pub_info = user_login.name.split('@')
              if pub_info.length <= 2
                auth_info[:user], auth_info[:domain] = pub_info
              else
                auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials
                auth_info[:error_msg] = "Invalid LDAP Login Attempt => DN:#{user_login.name}"
              end
            elsif user_login.name =~ /,/
              begin
                dn = Net::LDAP::DN.new(user_login.name)
                dn.each_pair do |key, value|
                  if key == 'cn'
                    names << value
                  elsif key == 'dc'
                    domains << value
                  end
                end
                auth_info[:user] = names.join('')
                auth_info[:domain] = domains.empty? ? nil : domains.join('.')
              rescue Net::LDAP::InvalidDNError => e
                auth_info[:error_msg] = "Invalid LDAP Login Attempt => DN:#{user_login.name}"
                raise e
              end
            elsif user_login.name =~ /\\/
              pub_info = user_login.name.split('\\')
              if pub_info.length <= 2
                auth_info[:domain], auth_info[:user] = pub_info
              else
                auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials
                auth_info[:error_msg] = "Invalid LDAP Login Attempt => DN:#{user_login.name}"
              end
            else
              auth_info[:user] = user_login.name
              auth_info[:domain] = nil
              auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials
            end
            auth_info[:private] = user_login.authentication
            auth_info[:private_type] = :password
            auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported if auth_info[:result_code].nil?
            auth_info[:auth_type] = 'Simple'
            auth_info
          end
        end

        #
        # Handle SASL authentication requests
        #
        # @param user_login [OpenStruct] User login information
        # @param auth_info [Hash] Processed authentication information
        #
        # @return auth_info [Hash] Processed authentication information
        def handle_sasl_request(user_login, auth_info = {})
          case user_login.authentication[1]
          when /NTLMSSP/
            message = Net::NTLM::Message.parse(user_login.authentication[1])
            if message.is_a?(::Net::NTLM::Message::Type1)
              auth_info[:server_creds] = generate_type2_response(message)
              auth_info[:result_code] = Net::LDAP::ResultCodeSaslBindInProgress
            elsif message.is_a?(::Net::NTLM::Message::Type3)
              auth_info = handle_type3_message(message, auth_info)
              auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported
            end
          else
            auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported
            auth_info[:error_msg] = 'Invalid LDAP Login Attempt => Unsupported SASL Format'
          end
          auth_info[:auth_type] = 'SASL'
          auth_info
        end

        private

        #
        # Determine if the supplied request is formatted for SASL auth
        #
        # @param user_login [OpenStruct] User login information
        #
        # @return [bool] True if the request can be processed for SASL auth
        def sasl?(user_login)
          if user_login.authentication.is_a?(Array) && SUPPORTS_SASL.include?(user_login.authentication[0])
            return true
          end

          false
        end

        #
        # Generate NTLM Type2 response from NTLM Type1 message
        #
        # @param message [Net::NTLM::Message::Type1] NTLM Type1 message
        #
        # @return server_hash [String] NTLM Type2 response that is sent as server credentials
        def generate_type2_response(message)
          dom = message.domain
          ws = message.workstation
          domain = dom.empty? ? @domain : dom
          server = ws.empty? ? @server : ws
          server_hash = MESSAGE.process_type1_message(message.encode64, @challenge, domain, server, @dnsname, @dnsdomain)
          Rex::Text.decode_base64(server_hash)
        end

        #
        # Handle NTLM Type3 message
        #
        # @param message [Net::NTLM::Message::Type3] NTLM Type3 message
        # @param auth_info [Hash] Processed authentication information
        #
        # @return auth_info [Hash] Processed authentication information
        def handle_type3_message(message, auth_info = {})
          arg = {
            domain: message.domain,
            user: message.user,
            host: message.workstation
          }

          domain, user, host, lm_hash, ntlm_hash = MESSAGE.process_type3_message(message.encode64)
          nt_len = ntlm_hash.length

          if nt_len == 48
            arg[:ntlm_ver] = NTLM_CONST::NTLM_V1_RESPONSE
            arg[:lm_hash] = lm_hash
            arg[:nt_hash] = ntlm_hash

            if arg[:lm_hash][16, 32] == '0' * 32
              arg[:ntlm_ver] = NTLM_CONST::NTLM_2_SESSION_RESPONSE
            end
          elsif nt_len > 48
            arg[:ntlm_ver] = NTLM_CONST::NTLM_V2_RESPONSE
            arg[:lm_hash] = lm_hash[0, 32]
            arg[:lm_cli_challenge] = lm_hash[32, 16]
            arg[:nt_hash] = ntlm_hash[0, 32]
            arg[:nt_cli_challenge] = ntlm_hash[32, nt_len - 32]
          else
            auth_info[:error_msg] = "Unknown hash type from #{host}, ignoring ..."
          end
          auth_info.merge(process_ntlm_hash(arg)) unless arg.nil?
        end

        #
        # Process the NTLM Hash received from NTLM Type3 message
        #
        # @param arg [Hash] authentication information received from Type3 message
        #
        # @return arg [Hash] Processed NTLM authentication information
        def process_ntlm_hash(arg = {})
          ntlm_ver = arg[:ntlm_ver]
          lm_hash = arg[:lm_hash]
          nt_hash = arg[:nt_hash]
          unless ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE || ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE
            lm_cli_challenge = arg[:lm_cli_challenge]
            nt_cli_challenge = arg[:nt_cli_challenge]
          end
          domain = Rex::Text.to_ascii(arg[:domain])
          user = Rex::Text.to_ascii(arg[:user])
          host = Rex::Text.to_ascii(arg[:host])

          case ntlm_ver
          when NTLM_CONST::NTLM_V1_RESPONSE
            if NTLM_CRYPT.is_hash_from_empty_pwd?({
              hash: [nt_hash].pack('H*'),
              srv_challenge: @challenge,
              ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE,
              type: 'ntlm'
            })
              arg[:error_msg] = 'NLMv1 Hash correspond to an empty password, ignoring ... '
              return
            end
            if lm_hash == nt_hash || lm_hash == '' || lm_hash =~ /^0*$/
              lm_hash_message = 'Disabled'
            elsif NTLM_CRYPT.is_hash_from_empty_pwd?({
              hash: [lm_hash].pack('H*'),
              srv_challenge: @challenge,
              ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE,
              type: 'lm'
            })
              lm_hash_message = 'Disabled (from empty password)'
            else
              lm_hash_message = lm_hash
            end

            hash = [
              lm_hash || '0' * 48,
              nt_hash || '0' * 48
            ].join(':').gsub(/\n/, '\\n')
            arg[:private] = hash
          when NTLM_CONST::NTLM_V2_RESPONSE
            if NTLM_CRYPT.is_hash_from_empty_pwd?({
              hash: [nt_hash].pack('H*'),
              srv_challenge: @challenge,
              cli_challenge: [nt_cli_challenge].pack('H*'),
              user: user,
              domain: domain,
              ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE,
              type: 'ntlm'
            })
              arg[:error_msg] = 'NTLMv2 Hash correspond to an empty password, ignoring ... '
              return
            end
            if (lm_hash == '0' * 32) && (lm_cli_challenge == '0' * 16)
              lm_hash_message = 'Disabled'
            elsif NTLM_CRYPT.is_hash_from_empty_pwd?({
              hash: [lm_hash].pack('H*'),
              srv_challenge: @challenge,
              cli_challenge: [lm_cli_challenge].pack('H*'),
              user: user,
              domain: domain,
              ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE,
              type: 'lm'
            })
              lm_hash_message = 'Disabled (from empty password)'
            else
              lm_hash_message = lm_hash
            end

            hash = [
              lm_hash || '0' * 32,
              nt_hash || '0' * 32
            ].join(':').gsub(/\n/, '\\n')
            arg[:private] = hash
          when NTLM_CONST::NTLM_2_SESSION_RESPONSE
            if NTLM_CRYPT.is_hash_from_empty_pwd?({
              hash: [nt_hash].pack('H*'),
              srv_challenge: @challenge,
              cli_challenge: [lm_hash].pack('H*')[0, 8],
              ntlm_ver: NTLM_CONST::NTLM_2_SESSION_RESPONSE,
              type: 'ntlm'
            })
              arg[:error_msg] = 'NTLM2_session Hash correspond to an empty password, ignoring ... '
              return
            end

            hash = [
              lm_hash || '0' * 48,
              nt_hash || '0' * 48
            ].join(':').gsub(/\n/, '\\n')
            arg[:private] = hash
          else
            return
          end
          arg[:domain] = domain
          arg[:user] = user
          arg[:host] = host
          arg[:private_type] = :ntlm_hash
          arg
        end
      end
    end
  end
end