|
| 1 | +module Msf::Exploit::Remote::Pkcs12 |
| 2 | + |
| 3 | + class Storage |
| 4 | + include Msf::Auxiliary::Report |
| 5 | + |
| 6 | + # @!attribute [r] framework |
| 7 | + # @return [Msf::Framework] the Metasploit framework instance |
| 8 | + attr_reader :framework |
| 9 | + |
| 10 | + # @!attribute [r] framework_module |
| 11 | + # @return [Msf::Module] the Metasploit framework module that is associated with the authentication instance |
| 12 | + attr_reader :framework_module |
| 13 | + |
| 14 | + def initialize(framework: nil, framework_module: nil) |
| 15 | + @framework = framework || framework_module&.framework |
| 16 | + @framework_module = framework_module |
| 17 | + end |
| 18 | + |
| 19 | + # Get stored pkcs12 matching the options query. |
| 20 | + # |
| 21 | + # @param [Hash] options The options for matching pkcs12's. |
| 22 | + # @option options [Integer, Array<Integer>] :id The identifier of the pkcs12 (optional) |
| 23 | + # @option options [String] :realm The realm of the pkcs12 (optional) |
| 24 | + # @option options [String] :username The username of the pkcs12 (optional) |
| 25 | + # @return [Array<StoredPkcs12>] |
| 26 | + def pkcs12(options = {}, &block) |
| 27 | + stored_pkcs12_array = filter_pkcs12(options).map do |pkcs12_entry| |
| 28 | + StoredPkcs12.new(pkcs12_entry) |
| 29 | + end |
| 30 | + |
| 31 | + stored_pkcs12_array.each do |stored_pkcs12| |
| 32 | + block.call(stored_pkcs12) if block_given? |
| 33 | + end |
| 34 | + |
| 35 | + stored_pkcs12_array |
| 36 | + end |
| 37 | + |
| 38 | + # Return the raw stored pkcs12. |
| 39 | + # |
| 40 | + # @param [Hash] options See the options hash description in {#pkcs12}. |
| 41 | + # @return [Array<Metasploit::Credential::Core>] |
| 42 | + def filter_pkcs12(options) |
| 43 | + return [] unless active_db? |
| 44 | + |
| 45 | + filter = {} |
| 46 | + filter[:id] = options[:id] if options[:id].present? |
| 47 | + |
| 48 | + creds = framework.db.creds( |
| 49 | + workspace: options.fetch(:workspace) { workspace }, |
| 50 | + type: 'Metasploit::Credential::Pkcs12', |
| 51 | + **filter |
| 52 | + ).select do |cred| |
| 53 | + # this is needed since if a filter is provided (e.g. `id:`) framework.db.creds will ignore the type: |
| 54 | + next false unless cred.private.type == 'Metasploit::Credential::Pkcs12' |
| 55 | + |
| 56 | + if options[:username].present? |
| 57 | + next false if options[:username].casecmp(cred.public.username) != 0 |
| 58 | + end |
| 59 | + |
| 60 | + if options[:realm].present? && cred.realm |
| 61 | + next false if options[:realm].casecmp(cred.realm.value) != 0 |
| 62 | + end |
| 63 | + |
| 64 | + if options[:status].present? |
| 65 | + # If status is not set on the credential, considere it is `active` |
| 66 | + status = cred.private.status || 'active' |
| 67 | + next false if status != options[:status] |
| 68 | + end |
| 69 | + |
| 70 | + cert = cred.private.openssl_pkcs12.certificate |
| 71 | + unless Time.now.between?(cert.not_before, cert.not_after) |
| 72 | + ilog("[filter_pkcs12] Found a matching certificate but it has expired") |
| 73 | + next false |
| 74 | + end |
| 75 | + |
| 76 | + if options[:tls_auth] |
| 77 | + eku = cert.extensions.select { |c| c.oid == 'extendedKeyUsage' }.first |
| 78 | + unless eku&.value.include?('TLS Web Client Authentication') |
| 79 | + ilog("[filter_pkcs12] Found a matching certificate but it doesn't have the 'TLS Web Client Authentication' EKU") |
| 80 | + next false |
| 81 | + end |
| 82 | + end |
| 83 | + |
| 84 | + true |
| 85 | + end |
| 86 | + end |
| 87 | + |
| 88 | + def delete(options = {}) |
| 89 | + if options.keys == [:ids] |
| 90 | + # skip calling #filter_pkcs12 which issues a query when the IDs are specified |
| 91 | + ids = options[:ids] |
| 92 | + else |
| 93 | + ids = filter_pkcs12(options).map(&:id) |
| 94 | + end |
| 95 | + |
| 96 | + framework.db.delete_credentials(ids: ids).map do |stored_pkcs12| |
| 97 | + StoredPkcs12.new(stored_pkcs12) |
| 98 | + end |
| 99 | + end |
| 100 | + |
| 101 | + # @return [String] The name of the workspace in which to operate. |
| 102 | + def workspace |
| 103 | + if @framework_module |
| 104 | + return @framework_module.workspace |
| 105 | + elsif @framework&.db&.active |
| 106 | + return @framework.db.workspace&.name |
| 107 | + end |
| 108 | + end |
| 109 | + |
| 110 | + # Mark Pkcs12(s) as inactive |
| 111 | + # |
| 112 | + # @param [Array<Integer>] ids The list of pkcs12 IDs. |
| 113 | + # @return [Array<StoredPkcs12>] |
| 114 | + def deactivate(ids:) |
| 115 | + set_status(ids: ids, status: 'inactive') |
| 116 | + end |
| 117 | + |
| 118 | + # Mark Pkcs12(s) as active |
| 119 | + # |
| 120 | + # @param [Array<Integer>] ids The list of pkcs12 IDs. |
| 121 | + # @return [Array<StoredPkcs12>] |
| 122 | + def activate(ids:) |
| 123 | + set_status(ids: ids, status: 'active') |
| 124 | + end |
| 125 | + |
| 126 | + private |
| 127 | + |
| 128 | + # @param [Array<Integer>] ids List of pkcs12 IDs to update |
| 129 | + # @param [String] status The status to set for the pkcs12 |
| 130 | + # @return [Array<StoredPkcs12>] |
| 131 | + def set_status(ids:, status:) |
| 132 | + updated_pkcs12 = [] |
| 133 | + ids.each do |id| |
| 134 | + pkcs12 = filter_pkcs12({ id: id }) |
| 135 | + if pkcs12.blank? |
| 136 | + print_warning("Pkcs12 with id: #{id} was not found in the database") |
| 137 | + next |
| 138 | + end |
| 139 | + private = pkcs12.first.private |
| 140 | + private.metadata.merge!({ 'status' => status } ) |
| 141 | + updated_pkcs12 << framework.db.update_credential({ id: id, private: { id: private.id, metadata: private.metadata }}) |
| 142 | + # I know this looks weird but the local db returns a single loot object, remote db returns an array of them |
| 143 | + #updated_certs << Array.wrap(framework.db.update_loot({ id: id, info: updated_pkcs12_status })).first |
| 144 | + end |
| 145 | + updated_pkcs12.map do |stored_pkcs12| |
| 146 | + StoredPkcs12.new(stored_pkcs12) |
| 147 | + end |
| 148 | + end |
| 149 | + |
| 150 | + end |
| 151 | +end |
0 commit comments