Merge pull request #97 from rapid7/feature/MSP-10656/unify-ssh-scanners
Feature/msp 10656/unify ssh scannersbug/bundler_fix
commit
80bee35b70
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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,22 +120,44 @@ describe Metasploit::Framework::LoginScanner::SSH do
|
||||||
ssh_scanner.attempt_login(pub_pri)
|
ssh_scanner.attempt_login(pub_pri)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'calls Net::SSH with the correct arguments' do
|
context 'with a password' do
|
||||||
opt_hash = {
|
it 'calls Net::SSH with the correct arguments' do
|
||||||
:auth_methods => ['password','keyboard-interactive'],
|
opt_hash = {
|
||||||
:port => ssh_scanner.port,
|
:auth_methods => ['password','keyboard-interactive'],
|
||||||
:disable_agent => true,
|
:port => ssh_scanner.port,
|
||||||
:password => private,
|
:disable_agent => true,
|
||||||
:config => false,
|
:password => private,
|
||||||
:verbose => ssh_scanner.verbosity,
|
:config => false,
|
||||||
:proxies => nil
|
:verbose => ssh_scanner.verbosity,
|
||||||
}
|
:proxies => nil
|
||||||
Net::SSH.should_receive(:start).with(
|
}
|
||||||
ssh_scanner.host,
|
Net::SSH.should_receive(:start).with(
|
||||||
public,
|
ssh_scanner.host,
|
||||||
opt_hash
|
public,
|
||||||
)
|
opt_hash
|
||||||
ssh_scanner.attempt_login(pub_pri)
|
)
|
||||||
|
ssh_scanner.attempt_login(pub_pri)
|
||||||
|
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
|
end
|
||||||
|
|
||||||
context 'when it fails' do
|
context 'when it fails' do
|
||||||
|
@ -186,4 +218,4 @@ describe Metasploit::Framework::LoginScanner::SSH do
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue