Merge pull request #45 from rapid7/feature/MSP-9988/credential-collection

Add a CredCollection class and refactor WinRM bruteforce module
bug/bundler_fix
dmaloney-r7 2014-06-06 11:53:28 -05:00
commit ff8e6d2c50
23 changed files with 385 additions and 144 deletions

View File

@ -0,0 +1,66 @@
require 'active_model'
module Metasploit
module Framework
# This class provides an in-memory representation of a conceptual Credential
#
# It contains the public, private, and realm if any.
class Credential
include ActiveModel::Validations
# @!attribute paired
# @return [Boolean] Whether BOTH a public and private are required
# (defaults to `true`)
attr_accessor :paired
# @!attribute private
# The private credential component (e.g. username)
#
# @return [String] if {#paired} is `true` or {#private} is `nil`
# @return [String, nil] if {#paired} is `false` or {#private} is not `nil`.
attr_accessor :private
# @!attribute public
# The public credential component (e.g. password)
#
# @return [String] if {#paired} is `true` or {#public} is `nil`
# @return [String, nil] if {#paired} is `false` or {#public} is not `nil`.
attr_accessor :public
# @!attribute realm
# @return [String,nil] The realm credential component (e.g domain name)
attr_accessor :realm
validates :paired,
inclusion: { in: [true, false] }
# If we have no public we MUST have a private (e.g. SNMP Community String)
validates :private,
exclusion: { in: [nil] },
if: "public.nil? or paired"
# If we have no private we MUST have a public
validates :public,
presence: true,
if: "private.nil? or paired"
# @param attributes [Hash{Symbol => String,nil}]
def initialize(attributes={})
attributes.each do |attribute, value|
public_send("#{attribute}=", value)
end
self.paired = true if self.paired.nil?
end
def inspect
"#<#{self.class} \"#{self}\" >"
end
def to_s
"#{self.public}:#{self.private}@#{self.realm}"
end
def ==(other)
other.public == self.public && other.private == self.private && other.realm == self.realm
end
end
end
end

View File

@ -0,0 +1,116 @@
require 'metasploit/framework/credential'
class Metasploit::Framework::CredentialCollection
# @!attribute blank_passwords
# Whether each username should be tried with a blank password
# @return [Boolean]
attr_accessor :blank_passwords
# @!attribute pass_file
# Path to a file containing passwords, one per line
# @return [String]
attr_accessor :pass_file
# @!attribute realm
# @return [String]
attr_accessor :realm
# @!attribute password
# @return [String]
attr_accessor :password
# @!attribute user_as_pass
# Whether each username should be tried as a password for that user
# @return [Boolean]
attr_accessor :user_as_pass
# @!attribute user_file
# Path to a file containing usernames, one per line
# @return [String]
attr_accessor :user_file
# @!attribute username
# @return [String]
attr_accessor :username
# @!attribute user_file
# Path to a file containing usernames and passwords seperated by a space,
# one pair per line
# @return [String]
attr_accessor :userpass_file
# @option opts [Boolean] :blank_passwords See {#blank_passwords}
# @option opts [String] :pass_file See {#pass_file}
# @option opts [String] :password See {#password}
# @option opts [Boolean] :user_as_pass See {#user_as_pass}
# @option opts [String] :user_file See {#user_file}
# @option opts [String] :username See {#username}
# @option opts [String] :userpass_file See {#userpass_file}
def initialize(opts = {})
opts.each do |attribute, value|
public_send("#{attribute}=", value)
end
end
# Combines all the provided credential sources into a stream of {Credential}
# objects, yielding them one at a time
#
# @yieldparam credential [Metasploit::Framework::Credential]
# @return [void]
def each
if pass_file
pass_fd = File.open(pass_file, 'r:binary')
end
if username
if password
yield Metasploit::Framework::Credential.new(public: username, private: password, realm: realm)
end
if user_as_pass
yield Metasploit::Framework::Credential.new(public: username, private: username, realm: realm)
end
if blank_passwords
yield Metasploit::Framework::Credential.new(public: username, private: "", realm: realm)
end
if pass_fd
pass_fd.each_line do |pass_from_file|
pass_from_file.chomp!
yield Metasploit::Framework::Credential.new(public: username, private: pass_from_file, realm: realm)
end
pass_fd.seek(0)
end
end
if user_file
File.open(user_file, 'r:binary') do |user_fd|
user_fd.each_line do |user_from_file|
user_from_file.chomp!
if password
yield Metasploit::Framework::Credential.new(public: user_from_file, private: password, realm: realm)
end
if user_as_pass
yield Metasploit::Framework::Credential.new(public: user_from_file, private: user_from_file, realm: realm)
end
if blank_passwords
yield Metasploit::Framework::Credential.new(public: user_from_file, private: "", realm: realm)
end
if pass_fd
pass_fd.each_line do |pass_from_file|
pass_from_file.chomp!
yield Metasploit::Framework::Credential.new(public: user_from_file, private: pass_from_file, realm: realm)
end
pass_fd.seek(0)
end
end
end
end
if userpass_file
File.open(userpass_file, 'r:binary') do |userpass_fd|
userpass_fd.each_line do |line|
user, pass = line.split(" ", 2)
pass.chomp!
yield Metasploit::Framework::Credential.new(public: user, private: pass, realm: realm)
end
end
end
ensure
pass_fd.close if pass_fd && !pass_fd.closed?
end
end

