diff --git a/Gemfile.lock b/Gemfile.lock index 3a675578d91d..529001b07214 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -150,7 +150,7 @@ GEM aws-eventstream (~> 1, >= 1.0.2) bcrypt (3.1.18) bcrypt_pbkdf (1.1.0) - bindata (2.4.14) + bindata (2.4.15) bson (4.15.0) builder (3.2.4) byebug (11.1.3) diff --git a/lib/rex/proto/kerberos/credential_cache/krb5_ccache_presenter.rb b/lib/rex/proto/kerberos/credential_cache/krb5_ccache_presenter.rb index f763095981de..a5fdf7c647b0 100644 --- a/lib/rex/proto/kerberos/credential_cache/krb5_ccache_presenter.rb +++ b/lib/rex/proto/kerberos/credential_cache/krb5_ccache_presenter.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'base64' +require 'rex/proto/kerberos/pac/krb5_pac' module Rex::Proto::Kerberos::CredentialCache class Krb5CcachePresenter @@ -48,8 +49,6 @@ def present(key: nil) output.join("\n") end - protected - # @return [Rex::Proto::Kerberos::CredentialCache::Krb5Ccache] attr_reader :ccache @@ -107,11 +106,10 @@ def present_cred(cred, key: nil) output.join("\n") end - # @param [Rex::Proto::Kerberos::Pac::Krb5PacInfoBuffer] info_buffer + # @param [Rex::Proto::Kerberos::Pac::Krb5LogonInfo] logon_info # @return [String] A human readable representation of a Logon Information - def present_logon_info(info_buffer) - validation_info = info_buffer.buffer.pac_element.data - + def present_logon_info(logon_info) + validation_info = logon_info.data output = [] output << 'Validation Info:' @@ -156,10 +154,9 @@ def present_logon_info(info_buffer) output.join("\n") end - # @param [Rex::Proto::Kerberos::Pac::Krb5PacInfoBuffer] info_buffer + # @param [Rex::Proto::Kerberos::Pac::Krb5ClientInfo] client_info # @return [String] A human readable representation of a Client Info - def present_client_info(info_buffer) - client_info = info_buffer.buffer.pac_element + def present_client_info(client_info) output = [] output << 'Client Info:' output << "Name: '#{client_info.name.encode('utf-8')}'".indent(2) @@ -167,39 +164,55 @@ def present_client_info(info_buffer) output.join("\n") end - # @param [Rex::Proto::Kerberos::Pac::Krb5PacInfoBuffer] info_buffer + # @param [Rex::Proto::Kerberos::Pac::Krb5PacServerChecksum] server_checksum # @return [String] A human readable representation of a Server Checksum - def present_server_checksum(info_buffer) - server_checksum = info_buffer.buffer.pac_element - + def present_server_checksum(server_checksum) sig = server_checksum.signature.bytes.map { |x| "#{x.to_s(16).rjust(2, '0')}" }.join "Pac Server Checksum:\n" + "Signature: #{sig}".indent(2) end - # @param [Rex::Proto::Kerberos::Pac::Krb5PacInfoBuffer] info_buffer + # @param [Rex::Proto::Kerberos::Pac::Krb5PacPrivServerChecksum] priv_server_checksum # @return [String] A human readable representation of a Privilege Server Checksum - def present_priv_server_checksum(info_buffer) - priv_server_checksum = info_buffer.buffer.pac_element - + def present_priv_server_checksum(priv_server_checksum) sig = priv_server_checksum.signature.bytes.map { |x| "#{x.to_s(16).rjust(2, '0')}" }.join "Pac Privilege Server Checksum:\n" + "Signature: #{sig}".indent(2) end + # @param [Rex::Proto::Kerberos::Pac::Krb5UpnDnsInfo] upn_and_dns_info + # @return [String] A human readable representation of a UPN and DNS information element + def present_upn_and_dns_information(upn_and_dns_info) + output = [] + output << 'UPN and DNS Information:' + output << "UPN: #{upn_and_dns_info.upn.encode('utf-8')}".indent(2) + output << "DNS Domain Name: #{upn_and_dns_info.dns_domain_name.encode('utf-8')}".indent(2) + + output << "Flags: #{upn_and_dns_info.flags}".indent(2) + + if upn_and_dns_info.has_s_flag? + output << "SAM Name: #{upn_and_dns_info.sam_name.encode('utf-8')}".indent(2) + output << "SID: #{upn_and_dns_info.sid}".indent(2) + end + output.join("\n") + end + # @param [Rex::Proto::Kerberos::Pac::Krb5PacInfoBuffer] info_buffer # @return [String] A human readable representation of a Pac Info Buffer def present_pac_info_buffer(info_buffer) ul_type = info_buffer.ul_type.to_i + pac_element = info_buffer.buffer.pac_element case ul_type when Rex::Proto::Kerberos::Pac::Krb5PacElementType::LOGON_INFORMATION - present_logon_info(info_buffer) + present_logon_info(pac_element) when Rex::Proto::Kerberos::Pac::Krb5PacElementType::CLIENT_INFORMATION - present_client_info(info_buffer) + present_client_info(pac_element) when Rex::Proto::Kerberos::Pac::Krb5PacElementType::SERVER_CHECKSUM - present_server_checksum(info_buffer) + present_server_checksum(pac_element) when Rex::Proto::Kerberos::Pac::Krb5PacElementType::PRIVILEGE_SERVER_CHECKSUM - present_priv_server_checksum(info_buffer) + present_priv_server_checksum(pac_element) + when Rex::Proto::Kerberos::Pac::Krb5PacElementType::USER_PRINCIPAL_NAME_AND_DNS_INFORMATION + present_upn_and_dns_information(pac_element) else ul_type_name = Rex::Proto::Kerberos::Pac::Krb5PacElementType.const_name(ul_type) ul_type_name = ul_type_name.gsub('_', ' ').capitalize if ul_type_name diff --git a/lib/rex/proto/kerberos/pac/krb5_pac.rb b/lib/rex/proto/kerberos/pac/krb5_pac.rb index fab61dc3d504..5adc5d2e8cad 100644 --- a/lib/rex/proto/kerberos/pac/krb5_pac.rb +++ b/lib/rex/proto/kerberos/pac/krb5_pac.rb @@ -2,6 +2,7 @@ require 'bindata' require 'ruby_smb/dcerpc' +require 'rex/proto/ms_dtyp' # full MIDL spec for PAC # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-pac/1d4912dd-5115-4124-94b6-fa414add575f @@ -381,6 +382,148 @@ def decrypt_serialized_data(key) end end + # See [2.10 UPN_DNS_INFO](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-pac/1c0d6e11-6443-4846-b744-f9f810a504eb) + class Krb5UpnDnsInfo < BinData::Record + auto_call_delayed_io + endian :little + # @!attribute [r] ul_type + # @return [Integer] Describes the type of data present in the buffer + virtual :ul_type, value: Krb5PacElementType::USER_PRINCIPAL_NAME_AND_DNS_INFORMATION + + # @!attribute [rw] upn_length + # @return [Integer] The length of the UPN + uint16 :upn_length, value: -> { upn.num_bytes } + + # @!attribute [rw] upn_offset + # @return [Integer] The relative offset of the UPN from the beginning of this structure + uint16 :upn_offset + + # @!attribute [rw] dns_domain_name_length + # @return [Integer] The length of the DNS domain name + uint16 :dns_domain_name_length, value: -> { dns_domain_name.num_bytes } + + # @!attribute [rw] dns_domain_name_offset + # @return [Integer] The relative offset of the DNS domain name from the beginning of this structure + uint16 :dns_domain_name_offset + + # @!attribute [rw] flags + # @return [Integer] + # U flag (bit 0) The user account object does not have the userPrincipalName attribute. + # S flag (bit 1) The structure has been extended with the user account’s SAM Name and SID. + # The remaining bits are ignored. + uint32 :flags + + # @!attribute [rw] sam_name_length + # @return [Integer] The length of the SAM name + # Only available if the S flag is set + uint16 :sam_name_length, value: -> { sam_name.num_bytes }, onlyif: :has_s_flag? + + # @!attribute [rw] sam_name_offset + # @return [Integer] The relative offset of the SAM name from the beginning of this structure + # Only available if the S flag is set + uint16 :sam_name_offset, onlyif: :has_s_flag? + + # @!attribute [rw] sid_length + # @return [Integer] The length of the SID + # Only available if the S flag is set + uint16 :sid_length, value: -> { sid.num_bytes }, onlyif: :has_s_flag? + + # @!attribute [rw] sid_offset + # @return [Integer] The relative offset of the SID from the beginning of this structure + # Only available if the S flag is set + uint16 :sid_offset, onlyif: :has_s_flag? + + # @!attribute [rw] upn + # @return [String] The UPN (User Principal Name) (e.g. test@windomain.local) + delayed_io :upn, read_abs_offset: -> { self.abs_offset + upn_offset } do + string16 read_length: :upn_length + end + + # @!attribute [rw] dns_domain_name + # @return [String] The DNS Domain Name (e.g. WINDOMAIN.LOCAL) + delayed_io :dns_domain_name, read_abs_offset: -> { self.abs_offset + dns_domain_name_offset } do + string16 read_length: :dns_domain_name_length + end + + # @!attribute [rw] sam_name + # @return [String] The SAM Name (e.g. test) + delayed_io :sam_name, read_abs_offset: -> { self.abs_offset + sam_name_offset }, onlyif: -> { has_s_flag? } do + string16 read_length: :sam_name_length + end + + # @!attribute [rw] sid + # @return [MsDtypSid] The SID (e.g. S-1-5-32-544) + delayed_io :sid, read_abs_offset: -> { self.abs_offset + sid_offset }, onlyif: -> { has_s_flag? } do + ms_dtyp_sid + end + + # def initialize_instance(*args) + # super + # set_offsets! + # end + # @return [Boolean] Returns the value of the S flag + def has_s_flag? + flags.anybits?(0b10) + end + + # @param [Boolean] bool The value to set the S flag to + # @return [void] + def set_s_flag(bool) + set_flag_bit(1, bool) + end + + # @return [Boolean] Returns the value of the U flag + def has_u_flag? + flags.anybits?(0b01) + end + + # @param [Boolean] bool The value to set the U flag to + # @return [void] + def set_u_flag(bool) + set_flag_bit(0, bool) + end + + # @param [Integer] upn The relative offset for the upn + # @param [Integer] dns_domain_name The relative offset for the dns_domain_name + # @param [Integer] sam_name The relative offset for the sam_name + # @param [Integer] sid The relative offset for the sid + # @return [void] + # + # Allows you to specify the offsets for the contents, otherwise defaults them + def set_offsets!(upn: nil, dns_domain_name: nil, sam_name: nil, sid: nil) + self.upn_offset = upn || calc_upn_offset + self.dns_domain_name_offset = dns_domain_name || calc_dns_domain_name_offset + self.sam_name_offset = sam_name || calc_sam_name_offset + self.sid_offset = sid || calc_sid_offset + end + + private + + def set_flag_bit(position, bool) + if bool + self.flags |= (1 << position) + else + self.flags &= ~(1 << position) + end + end + + def calc_upn_offset + has_s_flag? ? 24 : 16 + end + + def calc_dns_domain_name_offset + upn_offset + upn_length + end + + def calc_sam_name_offset + dns_domain_name_offset + dns_domain_name_length + end + + def calc_sid_offset + sam_name_offset + sam_name_length + end + end + class Krb5PacElement < BinData::Choice mandatory_parameter :data_length @@ -389,6 +532,7 @@ class Krb5PacElement < BinData::Choice krb5_pac_server_checksum Krb5PacElementType::SERVER_CHECKSUM krb5_pac_priv_server_checksum Krb5PacElementType::PRIVILEGE_SERVER_CHECKSUM krb5_pac_credential_info Krb5PacElementType::CREDENTIAL_INFORMATION, data_length: :data_length + krb5_upn_dns_info Krb5PacElementType::USER_PRINCIPAL_NAME_AND_DNS_INFORMATION unknown_pac_element :default, data_length: :data_length, selection: :selection end diff --git a/spec/lib/rex/proto/kerberos/credential_cache/krb5_ccache_presenter_spec.rb b/spec/lib/rex/proto/kerberos/credential_cache/krb5_ccache_presenter_spec.rb index 691ce1913ed8..bee0a15577e9 100644 --- a/spec/lib/rex/proto/kerberos/credential_cache/krb5_ccache_presenter_spec.rb +++ b/spec/lib/rex/proto/kerberos/credential_cache/krb5_ccache_presenter_spec.rb @@ -253,4 +253,45 @@ end end end + + describe '#present_upn_and_dns_information' do + let(:upn) { 'test@windomain.local' } + let(:dns_domain_name) { 'WINDOMAIN.LOCAL' } + let(:sam_name) { 'test' } + let(:sid) { 'S-1-5-32-544' } + + context 'with no sam name or sid' do + let(:flags) { 0b01 } + let(:upn_and_dns_info) do + Rex::Proto::Kerberos::Pac::Krb5UpnDnsInfo.new(upn: upn, dns_domain_name: dns_domain_name, flags: flags) + end + it 'returns the correct string' do + expect(subject.present_upn_and_dns_information(upn_and_dns_info)).to eq <<~EOF.rstrip + UPN and DNS Information: + UPN: test@windomain.local + DNS Domain Name: WINDOMAIN.LOCAL + Flags: 1 + EOF + end + end + + context 'with sam name and sid' do + let(:flags) { 0b11 } + let(:upn_and_dns_info) do + Rex::Proto::Kerberos::Pac::Krb5UpnDnsInfo.new( + upn: upn, dns_domain_name: dns_domain_name, sam_name: sam_name, sid: sid, flags: flags + ) + end + it 'returns the correct string' do + expect(subject.present_upn_and_dns_information(upn_and_dns_info)).to eq <<~EOF.rstrip + UPN and DNS Information: + UPN: test@windomain.local + DNS Domain Name: WINDOMAIN.LOCAL + Flags: 3 + SAM Name: test + SID: S-1-5-32-544 + EOF + end + end + end end diff --git a/spec/lib/rex/proto/kerberos/pac/krb5_pac_spec.rb b/spec/lib/rex/proto/kerberos/pac/krb5_pac_spec.rb index 9d75fdf83c23..5439c17f819c 100644 --- a/spec/lib/rex/proto/kerberos/pac/krb5_pac_spec.rb +++ b/spec/lib/rex/proto/kerberos/pac/krb5_pac_spec.rb @@ -300,3 +300,133 @@ end end end + +RSpec.describe Rex::Proto::Kerberos::Pac::Krb5UpnDnsInfo do + let(:upn) { 'test@windomain.local' } + let(:dns_domain_name) { 'WINDOMAIN.LOCAL' } + let(:sam_name) { 'test' } + let(:sid) { 'S-1-5-32-544' } + + let(:sample) do + "\x28\x00\x10\x00\x1e\x00\x38\x00\x01\x00\x00\x00\x00\x00\x00\x00" \ + "\x74\x00\x65\x00\x73\x00\x74\x00\x40\x00\x77\x00\x69\x00\x6e\x00" \ + "\x64\x00\x6f\x00\x6d\x00\x61\x00\x69\x00\x6e\x00\x2e\x00\x6c\x00" \ + "\x6f\x00\x63\x00\x61\x00\x6c\x00\x57\x00\x49\x00\x4e\x00\x44\x00" \ + "\x4f\x00\x4d\x00\x41\x00\x49\x00\x4e\x00\x2e\x00\x4c\x00\x4f\x00" \ + "\x43\x00\x41\x00\x4c\x00" + end + + let(:sample_ext) do + "\x28\x00\x18\x00\x1e\x00\x40\x00\x02\x00\x00\x00\x08\x00\x5e\x00" \ + "\x10\x00\x66\x00\x00\x00\x00\x00\x74\x00\x65\x00\x73\x00\x74\x00" \ + "\x40\x00\x77\x00\x69\x00\x6e\x00\x64\x00\x6f\x00\x6d\x00\x61\x00" \ + "\x69\x00\x6e\x00\x2e\x00\x6c\x00\x6f\x00\x63\x00\x61\x00\x6c\x00" \ + "\x57\x00\x49\x00\x4e\x00\x44\x00\x4f\x00\x4d\x00\x41\x00\x49\x00" \ + "\x4e\x00\x2e\x00\x4c\x00\x4f\x00\x43\x00\x41\x00\x4c\x00\x74\x00" \ + "\x65\x00\x73\x00\x74\x00\x01\x02\x00\x00\x00\x00\x00\x05\x20\x00" \ + "\x00\x00\x20\x02\x00\x00" + end + + context 'with non-extended upn dns info' do + describe '#read' do + it 'correctly parses the binary data' do + upn_dns_info = described_class.read(sample) + expect(upn_dns_info.to_binary_s).to eq(sample) + end + + it 'creates a valid upn dns info structure' do + upn_dns_info = described_class.new( + upn: upn, + dns_domain_name: dns_domain_name, + flags: 1 + ) + + upn_dns_info.set_offsets! + + parsed_sample = described_class.read(sample) + expect(upn_dns_info).to eq(parsed_sample) + end + + it 'ignores sam/sid values if set' do + upn_dns_info = described_class.new( + upn: upn, + dns_domain_name: dns_domain_name, + sam_name: sam_name, + sid: sid, + flags: 1 + ) + upn_dns_info.set_offsets! + + parsed_sample = described_class.read(sample) + expect(upn_dns_info).to eq(parsed_sample) + end + end + + describe '#write' do + it 'outputs the expected binary representation' do + upn_dns_info = described_class.new( + upn: upn, + dns_domain_name: dns_domain_name, + flags: 1 + ) + upn_dns_info.set_offsets! + + binary = upn_dns_info.to_binary_s + expect(binary).to eq(sample) + end + + it 'ignores sam/sid values if set' do + upn_dns_info = described_class.new( + upn: upn, + dns_domain_name: dns_domain_name, + sam_name: sam_name, + sid: sid, + flags: 1 + ) + upn_dns_info.set_offsets! + + binary = upn_dns_info.to_binary_s + expect(binary).to eq(sample) + end + end + end + + context 'with extended upn dns info' do + describe '#read' do + it 'correctly parses the binary data' do + upn_dns_info = described_class.read(sample_ext) + expect(upn_dns_info.to_binary_s).to eq(sample_ext) + end + + it 'creates a valid upn dns info structure' do + upn_dns_info = described_class.new( + upn: upn, + dns_domain_name: dns_domain_name, + sam_name: sam_name, + sid: sid, + flags: 2 + ) + upn_dns_info.set_offsets! + + parsed_sample = described_class.read(sample_ext) + expect(upn_dns_info).to eq(parsed_sample) + end + end + + describe '#write' do + it 'outputs the expected binary representation' do + upn_dns_info = described_class.new( + upn: upn, + dns_domain_name: dns_domain_name, + sam_name: sam_name, + sid: sid, + flags: 2 + ) + upn_dns_info.set_offsets! + + binary = upn_dns_info.to_binary_s + expect(binary).to eq(sample_ext) + end + end + end +end