Merge pull request #97 from rapid7/feature/MSP-10656/unify-ssh-scanners

Feature/msp 10656/unify ssh scanners
bug/bundler_fix
dmaloney-r7 2014-07-07 16:37:41 -05:00
commit 80bee35b70
5 changed files with 67 additions and 330 deletions

View File

@ -8,6 +8,7 @@ module Metasploit
# This is the LoginScanner class for dealing with the Secure Shell protocol. # This is the LoginScanner class for dealing with the Secure Shell protocol.
# It is responsible for taking a single target, and a list of credentials # It is responsible for taking a single target, and a list of credentials
# and attempting them. It then saves the results. # and attempting them. It then saves the results.
#
class SSH class SSH
include Metasploit::Framework::LoginScanner::Base include Metasploit::Framework::LoginScanner::Base
@ -19,7 +20,7 @@ module Metasploit
DEFAULT_PORT = 22 DEFAULT_PORT = 22
LIKELY_PORTS = [ DEFAULT_PORT ] LIKELY_PORTS = [ DEFAULT_PORT ]
LIKELY_SERVICE_NAMES = [ 'ssh' ] LIKELY_SERVICE_NAMES = [ 'ssh' ]
PRIVATE_TYPES = [ :password ] PRIVATE_TYPES = [ :password, :ssh_key ]
VERBOSITIES = [ VERBOSITIES = [
:debug, :debug,
@ -46,14 +47,24 @@ module Metasploit
def attempt_login(credential) def attempt_login(credential)
self.ssh_socket = nil self.ssh_socket = nil
opt_hash = { opt_hash = {
:auth_methods => ['password','keyboard-interactive'],
:port => port, :port => port,
:disable_agent => true, :disable_agent => true,
:password => credential.private,
:config => false, :config => false,
:verbose => verbosity, :verbose => verbosity,
:proxies => proxies :proxies => proxies
} }
case credential.private_type
when :password, nil
opt_hash.update(
:auth_methods => ['password','keyboard-interactive'],
:password => credential.private,
)
when :ssh_key
opt_hash.update(
:auth_methods => ['publickey'],
:key_data => credential.private,
)
end
result_options = { result_options = {
credential: credential credential: credential

View File

@ -1,128 +0,0 @@
require 'net/ssh'
require 'metasploit/framework/login_scanner/base'
module Metasploit
module Framework
module LoginScanner
# This is the LoginScanner class for dealing with the Secure Shell protocol and PKI.
# It is responsible for taking a single target, and a list of credentials
# and attempting them. It then saves the results. In this case it is expecting
# SSH private keys for the private credential.
class SSHKey
include Metasploit::Framework::LoginScanner::Base
#
# CONSTANTS
#
CAN_GET_SESSION = true
DEFAULT_PORT = 22
LIKELY_PORTS = [ DEFAULT_PORT ]
LIKELY_SERVICE_NAMES = [ 'ssh' ]
PRIVATE_TYPES = [ :ssh_key ]
VERBOSITIES = [
:debug,
:info,
:warn,
:error,
:fatal
]
# @!attribute ssh_socket
# @return [Net::SSH::Connection::Session] The current SSH connection
attr_accessor :ssh_socket
# @!attribute verbosity
# The verbosity level for the SSH client.
#
# @return [Symbol] An element of {VERBOSITIES}.
attr_accessor :verbosity
validates :verbosity,
presence: true,
inclusion: { in: VERBOSITIES }
# This method attempts a single login with a single credential against the target
# @param credential [Credential] The credential object to attmpt to login with
# @return [Metasploit::Framework::LoginScanner::Result] The LoginScanner Result object
def attempt_login(credential)
self.ssh_socket = nil
opt_hash = {
:auth_methods => ['publickey'],
:port => port,
:disable_agent => true,
:key_data => credential.private,
:config => false,
:verbose => verbosity,
:proxies => proxies,
:record_auth_info => true
}
result_options = {
credential: credential
}
begin
::Timeout.timeout(connection_timeout) do
self.ssh_socket = Net::SSH.start(
host,
credential.public,
opt_hash
)
end
rescue ::EOFError, Net::SSH::Disconnect, Rex::AddressInUse, Rex::ConnectionError, ::Timeout::Error
result_options.merge!( proof: nil, status: :connection_error)
rescue Net::SSH::Exception
result_options.merge!( proof: nil, status: :failed)
end
unless result_options.has_key? :status
if ssh_socket
proof = gather_proof
result_options.merge!( proof: proof, status: :success)
else
result_options.merge!( proof: nil, status: :failed)
end
end
::Metasploit::Framework::LoginScanner::Result.new(result_options)
end
private
# This method attempts to gather proof that we successfuly logged in.
# @return [String] The proof of a connection, May be empty.
def gather_proof
proof = ''
begin
Timeout.timeout(5) do
proof = ssh_socket.exec!("id\n").to_s
if(proof =~ /id=/)
proof << ssh_socket.exec!("uname -a\n").to_s
else
# Cisco IOS
if proof =~ /Unknown command or computer name/
proof = ssh_socket.exec!("ver\n").to_s
else
proof << ssh_socket.exec!("help\n?\n\n\n").to_s
end
end
end
rescue ::Exception
end
proof
end
def set_sane_defaults
self.connection_timeout = 30 if self.connection_timeout.nil?
self.port = DEFAULT_PORT if self.port.nil?
self.verbosity = :fatal if self.verbosity.nil?
end
end
end
end
end

View File

@ -5,7 +5,7 @@
require 'msf/core' require 'msf/core'
require 'net/ssh' require 'net/ssh'
require 'metasploit/framework/login_scanner/ssh_key' require 'metasploit/framework/login_scanner/ssh'
class Metasploit3 < Msf::Auxiliary class Metasploit3 < Msf::Auxiliary
@ -225,7 +225,7 @@ class Metasploit3 < Msf::Auxiliary
) )
print_brute :level => :vstatus, :ip => ip, :msg => "Testing #{keys.key_data.count} keys" print_brute :level => :vstatus, :ip => ip, :msg => "Testing #{keys.key_data.count} keys"
scanner = Metasploit::Framework::LoginScanner::SSHKey.new( scanner = Metasploit::Framework::LoginScanner::SSH.new(
host: ip, host: ip,
port: rport, port: rport,
cred_details: keys, cred_details: keys,
@ -297,7 +297,7 @@ class Metasploit3 < Msf::Auxiliary
user_fd.each_line do |user_from_file| user_fd.each_line do |user_from_file|
user_from_file.chomp! user_from_file.chomp!
each_key do |key_data| each_key do |key_data|
yield Metasploit::Framework::Credential.new(public: user_from_file, private: key_data, realm: realm) yield Metasploit::Framework::Credential.new(public: user_from_file, private: key_data, realm: realm, private_type: :ssh_key)
end end
end end
end end
@ -305,7 +305,7 @@ class Metasploit3 < Msf::Auxiliary
if @username.present? if @username.present?
each_key do |key_data| each_key do |key_data|
yield Metasploit::Framework::Credential.new(public: @username, private: key_data, realm: realm) yield Metasploit::Framework::Credential.new(public: @username, private: key_data, realm: realm, private_type: :ssh_key)
end end
end end
end end

View File

@ -1,178 +0,0 @@
require 'spec_helper'
require 'metasploit/framework/login_scanner/ssh_key'
describe Metasploit::Framework::LoginScanner::SSHKey do
let(:public) { 'root' }
let(:private) { OpenSSL::PKey::RSA.generate(2048).to_s }
let(:pub_pri) {
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: private
)
}
let(:invalid_detail) {
Metasploit::Framework::Credential.new(
paired: true,
public: nil,
private: nil
)
}
let(:detail_group) {
[ pub_pri]
}
subject(:ssh_scanner) {
described_class.new
}
it_behaves_like 'Metasploit::Framework::LoginScanner::Base'
it { should respond_to :port }
it { should respond_to :host }
it { should respond_to :cred_details }
it { should respond_to :connection_timeout }
it { should respond_to :verbosity }
it { should respond_to :stop_on_success }
it { should respond_to :valid! }
it { should respond_to :scan! }
it { should respond_to :proxies }
context 'validations' do
context 'verbosity' do
it 'is valid with :debug' do
ssh_scanner.verbosity = :debug
expect(ssh_scanner.errors[:verbosity]).to be_empty
end
it 'is valid with :info' do
ssh_scanner.verbosity = :info
expect(ssh_scanner.errors[:verbosity]).to be_empty
end
it 'is valid with :warn' do
ssh_scanner.verbosity = :warn
expect(ssh_scanner.errors[:verbosity]).to be_empty
end
it 'is valid with :error' do
ssh_scanner.verbosity = :error
expect(ssh_scanner.errors[:verbosity]).to be_empty
end
it 'is valid with :fatal' do
ssh_scanner.verbosity = :fatal
expect(ssh_scanner.errors[:verbosity]).to be_empty
end
it 'is invalid with a random symbol' do
ssh_scanner.verbosity = :foobar
expect(ssh_scanner).to_not be_valid
expect(ssh_scanner.errors[:verbosity]).to include 'is not included in the list'
end
it 'is invalid with a string' do
ssh_scanner.verbosity = 'debug'
expect(ssh_scanner).to_not be_valid
expect(ssh_scanner.errors[:verbosity]).to include 'is not included in the list'
end
end
end
context '#attempt_login' do
before(:each) do
ssh_scanner.host = '127.0.0.1'
ssh_scanner.port = 22
ssh_scanner.connection_timeout = 30
ssh_scanner.verbosity = :fatal
ssh_scanner.stop_on_success = true
ssh_scanner.cred_details = detail_group
end
it 'creates a Timeout based on the connection_timeout' do
::Timeout.should_receive(:timeout).with(ssh_scanner.connection_timeout)
ssh_scanner.attempt_login(pub_pri)
end
it 'calls Net::SSH with the correct arguments' do
opt_hash = {
:auth_methods => ['publickey'],
:port => ssh_scanner.port,
:disable_agent => true,
:key_data => private,
:config => false,
:verbose => ssh_scanner.verbosity,
:proxies => nil
}
Net::SSH.should_receive(:start).with(
ssh_scanner.host,
public,
hash_including(opt_hash)
)
ssh_scanner.attempt_login(pub_pri)
end
context 'when it fails' do
it 'returns :connection_error for a Rex::ConnectionError' do
Net::SSH.should_receive(:start) { raise Rex::ConnectionError }
expect(ssh_scanner.attempt_login(pub_pri).status).to eq :connection_error
end
it 'returns :connection_error for a Rex::AddressInUse' do
Net::SSH.should_receive(:start) { raise Rex::AddressInUse }
expect(ssh_scanner.attempt_login(pub_pri).status).to eq :connection_error
end
it 'returns :connection_disconnect for a Net::SSH::Disconnect' do
Net::SSH.should_receive(:start) { raise Net::SSH::Disconnect }
expect(ssh_scanner.attempt_login(pub_pri).status).to eq :connection_error
end
it 'returns :connection_disconnect for a ::EOFError' do
Net::SSH.should_receive(:start) { raise ::EOFError }
expect(ssh_scanner.attempt_login(pub_pri).status).to eq :connection_error
end
it 'returns :connection_disconnect for a ::Timeout::Error' do
Net::SSH.should_receive(:start) { raise ::Timeout::Error }
expect(ssh_scanner.attempt_login(pub_pri).status).to eq :connection_error
end
it 'returns [:fail,nil] for a Net::SSH::Exception' do
Net::SSH.should_receive(:start) { raise Net::SSH::Exception }
expect(ssh_scanner.attempt_login(pub_pri).status).to eq :failed
end
it 'returns [:fail,nil] if no socket returned' do
Net::SSH.should_receive(:start).and_return nil
expect(ssh_scanner.attempt_login(pub_pri).status).to eq :failed
end
end
context 'when it succeeds' do
it 'gathers proof of the connections' do
Net::SSH.should_receive(:start) {"fake_socket"}
my_scanner = ssh_scanner
my_scanner.should_receive(:gather_proof)
my_scanner.attempt_login(pub_pri)
end
it 'returns a success code and proof' do
Net::SSH.should_receive(:start) {"fake_socket"}
my_scanner = ssh_scanner
my_scanner.should_receive(:gather_proof).and_return(public)
expect(my_scanner.attempt_login(pub_pri).status).to eq :success
end
end
end
end

View File

@ -4,6 +4,7 @@ require 'metasploit/framework/login_scanner/ssh'
describe Metasploit::Framework::LoginScanner::SSH do describe Metasploit::Framework::LoginScanner::SSH do
let(:public) { 'root' } let(:public) { 'root' }
let(:private) { 'toor' } let(:private) { 'toor' }
let(:key) { OpenSSL::PKey::RSA.generate(2048).to_s }
let(:pub_blank) { let(:pub_blank) {
Metasploit::Framework::Credential.new( Metasploit::Framework::Credential.new(
@ -29,6 +30,15 @@ describe Metasploit::Framework::LoginScanner::SSH do
) )
} }
let(:pub_key) {
Metasploit::Framework::Credential.new(
paired: true,
public: public,
private: key,
private_type: :ssh_key
)
}
let(:invalid_detail) { let(:invalid_detail) {
Metasploit::Framework::Credential.new( Metasploit::Framework::Credential.new(
paired: true, paired: true,
@ -110,6 +120,7 @@ describe Metasploit::Framework::LoginScanner::SSH do
ssh_scanner.attempt_login(pub_pri) ssh_scanner.attempt_login(pub_pri)
end end
context 'with a password' do
it 'calls Net::SSH with the correct arguments' do it 'calls Net::SSH with the correct arguments' do
opt_hash = { opt_hash = {
:auth_methods => ['password','keyboard-interactive'], :auth_methods => ['password','keyboard-interactive'],
@ -127,6 +138,27 @@ describe Metasploit::Framework::LoginScanner::SSH do
) )
ssh_scanner.attempt_login(pub_pri) ssh_scanner.attempt_login(pub_pri)
end end
end
context 'with a key' do
it 'calls Net::SSH with the correct arguments' do
opt_hash = {
:auth_methods => ['publickey'],
:port => ssh_scanner.port,
:disable_agent => true,
:key_data => key,
:config => false,
:verbose => ssh_scanner.verbosity,
:proxies => nil
}
Net::SSH.should_receive(:start).with(
ssh_scanner.host,
public,
hash_including(opt_hash)
)
ssh_scanner.attempt_login(pub_key)
end
end
context 'when it fails' do context 'when it fails' do