View File

@ -1,4 +1,4 @@
require 'metasploit/framework/login_scanner/credential'
require 'metasploit/framework/credential'
require 'metasploit/framework/login_scanner/invalid'
require 'metasploit/framework/login_scanner/result'

View File

@ -16,7 +16,7 @@ module Metasploit
# @return [Fixnum] The timeout in seconds for a single SSH connection
attr_accessor :connection_timeout
# @!attribute cred_details
# @return [Array] An array of Credential objects
# @return [CredentialCollection] Collection of Credential objects
attr_accessor :cred_details
# @!attribute failures
# @return [Array<Result>] Array of failing {Result results}
@ -172,18 +172,8 @@ module Metasploit
# are all valid.
# @return [void]
def validate_cred_details
if cred_details.kind_of? Array
cred_details.each do |detail|
unless detail.kind_of? Metasploit::Framework::LoginScanner::Credential
errors.add(:cred_details, "has invalid element #{detail.inspect}")
next
end
unless detail.valid?
errors.add(:cred_details, "has invalid element #{detail.inspect}")
end
end
else
errors.add(:cred_details, "must be an array")
unless cred_details.respond_to? :each
errors.add(:cred_details, "must respond to :each")
end
end

View File

@ -1,61 +0,0 @@
require 'active_model'
module Metasploit
module Framework
module LoginScanner
# This class provides an in-memory representation of a conceptual Credential
#
# It contains the public, private, and realm if any.
class Credential
include ActiveModel::Validations
# @!attribute paired
# @return [Boolean] Whether BOTH a public and private are required
# (defaults to `true`)
attr_accessor :paired
# @!attribute private
# The private credential component (e.g. username)
#
# @return [String] if {#paired} is `true` or {#private} is `nil`
# @return [String, nil] if {#paired} is `false` or {#private} is not `nil`.
attr_accessor :private
# @!attribute public
# The public credential component (e.g. password)
#
# @return [String] if {#paired} is `true` or {#public} is `nil`
# @return [String, nil] if {#paired} is `false` or {#public} is not `nil`.
attr_accessor :public
# @!attribute realm
# @return [String,nil] The realm credential component (e.g domain name)
attr_accessor :realm
validates :paired,
inclusion: { in: [true, false] }
# If we have no public we MUST have a private (e.g. SNMP Community String)
validates :private,
exclusion: { in: [nil] },
if: "public.nil? or paired"
# If we have no private we MUST have a public
validates :public,
presence: true,
if: "private.nil? or paired"
# @param attributes [Hash{Symbol => String,nil}]
def initialize(attributes={})
attributes.each do |attribute, value|
public_send("#{attribute}=", value)
end
self.paired = true if self.paired.nil?
end
def inspect
"#<#{self.class} \"#{self.public}:#{self.private}@#{self.realm}\" >"
end
end
end
end
end

View File

