diff --git a/Gemfile b/Gemfile index da51f4999b..ac228f3df8 100755 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,7 @@ group :db do # Needed for Msf::DbManager gem 'activerecord', '>= 3.0.0', '< 4.0.0' # Metasploit::Credential database models - gem 'metasploit-credential', '>= 0.7.10.pre.core.pre.search', '< 0.8' + gem 'metasploit-credential', '~> 0.7.14', '< 0.8' # Database models shared between framework and Pro. gem 'metasploit_data_models', '~> 0.19' # Needed for module caching in Mdm::ModuleDetails diff --git a/Gemfile.lock b/Gemfile.lock index c36333f202..7545a39821 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,6 +5,7 @@ PATH activesupport (>= 3.0.0, < 4.0.0) bcrypt json + metasploit-model (~> 0.25.7) meterpreter_bins (= 0.0.6) msgpack nokogiri @@ -41,7 +42,7 @@ GEM i18n (~> 0.6, >= 0.6.4) multi_json (~> 1.0) arel (3.0.3) - arel-helpers (2.0.0) + arel-helpers (2.0.1) activerecord (>= 3.1.0, < 5) bcrypt (3.1.7) builder (3.0.4) @@ -60,16 +61,16 @@ GEM json (1.8.1) metasploit-concern (0.1.1) activesupport (~> 3.0, >= 3.0.0) - metasploit-credential (0.7.10.pre.core.pre.search) + metasploit-credential (0.7.16) metasploit-concern (~> 0.1.0) metasploit-model (>= 0.25.6) metasploit_data_models (~> 0.19) pg rubyntlm rubyzip (~> 1.1) - metasploit-model (0.25.6) + metasploit-model (0.25.7) activesupport - metasploit_data_models (0.19.0) + metasploit_data_models (0.19.3) activerecord (>= 3.2.13, < 4.0.0) activesupport arel-helpers @@ -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) @@ -159,7 +160,7 @@ DEPENDENCIES factory_girl (>= 4.1.0) factory_girl_rails fivemat (= 1.2.1) - metasploit-credential (>= 0.7.10.pre.core.pre.search, < 0.8) + metasploit-credential (~> 0.7.14, < 0.8) metasploit-framework! metasploit_data_models (~> 0.19) network_interface (~> 0.0.1) 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/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..0eac8fca7e 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.25.7' # 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