diff --git a/lib/metasploit/framework/login_scanner/ssh.rb b/lib/metasploit/framework/login_scanner/ssh.rb index fb4d971e35..b5e4c2252a 100644 --- a/lib/metasploit/framework/login_scanner/ssh.rb +++ b/lib/metasploit/framework/login_scanner/ssh.rb @@ -8,6 +8,7 @@ module Metasploit # 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 # and attempting them. It then saves the results. + # class SSH include Metasploit::Framework::LoginScanner::Base @@ -19,7 +20,7 @@ module Metasploit DEFAULT_PORT = 22 LIKELY_PORTS = [ DEFAULT_PORT ] LIKELY_SERVICE_NAMES = [ 'ssh' ] - PRIVATE_TYPES = [ :password ] + PRIVATE_TYPES = [ :password, :ssh_key ] VERBOSITIES = [ :debug, @@ -46,14 +47,24 @@ module Metasploit def attempt_login(credential) self.ssh_socket = nil opt_hash = { - :auth_methods => ['password','keyboard-interactive'], :port => port, :disable_agent => true, - :password => credential.private, :config => false, :verbose => verbosity, :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 = { credential: credential diff --git a/lib/metasploit/framework/login_scanner/ssh_key.rb b/lib/metasploit/framework/login_scanner/ssh_key.rb deleted file mode 100644 index b883f7bec1..0000000000 --- a/lib/metasploit/framework/login_scanner/ssh_key.rb +++ /dev/null @@ -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 diff --git a/modules/auxiliary/scanner/ssh/ssh_login_pubkey.rb b/modules/auxiliary/scanner/ssh/ssh_login_pubkey.rb index de0501f0c3..b345aef9a1 100644 --- a/modules/auxiliary/scanner/ssh/ssh_login_pubkey.rb +++ b/modules/auxiliary/scanner/ssh/ssh_login_pubkey.rb @@ -5,7 +5,7 @@ require 'msf/core' require 'net/ssh' -require 'metasploit/framework/login_scanner/ssh_key' +require 'metasploit/framework/login_scanner/ssh' 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" - scanner = Metasploit::Framework::LoginScanner::SSHKey.new( + scanner = Metasploit::Framework::LoginScanner::SSH.new( host: ip, port: rport, cred_details: keys, @@ -297,7 +297,7 @@ class Metasploit3 < Msf::Auxiliary user_fd.each_line do |user_from_file| user_from_file.chomp! 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 @@ -305,7 +305,7 @@ class Metasploit3 < Msf::Auxiliary if @username.present? 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 diff --git a/spec/lib/metasploit/framework/login_scanner/ssh_key_spec.rb b/spec/lib/metasploit/framework/login_scanner/ssh_key_spec.rb deleted file mode 100644 index 418ce48dd2..0000000000 --- a/spec/lib/metasploit/framework/login_scanner/ssh_key_spec.rb +++ /dev/null @@ -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 \ No newline at end of file diff --git a/spec/lib/metasploit/framework/login_scanner/ssh_spec.rb b/spec/lib/metasploit/framework/login_scanner/ssh_spec.rb index f0a1ad8f1e..03c132c1f2 100644 --- a/spec/lib/metasploit/framework/login_scanner/ssh_spec.rb +++ b/spec/lib/metasploit/framework/login_scanner/ssh_spec.rb @@ -4,6 +4,7 @@ require 'metasploit/framework/login_scanner/ssh' describe Metasploit::Framework::LoginScanner::SSH do let(:public) { 'root' } let(:private) { 'toor' } + let(:key) { OpenSSL::PKey::RSA.generate(2048).to_s } let(:pub_blank) { 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) { Metasploit::Framework::Credential.new( paired: true, @@ -110,22 +120,44 @@ describe Metasploit::Framework::LoginScanner::SSH do ssh_scanner.attempt_login(pub_pri) end - it 'calls Net::SSH with the correct arguments' do - opt_hash = { - :auth_methods => ['password','keyboard-interactive'], - :port => ssh_scanner.port, - :disable_agent => true, - :password => private, - :config => false, - :verbose => ssh_scanner.verbosity, - :proxies => nil - } - Net::SSH.should_receive(:start).with( - ssh_scanner.host, - public, - opt_hash - ) - ssh_scanner.attempt_login(pub_pri) + context 'with a password' do + it 'calls Net::SSH with the correct arguments' do + opt_hash = { + :auth_methods => ['password','keyboard-interactive'], + :port => ssh_scanner.port, + :disable_agent => true, + :password => private, + :config => false, + :verbose => ssh_scanner.verbosity, + :proxies => nil + } + Net::SSH.should_receive(:start).with( + ssh_scanner.host, + public, + opt_hash + ) + 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 context 'when it fails' do @@ -186,4 +218,4 @@ describe Metasploit::Framework::LoginScanner::SSH do -end \ No newline at end of file +end