@ -6,6 +6,9 @@
require 'msf/core'
require 'rex/proto/ntlm/message'
require 'metasploit/framework/credential_collection'
require 'metasploit/framework/login_scanner'
require 'metasploit/framework/login_scanner/winrm'
class Metasploit3 < Msf::Auxiliary
@ -37,34 +40,46 @@ class Metasploit3 < Msf::Auxiliary
def run_host(ip)
each_user_pass do |user, pass|
resp = send_winrm_request(test_request)
if resp.nil?
print_error "#{ip}:#{rport}: Got no reply from the server, connection may have timed out"
return
elsif resp.code == 200
cred_collection = Metasploit::Framework::CredentialCollection.new(
blank_passwords: datastore['BLANK_PASSWORDS'],
pass_file: datastore['PASS_FILE'],
password: datastore['PASSWORD'],
user_file: datastore['USER_FILE'],
userpass_file: datastore['USERPASS_FILE'],
username: datastore['USERNAME'],
user_as_pass: datastore['USER_AS_PASS'],
realm: datastore['DOMAIN'],
)
scanner = Metasploit::Framework::LoginScanner::WinRM.new(
host: ip,
port: rport,
proxies: datastore["PROXIES"],
cred_details: cred_collection,
stop_on_success: datastore['STOP_ON_SUCCESS'],
connection_timeout: 10,
)
scanner.scan! do |result|
if result.success?
cred_hash = {
:host => ip,
:port => rport,
:sname => 'winrm',
:pass => pass,
:user => user,
:host => ip,
:port => rport,
:sname => 'winrm',
:pass => result.credential.private,
:user => result.credential.public,
:source_type => "user_supplied",
:active => true
:active => true
}
report_auth_info(cred_hash)
print_good "#{ip}:#{rport}: Valid credential found: #{user}:#{pass}"
elsif resp.code == 401
print_error "#{ip}:#{rport}: Login failed: #{user}:#{pass}"
print_good "#{ip}:#{rport}: Valid credential found: #{result.credential}"
else
print_error "Recieved unexpected Response Code: #{resp.code}"
vprint_status "#{ip}:#{rport}: Login failed: #{result.credential}"
end
end
end
def test_request
data = winrm_wql_msg("Select Name,Status from Win32_Service")
return winrm_wql_msg("Select Name,Status from Win32_Service")
end
end

View File

@ -0,0 +1,85 @@
require 'spec_helper'
require 'metasploit/framework/credential_collection'
describe Metasploit::Framework::CredentialCollection do
describe "#each" do
subject(:collection) do
described_class.new(
username: username,
password: password,
user_file: user_file,
pass_file: pass_file,
userpass_file: userpass_file,
)
end
let(:username) { "user" }
let(:password) { "pass" }
let(:user_file) { nil }
let(:pass_file) { nil }
let(:userpass_file) { nil }
specify do
expect { |b| collection.each(&b) }.to yield_with_args(Metasploit::Framework::Credential)
end
context "when given a user_file and password" do
let(:username) { nil }
let(:user_file) do
filename = "foo"
stub_file = StringIO.new("asdf\njkl\n")
File.stub(:open).with(filename,/^r/).and_yield stub_file
filename
end
specify do
expect { |b| collection.each(&b) }.to yield_successive_args(
Metasploit::Framework::Credential.new(public: "asdf", private: password),
Metasploit::Framework::Credential.new(public: "jkl", private: password),
)
end
end
context "when given a pass_file and username" do
let(:password) { nil }
let(:pass_file) do
filename = "foo"
stub_file = StringIO.new("asdf\njkl\n")
File.stub(:open).with(filename,/^r/).and_return stub_file
filename
end
specify do
expect { |b| collection.each(&b) }.to yield_successive_args(
Metasploit::Framework::Credential.new(public: username, private: "asdf"),
Metasploit::Framework::Credential.new(public: username, private: "jkl"),
)
end
end
context "when given a userspass_file" do
let(:username) { nil }
let(:password) { nil }
let(:userpass_file) do
filename = "foo"
stub_file = StringIO.new("asdf jkl\nfoo bar\n")
File.stub(:open).with(filename,/^r/).and_yield stub_file
filename
end
specify do
expect { |b| collection.each(&b) }.to yield_successive_args(
Metasploit::Framework::Credential.new(public: "asdf", private: "jkl"),
Metasploit::Framework::Credential.new(public: "foo", private: "bar"),
)
end
end
end
end

View File

