diff --git a/features/commands/help.feature b/features/commands/help.feature index f842dd8272..f73763436b 100644 --- a/features/commands/help.feature +++ b/features/commands/help.feature @@ -84,7 +84,6 @@ Feature: Help command Command Description ------- ----------- - creds List all credentials in the database db_connect Connect to an existing database db_disconnect Disconnect from the current database instance db_export Export a file containing the contents of the database @@ -98,5 +97,15 @@ Feature: Help command services List all services in the database vulns List all vulnerabilities in the database workspace Switch between database workspaces + + + Credentials Backend Commands + ============================ + + Command Description + ------- ----------- + creds List all credentials in the database + + """ diff --git a/lib/msf/ui/console/command_dispatcher/creds.rb b/lib/msf/ui/console/command_dispatcher/creds.rb new file mode 100644 index 0000000000..e373803cde --- /dev/null +++ b/lib/msf/ui/console/command_dispatcher/creds.rb @@ -0,0 +1,505 @@ +# -*- coding: binary -*- + +require 'rexml/document' +require 'rex/parser/nmap_xml' +require 'msf/core/db_export' + +module Msf +module Ui +module Console +module CommandDispatcher + +class Creds + require 'tempfile' + + include Msf::Ui::Console::CommandDispatcher + include Metasploit::Credential::Creation + + # + # The dispatcher's name. + # + def name + "Credentials Backend" + end + + # + # Returns the hash of commands supported by this dispatcher. + # + def commands + { + "creds" => "List all credentials in the database" + } + end + + def allowed_cred_types + %w(password ntlm hash) + end + + # + # Returns true if the db is connected, prints an error and returns + # false if not. + # + # All commands that require an active database should call this before + # doing anything. + # TODO: abstract the db methothds to a mixin that can be used by both dispatchers + # + def active? + if not framework.db.active + print_error("Database not connected") + return false + end + true + end + + # + # Miscellaneous option helpers + # + + # Parse +arg+ into a {Rex::Socket::RangeWalker} and append the result into +host_ranges+ + # + # @note This modifies +host_ranges+ in place + # + # @param arg [String] The thing to turn into a RangeWalker + # @param host_ranges [Array] The array of ranges to append + # @param required [Boolean] Whether an empty +arg+ should be an error + # @return [Boolean] true if parsing was successful or false otherwise + def arg_host_range(arg, host_ranges, required=false) + if (!arg and required) + print_error("Missing required host argument") + return false + end + begin + rw = Rex::Socket::RangeWalker.new(arg) + rescue + print_error("Invalid host parameter, #{arg}.") + return false + end + + if rw.valid? + host_ranges << rw + else + print_error("Invalid host parameter, #{arg}.") + return false + end + return true + end + + # + # Can return return active or all, on a certain host or range, on a + # certain port or range, and/or on a service name. + # + def cmd_creds(*args) + return unless active? + + # Short-circuit help + if args.delete "-h" + cmd_creds_help + return + end + + subcommand = args.shift + + case subcommand + when 'help' + cmd_creds_help + when 'add' + creds_add(*args) + else + # then it's not actually a subcommand + args.unshift(subcommand) if subcommand + creds_search(*args) + end + + end + + # + # TODO: this needs to be cleaned up to use the new syntax + # + def cmd_creds_help + print_line + print_line "With no sub-command, list credentials. If an address range is" + print_line "given, show only credentials with logins on hosts within that" + print_line "range." + + print_line + print_line "Usage - Listing credentials:" + print_line " creds [filter options] [address range]" + print_line + print_line "Usage - Adding credentials:" + print_line " creds add uses the following named parameters." + { + user: 'Public, usually a username', + password: 'Private, private_type Password.', + ntlm: 'Private, private_type NTLM Hash.', + 'ssh-key': 'Private, private_type SSH key, must be a file path.', + hash: 'Private, private_type Nonreplayable hash', + realm: 'Realm, ', + 'realm-type': "Realm, realm_type (#{Metasploit::Model::Realm::Key::SHORT_NAMES.keys.join(' ')}), defaults to domain." + }.each_pair do |keyword, description| + print_line " #{keyword.to_s.ljust 10}: #{description}" + end + print_line + print_line "Examples: Adding" + print_line " # Add a user, password and realm" + print_line " creds add user:admin password:notpassword realm:workgroup" + print_line " # Add a user and password" + print_line " creds add user:guest password:'guest password'" + print_line " # Add a password" + print_line " creds add password:'password without username'" + print_line " # Add a user with an NTLMHash" + print_line " creds add user:admin ntlm:E2FC15074BF7751DD408E6B105741864:A1074A69B1BDE45403AB680504BBDD1A" + print_line " # Add a NTLMHash" + print_line " creds add ntlm:E2FC15074BF7751DD408E6B105741864:A1074A69B1BDE45403AB680504BBDD1A" + print_line " # Add a user with an SSH key" + print_line " creds add user:sshadmin ssh-key:/path/to/id_rsa" + print_line " # Add a SSH key" + print_line " creds add ssh-key:/path/to/id_rsa" + print_line " # Add a user and a NonReplayableHash" + print_line " creds add user:other hash:d19c32489b870735b5f587d76b934283" + print_line " # Add a NonReplayableHash" + print_line " creds add hash:d19c32489b870735b5f587d76b934283" + + print_line + print_line "General options" + print_line " -h,--help Show this help information" + print_line " -o Send output to a file in csv format" + print_line " -d Delete one or more credentials" + print_line + print_line "Filter options for listing" + print_line " -P,--password List passwords that match this regex" + print_line " -p,--port List creds with logins on services matching this port spec" + print_line " -s List creds matching comma-separated service names" + print_line " -u,--user List users that match this regex" + print_line " -t,--type List creds that match the following types: #{allowed_cred_types.join(',')}" + print_line " -O,--origins List creds that match these origins" + print_line " -R,--rhosts Set RHOSTS from the results of the search" + + print_line + print_line "Examples, listing:" + print_line " creds # Default, returns all credentials" + print_line " creds 1.2.3.4/24 # nmap host specification" + print_line " creds -p 22-25,445 # nmap port specification" + print_line " creds -s ssh,smb # All creds associated with a login on SSH or SMB services" + print_line " creds -t ntlm # All NTLM creds" + print_line + + print_line "Example, deleting:" + print_line " # Delete all SMB credentials" + print_line " creds -d -s smb" + print_line + end + + # @param private_type [Symbol] See `Metasploit::Credential::Creation#create_credential` + # @param username [String] + # @param password [String] + # @param realm [String] + # @param realm_type [String] A key in `Metasploit::Model::Realm::Key::SHORT_NAMES` + def creds_add(*args) + params = args.inject({}) do |hsh, n| + opt = n.split(':') # Splitting the string on colons. + hsh[opt[0]] = opt[1..-1].join(':') # everything before the first : is the key, reasembling everything after the colon. why ntlm hashes + hsh + end + + begin + params.assert_valid_keys('user','password','realm','realm-type','ntlm','ssh-key','hash','host','port') + rescue ArgumentError => e + print_error(e.message) + end + + # Verify we only have one type of private + if params.slice('password','ntlm','ssh-key','hash').length > 1 + private_keys = params.slice('password','ntlm','ssh-key','hash').keys + print_error("You can only specify a single Private type. Private types given: #{private_keys.join(', ')}") + return + end + + data = { + workspace_id: framework.db.workspace, + origin_type: :import, + filename: 'msfconsole' + } + + data[:username] = params['user'] if params.key? 'user' + + if params.key? 'realm' + if params.key? 'realm-type' + if Metasploit::Model::Realm::Key::SHORT_NAMES.key? params['realm-type'] + data[:realm_key] = Metasploit::Model::Realm::Key::SHORT_NAMES[params['realm-type']] + else + valid = Metasploit::Model::Realm::Key::SHORT_NAMES.keys.map{|n|"'#{n}'"}.join(", ") + print_error("Invalid realm type: #{params['realm_type']}. Valid Values: #{valid}") + end + else + data[:realm_key] = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN + end + data[:realm_value] = params['realm'] + end + + if params.key? 'password' + data[:private_type] = :password + data[:private_data] = params['password'] + end + + if params.key? 'ntlm' + data[:private_type] = :ntlm_hash + data[:private_data] = params['ntlm'] + end + + if params.key? 'ssh-key' + begin + key_data = File.read(params['ssh-key']) + rescue ::Errno::EACCES, ::Errno::ENOENT => e + print_error("Failed to add ssh key: #{e}") + end + data[:private_type] = :ssh_key + data[:private_data] = key_data + end + + if params.key? 'hash' + data[:private_type] = :nonreplayable_hash + data[:private_data] = params['hash'] + end + + begin + create_credential(data) + rescue ActiveRecord::RecordInvalid => e + print_error("Failed to add #{data['private_type']}: #{e}") + end + end + + def creds_search(*args) + host_ranges = [] + origin_ranges = [] + port_ranges = [] + svcs = [] + rhosts = [] + + set_rhosts = false + + #cred_table_columns = [ 'host', 'port', 'user', 'pass', 'type', 'proof', 'active?' ] + cred_table_columns = [ 'host', 'origin' , 'service', 'public', 'private', 'realm', 'private_type' ] + user = nil + delete_count = 0 + + while (arg = args.shift) + case arg + when '-o' + output_file = args.shift + if (!output_file) + print_error("Invalid output filename") + return + end + output_file = ::File.expand_path(output_file) + when "-p","--port" + unless (arg_port_range(args.shift, port_ranges, true)) + return + end + when "-t","--type" + ptype = args.shift + if (!ptype) + print_error("Argument required for -t") + return + end + when "-s","--service" + service = args.shift + if (!service) + print_error("Argument required for -s") + return + end + svcs = service.split(/[\s]*,[\s]*/) + when "-P","--password" + pass = args.shift + if (!pass) + print_error("Argument required for -P") + return + end + when "-u","--user" + user = args.shift + if (!user) + print_error("Argument required for -u") + return + end + when "-d" + mode = :delete + when '-R', '--rhosts' + set_rhosts = true + when '-O', '--origins' + hosts = args.shift + if !hosts + print_error("Argument required for -O") + return + end + arg_host_range(hosts, origin_ranges) + else + # Anything that wasn't an option is a host to search for + unless (arg_host_range(arg, host_ranges)) + return + end + end + end + + # If we get here, we're searching. Delete implies search + + if ptype + type = case ptype + when 'password' + Metasploit::Credential::Password + when 'hash' + Metasploit::Credential::PasswordHash + when 'ntlm' + Metasploit::Credential::NTLMHash + else + print_error("Unrecognized credential type #{ptype} -- must be one of #{allowed_cred_types.join(',')}") + return + end + end + + # normalize + ports = port_ranges.flatten.uniq + svcs.flatten! + tbl_opts = { + 'Header' => "Credentials", + 'Columns' => cred_table_columns + } + + tbl = Rex::Text::Table.new(tbl_opts) + + ::ActiveRecord::Base.connection_pool.with_connection { + query = Metasploit::Credential::Core.where( workspace_id: framework.db.workspace ) + query = query.includes(:private, :public, :logins).references(:private, :public, :logins) + query = query.includes(logins: [ :service, { service: :host } ]) + + if type.present? + query = query.where(metasploit_credential_privates: { type: type }) + end + + if svcs.present? + query = query.where(Mdm::Service[:name].in(svcs)) + end + + if ports.present? + query = query.where(Mdm::Service[:port].in(ports)) + end + + if user.present? + # If we have a user regex, only include those that match + query = query.where('"metasploit_credential_publics"."username" ~* ?', user) + end + + if pass.present? + # If we have a password regex, only include those that match + query = query.where('"metasploit_credential_privates"."data" ~* ?', pass) + end + + if host_ranges.any? || ports.any? || svcs.any? + # Only find Cores that have non-zero Logins if the user specified a + # filter based on host, port, or service name + query = query.where(Metasploit::Credential::Login[:id].not_eq(nil)) + end + + query.find_each do |core| + + # Exclude non-blank username creds if that's what we're after + if user == "" && core.public && !(core.public.username.blank?) + next + end + + # Exclude non-blank password creds if that's what we're after + if pass == "" && core.private && !(core.private.data.blank?) + next + end + + origin = '' + if core.origin.kind_of?(Metasploit::Credential::Origin::Service) + origin = core.origin.service.host.address + elsif core.origin.kind_of?(Metasploit::Credential::Origin::Session) + origin = core.origin.session.host.address + end + + if !origin.empty? && origin_ranges.present? && !origin_ranges.any? {|range| range.include?(origin) } + next + end + + if core.logins.empty? && origin_ranges.empty? + tbl << [ + "", # host + "", # cred + "", # service + core.public, + core.private, + core.realm, + core.private ? core.private.class.model_name.human : "", + ] + else + core.logins.each do |login| + # If none of this Core's associated Logins is for a host within + # the user-supplied RangeWalker, then we don't have any reason to + # print it out. However, we treat the absence of ranges as meaning + # all hosts. + if host_ranges.present? && !host_ranges.any? { |range| range.include?(login.service.host.address) } + next + end + + row = [ login.service.host.address ] + row << origin + rhosts << login.service.host.address + if login.service.name.present? + row << "#{login.service.port}/#{login.service.proto} (#{login.service.name})" + else + row << "#{login.service.port}/#{login.service.proto}" + end + + row += [ + core.public, + core.private, + core.realm, + core.private ? core.private.class.model_name.human : "", + ] + tbl << row + end + end + if mode == :delete + core.destroy + delete_count += 1 + end + end + + if output_file.nil? + print_line(tbl.to_s) + else + # create the output file + ::File.open(output_file, "wb") { |f| f.write(tbl.to_csv) } + print_status("Wrote creds to #{output_file}") + end + + # Finally, handle the case where the user wants the resulting list + # of hosts to go into RHOSTS. + set_rhosts_from_addrs(rhosts.uniq) if set_rhosts + print_status("Deleted #{delete_count} creds") if delete_count > 0 + } + end + + def cmd_creds_tabs(str, words) + case words.length + when 1 + # subcommands + tabs = [ 'add-ntlm', 'add-password', 'add-hash', 'add-ssh-key', ] + when 2 + tabs = if words[1] == 'add-ssh-key' + tab_complete_filenames(str, words) + else + [] + end + #when 5 + # tabs = Metasploit::Model::Realm::Key::SHORT_NAMES.keys + else + tabs = [] + end + return tabs + end + + +end + +end end end end diff --git a/lib/msf/ui/console/command_dispatcher/db.rb b/lib/msf/ui/console/command_dispatcher/db.rb index 88880ea100..535abfa499 100644 --- a/lib/msf/ui/console/command_dispatcher/db.rb +++ b/lib/msf/ui/console/command_dispatcher/db.rb @@ -14,7 +14,6 @@ class Db require 'tempfile' include Msf::Ui::Console::CommandDispatcher - include Metasploit::Credential::Creation # # The dispatcher's name. @@ -40,7 +39,6 @@ class Db "vulns" => "List all vulnerabilities in the database", "notes" => "List all notes in the database", "loot" => "List all loot in the database", - "creds" => "List all credentials in the database", "db_import" => "Import a scan result file (filetype will be auto-detected)", "db_export" => "Export a file containing the contents of the database", "db_nmap" => "Executes nmap and records the output automatically", @@ -64,10 +62,6 @@ class Db ] end - def allowed_cred_types - %w(password ntlm hash) - end - # # Returns true if the db is connected, prints an error and returns # false if not. @@ -773,7 +767,7 @@ class Db host_ranges = [] port_ranges = [] svcs = [] - rhosts = [] + rhosts = [] search_term = nil show_info = false @@ -861,387 +855,6 @@ class Db } end - def cmd_creds_help - print_line - print_line "With no sub-command, list credentials. If an address range is" - print_line "given, show only credentials with logins on hosts within that" - print_line "range." - - print_line - print_line "Usage - Listing credentials:" - print_line " creds [filter options] [address range]" - print_line - print_line "Usage - Adding credentials:" - print_line " creds add-ntlm [domain]" - print_line " creds add-password [realm] [realm-type]" - print_line " creds add-ssh-key [realm-type]" - print_line "Where [realm type] can be one of:" - Metasploit::Model::Realm::Key::SHORT_NAMES.each do |short, description| - print_line " #{short} - #{description}" - end - - print_line - print_line "General options" - print_line " -h,--help Show this help information" - print_line " -o Send output to a file in csv format" - print_line " -d Delete one or more credentials" - print_line - print_line "Filter options for listing" - print_line " -P,--password List passwords that match this regex" - print_line " -p,--port List creds with logins on services matching this port spec" - print_line " -s List creds matching comma-separated service names" - print_line " -u,--user List users that match this regex" - print_line " -t,--type List creds that match the following types: #{allowed_cred_types.join(',')}" - print_line " -O,--origins List creds that match these origins" - print_line " -R,--rhosts Set RHOSTS from the results of the search" - - print_line - print_line "Examples, listing:" - print_line " creds # Default, returns all credentials" - print_line " creds 1.2.3.4/24 # nmap host specification" - print_line " creds -p 22-25,445 # nmap port specification" - print_line " creds -s ssh,smb # All creds associated with a login on SSH or SMB services" - print_line " creds -t ntlm # All NTLM creds" - print_line - - print_line - print_line "Examples, adding:" - print_line " # Add a user with an NTLMHash" - print_line " creds add-ntlm alice 5cfe4c82d9ab8c66590f5b47cd6690f1:978a2e2e1dec9804c6b936f254727f9a" - print_line " # Add a user with a blank password and a domain" - print_line " creds add-password bob '' contosso" - print_line " # Add a user with an SSH key" - print_line " creds add-ssh-key root /root/.ssh/id_rsa" - print_line - - print_line "Example, deleting:" - print_line " # Delete all SMB credentials" - print_line " creds -d -s smb" - print_line - end - - # @param private_type [Symbol] See `Metasploit::Credential::Creation#create_credential` - # @param username [String] - # @param password [String] - # @param realm [String] - # @param realm_type [String] A key in `Metasploit::Model::Realm::Key::SHORT_NAMES` - def creds_add(private_type, username, password=nil, realm=nil, realm_type=nil) - cred_data = { - username: username, - private_data: password, - private_type: private_type, - workspace_id: framework.db.workspace, - origin_type: :import, - filename: "msfconsole" - } - if realm.present? - if realm_type.present? - realm_key = Metasploit::Model::Realm::Key::SHORT_NAMES[realm_type] - if realm_key.nil? - valid = Metasploit::Model::Realm::Key::SHORT_NAMES.keys.map{|n|"'#{n}'"}.join(", ") - print_error("Invalid realm type: #{realm_type}. Valid values: #{valid}") - return - end - end - realm_key ||= Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN - cred_data.merge!( - realm_value: realm, - realm_key: realm_key - ) - end - - begin - create_credential(cred_data) - rescue ActiveRecord::RecordInvalid => e - print_error("Failed to add #{private_type}: #{e}") - end - end - - def creds_add_non_replayable_hash(*args) - creds_add(:non_replayable_hash, *args) - end - - def creds_add_ntlm_hash(*args) - creds_add(:ntlm_hash, *args) - end - - def creds_add_password(*args) - creds_add(:password, *args) - end - - def creds_add_ssh_key(username, *args) - key_file, realm = args - begin - key_data = File.read(key_file) - rescue ::Errno::EACCES, ::Errno::ENOENT => e - print_error("Failed to add ssh key: #{e}") - else - creds_add(:ssh_key, username, key_data, realm) - end - end - - def creds_search(*args) - host_ranges = [] - origin_ranges = [] - port_ranges = [] - svcs = [] - rhosts = [] - - set_rhosts = false - - #cred_table_columns = [ 'host', 'port', 'user', 'pass', 'type', 'proof', 'active?' ] - cred_table_columns = [ 'host', 'origin' , 'service', 'public', 'private', 'realm', 'private_type' ] - user = nil - delete_count = 0 - - while (arg = args.shift) - case arg - when '-o' - output_file = args.shift - if (!output_file) - print_error("Invalid output filename") - return - end - output_file = ::File.expand_path(output_file) - when "-p","--port" - unless (arg_port_range(args.shift, port_ranges, true)) - return - end - when "-t","--type" - ptype = args.shift - if (!ptype) - print_error("Argument required for -t") - return - end - when "-s","--service" - service = args.shift - if (!service) - print_error("Argument required for -s") - return - end - svcs = service.split(/[\s]*,[\s]*/) - when "-P","--password" - pass = args.shift - if (!pass) - print_error("Argument required for -P") - return - end - when "-u","--user" - user = args.shift - if (!user) - print_error("Argument required for -u") - return - end - when "-d" - mode = :delete - when '-R', '--rhosts' - set_rhosts = true - when '-O', '--origins' - hosts = args.shift - if !hosts - print_error("Argument required for -O") - return - end - arg_host_range(hosts, origin_ranges) - else - # Anything that wasn't an option is a host to search for - unless (arg_host_range(arg, host_ranges)) - return - end - end - end - - # If we get here, we're searching. Delete implies search - - if ptype - type = case ptype - when 'password' - Metasploit::Credential::Password - when 'hash' - Metasploit::Credential::PasswordHash - when 'ntlm' - Metasploit::Credential::NTLMHash - else - print_error("Unrecognized credential type #{ptype} -- must be one of #{allowed_cred_types.join(',')}") - return - end - end - - # normalize - ports = port_ranges.flatten.uniq - svcs.flatten! - tbl_opts = { - 'Header' => "Credentials", - 'Columns' => cred_table_columns - } - - tbl = Rex::Text::Table.new(tbl_opts) - - ::ActiveRecord::Base.connection_pool.with_connection { - query = Metasploit::Credential::Core.where( workspace_id: framework.db.workspace ) - query = query.includes(:private, :public, :logins).references(:private, :public, :logins) - query = query.includes(logins: [ :service, { service: :host } ]) - - if type.present? - query = query.where(metasploit_credential_privates: { type: type }) - end - - if svcs.present? - query = query.where(Mdm::Service[:name].in(svcs)) - end - - if ports.present? - query = query.where(Mdm::Service[:port].in(ports)) - end - - if user.present? - # If we have a user regex, only include those that match - query = query.where('"metasploit_credential_publics"."username" ~* ?', user) - end - - if pass.present? - # If we have a password regex, only include those that match - query = query.where('"metasploit_credential_privates"."data" ~* ?', pass) - end - - if host_ranges.any? || ports.any? || svcs.any? - # Only find Cores that have non-zero Logins if the user specified a - # filter based on host, port, or service name - query = query.where(Metasploit::Credential::Login[:id].not_eq(nil)) - end - - query.find_each do |core| - - # Exclude non-blank username creds if that's what we're after - if user == "" && core.public && !(core.public.username.blank?) - next - end - - # Exclude non-blank password creds if that's what we're after - if pass == "" && core.private && !(core.private.data.blank?) - next - end - - origin = '' - if core.origin.kind_of?(Metasploit::Credential::Origin::Service) - origin = core.origin.service.host.address - elsif core.origin.kind_of?(Metasploit::Credential::Origin::Session) - origin = core.origin.session.host.address - end - - if !origin.empty? && origin_ranges.present? && !origin_ranges.any? {|range| range.include?(origin) } - next - end - - if core.logins.empty? && origin_ranges.empty? - tbl << [ - "", # host - "", # cred - "", # service - core.public, - core.private, - core.realm, - core.private ? core.private.class.model_name.human : "", - ] - else - core.logins.each do |login| - # If none of this Core's associated Logins is for a host within - # the user-supplied RangeWalker, then we don't have any reason to - # print it out. However, we treat the absence of ranges as meaning - # all hosts. - if host_ranges.present? && !host_ranges.any? { |range| range.include?(login.service.host.address) } - next - end - - row = [ login.service.host.address ] - row << origin - rhosts << login.service.host.address - if login.service.name.present? - row << "#{login.service.port}/#{login.service.proto} (#{login.service.name})" - else - row << "#{login.service.port}/#{login.service.proto}" - end - - row += [ - core.public, - core.private, - core.realm, - core.private ? core.private.class.model_name.human : "", - ] - tbl << row - end - end - if mode == :delete - core.destroy - delete_count += 1 - end - end - - if output_file.nil? - print_line(tbl.to_s) - else - # create the output file - ::File.open(output_file, "wb") { |f| f.write(tbl.to_csv) } - print_status("Wrote creds to #{output_file}") - end - - # Finally, handle the case where the user wants the resulting list - # of hosts to go into RHOSTS. - set_rhosts_from_addrs(rhosts.uniq) if set_rhosts - print_status("Deleted #{delete_count} creds") if delete_count > 0 - } - end - - # - # Can return return active or all, on a certain host or range, on a - # certain port or range, and/or on a service name. - # - def cmd_creds(*args) - return unless active? - - # Short-circuit help - if args.delete "-h" - cmd_creds_help - return - end - - subcommand = args.shift - case subcommand - when "add-ntlm" - creds_add_ntlm_hash(*args) - when "add-password" - creds_add_password(*args) - when "add-hash" - creds_add_non_replayable_hash(*args) - when "add-ssh-key" - creds_add_ssh_key(*args) - else - # then it's not actually a subcommand - args.unshift(subcommand) if subcommand - creds_search(*args) - end - - end - - def cmd_creds_tabs(str, words) - case words.length - when 1 - # subcommands - tabs = [ 'add-ntlm', 'add-password', 'add-hash', 'add-ssh-key', ] - when 2 - tabs = if words[1] == 'add-ssh-key' - tab_complete_filenames(str, words) - else - [] - end - #when 5 - # tabs = Metasploit::Model::Realm::Key::SHORT_NAMES.keys - else - tabs = [] - end - return tabs - end - def cmd_notes_help print_line "Usage: notes [-h] [-t ] [-n ] [-a] [addr range]" print_line diff --git a/lib/msf/ui/console/driver.rb b/lib/msf/ui/console/driver.rb index dd5e18a01f..f43d99e36c 100644 --- a/lib/msf/ui/console/driver.rb +++ b/lib/msf/ui/console/driver.rb @@ -126,6 +126,8 @@ class Driver < Msf::Ui::Driver if (framework.db.usable) require 'msf/ui/console/command_dispatcher/db' enstack_dispatcher(CommandDispatcher::Db) + require 'msf/ui/console/command_dispatcher/creds' + enstack_dispatcher(CommandDispatcher::Creds) else print_error("***") if framework.db.error == "disabled" diff --git a/spec/lib/msf/ui/console/command_dispatcher/creds_spec.rb b/spec/lib/msf/ui/console/command_dispatcher/creds_spec.rb new file mode 100644 index 0000000000..5b77460b24 --- /dev/null +++ b/spec/lib/msf/ui/console/command_dispatcher/creds_spec.rb @@ -0,0 +1,449 @@ +# frozen_string_literal: true +require 'spec_helper' + +require 'msf/ui' +require 'msf/ui/console/command_dispatcher/creds' + +RSpec.describe Msf::Ui::Console::CommandDispatcher::Creds do + include_context 'Msf::DBManager' + include_context 'Msf::UIDriver' + + subject(:creds) do + described_class.new(driver) + end + + it { is_expected.to respond_to :active? } + it { is_expected.to respond_to :creds_add } + it { is_expected.to respond_to :creds_search } + + describe '#cmd_creds' do + let(:username) { 'thisuser' } + let(:password) { 'thispass' } + let(:realm) { 'thisrealm' } + let(:realm_type) { 'Active Directory Domain' } + describe '-u' do + let(:nomatch_username) { 'thatuser' } + let(:nomatch_password) { 'thatpass' } + let(:blank_username) { '' } + let(:blank_password) { '' } + let(:nonblank_username) { 'nonblank_user' } + let(:nonblank_password) { 'nonblank_pass' } + + let!(:origin) { FactoryGirl.create(:metasploit_credential_origin_import) } + + before(:example) do + priv = FactoryGirl.create(:metasploit_credential_password, data: password) + pub = FactoryGirl.create(:metasploit_credential_username, username: username) + FactoryGirl.create(:metasploit_credential_core, + origin: origin, + private: priv, + public: pub, + realm: nil, + workspace: framework.db.workspace) + blank_pub = FactoryGirl.create(:metasploit_credential_blank_username) + nonblank_priv = FactoryGirl.create(:metasploit_credential_password, data: nonblank_password) + FactoryGirl.create(:metasploit_credential_core, + origin: origin, + private: nonblank_priv, + public: blank_pub, + realm: nil, + workspace: framework.db.workspace) + nonblank_pub = FactoryGirl.create(:metasploit_credential_username, username: nonblank_username) + blank_priv = FactoryGirl.create(:metasploit_credential_password, data: blank_password) + FactoryGirl.create(:metasploit_credential_core, + origin: origin, + private: blank_priv, + public: nonblank_pub, + realm: nil, + workspace: framework.db.workspace) + end + + context 'when the credential is present' do + it 'should show a user that matches the given expression' do + creds.cmd_creds('-u', username) + expect(@output).to eq([ + 'Credentials', + '===========', + '', + 'host origin service public private realm private_type', + '---- ------ ------- ------ ------- ----- ------------', + ' thisuser thispass Password' + ]) + end + + it 'should match a regular expression' do + creds.cmd_creds('-u', "^#{username}$") + expect(@output).to eq([ + 'Credentials', + '===========', + '', + 'host origin service public private realm private_type', + '---- ------ ------- ------ ------- ----- ------------', + ' thisuser thispass Password' + ]) + end + + it 'should return nothing for a non-matching regular expression' do + creds.cmd_creds('-u', "^#{nomatch_username}$") + expect(@output).to eq([ + 'Credentials', + '===========', + '', + 'host origin service public private realm private_type', + '---- ------ ------- ------ ------- ----- ------------' + ]) + end + + context 'and when the username is blank' do + it 'should show a user that matches the given expression' do + creds.cmd_creds('-u', blank_username) + expect(@output).to eq([ + 'Credentials', + '===========', + '', + 'host origin service public private realm private_type', + '---- ------ ------- ------ ------- ----- ------------', + ' nonblank_pass Password' + ]) + end + end + context 'and when the password is blank' do + it 'should show a user that matches the given expression' do + creds.cmd_creds('-P', blank_password) + expect(@output).to eq([ + 'Credentials', + '===========', + '', + 'host origin service public private realm private_type', + '---- ------ ------- ------ ------- ----- ------------', + ' nonblank_user Password' + ]) + end + end + end + + context 'when the credential is absent' do + context 'due to a nonmatching username' do + it 'should return a blank set' do + creds.cmd_creds('-u', nomatch_username) + expect(@output).to eq([ + 'Credentials', + '===========', + '', + 'host origin service public private realm private_type', + '---- ------ ------- ------ ------- ----- ------------' + ]) + end + end + context 'due to a nonmatching password' do + it 'should return a blank set' do + creds.cmd_creds('-P', nomatch_password) + expect(@output).to eq([ + 'Credentials', + '===========', + '', + 'host origin service public private realm private_type', + '---- ------ ------- ------ ------- ----- ------------' + ]) + end + end + end + end + + describe '-t' do + context 'with an invalid type' do + it 'should print the list of valid types' do + creds.cmd_creds('-t', 'asdf') + expect(@error).to match_array [ + 'Unrecognized credential type asdf -- must be one of password,ntlm,hash' + ] + end + end + + context 'with valid types' do + let(:ntlm_hash) { '1443d06412d8c0e6e72c57ef50f76a05:27c433245e4763d074d30a05aae0af2c' } + + let!(:pub) do + FactoryGirl.create(:metasploit_credential_username, username: username) + end + let!(:password_core) do + priv = FactoryGirl.create(:metasploit_credential_password, data: password) + FactoryGirl.create(:metasploit_credential_core, + origin: FactoryGirl.create(:metasploit_credential_origin_import), + private: priv, + public: pub, + realm: nil, + workspace: framework.db.workspace) + end + + # # Somehow this is hitting a unique constraint on Cores with the same + # # Public, even though it has a different Private. Skip for now + # let!(:ntlm_core) do + # priv = FactoryGirl.create(:metasploit_credential_ntlm_hash, data: ntlm_hash) + # FactoryGirl.create(:metasploit_credential_core, + # origin: FactoryGirl.create(:metasploit_credential_origin_import), + # private: priv, + # public: pub, + # realm: nil, + # workspace: framework.db.workspace) + # end + # let!(:nonreplayable_core) do + # priv = FactoryGirl.create(:metasploit_credential_nonreplayable_hash, data: 'asdf') + # FactoryGirl.create(:metasploit_credential_core, + # origin: FactoryGirl.create(:metasploit_credential_origin_import), + # private: priv, + # public: pub, + # realm: nil, + # workspace: framework.db.workspace) + # end + + after(:example) do + # ntlm_core.destroy + password_core.destroy + # nonreplayable_core.destroy + end + + context 'password' do + it 'should show just the password' do + creds.cmd_creds('-t', 'password') + # Table matching really sucks + expect(@output).to eq([ + 'Credentials', + '===========', + '', + 'host origin service public private realm private_type', + '---- ------ ------- ------ ------- ----- ------------', + ' thisuser thispass Password' + ]) + end + end + + context 'ntlm' do + it 'should show just the ntlm' do + skip 'Weird uniqueness constraint on Core (workspace_id, public_id)' + + creds.cmd_creds('-t', 'ntlm') + # Table matching really sucks + expect(@output).to =~ [ + 'Credentials', + '===========', + '', + 'host service public private realm private_type', + '---- ------- ------ ------- ----- ------------', + " thisuser #{ntlm_hash} NTLM hash" + ] + end + end + end + end + + describe 'add' do + let(:pub) { FactoryGirl.create(:metasploit_credential_username, username: username) } + let(:priv) { FactoryGirl.create(:metasploit_credential_password, data: password) } + let(:r) { FactoryGirl.create(:metasploit_credential_realm, key: realm_type, value: realm) } + + context 'username password and realm' do + it 'creates a core if one does not exist' do + expect { + creds.cmd_creds('add', "user:#{username}", "password:#{password}", "realm:#{realm}") + }.to change { Metasploit::Credential::Core.count }.by 1 + end + it 'does not create a core if it already exists' do + FactoryGirl.create(:metasploit_credential_core, + origin: FactoryGirl.create(:metasploit_credential_origin_import), + private: priv, + public: pub, + realm: r, + workspace: framework.db.workspace) + expect { + creds.cmd_creds('add', "user:#{username}", "password:#{password}", "realm:#{realm}") + }.to_not change { Metasploit::Credential::Core.count } + end + end + + context 'username and realm' do + it 'creates a core if one does not exist' do + expect { + creds.cmd_creds('add', "user:#{username}", "realm:#{realm}") + }.to change { Metasploit::Credential::Core.count }.by 1 + end + it 'does not create a core if it already exists' do + FactoryGirl.create(:metasploit_credential_core, + origin: FactoryGirl.create(:metasploit_credential_origin_import), + private: nil, + public: pub, + realm: r, + workspace: framework.db.workspace) + expect { + creds.cmd_creds('add', "user:#{username}", "realm:#{realm}") + }.to_not change { Metasploit::Credential::Core.count } + end + end + + context 'username and password' do + it 'creates a core if one does not exist' do + expect { + creds.cmd_creds('add', "user:#{username}", "password:#{password}") + }.to change { Metasploit::Credential::Core.count }.by 1 + end + it 'does not create a core if it already exists' do + FactoryGirl.create(:metasploit_credential_core, + origin: FactoryGirl.create(:metasploit_credential_origin_import), + private: priv, + public: pub, + realm: nil, + workspace: framework.db.workspace) + expect { + creds.cmd_creds('add', "user:#{username}", "password:#{password}") + }.to_not change { Metasploit::Credential::Core.count } + end + end + + context 'password and realm' do + it 'creates a core if one does not exist' do + expect { + creds.cmd_creds('add', "password:#{password}", "realm:#{realm}") + }.to change { Metasploit::Credential::Core.count }.by 1 + end + it 'does not create a core if it already exists' do + FactoryGirl.create(:metasploit_credential_core, + origin: FactoryGirl.create(:metasploit_credential_origin_import), + private: priv, + public: nil, + realm: r, + workspace: framework.db.workspace) + expect { + creds.cmd_creds('add', "password:#{password}", "realm:#{realm}") + }.to_not change { Metasploit::Credential::Core.count } + end + end + + context 'username' do + it 'creates a core if one does not exist' do + expect { + creds.cmd_creds('add', "user:#{username}") + }.to change { Metasploit::Credential::Core.count }.by 1 + end + it 'does not create a core if it already exists' do + FactoryGirl.create(:metasploit_credential_core, + origin: FactoryGirl.create(:metasploit_credential_origin_import), + private: nil, + public: pub, + realm: nil, + workspace: framework.db.workspace) + expect { + creds.cmd_creds('add', "user:#{username}") + }.to_not change { Metasploit::Credential::Core.count } + end + end + + context 'private_types' do + context 'password' do + it 'creates a core if one does not exist' do + expect { + creds.cmd_creds('add', "password:#{password}") + }.to change { Metasploit::Credential::Core.count }.by 1 + end + it 'does not create a core if it already exists' do + FactoryGirl.create(:metasploit_credential_core, + origin: FactoryGirl.create(:metasploit_credential_origin_import), + private: priv, + public: nil, + realm: nil, + workspace: framework.db.workspace) + expect { + creds.cmd_creds('add', "password:#{password}") + }.to_not change { Metasploit::Credential::Core.count } + end + end + context 'ntlm' do + let(:priv) { FactoryGirl.create(:metasploit_credential_ntlm_hash) } + it 'creates a core if one does not exist' do + expect { + creds.cmd_creds('add', "ntlm:#{priv.data}") + }.to change { Metasploit::Credential::Core.count }.by 1 + end + it 'does not create a core if it already exists' do + FactoryGirl.create(:metasploit_credential_core, + origin: FactoryGirl.create(:metasploit_credential_origin_import), + private: priv, + public: nil, + realm: nil, + workspace: framework.db.workspace) + expect { + creds.cmd_creds('add', "ntlm:#{priv.data}") + }.to_not change { Metasploit::Credential::Core.count } + end + end + context 'hash' do + let(:priv) { FactoryGirl.create(:metasploit_credential_nonreplayable_hash) } + it 'creates a core if one does not exist' do + expect { + creds.cmd_creds('add', "hash:#{priv.data}") + }.to change { Metasploit::Credential::Core.count }.by 1 + end + it 'does not create a core if it already exists' do + FactoryGirl.create(:metasploit_credential_core, + origin: FactoryGirl.create(:metasploit_credential_origin_import), + private: priv, + public: nil, + realm: nil, + workspace: framework.db.workspace) + expect { + creds.cmd_creds('add', "hash:#{priv.data}") + }.to_not change { Metasploit::Credential::Core.count } + end + end + context 'ssh-key' do + let(:priv) { FactoryGirl.create(:metasploit_credential_ssh_key) } + before(:each) do + @file = Tempfile.new('id_rsa') + @file.write(priv.data) + @file.close + end + it 'creates a core if one does not exist' do + expect { + creds.cmd_creds('add', "user:#{username}", "ssh-key:#{@file.path}") + }.to change { Metasploit::Credential::Core.count }.by 1 + end + it 'does not create a core if it already exists' do + FactoryGirl.create(:metasploit_credential_core, + origin: FactoryGirl.create(:metasploit_credential_origin_import), + private: priv, + public: pub, + realm: nil, + workspace: framework.db.workspace) + expect { + creds.cmd_creds('add', "user:#{username}", "ssh-key:#{@file.path}") + }.to_not change { Metasploit::Credential::Core.count } + end + end + end + + context 'realm-types' do + Metasploit::Model::Realm::Key::SHORT_NAMES.each do |short_name, long_name| + context "#{short_name}" do + let(:r) { FactoryGirl.create(:metasploit_credential_realm, key: long_name) } + it 'creates a core if one does not exist' do + expect { + creds.cmd_creds('add', "realm:#{r.value}", "realm-type:#{short_name}") + }.to change { Metasploit::Credential::Core.count }.by 1 + end + it 'does not create a core if it already exists' do + FactoryGirl.create(:metasploit_credential_core, + origin: FactoryGirl.create(:metasploit_credential_origin_import), + private: nil, + public: nil, + realm: r, + workspace: framework.db.workspace) + expect { + creds.cmd_creds('add', "realm:#{r.value}", "realm-type:#{short_name}") + }.to_not change { Metasploit::Credential::Core.count } + end + end + end + + end + end + end +end diff --git a/spec/lib/msf/ui/console/command_dispatcher/db_spec.rb b/spec/lib/msf/ui/console/command_dispatcher/db_spec.rb index 4900729224..ca088e23d0 100644 --- a/spec/lib/msf/ui/console/command_dispatcher/db_spec.rb +++ b/spec/lib/msf/ui/console/command_dispatcher/db_spec.rb @@ -14,8 +14,6 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Db do it { is_expected.to respond_to :active? } it { is_expected.to respond_to :arg_host_range } it { is_expected.to respond_to :arg_port_range } - it { is_expected.to respond_to :cmd_creds_help } - it { is_expected.to respond_to :cmd_creds_tabs } it { is_expected.to respond_to :cmd_db_autopwn } it { is_expected.to respond_to :cmd_db_autopwn_help } it { is_expected.to respond_to :cmd_db_connect } @@ -47,12 +45,6 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Db do it { is_expected.to respond_to :cmd_workspace_help } it { is_expected.to respond_to :cmd_workspace_tabs } it { is_expected.to respond_to :commands } - it { is_expected.to respond_to :creds_add } - it { is_expected.to respond_to :creds_add_non_replayable_hash } - it { is_expected.to respond_to :creds_add_ntlm_hash } - it { is_expected.to respond_to :creds_add_password } - it { is_expected.to respond_to :creds_add_ssh_key } - it { is_expected.to respond_to :creds_search } it { is_expected.to respond_to :db_check_driver } it { is_expected.to respond_to :db_connect_postgresql } it { is_expected.to respond_to :db_find_tools } @@ -63,258 +55,6 @@ RSpec.describe Msf::Ui::Console::CommandDispatcher::Db do it { is_expected.to respond_to :name } it { is_expected.to respond_to :set_rhosts_from_addrs } - describe "#cmd_creds" do - let(:username) { "thisuser" } - let(:password) { "thispass" } - - describe "-u" do - let(:nomatch_username) { "thatuser" } - let(:nomatch_password) { "thatpass" } - let(:blank_username) { "" } - let(:blank_password) { "" } - let(:nonblank_username) { "nonblank_user" } - let(:nonblank_password) { "nonblank_pass" } - - let!(:origin) { FactoryGirl.create(:metasploit_credential_origin_import) } - - before(:example) do - priv = FactoryGirl.create(:metasploit_credential_password, data: password) - pub = FactoryGirl.create(:metasploit_credential_username, username: username) - FactoryGirl.create(:metasploit_credential_core, - origin: origin, - private: priv, - public: pub, - realm: nil, - workspace: framework.db.workspace) - blank_pub = FactoryGirl.create(:metasploit_credential_blank_username) - nonblank_priv = FactoryGirl.create(:metasploit_credential_password, data: nonblank_password) - FactoryGirl.create(:metasploit_credential_core, - origin: origin, - private: nonblank_priv, - public: blank_pub, - realm: nil, - workspace: framework.db.workspace) - nonblank_pub = FactoryGirl.create(:metasploit_credential_username, username: nonblank_username) - blank_priv = FactoryGirl.create(:metasploit_credential_password, data: blank_password) - FactoryGirl.create(:metasploit_credential_core, - origin: origin, - private: blank_priv, - public: nonblank_pub, - realm: nil, - workspace: framework.db.workspace) - end - - context "when the credential is present" do - it "should show a user that matches the given expression" do - db.cmd_creds("-u", username) - expect(@output).to eq([ - "Credentials", - "===========", - "", - "host origin service public private realm private_type", - "---- ------ ------- ------ ------- ----- ------------", - " thisuser thispass Password" - ]) - end - - it 'should match a regular expression' do - subject.cmd_creds("-u", "^#{username}$") - expect(@output).to eq([ - "Credentials", - "===========", - "", - "host origin service public private realm private_type", - "---- ------ ------- ------ ------- ----- ------------", - " thisuser thispass Password" - ]) - end - - it 'should return nothing for a non-matching regular expression' do - subject.cmd_creds("-u", "^#{nomatch_username}$") - expect(@output).to eq([ - "Credentials", - "===========", - "", - "host origin service public private realm private_type", - "---- ------ ------- ------ ------- ----- ------------" - ]) - end - - context "and when the username is blank" do - it "should show a user that matches the given expression" do - db.cmd_creds("-u", blank_username) - expect(@output).to eq([ - "Credentials", - "===========", - "", - "host origin service public private realm private_type", - "---- ------ ------- ------ ------- ----- ------------", - " nonblank_pass Password" - ]) - end - end - context "and when the password is blank" do - it "should show a user that matches the given expression" do - db.cmd_creds("-P", blank_password) - expect(@output).to eq([ - "Credentials", - "===========", - "", - "host origin service public private realm private_type", - "---- ------ ------- ------ ------- ----- ------------", - " nonblank_user Password" - ]) - end - end - end - - context "when the credential is absent" do - context "due to a nonmatching username" do - it "should return a blank set" do - db.cmd_creds("-u", nomatch_username) - expect(@output).to eq([ - "Credentials", - "===========", - "", - "host origin service public private realm private_type", - "---- ------ ------- ------ ------- ----- ------------" - ]) - end - end - context "due to a nonmatching password" do - it "should return a blank set" do - db.cmd_creds("-P", nomatch_password) - expect(@output).to eq([ - "Credentials", - "===========", - "", - "host origin service public private realm private_type", - "---- ------ ------- ------ ------- ----- ------------" - ]) - end - end - end - end - - describe "-t" do - context "with an invalid type" do - it "should print the list of valid types" do - db.cmd_creds("-t", "asdf") - expect(@error).to match_array [ - "Unrecognized credential type asdf -- must be one of password,ntlm,hash" - ] - end - end - - context "with valid types" do - let(:ntlm_hash) { "1443d06412d8c0e6e72c57ef50f76a05:27c433245e4763d074d30a05aae0af2c" } - - let!(:pub) do - FactoryGirl.create(:metasploit_credential_username, username: username) - end - let!(:password_core) do - priv = FactoryGirl.create(:metasploit_credential_password, data: password) - FactoryGirl.create(:metasploit_credential_core, - origin: FactoryGirl.create(:metasploit_credential_origin_import), - private: priv, - public: pub, - realm: nil, - workspace: framework.db.workspace) - end - -=begin - # Somehow this is hitting a unique constraint on Cores with the same - # Public, even though it has a different Private. Skip for now - let!(:ntlm_core) do - priv = FactoryGirl.create(:metasploit_credential_ntlm_hash, data: ntlm_hash) - FactoryGirl.create(:metasploit_credential_core, - origin: FactoryGirl.create(:metasploit_credential_origin_import), - private: priv, - public: pub, - realm: nil, - workspace: framework.db.workspace) - end - let!(:nonreplayable_core) do - priv = FactoryGirl.create(:metasploit_credential_nonreplayable_hash, data: 'asdf') - FactoryGirl.create(:metasploit_credential_core, - origin: FactoryGirl.create(:metasploit_credential_origin_import), - private: priv, - public: pub, - realm: nil, - workspace: framework.db.workspace) - end -=end - - after(:example) do - #ntlm_core.destroy - password_core.destroy - #nonreplayable_core.destroy - end - - context "password" do - it "should show just the password" do - db.cmd_creds("-t", "password") - # Table matching really sucks - expect(@output).to eq([ - "Credentials", - "===========", - "", - "host origin service public private realm private_type", - "---- ------ ------- ------ ------- ----- ------------", - " thisuser thispass Password" - ]) - end - end - - context "ntlm" do - it "should show just the ntlm" do - skip "Weird uniqueness constraint on Core (workspace_id, public_id)" - - db.cmd_creds("-t", "ntlm") - # Table matching really sucks - expect(@output).to =~ [ - "Credentials", - "===========", - "", - "host service public private realm private_type", - "---- ------- ------ ------- ----- ------------", - " thisuser #{ntlm_hash } NTLM hash" - ] - end - end - - end - end - - describe "add-password" do - context "when no core exists" do - it "should add a Core" do - expect { - subject.cmd_creds("add-password", username, password) - }.to change{ Metasploit::Credential::Core.count }.by 1 - end - end - context "when a core already exists" do - before(:example) do - priv = FactoryGirl.create(:metasploit_credential_password, data: password) - pub = FactoryGirl.create(:metasploit_credential_username, username: username) - FactoryGirl.create(:metasploit_credential_core, - origin: FactoryGirl.create(:metasploit_credential_origin_import), - private: priv, - public: pub, - realm: nil, - workspace: framework.db.workspace) - end - it "should not add a Core" do - expect { - subject.cmd_creds("add-password", username, password) - }.to_not change{ Metasploit::Credential::Core.count } - end - end - end - - end - describe "#cmd_db_export" do describe "-h" do it "should show a help message" do