rapid7/metasploit-framework

View on GitHub
lib/msf/core/exploit/remote/kerberos/client/pkinit.rb

Summary

Maintainability
C
1 day
Test Coverage
# -*- coding: binary -*-

require 'rasn1'

module Msf
  class Exploit
    class Remote
      module Kerberos
        module Client
          # Methods for interacting with Kerberos's PKINIT extension for obtaining a
          # TGT from a certificate
          #
          # https://www.rfc-editor.org/rfc/rfc4556
          # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-pkca/d0cf1763-3541-4008-a75f-a577fa5e8c5b
          module Pkinit
            # Builds a Diffie Helman object with parameters set up
            #
            # @return [OpenSSL::PKey::DH, string] The Diffie Hellman object, and a random client nonce
            def build_dh
              # When using the Diffie-Hellman key agreement method, implementations MUST support Oakley 1024-bit Modular
              # Exponential (MODP) well-known group 2 RFC2412
              # Kerberos spec: https://www.rfc-editor.org/rfc/rfc4556
              # Value: https://www.rfc-editor.org/rfc/rfc2412#appendix-E.2
              prime_modulus = 0 # built 256 bits at a time
              prime_modulus |= 0xffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74 << (256 * 3)
              prime_modulus |= 0x020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f1437 << (256 * 2)
              prime_modulus |= 0x4fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7ed << (256 * 1)
              prime_modulus |= 0xee386bfb5a899fa5ae9f24117c4b1fe649286651ece65381ffffffffffffffff
              dh = OpenSSL::PKey::DH.new(
                OpenSSL::ASN1::Sequence([
                  OpenSSL::ASN1::Integer(prime_modulus),
                  OpenSSL::ASN1::Integer(2)
                ]).to_der
              )
              if OpenSSL::PKey.respond_to?(:generate_key)
                # OpenSSL v3.x path
                # see:
                #  * https://github.com/rapid7/metasploit-framework/pull/17308
                #  * https://ruby-doc.org/stdlib-3.1.0/libdoc/openssl/rdoc/OpenSSL/PKey/DH.html#method-i-generate_key-21
                dh = OpenSSL::PKey.generate_key(dh)
              else
                dh.generate_key!
              end

              dh_nonce = SecureRandom.random_bytes(32)
              [dh, dh_nonce]
            end

            # Extracts the user and realm from a certificate, deferring to
            # the provided values if they are not nil.
            #
            # @param certificate [OpenSSL::X509::Certificate]
            # @param username [String] A default value for username. A warning is presented if this is not in the certificate.
            # @param realm [String] A default value for realm. A warning is presented if this is not in the certificate.
            # @return [Array<String>] A tuple of the username and realm retrieved from the certificate, or parameters provided
            # @raise [ArgumentError] If the certificate contains a corrupted SAN
            # @raise [ArgumentError] If a username is provided without also providing a realm; or vice versa
            def extract_user_and_realm(certificate, username, realm)
              raise ArgumentError, 'Must provide username if providing realm' if username.nil? && !realm.nil?
              raise ArgumentError, 'Must provide realm if providing username' if realm.nil? && !username.nil?

              results = []
              asn_san_seq = []

              # MS's SAN extension isn't handled nicely by OpenSSL, so we need to read it ourselves
              # https://manas.tech/blog/2013/01/29/extracting-subject-alternative-name-from-microsoft-authentication-client-certificates/
              certificate.extensions.select { |ext| ext.oid == 'subjectAltName' }.each do |san_extension|
                begin
                  asn_san = OpenSSL::ASN1.decode(san_extension)
                  asn_san_value = asn_san.value[1]&.value
                  if asn_san_value.nil?
                    raise ArgumentError, 'Invalid certificate provided: unable to decode SAN'
                  end

                  asn_san_seq = OpenSSL::ASN1.decode(asn_san_value)
                rescue OpenSSL::ASN1::ASN1Error
                  raise ArgumentError, 'Invalid certificate provided: unable to decode SAN'
                end

                asn_san_seq.each do |san_entry|
                  if san_entry.tag == 0 # x509.OtherName
                    key = san_entry.value[0]&.value
                    next if key != 'msUPN' # Principal Name

                    principal = san_entry.value[1].value[0].value
                    parts = principal.split('@')
                    if parts.length == 1
                      user = principal
                      domain = ''
                    else
                      user = parts[0..-2].join('@')
                      domain = parts[-1]
                    end
                  elsif san_entry.tag == 2 # dNSName
                    parts = san_entry.value.split('.')
                    if parts.length == 1
                      user = san_entry
                      domain = ''
                    else
                      user = parts[0] + '$'
                      domain = parts[1..].join('.')
                    end
                  else
                    next
                  end

                  results.append([user, domain])
                end
              end

              unless realm.nil? # and also username, since it's both or neither
                unless results.map { |x| x.map(&:downcase) }.include?([username.downcase, realm.downcase])
                  # If we've been provided an override but can't find them in a SAN, give a warning
                  print_warning("Warning: Provided principal and realm (#{username}@#{realm}) do not match entries in certificate:")
                  results.each do |cert_username, cert_realm|
                    print_warning("  * #{cert_username}@#{cert_realm}")
                  end
                end

                # But hey, they've overridden it, so off we go
                return [username, realm]
              end

              # No override was provided, so hopefully we only extracted one value from the certificate
              if results.length == 1
                return results[0]
              else
                raise ArgumentError, "Failed to retrieve Principal from certificate (contained #{results.length} SAN entries). Provide an override user and domain."
              end
            end

            # Transform a key into a key of a certain size, using the k-truncate algorithm
            # described in https://www.rfc-editor.org/rfc/rfc4556#section-3.2.3.1
            #
            # @param data [String] The full key to transform
            # @param etype [Integer] The encryption type, from Rex::Proto::Kerberos::Crypto::Encryption
            # @return [String] The truncated key
            def k_truncate(data, etype)
              if etype == Rex::Proto::Kerberos::Crypto::Encryption::AES256
                keysize = 32
              elsif etype == Rex::Proto::Kerberos::Crypto::Encryption::AES128
                keysize = 16
              else
                # This is unsupported per the spec
                raise Rex::Proto::Kerberos::Model::Error::KerberosEncryptionNotSupported.new("Unsupported DH Key exchange encryption type #{etype}", encryption_type: etype)
              end

              result = ''
              x = 0
              while result.length < keysize
                digest = Digest::SHA1.digest(x.chr + data)
                if result.length + digest.length > keysize
                  result += digest[0..(keysize - result.length - 1)] # Just take the first few bytes until we reach the desired length
                  return result
                end
                result += digest
                x += 1
              end

              result
            end

            # Given all the Diffie Hellman parameters and response from the server,
            # calculate the shared key using the steps described in
            # https://www.rfc-editor.org/rfc/rfc4556#section-3.2.3.1
            #
            # @param pa_pk_as_rep [Rex::Proto::Kerberos::Model::PreAuthPkAsRep] The PA_DATA response from the server containing the server's public key
            # @param dh [OpenSSL::PKey::DH, string] The Diffie Hellman object
            # @param dh_nonce [String] The random client nonce we sent to the server
            # @param etype [Integer] The encryption type, from Rex::Proto::Kerberos::Crypto::Encryption
            # @return [String] The calculated shared key
            def calculate_shared_key(pa_pk_as_rep, dh, dh_nonce, etype)
              dh_rep_info = pa_pk_as_rep.dh_rep_info
              signed_data = dh_rep_info.signed_data
              dh_key_info = signed_data[:encap_content_info].econtent
              server_public_key = RASN1::Types::Integer.parse(dh_key_info[:subject_public_key].value).value
              shared_key = dh.compute_key(server_public_key.to_bn)
              server_nonce = pa_pk_as_rep[:server_dh_nonce].value
              full_key = shared_key + dh_nonce + server_nonce
              k_truncate(full_key, etype)
            end

            # Build a PreAuth data entry structure for negotiating a shared DH key with the server
            #
            # @param pfx [OpenSSL::PKCS12] A PKCS12-encoded certificate
            # @param dh [OpenSSL::PKey::DH, string] The Diffie Hellman object
            # @param dh_nonce [String] The random client nonce we sent to the server
            # @param request_body [Rex::Proto::Kerberos::Model::KdcRequest] The request body accompanying this PreAuth entry
            # @param opts [Hash] Options to override default values for certain PreAuth entry fields
            # @return  [Rex::Proto::Kerberos::Model::PreAuthDataEntry] The constructed PreAuth data entry request
            def build_pa_pk_as_req(pfx, dh, dh_nonce, request_body, opts)
              certificate = pfx.certificate
              now_time = Time.now.utc
              now_ctime = now_time.round
              ctime = opts.fetch(:ctime) { now_ctime }
              cusec = opts.fetch(:cusec) { now_time&.usec || 0 }
              nonce = opts.fetch(:nonce) { rand(1 << 31) }
              data = request_body.encode
              checksum = Digest::SHA1.digest(data)
              pub_key_encoded = RASN1::Types::Integer.new(value: dh.pub_key.to_i).to_der
              auth_pack = Rex::Proto::Kerberos::Model::Pkinit::AuthPack.new(
                pk_authenticator: {
                  cusec: cusec,
                  ctime: ctime,
                  nonce: nonce,
                  pa_checksum: checksum
                },
                client_public_value: {
                  algorithm: {
                    algorithm: Rex::Proto::Kerberos::Model::OID::DiffieHellman, # Diffie-Hellman
                    parameters: Rex::Proto::Kerberos::Model::Pkinit::DomainParameters.new(
                      p: dh.p.to_i,
                      g: dh.g.to_i,
                      q: 0
                    )
                  },
                  subject_public_key: pub_key_encoded
                },
                client_dh_nonce: RASN1::Types::OctetString.new(value: dh_nonce)
              )

              auth_pack[:client_public_value][:subject_public_key].bit_length = pub_key_encoded.length * 8

              signed_auth_pack = sign_auth_pack(auth_pack, pfx.key, certificate)

              pa_as_req = Rex::Proto::Kerberos::Model::PreAuthPkAsReq.new

              pa_as_req.signed_auth_pack = signed_auth_pack

              Rex::Proto::Kerberos::Model::PreAuthDataEntry.new(type: Rex::Proto::Kerberos::Model::PreAuthType::PA_PK_AS_REQ,
                                                                value: pa_as_req.to_der)
            end

            # Calculate the cryptographic signatures over the AuthPack, and create the appropriate
            # ASN.1-encoded structure, per https://www.rfc-editor.org/rfc/rfc4556#section-3.2.1
            #
            # @param auth_pack [Rex::Proto::Kerberos::Model::Pkinit::AuthPack] The AuthPack to sign
            # @param key [OpenSSL::PKey] The private key to digitally sign the data
            # @param dh [OpenSSL::X509::Certificate] The certificate associated with the private key
            # @return [Rex::Proto::Kerberos::Model::Pkinit::ContentInfo] The signed AuthPack
            def sign_auth_pack(auth_pack, key, certificate)
              signer_info = Rex::Proto::Kerberos::Model::Pkinit::SignerInfo.new(
                version: 1,
                sid: {
                  issuer: certificate.issuer,
                  serial_number: certificate.serial.to_i
                },
                digest_algorithm: {
                  algorithm: Rex::Proto::Kerberos::Model::OID::SHA1
                },
                signed_attrs: [
                  {
                    attribute_type: Rex::Proto::Kerberos::Model::OID::ContentType,
                    attribute_values: [RASN1::Types::Any.new(value: RASN1::Types::ObjectId.new(value: Rex::Proto::Kerberos::Model::OID::PkinitAuthData))]
                  },
                  {
                    attribute_type: Rex::Proto::Kerberos::Model::OID::MessageDigest,
                    attribute_values: [RASN1::Types::Any.new(value: RASN1::Types::OctetString.new(value: Digest::SHA1.digest(auth_pack.to_der)))]
                  }
                ],
                signature_algorithm: {
                  algorithm: Rex::Proto::Kerberos::Model::OID::RSAWithSHA1
                }
              )
              data = RASN1::Types::Set.new(value: signer_info[:signed_attrs].value).to_der
              signature = key.sign(OpenSSL::Digest.new('SHA1'), data)

              signer_info[:signature] = signature

              signed_data = Rex::Proto::Kerberos::Model::Pkinit::SignedData.new(
                version: 3,
                digest_algorithms: [
                  {
                    algorithm: Rex::Proto::Kerberos::Model::OID::SHA1
                  }
                ],
                encap_content_info: {
                  econtent_type: Rex::Proto::Kerberos::Model::OID::PkinitAuthData,
                  econtent: auth_pack.to_der
                },
                certificates: [{ openssl_certificate: certificate }],
                signer_infos: [signer_info]
              )

              Rex::Proto::Kerberos::Model::Pkinit::ContentInfo.new(
                content_type: Rex::Proto::Kerberos::Model::OID::SignedData,
                signed_data: signed_data
              )
            end
          end
        end
      end
    end
  end
end