Merge pull request #3583 from jlee-r7/feature/MSP-9932/creds-add-subcommands

Add `creds` subcommands

MSP-9932 #land
bug/bundler_fix
Trevor Rosen 2014-07-30 12:01:36 -05:00
commit ea72a7e5c3
7 changed files with 267 additions and 97 deletions

View File

@ -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

View File

@ -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)

View File

@ -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
end

View File

@ -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 <user> <ntlm hash> [domain]"
print_line " creds add-password <user> <password> [realm] [realm-type]"
print_line " creds add-ssh-key <user> </path/to/id_rsa> [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 <regex> List passwords that match this regex"
print_line " -p,--port <portspec> List creds with logins on services matching this port spec"
print_line " -s <svc names> List creds matching comma-separated service names"
print_line " -u,--user <regex> 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))

View File

@ -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)

View File

@ -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

View File

@ -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