From bebb43f8f64644e6fd7495d5a202fa7c1954d73a Mon Sep 17 00:00:00 2001 From: adfoster-r7 Date: Wed, 13 Aug 2025 08:10:00 +0100 Subject: [PATCH] Improve kerberos file load error messages --- .../kerberos/service_authenticator/base.rb | 60 +++++++---- ...dministrator_dev_demo_local_expired.ccache | Bin 0 -> 1227 bytes ...ke_user_example_local_forged_silver.ccache | Bin 0 -> 1160 bytes .../service_authenticator/base_spec.rb | 102 ++++++++++++++++-- spec/support/shared/contexts/msf/ui_driver.rb | 7 +- 5 files changed, 138 insertions(+), 31 deletions(-) create mode 100644 spec/file_fixtures/ccache/administrator_dev_demo_local_expired.ccache create mode 100644 spec/file_fixtures/ccache/fake_user_example_local_forged_silver.ccache diff --git a/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb b/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb index 7cad02f524..53d6edbf4c 100644 --- a/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb +++ b/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb @@ -157,8 +157,9 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base credential = nil if cache_file.present? # the cache file is only used for loading credentials, it is *not* written to - credential = load_credential_from_file(cache_file, sname: nil, sname_hostname: @hostname) - serviceclass = build_spn.name_string.first + load_sname_hostname_credential_result = load_credential_from_file(cache_file, sname: nil, sname_hostname: @hostname) + credential = load_sname_hostname_credential_result[:credential] + serviceclass = build_spn&.name_string&.first if credential && credential.server.components[0] != serviceclass old_sname = credential.server.components.snapshot.join('/') credential.server.components[0] = serviceclass @@ -168,9 +169,18 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base ticket.sname.name_string[0] = serviceclass credential.ticket = ticket.encode elsif credential.nil? && hostname.present? - credential = load_credential_from_file(cache_file, sname: "krbtgt/#{hostname.split('.', 2).last}") + load_sname_krbtgt_hostname_credential_result = load_credential_from_file(cache_file, sname: "krbtgt/#{hostname.split('.', 2).last}") + credential = load_sname_krbtgt_hostname_credential_result[:credential] end if credential.nil? + print_error("Failed to load a usable credential from ticket file: #{cache_file}") + print_error("Attempt failed to find a valid credential in #{cache_file} for #{load_sname_hostname_credential_result[:filter].map { |k, v| "#{k}=#{v.inspect}" }.join(', ')}:") + print_error(load_sname_hostname_credential_result[:filter_reasons].join("\n").indent(2)) + + if load_sname_krbtgt_hostname_credential_result + print_error("Attempt failed to find a valid credential in #{cache_file} for #{load_sname_krbtgt_hostname_credential_result[:filter].map { |k, v| "#{k}=#{v.inspect}" }.join(', ')}") + print_error(load_sname_krbtgt_hostname_credential_result[:filter_reasons].join("\n").indent(2)) + end raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new("Failed to load a usable credential from ticket file: #{cache_file}") end print_status("Loaded a credential from ticket file: #{cache_file}") @@ -362,7 +372,7 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base # @return [Rex::Proto::Kerberos::CredentialCache::Krb5CcacheCredential] The ccache credential def request_tgt_only(options = {}) if options[:cache_file] - credential = load_credential_from_file(options[:cache_file]) + credential = load_credential_from_file(options[:cache_file])&.fetch(:credential, nil) else credential = get_cached_credential( options.merge( @@ -1058,18 +1068,18 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base # Load a credential object from a file for authentication. Credentials in the file will be filtered by multiple # attributes including their timestamps to ensure that the returned credential appears usable. # - # @param [String] file_path The file path to load a credential object from - # @return [Rex::Proto::Kerberos::CredentialCache::Krb5CacheCredential] the credential object for authentication - def load_credential_from_file(file_path, options = {}) - unless File.readable?(file_path.to_s) - wlog("Failed to load ticket file '#{file_path}' (file not readable)") + # @param [String] path The path to load a credential object from + # @return [Hash] :credential [Rex::Proto::Kerberos::CredentialCache::Krb5CacheCredential] the credential object for authentication + # @return [Hash] :filter_reasons [Array] the reasons for filtering tickets + def load_credential_from_file(path, options = {}) + unless File.readable?(path.to_s) return nil end begin - cache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.read(File.binread(file_path)) + cache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.read(File.binread(path)) rescue StandardError => e - elog("Failed to load ticket file '#{file_path}' (parsing failed)", error: e) + elog("Failed to load ticket file '#{path}' (parsing failed)", error: e) return nil end @@ -1077,45 +1087,53 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base sname_hostname = options.fetch(:sname_hostname, nil) now = Time.now.utc + filter = { + realm: @realm, + sname: sname, + sname_hostname: sname_hostname + }.merge(options) + filter_reasons = [] + cache.credentials.to_ary.each.with_index(1) do |credential, index| tkt_start = credential.starttime == Time.at(0).utc ? credential.authtime : credential.starttime tkt_end = credential.endtime + filter_reason_prefix = "Filtered credential #{path} ##{index} reason: " unless tkt_start < now - wlog("Filtered credential #{file_path} ##{index} reason: Ticket start time is before now (start: #{tkt_start})") + filter_reasons << "#{filter_reason_prefix}Ticket start time is before now (start: #{tkt_start})" next end unless now < tkt_end - wlog("Filtered credential #{file_path} ##{index} reason: Ticket is expired (expiration: #{tkt_end})") + filter_reasons << "#{filter_reason_prefix}Ticket is expired (expiration: #{tkt_end})" next end unless !@realm || @realm.casecmp?(credential.server.realm.to_s) - wlog("Filtered credential #{file_path} ##{index} reason: Realm (#{@realm}) does not match (realm: #{credential.server.realm})") + filter_reasons << "#{filter_reason_prefix} Realm (#{@realm}) does not match (realm: #{credential.server.realm})" next end unless !sname || sname.to_s.casecmp?(credential.server.components.snapshot.join('/')) - wlog("Filtered credential #{file_path} ##{index} reason: SPN (#{sname}) does not match (spn: #{credential.server.components.snapshot.join('/')})") + filter_reasons << "#{filter_reason_prefix}SPN (#{sname}) does not match (spn: #{credential.server.components.snapshot.join('/')})" next end unless !sname_hostname || - sname_hostname.to_s.downcase == credential.server.components[1].downcase || - sname_hostname.to_s.downcase.ends_with?('.' + credential.server.components[1].downcase) - wlog("Filtered credential #{file_path} ##{index} reason: SPN (#{sname_hostname}) hostname does not match (spn: #{credential.server.components.snapshot.join('/')})") + sname_hostname.to_s.downcase == credential.server.components[1].downcase || + sname_hostname.to_s.downcase.ends_with?('.' + credential.server.components[1].downcase) + filter_reasons << "#{filter_reason_prefix}SPN (#{sname_hostname}) hostname does not match (spn: #{credential.server.components.snapshot.join('/')})" next end unless !@username || @username.casecmp?(credential.client.components.last.to_s) - wlog("Filtered credential #{file_path} ##{index} reason: Username (#{@username}) does not match (username: #{credential.client.components.last})") + filter_reasons << "Filtered credential #{path} ##{index} reason: Username (#{@username}) does not match (username: #{credential.client.components.last})" next end - return credential + return { credential: credential, filter: filter, filter_reasons: filter_reasons } end - nil + { credential: nil, filter: filter, filter_reasons: filter_reasons } end end diff --git a/spec/file_fixtures/ccache/administrator_dev_demo_local_expired.ccache b/spec/file_fixtures/ccache/administrator_dev_demo_local_expired.ccache new file mode 100644 index 0000000000000000000000000000000000000000..9a942a62ac53112a4fbd2248f87926e803236322 GIT binary patch literal 1227 zcmZQ&Vc=n4WZ?J@1Pl zMx;_EpmGq#G?gVeGp!gXBb<_KV5FClS_Z_q`Fc6|$%#1(;y?+3^%JkR^Sb_P3RDIM%pVh*nBN*SF~3~E%*4pLP(T{Z(nV?p zDj+GQg|Y_H(qQ|gvDv=3iFp~&m_;D9LJL6}7Bw+XVQFHXu%dho%Yuxn2bM(d=1tz1snDQ+$8rSy}Zc#CIzQdU>UR{~Qbl{i&@-1dH6ODf#zB|G54}01*Bb%c5 zb2n-ad;Wd2=dhYt;U=qY?%(c8g7e;PVLi*h;r{)xnSge`b@3B zGRE&03?EfBnrxl>mdUKRP|>vK&b+z0ynmC=KiWK%A*i7_x3~Iqo29b6$hn4W&%TXq ze{2;OulnBq{OJ9p%Z5=3QNeY-CpBhWlsi@V+tF>?tFq2_H)2;^`Q_1``%vWPf`FEq z1JPm8%+h;T6*O-DRG4~E>hHI8vQ=Eqg>Gd=YrVCuJ!+twl<$ADQBwC;kZN~JG^d2( z+px)Zg;K=D%3kX^*H+v8I`Uz6ucDLh`^T}9qxgc8A4V?NzDjTB$}N9Z6iS_$+WzLo zZ|6_mAG>_t6vWv}+Fy2IHha%xR8cdxdCjihb5=^HuJr7*NK}1)=y=Kd{ry)qzMFAu z_QH#9EPqsNU`|cY@BU zx7?TI&Mgsn9eq;aPy9a3t^ON>543bf^Bgrg-o2OOyRPfL$1j znztg8Z+MoN`N&O}^6bIqnq(G3oy%_xls@*oI@iB$()W!MEVJG9&UZ~+C|YRQEE!$6 zLC9~7PmW~wvHH}%#cLbReQMn{|JSw+?la~H^Pk)jZSsB6;?8s1e(#=cwdGUD^fTN6 zNmD+2J9lE?t(|9@RF{Z+={bAXKX0$p<(KJ-+w(Z4O}e2Lx^idFMtPIoS)6eCR8z;xJYInaG!;T#QfW2s)@I)|#y}kiN3w-|FQHXZ%0Wx$oA**5(^?US_Ow z@R+{7Bz3obgZ-HcVk)JditIi=iI1wEnqr>ABEk_`{!R1qB&L*q%`Zc3{!A0OoE`jE Rc=2{8gJ7kcCQvEC0068s7gzuQ literal 0 HcmV?d00001 diff --git a/spec/file_fixtures/ccache/fake_user_example_local_forged_silver.ccache b/spec/file_fixtures/ccache/fake_user_example_local_forged_silver.ccache new file mode 100644 index 0000000000000000000000000000000000000000..e0de6ddb83fc1424d8662c83da6580b6c2a5aeeb GIT binary patch literal 1160 zcmZQ&VE_R}DCTvIaP$rEan2fs8pA9O-{9cFoZzx# zprlwzvY}pTMPhD2PO4r`esW@tm7$5TF@rczP9Qne)Wp~@IW^VH$iToN#WG`d)C~}> zPLBm+hJXb?WpKc}FR_Vvmq8Qrwgt>gjI0a!rBSV3q-&rJl3`q^VxS}qcZD<#H!N;q z&IKBl3DP3I5J)gBYGRINX<`mvxpST5@7USPT%EKpN2&UG^Om{ZJblkE_@uS}%aZry zn%ol;Cbg*d>?!;_DHxD)V;S=PE+LNY5Ctj zSsNz&toi<5XbsQgIY-ScE~PSu=lPy`cqeE#mvx5r`uZKKE&k6xxH)Flg}IB{c!YVT z%Yh5$4_YX`YFpg;jQ#E_nT}u~tpfMWe8ml!$IqLlM#=G{N}Wi`;}dqtF$%QjkP!*H zbdpU(aQ^2xOV(96*PRPLyuvH|Uzf-|*7c9pw>_M4Qsm5Hp1(!CHA-GLmY0TnSRC7U zJTpze>wTQr{1@xyh~514@2JTZ5Ef;+1n16YW!-kw+ zCoV3UvbJ^lY^7C8gjdXZ{A13QkmJ{q&-}_2N;oY(^YeFwd746(*}kpJ)teI7Sd)4< zaL4^?I@e`?{jYuh`To7DK}qv+t}M9Hv}De`U$G*eB3W)}ToP^4e(KlXB6#Di(w@2* z(<{2pJe0lAyLf-`ma9Gn#jmH_nla_Urt!Uc=1jppxO%x~X!WV+$he5UG4T%6DL9Xa^rRp_0|%Z-It z*4zxc@aX9Lc;`>uN!sfltFG~Tdv3#Nn^(NM`HM~XWOYOIW^UD$mD&(soBMOOuBrZA z4vuF12AQvR2UdBi%;US05%@@NG2`JMacg;cG*+xEo7u3b#=fBL4{yU-rk|U>Gl%y5 zdU?&sVTtJ(zJ+>)I_z>bw{}eC5lGW3-WwremAOOF=MyR_ryyd5_#NqIdJ_j&N9>0RHiciZQua=tOz@j>2<8