@ -1,7 +1,7 @@
require 'spec_helper'
require 'metasploit/framework/login_scanner'
require 'metasploit/framework/credential'
describe Metasploit::Framework::LoginScanner::Credential do
describe Metasploit::Framework::Credential do
subject(:cred_detail) {
described_class.new
@ -72,6 +72,51 @@ describe Metasploit::Framework::LoginScanner::Credential do
end
end
end
describe "#==" do
let(:public) { "public" }
let(:private) { "private" }
let(:realm) { "realm" }
subject(:cred_detail) do
described_class.new(public: public, private: private, realm: realm)
end
context "when all attributes match" do
let(:other) do
described_class.new(public: public, private: private, realm: realm)
end
specify do
expect(other).to eq(cred_detail)
end
end
context "when realm does not match" do
let(:other) do
described_class.new(public: public, private: private, realm: "")
end
specify do
expect(other).not_to eq(cred_detail)
end
end
context "when private does not match" do
let(:other) do
described_class.new(public: public, private: "", realm: realm)
end
specify do
expect(other).not_to eq(cred_detail)
end
end
context "when public does not match" do
let(:other) do
described_class.new(public: "", private: private, realm: realm)
end
specify do
expect(other).not_to eq(cred_detail)
end
end
end
end

View File

@ -13,7 +13,7 @@ describe Metasploit::Framework::LoginScanner::AFP do
describe "#attempt_login" do
let(:pub_blank) do
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: "public",
private: ''

View File

@ -5,7 +5,7 @@ describe Metasploit::Framework::LoginScanner::DB2 do
let(:public) { 'root' }
let(:private) { 'toor' }
let(:test_cred) {
Metasploit::Framework::LoginScanner::Credential.new( public: public, private: private )
Metasploit::Framework::Credential.new( public: public, private: private )
}
subject(:login_scanner) { described_class.new }

View File

@ -6,7 +6,7 @@ describe Metasploit::Framework::LoginScanner::FTP do
let(:private) { 'toor' }
let(:pub_blank) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: ''
@ -14,7 +14,7 @@ describe Metasploit::Framework::LoginScanner::FTP do
}
let(:pub_pub) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: public
@ -22,7 +22,7 @@ describe Metasploit::Framework::LoginScanner::FTP do
}
let(:pub_pri) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: private
@ -30,7 +30,7 @@ describe Metasploit::Framework::LoginScanner::FTP do
}
let(:invalid_detail) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: nil,
private: nil

View File

@ -6,7 +6,7 @@ describe Metasploit::Framework::LoginScanner::MSSQL do
let(:private) { 'toor' }
let(:pub_blank) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: ''
@ -14,7 +14,7 @@ describe Metasploit::Framework::LoginScanner::MSSQL do
}
let(:pub_pub) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: public
@ -22,7 +22,7 @@ describe Metasploit::Framework::LoginScanner::MSSQL do
}
let(:pub_pri) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: private

View File

@ -5,7 +5,7 @@ describe Metasploit::Framework::LoginScanner::MySQL do
let(:public) { 'root' }
let(:private) { 'toor' }
let(:pub_blank) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: ''
@ -13,7 +13,7 @@ describe Metasploit::Framework::LoginScanner::MySQL do
}
let(:pub_pub) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: public
@ -21,7 +21,7 @@ describe Metasploit::Framework::LoginScanner::MySQL do
}
let(:pub_pri) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: private

View File

@ -10,7 +10,7 @@ describe Metasploit::Framework::LoginScanner::POP3 do
context "#attempt_login" do
let(:pub_blank) do
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: "public",
private: ''

View File

@ -7,7 +7,7 @@ describe Metasploit::Framework::LoginScanner::Postgres do
let(:realm) { 'template1' }
let(:full_cred) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: private,
@ -16,7 +16,7 @@ describe Metasploit::Framework::LoginScanner::Postgres do
}
let(:cred_no_realm) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: private

View File

@ -9,7 +9,7 @@ describe Metasploit::Framework::LoginScanner::Result do
let(:realm) { nil }
let(:status) { :success }
let(:cred) {
Metasploit::Framework::LoginScanner::Credential.new(public: public, private: private, realm: realm, paired: true)
Metasploit::Framework::Credential.new(public: public, private: private, realm: realm, paired: true)
}
subject(:login_result) {

View File

@ -6,7 +6,7 @@ describe Metasploit::Framework::LoginScanner::SMB do
let(:private) { 'toor' }
let(:pub_blank) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: ''
@ -14,7 +14,7 @@ describe Metasploit::Framework::LoginScanner::SMB do
}
let(:pub_pub) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: public
@ -22,7 +22,7 @@ describe Metasploit::Framework::LoginScanner::SMB do
}
let(:pub_pri) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: private

View File

@ -6,7 +6,7 @@ describe Metasploit::Framework::LoginScanner::SNMP do
let(:private) { nil }
let(:pub_comm) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: false,
public: public,
private: private
@ -14,7 +14,7 @@ describe Metasploit::Framework::LoginScanner::SNMP do
}
let(:invalid_detail) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: nil,
private: nil

View File

@ -6,7 +6,7 @@ describe Metasploit::Framework::LoginScanner::SSHKey do
let(:private) { OpenSSL::PKey::RSA.generate(2048).to_s }
let(:pub_pri) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: private
@ -14,7 +14,7 @@ describe Metasploit::Framework::LoginScanner::SSHKey do
}
let(:invalid_detail) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: nil,
private: nil

View File

@ -6,7 +6,7 @@ describe Metasploit::Framework::LoginScanner::SSH do
let(:private) { 'toor' }
let(:pub_blank) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: ''
@ -14,7 +14,7 @@ describe Metasploit::Framework::LoginScanner::SSH do
}
let(:pub_pub) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: public
@ -22,7 +22,7 @@ describe Metasploit::Framework::LoginScanner::SSH do
}
let(:pub_pri) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: private
@ -30,7 +30,7 @@ describe Metasploit::Framework::LoginScanner::SSH do
}
let(:invalid_detail) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: nil,
private: nil

View File

@ -5,10 +5,10 @@ describe Metasploit::Framework::LoginScanner::VNC do
let(:private) { 'password' }
let(:blank) { '' }
let(:test_cred) {
Metasploit::Framework::LoginScanner::Credential.new( paired: false, private: private )
Metasploit::Framework::Credential.new( paired: false, private: private )
}
let(:blank_cred) {
Metasploit::Framework::LoginScanner::Credential.new( paired: false, private: blank )
Metasploit::Framework::Credential.new( paired: false, private: blank )
}
subject(:login_scanner) { described_class.new }

View File

@ -49,7 +49,7 @@ shared_examples_for 'Metasploit::Framework::LoginScanner::HTTP' do
context "#attempt_login" do
let(:pub_blank) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: "public",
private: ''

View File

@ -7,7 +7,7 @@ shared_examples_for 'Metasploit::Framework::LoginScanner::Base' do
let(:private) { 'toor' }
let(:pub_blank) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: ''
@ -15,7 +15,7 @@ shared_examples_for 'Metasploit::Framework::LoginScanner::Base' do
}
let(:pub_pub) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: public
@ -23,7 +23,7 @@ shared_examples_for 'Metasploit::Framework::LoginScanner::Base' do
}
let(:pub_pri) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: private
@ -31,7 +31,7 @@ shared_examples_for 'Metasploit::Framework::LoginScanner::Base' do
}
let(:invalid_detail) {
Metasploit::Framework::LoginScanner::Credential.new(
Metasploit::Framework::Credential.new(
paired: true,
public: nil,
private: nil
@ -147,24 +147,9 @@ shared_examples_for 'Metasploit::Framework::LoginScanner::Base' do
it 'is not valid for a non-array input' do
login_scanner.cred_details = rand(10)
expect(login_scanner).to_not be_valid
expect(login_scanner.errors[:cred_details]).to include "must be an array"
expect(login_scanner.errors[:cred_details]).to include "must respond to :each"
end
it 'is not valid if any of the elements are not a Credential' do
login_scanner.cred_details = [1,2]
expect(login_scanner).to_not be_valid
expect(login_scanner.errors[:cred_details]).to include "has invalid element 1"
end
it 'is not valid if any of the CredDetails are invalid' do
login_scanner.cred_details = [pub_blank, invalid_detail]
expect(login_scanner).to_not be_valid
end
it 'is valid if all of the elements are valid' do
login_scanner.cred_details = [pub_blank, pub_pub, pub_pri]
expect(login_scanner.errors[:cred_details]).to be_empty
end
end
context 'connection_timeout' do