From e73159c4b6e1204ba5b5c68430088f2c995d1c18 Mon Sep 17 00:00:00 2001 From: h00die Date: Tue, 9 Jan 2024 17:50:11 -0500 Subject: [PATCH] ssh_login module --- modules/auxiliary/scanner/ssh/ssh_login.rb | 291 ++++++++++++--------- 1 file changed, 171 insertions(+), 120 deletions(-) diff --git a/modules/auxiliary/scanner/ssh/ssh_login.rb b/modules/auxiliary/scanner/ssh/ssh_login.rb index 50dc278a09415..11e31b6dcbca8 100644 --- a/modules/auxiliary/scanner/ssh/ssh_login.rb +++ b/modules/auxiliary/scanner/ssh/ssh_login.rb @@ -3,159 +3,210 @@ # Current source: https://github.com/rapid7/metasploit-framework ## -require 'net/ssh' -require 'net/ssh/command_stream' -require 'metasploit/framework/login_scanner/ssh' -require 'metasploit/framework/credential_collection' +require 'recog' +require 'net/ssh/transport/session' class MetasploitModule < Msf::Auxiliary - include Msf::Auxiliary::AuthBrute - include Msf::Auxiliary::Report - include Msf::Auxiliary::CommandShell include Msf::Auxiliary::Scanner - include Msf::Exploit::Remote::SSH::Options + include Msf::Auxiliary::Report def initialize super( - 'Name' => 'SSH Login Check Scanner', - 'Description' => %q{ - This module will test ssh logins on a range of machines and - report successful logins. If you have loaded a database plugin - and connected to a database this module will record successful - logins and hosts so you can track your access. - }, - 'Author' => ['todb'], - 'References' => - [ - [ 'CVE', '1999-0502'] # Weak password - ], - 'License' => MSF_LICENSE, - 'DefaultOptions' => {'VERBOSE' => false} # Disable annoying connect errors + 'Name' => 'SSH Version Scanner', + 'Description' => 'Detect SSH Version, and the algorithms available from the server', + 'References' => [ + ['URL', 'https://en.wikipedia.org/wiki/SecureShell'], # general info + ['URL', 'https://datatracker.ietf.org/doc/html/rfc8732#name-deprecated-algorithms'], # deprecation of kex gss-sha1 stuff + ['URL', 'https://datatracker.ietf.org/doc/html/draft-ietf-curdle-ssh-kex-sha2-20#page-16'], # diffie-hellman-group-exchange-sha1, diffie-hellman-group1-sha1, rsa1024-sha1 + ['URL', 'https://datatracker.ietf.org/doc/html/rfc8758#name-iana-considerations'], # arc4 deprecation + ['URL', 'https://github.com/net-ssh/net-ssh?tab=readme-ov-file#supported-algorithms'] # a bunch of diff removed things from the ruby lib + ], + 'Author' => [ + 'Daniel van Eeden ', # original author + 'h00die' # algorithms enhancements + ], + 'License' => MSF_LICENSE ) register_options( [ - Opt::RPORT(22) - ], self.class - ) - - register_advanced_options( - [ - Opt::Proxies, - OptBool.new('SSH_DEBUG', [false, 'Enable SSH debugging output (Extreme verbosity!)', false]), - OptInt.new('SSH_TIMEOUT', [false, 'Specify the maximum time to negotiate a SSH session', 30]), - OptBool.new('GatherProof', [true, 'Gather proof of access via pre-session shell commands', true]) - ] + Opt::RPORT(22), + OptInt.new('TIMEOUT', [true, 'Timeout for the SSH probe', 30]) + ], + self.class ) + end - deregister_options('PASSWORD_SPRAY') + def timeout + datastore['TIMEOUT'] end def rport datastore['RPORT'] end - def session_setup(result, scanner) - return unless scanner.ssh_socket - - platform = scanner.get_platform(result.proof) - - # Create a new session - sess = Msf::Sessions::SshCommandShellBind.new(scanner.ssh_socket) + def run_host(target_host) + ::Timeout.timeout(timeout) do + transport = Net::SSH::Transport::Session.new(target_host, { port: rport }) - merge_me = { - 'USERPASS_FILE' => nil, - 'USER_FILE' => nil, - 'PASS_FILE' => nil, - 'USERNAME' => result.credential.public, - 'PASSWORD' => result.credential.private - } - s = start_session(self, nil, merge_me, false, sess.rstream, sess) - self.sockets.delete(scanner.ssh_socket.transport.socket) + server_data = transport.algorithms.instance_variable_get(:@server_data) + host_keys = transport.algorithms.session.instance_variable_get(:@host_keys).instance_variable_get(:@host_keys) + if !host_keys.empty? + print_status("Key Fingerprint: #{host_keys[0].fingerprint}") + end - # Set the session platform - s.platform = platform + ident = transport.server_version.version - # Create database host information - host_info = {host: scanner.host} + table = Rex::Text::Table.new( + 'Header' => 'Server Encryption', + 'Indent' => 2, + 'SortIndex' => 0, + 'Columns' => [ 'Type', 'Value'] + ) - unless s.platform == 'unknown' - host_info[:os_name] = s.platform - end + server_data[:language_server].each do |language| + table << ['Language', language] + end - report_host(host_info) + server_data[:compression_server].each do |compression| + table << ['Compression', compression] + end - s - end + server_data[:encryption_server].each do |encryption| + ['arcfour', 'arcfour128', 'arcfour256'].each do |bad_enc| + next unless encryption.downcase.start_with? bad_enc + + print_good("Encryption #{encryption} is deprecated and should not be used.") + report_vuln( + host: target_host, + port: rport, + proto: 'tcp', + name: name, + info: "Module #{fullname} confirmed SSH Encryption #{encryption} is available, but should be deprecated", + refs: ['https://datatracker.ietf.org/doc/html/rfc8758#name-iana-considerations'] + ) + end + [ + 'aes256-cbc', 'aes192-cbc', 'aes128-cbc', 'rijndael-cbc@lysator.liu.se', + 'blowfish-ctr blowfish-cbc', 'cast128-ctr', 'cast128-cbc', '3des-ctr', '3des-cbc', 'idea-cbc', 'none' + ].each do |bad_enc| + next unless encryption.downcase.start_with? bad_enc + + print_good("Encryption #{encryption} is deprecated and should not be used.") + report_vuln( + host: target_host, + port: rport, + proto: 'tcp', + name: name, + info: "Module #{fullname} confirmed SSH Encryption #{encryption} is available, but should be deprecated", + refs: ['https://github.com/net-ssh/net-ssh?tab=readme-ov-file#encryption-algorithms-ciphers'] + ) + end + table << ['Encryption', encryption] + end + server_data[:hmac_server].each do |hmac| + ['hmac-sha2-512-96', 'hmac-sha2-256-96', 'hmac-sha1-96', 'hmac-ripemd160', 'hmac-md5', 'hmac-md5-96', 'none'].each do |bad_hmac| + next unless hmac.downcase.start_with? bad_hmac + + print_good("HMAC #{hmac} is deprecated and should not be used.") + report_vuln( + host: target_host, + port: rport, + proto: 'tcp', + name: name, + info: "Module #{fullname} confirmed SSH HMAC #{hmac} is available, but should be deprecated", + refs: ['https://github.com/net-ssh/net-ssh?tab=readme-ov-file#message-authentication-code-algorithms'] + ) + end + table << ['HMAC', hmac] + end - def run_host(ip) - @ip = ip - print_brute :ip => ip, :msg => 'Starting bruteforce' + server_data[:host_key].each do |host_key| + ['ecdsa-sha2-nistp521', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp256'].each do |bad_key| + next unless host_key.downcase.start_with? bad_key + + print_good("Host Key Encryption #{host_key} uses a weak elliptic curve and should not be used.") + report_vuln( + host: target_host, + port: rport, + proto: 'tcp', + name: name, + info: "Module #{fullname} confirmed SSH Host Key Encryption #{host_key} is available, but should be deprecated", + refs: ['https://github.com/net-ssh/net-ssh?tab=readme-ov-file#host-keys'] + ) + end + table << ['Host Key', host_key] + end - cred_collection = build_credential_collection( - username: datastore['USERNAME'], - password: datastore['PASSWORD'], - ) + server_data[:kex].each do |kex| + ['gss-group1-sha1-', 'gss-group14-sha1-', 'gss-gex-sha1-'].each do |bad_kex| + next unless kex.downcase.start_with? bad_kex + + print_good("Key Exchange (kex) #{kex} is deprecated and should not be used.") + report_vuln( + host: target_host, + port: rport, + proto: 'tcp', + name: name, + info: "Module #{fullname} confirmed SSH Encryption #{kex} is available, but should be deprecated", + refs: ['https://datatracker.ietf.org/doc/html/rfc8732#name-deprecated-algorithms'] + ) + end + ['ecdsa-sha2-nistp521', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp256'].each do |bad_kex| + next unless kex.downcase.start_with? bad_kex + + print_good("Key Exchange (kex) #{kex} uses a weak elliptic curve and should not be used.") + report_vuln( + host: target_host, + port: rport, + proto: 'tcp', + name: name, + info: "Module #{fullname} confirmed SSH Encryption #{kex} is available, but should be deprecated", + refs: ['https://github.com/net-ssh/net-ssh?tab=readme-ov-file#key-exchange'] + ) + end + ['diffie-hellman-group-exchange-sha1', 'diffie-hellman-group1-sha1', 'rsa1024-sha1'].each do |bad_kex| + next unless kex.downcase.start_with? bad_kex + + print_good("Key Exchange (kex) #{kex} is deprecated and should not be used.") + report_vuln( + host: target_host, + port: rport, + proto: 'tcp', + name: name, + info: "Module #{fullname} confirmed SSH Encryption #{kex} is available, but should be deprecated", + refs: ['https://datatracker.ietf.org/doc/html/draft-ietf-curdle-ssh-kex-sha2-20#page-16'] + ) + end + table << ['Key Exchange (kex)', kex] + end - scanner = Metasploit::Framework::LoginScanner::SSH.new( - host: ip, - port: rport, - cred_details: cred_collection, - proxies: datastore['Proxies'], - stop_on_success: datastore['STOP_ON_SUCCESS'], - bruteforce_speed: datastore['BRUTEFORCE_SPEED'], - connection_timeout: datastore['SSH_TIMEOUT'], - framework: framework, - framework_module: self, - skip_gather_proof: !datastore['GatherProof'] - ) + # XXX check for host key size? + # https://www.tenable.com/plugins/nessus/153954 - scanner.verbosity = :debug if datastore['SSH_DEBUG'] + # Try to match with Recog and show the relevant fields to the user + info = '' + if /^SSH-\d+\.\d+-(.*)$/ =~ ident + recog_match = Recog::Nizer.match('ssh.banner', ::Regexp.last_match(1)) + if recog_match + info << ' ( ' + recog_match.each_pair do |k, v| + next if k == 'matched' - scanner.scan! do |result| - credential_data = result.to_h - credential_data.merge!( - module_fullname: self.fullname, - workspace_id: myworkspace_id - ) - case result.status - when Metasploit::Model::Login::Status::SUCCESSFUL - print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}' '#{result.proof.to_s.gsub(/[\r\n\e\b\a]/, ' ')}'" - credential_data[:private_type] = :password - credential_core = create_credential(credential_data) - credential_data[:core] = credential_core - create_credential_login(credential_data) - - if datastore['CreateSession'] - begin - session_setup(result, scanner) - rescue StandardError => e - elog('Failed to setup the session', error: e) - print_brute :level => :error, :ip => ip, :msg => "Failed to setup the session - #{e.class} #{e.message}" + info << "#{k}=#{v} " end + info << ')' end - - if datastore['GatherProof'] && scanner.get_platform(result.proof) == 'unknown' - msg = "While a session may have opened, it may be bugged. If you experience issues with it, re-run this module with" - msg << " 'set gatherproof false'. Also consider submitting an issue at github.com/rapid7/metasploit-framework with" - msg << " device details so it can be handled in the future." - print_brute :level => :error, :ip => ip, :msg => msg - end - :next_user - when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT - vprint_brute :level => :verror, :ip => ip, :msg => "Could not connect: #{result.proof}" - scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed? - invalidate_login(credential_data) - :abort - when Metasploit::Model::Login::Status::INCORRECT - vprint_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'" - invalidate_login(credential_data) - scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed? - else - invalidate_login(credential_data) - scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed? end + + print_status("SSH server version: #{ident}#{info}") + report_service(host: target_host, port: rport, name: 'ssh', proto: 'tcp', info: ident) + print_status(table.to_s) end + rescue EOFError, Rex::ConnectionError => e + vprint_error(e.message) # This may be a little noisy, but it is consistent + rescue Timeout::Error + vprint_warning("Timed out after #{timeout} seconds. Skipping.") end end