diff --git a/Gemfile.lock b/Gemfile.lock index 338eee01cf..f8f3df1b9b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,6 +5,7 @@ PATH activesupport (>= 3.0.0, < 4.0.0) bcrypt json + metasploit-model (~> 0.26.1) meterpreter_bins (= 0.0.6) msgpack nokogiri @@ -82,7 +83,7 @@ GEM msgpack (0.5.8) multi_json (1.0.4) network_interface (0.0.1) - nokogiri (1.6.2.1) + nokogiri (1.6.3.1) mini_portile (= 0.6.0) packetfu (1.1.9) pcaprub (0.11.3) @@ -118,9 +119,9 @@ GEM rspec-collection_matchers (1.0.0) rspec-expectations (>= 2.99.0.beta1) rspec-core (2.99.1) - rspec-expectations (2.99.1) + rspec-expectations (2.99.2) diff-lcs (>= 1.1.3, < 2.0) - rspec-mocks (2.99.1) + rspec-mocks (2.99.2) rspec-rails (2.99.0) actionpack (>= 3.0) activemodel (>= 3.0) @@ -132,13 +133,13 @@ GEM rspec-mocks (~> 2.99.0) rubyntlm (0.4.0) rubyzip (1.1.6) - shoulda-matchers (2.6.1) + shoulda-matchers (2.6.2) activesupport (>= 3.0.0) simplecov (0.5.4) multi_json (~> 1.0.3) simplecov-html (~> 0.5.3) simplecov-html (0.5.3) - slop (3.5.0) + slop (3.6.0) sprockets (2.2.2) hike (~> 1.2) multi_json (~> 1.0) diff --git a/lib/metasploit/framework.rb b/lib/metasploit/framework.rb index 61d2f70d31..1555b00532 100644 --- a/lib/metasploit/framework.rb +++ b/lib/metasploit/framework.rb @@ -9,6 +9,7 @@ require 'active_support' require 'bcrypt' require 'json' require 'msgpack' +require 'metasploit/model' require 'nokogiri' require 'packetfu' # railties has not autorequire defined @@ -43,4 +44,4 @@ module Metasploit @root end end -end \ No newline at end of file +end diff --git a/lib/metasploit/framework/credential.rb b/lib/metasploit/framework/credential.rb index 800a526344..c78af1baf5 100644 --- a/lib/metasploit/framework/credential.rb +++ b/lib/metasploit/framework/credential.rb @@ -12,6 +12,9 @@ module Metasploit # @return [Boolean] Whether BOTH a public and private are required # (defaults to `true`) attr_accessor :paired + # @!attribute parent + # @return [Object] the parent object that had .to_credential called on it to create this object + attr_accessor :parent # @!attribute private # The private credential component (e.g. username) # diff --git a/lib/metasploit/framework/login_scanner/base.rb b/lib/metasploit/framework/login_scanner/base.rb index d34db46aba..a69bed242d 100644 --- a/lib/metasploit/framework/login_scanner/base.rb +++ b/lib/metasploit/framework/login_scanner/base.rb @@ -83,6 +83,7 @@ module Metasploit # This could be a Credential object, or a Credential Core, or an Attempt object # so make sure that whatever it is, we end up with a Credential. credential = raw_cred.to_credential + credential.parent = raw_cred if credential.realm.present? && self.class::REALM_KEY.present? credential.realm_key = self.class::REALM_KEY @@ -129,7 +130,14 @@ module Metasploit successful_users = Set.new each_credential do |credential| - next if successful_users.include?(credential.public) + # For Pro bruteforce Reuse and Guess we need to note that we skipped an attempt. + if successful_users.include?(credential.public) + if credential.parent.respond_to?(:skipped) + credential.parent.skipped = true + credential.parent.save! + end + next + end result = attempt_login(credential) result.freeze diff --git a/lib/msf/core/rpc/v10/rpc_db.rb b/lib/msf/core/rpc/v10/rpc_db.rb index b22eeac670..32da0c8829 100644 --- a/lib/msf/core/rpc/v10/rpc_db.rb +++ b/lib/msf/core/rpc/v10/rpc_db.rb @@ -4,6 +4,9 @@ module RPC class RPC_Db < RPC_Base private + + include Metasploit::Credential::Creation + def db self.framework.db.active end @@ -15,6 +18,21 @@ private self.framework.db.workspace end + def fix_cred_options(opts) + new_opts = fix_options(opts) + + # Convert some of are data back to symbols + if new_opts[:origin_type] + new_opts[:origin_type] = new_opts[:origin_type].to_sym + end + + if new_opts[:private_type] + new_opts[:private_type] = new_opts[:private_type].to_sym + end + + new_opts + end + def fix_options(opts) newopts = {} opts.each do |k,v| @@ -88,6 +106,40 @@ private public + def rpc_create_cracked_credential(xopts) + opts = fix_cred_options(xopts) + create_credential(opts) + end + + def rpc_create_credential(xopts) + opts = fix_cred_options(xopts) + core = create_credential(opts) + + ret = { + username: core.public.try(:username), + private: core.private.try(:data), + private_type: core.private.try(:type), + realm_value: core.realm.try(:value), + realm_key: core.realm.try(:key) + } + + if opts[:last_attempted_at] && opts[:status] + opts[:core] = core + opts[:last_attempted_at] = opts[:last_attempted_at].to_datetime + login = create_credential_login(opts) + + ret[:host] = login.service.host.address, + ret[:sname] = login.service.name + ret[:status] = login.status + end + ret + end + + def rpc_invalidate_login(xopts) + opts = fix_cred_options(xopts) + invalidate_login(opts) + end + def rpc_hosts(xopts) ::ActiveRecord::Base.connection_pool.with_connection { opts, wspace = init_db_opts_workspace(xopts) @@ -490,33 +542,6 @@ public } end - def rpc_report_auth_info(xopts) - ::ActiveRecord::Base.connection_pool.with_connection { - opts, wspace = init_db_opts_workspace(xopts) - res = self.framework.db.report_auth_info(opts) - return { :result => 'success' } if(res) - { :result => 'failed' } - } - end - - def rpc_get_auth_info(xopts) - ::ActiveRecord::Base.connection_pool.with_connection { - opts, wspace = init_db_opts_workspace(xopts) - ret = {} - ret[:auth_info] = [] - # XXX: This method doesn't exist... - ai = self.framework.db.get_auth_info(opts) - ai.each do |i| - info = {} - i.each do |k,v| - info[k.to_sym] = v - end - ret[:auth_info] << info - end - ret - } - end - def rpc_get_ref(name) ::ActiveRecord::Base.connection_pool.with_connection { db_check @@ -828,42 +853,6 @@ public } end - # requires host, port, user, pass, ptype, and active - def rpc_report_cred(xopts) - ::ActiveRecord::Base.connection_pool.with_connection { - opts, wspace = init_db_opts_workspace(xopts) - res = framework.db.find_or_create_cred(opts) - return { :result => 'success' } if res - { :result => 'failed' } - } - end - - #right now workspace is the only option supported - def rpc_creds(xopts) - ::ActiveRecord::Base.connection_pool.with_connection { - opts, wspace = init_db_opts_workspace(xopts) - limit = opts.delete(:limit) || 100 - offset = opts.delete(:offset) || 0 - - ret = {} - ret[:creds] = [] - ::Mdm::Cred.find(:all, :include => {:service => :host}, :conditions => ["hosts.workspace_id = ?", - framework.db.workspace.id ], :limit => limit, :offset => offset).each do |c| - cred = {} - cred[:host] = c.service.host.address if(c.service.host) - cred[:updated_at] = c.updated_at.to_i - cred[:port] = c.service.port - cred[:proto] = c.service.proto - cred[:sname] = c.service.name - cred[:type] = c.ptype - cred[:user] = c.user - cred[:pass] = c.pass - cred[:active] = c.active - ret[:creds] << cred - end - ret - } - end def rpc_import_data(xopts) ::ActiveRecord::Base.connection_pool.with_connection { diff --git a/lib/msf/ui/console/command_dispatcher/db.rb b/lib/msf/ui/console/command_dispatcher/db.rb index ccd0450953..32f4c32ae1 100644 --- a/lib/msf/ui/console/command_dispatcher/db.rb +++ b/lib/msf/ui/console/command_dispatcher/db.rb @@ -18,6 +18,8 @@ class Db # TODO: Not thrilled about including this entire module for just store_local. include Msf::Auxiliary::Report + include Metasploit::Credential::Creation + # # The dispatcher's name. # @@ -217,7 +219,6 @@ class Db return unless active? ::ActiveRecord::Base.connection_pool.with_connection { onlyup = false - host_search = nil set_rhosts = false mode = :search delete_count = 0 @@ -580,7 +581,6 @@ class Db return end - mode = :search while (arg = args.shift) case arg #when "-a","--add" @@ -648,53 +648,123 @@ class Db end def cmd_creds_help - print_line "Usage: creds [addr range]" - print_line "List credentials. If an address range is given, show only credentials with" - print_line "logins on hosts within that range." 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 " -c,--columns Columns of interest" + 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 - print_line "Examples:" + 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 + + 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 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? - ::ActiveRecord::Base.connection_pool.with_connection { + # @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 = [] port_ranges = [] svcs = [] #cred_table_columns = [ 'host', 'port', 'user', 'pass', 'type', 'proof', 'active?' ] - cred_table_columns = [ 'host', 'port', 'public', 'private', 'realm', 'private_type' ] + cred_table_columns = [ 'host', 'service', 'public', 'private', 'realm', 'private_type' ] user = nil - - # Short-circuit help - if args.delete "-h" - cmd_creds_help - return - end + delete_count = 0 while (arg = args.shift) case arg - when "-h" - cmd_creds_help - return when '-o' output_file = args.shift if (!output_file) @@ -731,6 +801,8 @@ class Db print_error("Argument required for -u") return end + when "-d" + mode = :delete else # Anything that wasn't an option is a host to search for unless (arg_host_range(arg, host_ranges)) @@ -757,67 +829,128 @@ class Db tbl = Rex::Ui::Text::Table.new(tbl_opts) - query = Metasploit::Credential::Core.where( - workspace_id: framework.db.workspace, - ) + ::ActiveRecord::Base.connection_pool.with_connection { + query = Metasploit::Credential::Core.where( + workspace_id: framework.db.workspace, + ) - query.each do |core| + query.each do |core| - # Exclude creds that don't match the given user - if user_regex.present? && !core.public.username.match(user_regex) - next - end + # Exclude creds that don't match the given user + if user_regex.present? && !core.public.username.match(user_regex) + next + end - # Exclude creds that don't match the given pass - if pass_regex.present? && !core.private.data.match(pass_regex) - next - end + # Exclude creds that don't match the given pass + if pass_regex.present? && !core.private.data.match(pass_regex) + next + end - if core.logins.empty? - # Skip cores that don't have any logins if the user specified a - # filter based on host, port, or service name - next if host_ranges.any? || ports.any? || svcs.any? - - tbl << [ - "", # host - "", # port - core.public ? core.public.username : "", - core.private ? core.private.data : "", - core.realm ? core.realm.value : "", - core.private ? core.private.class.model_name.human : "", - ] - else - core.logins.each do |login| - if svcs.present? && !svcs.include?(login.service.name) - next - end - - if ports.present? && !ports.include?(login.service.port) - next - end - - # 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 + if core.logins.empty? + # Skip cores that don't have any logins if the user specified a + # filter based on host, port, or service name + next if host_ranges.any? || ports.any? || svcs.any? tbl << [ - login.service.host.address, - login.service.port, - core.public ? core.public.username : "", - core.private ? core.private.data : "", - core.realm ? core.realm.value : "", + "", # host + "", # port + core.public, + core.private, + core.realm, core.private ? core.private.class.model_name.human : "", ] + else + core.logins.each do |login| + if svcs.present? && !svcs.include?(login.service.name) + next + end + + if ports.present? && !ports.include?(login.service.port) + next + end + + # 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 ] + 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 + + print_line(tbl.to_s) + 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 - print_line(tbl.to_s) - } + 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 @@ -1057,8 +1190,8 @@ class Db info = args.shift if(!info) print_error("Can't make loot with no info") - return - end + return + end when '-t' typelist = args.shift if(!typelist) @@ -1106,8 +1239,8 @@ class Db range.each do |host| file = File.open(filename, "rb") contents = file.read - lootfile = framework.db.find_or_create_loot(:type => type, :host => host,:info => info, :data => contents,:path => filename,:name => name) - print_status("Added loot #{host}") + lootfile = framework.db.find_or_create_loot(:type => type, :host => host, :info => info, :data => contents, :path => filename, :name => name) + print_status("Added loot for #{host} (#{lootfile})") end end return @@ -1263,7 +1396,7 @@ class Db def cmd_db_import(*args) return unless active? ::ActiveRecord::Base.connection_pool.with_connection { - if (args.include?("-h") or not (args and args.length > 0)) + if args.include?("-h") || ! (args && args.length > 0) cmd_db_import_help return end @@ -1326,8 +1459,8 @@ class Db next rescue REXML::ParseException => e print_error("Failed to import #{filename} due to malformed XML:") - print_error("#{$!.class}: #{$!}") - elog("Failed to import #{filename}: #{$!.class}: #{$!}") + print_error("#{e.class}: #{e}") + elog("Failed to import #{filename}: #{e.class}: #{e}") dlog("Call stack: #{$@.join("\n")}", LEV_3) next end @@ -1629,7 +1762,6 @@ class Db end def db_find_tools(tools) - found = true missed = [] tools.each do |name| if(! Rex::FileUtils.find_full_path(name)) diff --git a/lib/rex/ui/text/table.rb b/lib/rex/ui/text/table.rb index ca1c3ec5d3..0cbb315494 100644 --- a/lib/rex/ui/text/table.rb +++ b/lib/rex/ui/text/table.rb @@ -275,9 +275,9 @@ protected nameline << pad(' ', last_col, last_idx) remainder = colprops[last_idx]['MaxWidth'] - last_col.length - if (remainder < 0) - remainder = 0 - end + if (remainder < 0) + remainder = 0 + end barline << (' ' * (cellpad + remainder)) end nameline << col @@ -305,7 +305,7 @@ protected last_cell = nil last_idx = nil row.each_with_index { |cell, idx| - if (last_cell) + if (idx != 0) line << pad(' ', last_cell.to_s, last_idx) end # line << pad(' ', cell.to_s, idx) diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec index e0980416d6..190059ebd6 100644 --- a/metasploit-framework.gemspec +++ b/metasploit-framework.gemspec @@ -55,6 +55,9 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'bcrypt' # Needed for some admin modules (scrutinizer_add_user.rb) spec.add_runtime_dependency 'json' + # Things that would normally be part of the database model, but which + # are needed when there's no database + spec.add_runtime_dependency 'metasploit-model', '~> 0.26.1' # Needed for Meterpreter on Windows, soon others. spec.add_runtime_dependency 'meterpreter_bins', '0.0.6' # Needed by msfgui and other rpc components diff --git a/spec/lib/msf/ui/command_dispatcher/db_spec.rb b/spec/lib/msf/ui/command_dispatcher/db_spec.rb index 3498dcd25b..25319a5456 100644 --- a/spec/lib/msf/ui/command_dispatcher/db_spec.rb +++ b/spec/lib/msf/ui/command_dispatcher/db_spec.rb @@ -11,6 +11,37 @@ describe Msf::Ui::Console::CommandDispatcher::Db do described_class.new(driver) end + describe "#cmd_creds" do + describe "add-password" do + let(:username) { "username" } + let(:password) { "password" } + 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(:each) do + priv = FactoryGirl.create(:metasploit_credential_password, data: password) + pub = FactoryGirl.create(:metasploit_credential_public, username: username) + core = 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_workspace" do describe "-h" do it "should show a help message" do @@ -183,6 +214,7 @@ describe Msf::Ui::Console::CommandDispatcher::Db do end +=begin describe "#cmd_creds" do describe "-h" do it "should show a help message" do @@ -206,6 +238,7 @@ describe Msf::Ui::Console::CommandDispatcher::Db do end end end +=end describe "#cmd_db_import" do describe "-h" do