# -*- coding: binary -*-

module Msf
  class Exploit
    class Remote
      module Kerberos
        module Client
          include Msf::Exploit::Remote::Kerberos::Client::Base
          include Msf::Exploit::Remote::Kerberos::Client::ApRequest
          include Msf::Exploit::Remote::Kerberos::Client::AsRequest
          include Msf::Exploit::Remote::Kerberos::Client::AsResponse
          include Msf::Exploit::Remote::Kerberos::Client::TgsRequest
          include Msf::Exploit::Remote::Kerberos::Client::TgsResponse
          include Msf::Exploit::Remote::Kerberos::Client::Pac
          include Msf::Exploit::Remote::Kerberos::Client::Pkinit

          # https://datatracker.ietf.org/doc/html/rfc4121#section-4.1
          TOK_ID_KRB_AP_REQ = "\x01\x00"
          TOK_ID_KRB_AP_REP = "\x02\x00"
          TOK_ID_KRB_ERROR  = "\x03\x00"

          # https://datatracker.ietf.org/doc/html/rfc4178#section-4.2.2
          NEG_TOKEN_ACCEPT_COMPLETED      = 0
          NEG_TOKEN_ACCEPT_INCOMPLETE     = 1
          NEG_TOKEN_REJECT                = 2
          NEG_TOKEN_REQUEST_MIC           = 3

          # @!attribute client
          #   @return [Rex::Proto::Kerberos::Client] The kerberos client
          attr_accessor :client

          def initialize(info = {})
            super

            register_options(
              [
                Opt::RHOST,
                Opt::RPORT(88),
                OptInt.new('Timeout', [true, 'The TCP timeout to establish Kerberos connection and read data', 10])
              ], self.class
            )
          end

          # Returns the target host
          #
          # @return [String]
          def rhost
            datastore['RHOST']
          end

          # Returns the remote port
          #
          # @return [Integer]
          def rport
            datastore['RPORT']
          end

          # Returns the TCP timeout
          #
          # @return [Integer]
          def timeout
            datastore['Timeout']
          end

          # Returns the kdc peer
          #
          # @return [String]
          def peer
            "#{rhost}:#{rport}"
          end

          # Returns the configured proxy list
          #
          # @return [String,nil]
          def proxies
            datastore['Proxies']
          end

          # Creates a kerberos connection
          #
          # @param opts [Hash{Symbol => <String, Integer>}]
          # @option opts [String] :rhost
          # @option opts [<String, Integer>] :rport
          # @return [Rex::Proto::Kerberos::Client]
          def connect(opts={})
            kerb_client = Rex::Proto::Kerberos::Client.new(
              host: opts[:rhost] || rhost,
              port: (opts[:rport] || rport).to_i,
              proxies: opts[:proxies] || proxies,
              timeout: (opts[:timeout] || timeout).to_i,
              context:
                {
                  'Msf'        => framework,
                  'MsfExploit' => framework_module,
                },
              protocol: 'tcp'
            )

            disconnect if client
            self.client = kerb_client

            kerb_client
          end

          # Disconnects the Kerberos client
          #
          # @param kerb_client [Rex::Proto::Kerberos::Client] the client to disconnect
          def disconnect(kerb_client = client)
            kerb_client.close if kerb_client

            if kerb_client == client
              self.client = nil
            end
          end

          # Performs cleanup as necessary, disconnecting the Kerberos client
          # if it's still established.
          def cleanup
            super
            disconnect
          end

          # Sends a kerberos AS request and reads the response
          #
          # @param opts [Hash]
          # @return [Rex::Proto::Kerberos::Model::KdcResponse]
          # @see Msf::Kerberos::Client::AsRequest#build_as_request
          # @see Rex::Proto::Kerberos::Model::KdcResponse
          def send_request_as(opts = {})
            connect(opts)
            req = opts.fetch(:req) { build_as_request(opts) }
            res = client.send_recv(req)
            disconnect
            res
          end

          # Sends a kerberos AS request and reads the response
          #
          # @param opts [Hash]
          # @return [Rex::Proto::Kerberos::Model::KdcResponse]
          # @see Msf::Kerberos::Client::TgsRequest#build_tgs_request
          # @see Rex::Proto::Kerberos::Model::KdcResponse
          def send_request_tgs(opts = {})
            connect(opts)
            req = opts.fetch(:req) { build_tgs_request(opts) }
            res = client.send_recv(req)
            disconnect
            res
          end

          # Select a cipher that both the server and client support,
          # preferencing ours in order. This may just be the default
          # behaviour on Windows, but let's be sure about it.
          #
          # @param client_etypes [Array<Integer>] Available ciphers on the client side (etypes from Rex::Proto::Kerberos::Crypto::Encryption)
          # @param server_etypeinfos_entries [Array<Rex::Proto::Kerberos::Model::PreAuthEtypeInfo2Entry>] Available ciphers (including additional info such as salts) on the server
          # @return [Rex::Proto::Kerberos::Model::EtypeInfo] The selected cipher
          def select_cipher(client_etypes, server_etypeinfos_entries)
            client_etypes.each do |client_etype|
              server_etypeinfos_entries.each do |server_etypeinfo2_entry|
                if server_etypeinfo2_entry.etype == client_etype
                  return server_etypeinfo2_entry
                end
              end
            end
          end

          # Send a TGT request using PKINIT (certificate) authentication
          #
          # @param options [Hash]
          # @option [OpenSSL::PKCS12] :pfx A PKCS12-encoded certificate
          # @option [Boolean] :request_pac Whether or not to request the PAC
          # @option [String] :client_name The client name for the request
          # @option [String] :realm The realm for the request
          # @option [String] :server_name The server name (e.g. krbtgt) for the request
          # @option [Array<Integer>] :offered_etypes The encryption types to offer
          # @return [Msf::Exploit::Remote::Kerberos::Model::TgtResponse] The TGT response and the key
          def send_request_tgt_pkinit(options = {})
            pfx = options[:pfx]
            request_pac = options.fetch(:request_pac, true)
            realm = options[:realm]
            server_name = options.fetch(:server_name, "krbtgt/#{realm}")
            client_name = options[:client_name]
            client_name = client_name.dup.force_encoding('utf-8') if client_name
            ticket_options = options.fetch(:options) { 0x50800000 } # Forwardable, Proxiable, Renewable

            # The diffie hellman client parameters
            dh, dh_nonce = build_dh

            now = Time.now.utc
            expiry_time = now + 1.day
            offered_etypes = options[:offered_etypes] || Rex::Proto::Kerberos::Crypto::Encryption::PkinitEtypes
            request_body = build_as_request_body(
              client_name: client_name,
              server_name: server_name,
              realm: realm,

              etype: offered_etypes,

              # Specify nil to ensure the KDC uses the current time for the desired starttime of the requested ticket
              from: nil,
              till: expiry_time,
              rtime: expiry_time,
              options: ticket_options
            )
            as_req = build_as_request(
              pa_data: [
                build_pa_pac_request(pac_request_value: request_pac),
                build_pa_pk_as_req(pfx, dh, dh_nonce, request_body, options)
              ],
              body: request_body
            )

            # Send the request
            options[:req] = as_req
            as_res = send_request_as(options)

            if as_res.msg_type == Rex::Proto::Kerberos::Model::AS_REP
              entry = as_res.pa_data.find {|entry| entry.type == Rex::Proto::Kerberos::Model::PreAuthType::PA_PK_AS_REP}
              raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new('No PKINIT PreAuth data received') if entry.nil? # Should never happen from a spec-compliant server

              pa_pk_as_rep = entry.decoded_value
              key = calculate_shared_key(pa_pk_as_rep, dh, dh_nonce, as_res.enc_part.etype)
              return Msf::Exploit::Remote::Kerberos::Model::TgtResponse.new(
                as_rep: as_res,
                preauth_required: true,
                decrypted_part: decrypt_kdc_as_rep_enc_part(as_res, key),
                krb_enc_key: {
                  enctype: as_res.enc_part.etype,
                  key: key
                }
              )
            elsif as_res.msg_type == Rex::Proto::Kerberos::Model::KRB_ERROR
              raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new(res: as_res)
            else
              # Should never happen, per the spec
              raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new('Unexpected response type (expected AS_REP or KRB_ERROR)')
            end
          end

          # Sends the required kerberos AS requests for a kerberos Ticket Granting Ticket
          #
          # @param options [Hash]
          # @return [Msf::Exploit::Remote::Kerberos::Model::TgtResponse] The TGT response and the key
          # @raise [Rex::Proto::Kerberos::Model::Error::KerberosError] if the provided credentials are invalid
          def send_request_tgt(options = {})
            realm = options[:realm]
            server_name = options[:server_name]
            client_name = options[:client_name]
            client_name = client_name.dup.force_encoding('utf-8') if client_name
            password = options[:password]
            password = password.dup.force_encoding('utf-8') if password
            key = options[:key]
            request_pac = options.fetch(:request_pac, true)
            ticket_options = options.fetch(:options) { 0x50800000 } # Forwardable, Proxiable, Renewable

            # First stage: Send an initial AS-REQ request, used to exchange supported encryption methods.
            # The server may respond with a ticket granting ticket (TGT) immediately,
            # or the client may require preauthentication, and a second AS-REQ is required

            now = Time.now.utc
            expiry_time = now + 1.day

            offered_etypes = options[:offered_etypes] || Rex::Proto::Kerberos::Crypto::Encryption::DefaultOfferedEtypes
            if !password && key && offered_etypes.length != 1
              raise ArgumentError.new('Exactly one etype must be specified in :offered_etypes when a key is is defined without a password')
            end

            initial_as_req = build_as_request(
              pa_data: [
                build_pa_pac_request(pac_request_value: request_pac)
              ],
              body: build_as_request_body(
                client_name: client_name,
                server_name: server_name,
                realm: realm,

                etype: offered_etypes,

                # Specify nil to ensure the KDC uses the current time for the desired starttime of the requested ticket
                from: nil,
                till: expiry_time,
                rtime: expiry_time,
                options: ticket_options
              ),
            )

            initial_as_res = send_request_as(req: initial_as_req)

            # If we receive an AS_REP response immediately, no-preauthentication was required and we can return immediately
            if initial_as_res.msg_type == Rex::Proto::Kerberos::Model::AS_REP
              return Msf::Exploit::Remote::Kerberos::Model::TgtResponse.new(
                as_rep: initial_as_res,
                preauth_required: false,
                decrypted_part: nil,
                krb_enc_key: nil,
              )
            end

            # If we're just AS_REP Roasting, we can't go any further
            raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new(res: initial_as_res) if password.nil? && key.nil?

            # Verify error codes. Anything other than the server requiring an additional preauth request is considered a failure.
            if initial_as_res.msg_type == Rex::Proto::Kerberos::Model::KRB_ERROR && initial_as_res.error_code != Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_PREAUTH_REQUIRED
              if initial_as_res.error_code == Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_ETYPE_NOSUPP
                raise Rex::Proto::Kerberos::Model::Error::KerberosEncryptionNotSupported.new(encryption_type: offered_etypes)
              end

              raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new(res: initial_as_res)
            end

            # Second stage: Send an additional AS-REQ request with preauthentication provided
            # Note that Clock skew issues may be raised at this point

            pa_data = initial_as_res.e_data_as_pa_data
            etype_entries = pa_data.find {|entry| entry.type == Rex::Proto::Kerberos::Model::PreAuthType::PA_ETYPE_INFO2}

            # No etypes specified - how are we supposed to negotiate ciphers?
            raise Rex::Proto::Kerberos::Model::Error::KerberosEncryptionNotSupported.new(encryption_type: offered_etypes) unless etype_entries

            server_ciphers = etype_entries.decoded_value
            remaining_server_ciphers_to_attempt = server_ciphers.etype_info2_entries.select do |server_etypeinfo2_entry|
              offered_etypes.include?(server_etypeinfo2_entry.etype)
            end

            if remaining_server_ciphers_to_attempt.empty?
              raise Rex::Proto::Kerberos::Model::Error::KerberosEncryptionNotSupported.new(encryption_type: offered_etypes)
            end

            # Attempt to use the available ciphers; In some scenarios they can fail due to GPO configurations
            # So we need to iterate until a success - or there's no more ciphers available
            while remaining_server_ciphers_to_attempt.any?
              selected_etypeinfo = select_cipher(offered_etypes, remaining_server_ciphers_to_attempt)
              selected_etype = selected_etypeinfo.etype

              if password
                salt = selected_etypeinfo.salt
                salt = salt.dup.force_encoding('utf-8') if salt
                params = selected_etypeinfo.s2kparams

                encryptor = Rex::Proto::Kerberos::Crypto::Encryption::from_etype(selected_etype)
                enc_key = encryptor.string_to_key(password, salt, params: params)
              elsif key
                raise ArgumentError.new('Encryption key provided without one offered encryption type') unless options[:offered_etypes]&.length == 1
                enc_key = key
              end

              preauth_as_req = build_as_request(
                pa_data: [
                  build_as_pa_time_stamp(key: enc_key, etype: selected_etype),
                  build_pa_pac_request(pac_request_value: request_pac)
                ],
                body: build_as_request_body(
                  client_name: client_name,
                  server_name: server_name,
                  realm: realm,
                  key: enc_key,

                  etype: remaining_server_ciphers_to_attempt.map(&:etype),

                  # Specify nil to ensure the KDC uses the current time for the desired starttime of the requested ticket
                  from: nil,
                  till: expiry_time,
                  rtime: expiry_time
                )
              )

              preauth_as_res = send_request_as(req: preauth_as_req)

              # If we've succeeded - break out of trying ciphers
              break if preauth_as_res.msg_type == Rex::Proto::Kerberos::Model::AS_REP

              # If we've hit a cipher not supported error, try the next cipher if there's more to try
              is_etype_not_supported_error = preauth_as_res.msg_type == Rex::Proto::Kerberos::Model::KRB_ERROR && preauth_as_res.error_code == Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_ETYPE_NOSUPP
              if is_etype_not_supported_error
                remaining_server_ciphers_to_attempt -= [selected_etypeinfo]
                next if remaining_server_ciphers_to_attempt.any?
              end

              # Unexpected server response
              raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new(res: preauth_as_res)
            end

            Msf::Exploit::Remote::Kerberos::Model::TgtResponse.new(
              as_rep: preauth_as_res,
              preauth_required: true,
              krb_enc_key: {
                enctype: selected_etype,
                key: enc_key,
                salt: salt
              },
              decrypted_part: decrypt_kdc_as_rep_enc_part(
                preauth_as_res,
                enc_key,
              )
            )
          end

          protected

          def framework_module
            self
          end
        end
      end
    end
  end
end
