Fix ssh login pubkey module

This commit is contained in:
Dean Welch
2025-09-15 14:08:25 +01:00
parent 8ad35c0534
commit 8c5c395ce7
5 changed files with 86 additions and 65 deletions
+2 -2
View File
@@ -327,7 +327,7 @@ GEM
mutex_m
railties (~> 7.0)
zeitwerk
metasploit-credential (6.0.16)
metasploit-credential (6.0.19)
bigdecimal
csv
drb
@@ -340,7 +340,7 @@ GEM
railties
rex-socket
rubyntlm
rubyzip
rubyzip (< 3.0.0)
metasploit-model (5.0.4)
activemodel (~> 7.0)
activesupport (~> 7.0)
@@ -68,6 +68,7 @@ module Metasploit
:key_data => credential.private,
)
end
opt_hash[:passphrase] = cred_details.password
result_options = {
credential: credential
+1 -1
View File
@@ -120,7 +120,7 @@ module Msf
# @param [Msf::Sessions::<SESSION_CLASS>] sess
# @return [Msf::Sessions::<SESSION_CLASS>]
def start_session(obj, info, ds_merge, crlf = false, sock = nil, sess = nil)
return super unless framework.features.enabled?(Msf::FeatureManager::SHOW_SUCCESSFUL_LOGINS) && datastore['ShowSuccessfulLogins']
return super unless framework.features.enabled?(Msf::FeatureManager::SHOW_SUCCESSFUL_LOGINS) && datastore['ShowSuccessfulLogins'] && @report
result = super
@report[rhost] ||= {}
@@ -88,7 +88,7 @@ class MetasploitModule < Msf::Auxiliary
'PASS_FILE' => nil,
'USERNAME' => result.credential.public,
'CRED_CORE_PRIVATE_ID' => cred_core_private_id,
'SSH_KEYFILE_B64' => [result.credential.private].pack("m*").gsub("\n", ""),
'SSH_KEYFILE_B64' => [result.credential.private].pack('m*').gsub("\n", ''),
'KEY_PATH' => nil
}
@@ -113,12 +113,12 @@ class MetasploitModule < Msf::Auxiliary
def run_host(ip)
print_status("#{ip}:#{rport} SSH - Testing Cleartext Keys")
if datastore["USER_FILE"].blank? && datastore["USERNAME"].blank?
validation_reason = "At least one of USER_FILE or USERNAME must be given"
if datastore['USER_FILE'].blank? && datastore['USERNAME'].blank?
validation_reason = 'At least one of USER_FILE or USERNAME must be given'
raise Msf::OptionValidateError.new(
{
"USER_FILE" => validation_reason,
"USERNAME" => validation_reason
'USER_FILE' => validation_reason,
'USERNAME' => validation_reason
}
)
end
@@ -132,7 +132,7 @@ class MetasploitModule < Msf::Auxiliary
)
unless keys.valid?
print_error("Files that failed to be read:")
print_error('Files that failed to be read:')
keys.error_list.each do |err|
print_line("\t- #{err}")
end
@@ -150,7 +150,7 @@ class MetasploitModule < Msf::Auxiliary
key_sources.append('PRIVATE_KEY')
end
print_brute :level => :vstatus, :ip => ip, :msg => "Testing #{key_count} #{'key'.pluralize(key_count)} from #{key_sources.join(' and ')}"
print_brute level: :vstatus, ip: ip, msg: "Testing #{key_count} #{'key'.pluralize(key_count)} from #{key_sources.join(' and ')}"
scanner = Metasploit::Framework::LoginScanner::SSH.new(
configure_login_scanner(
host: ip,
@@ -176,36 +176,40 @@ class MetasploitModule < Msf::Auxiliary
)
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_core = create_credential(credential_data)
credential_data[:core] = credential_core
create_credential_login(credential_data)
tmp_key = result.credential.private
ssh_key = SSHKey.new tmp_key
print_brute level: :good, ip: ip, msg: "Success: '#{result.credential}' '#{result.proof.to_s.gsub(/[\r\n\e\b\a]/, ' ')}'"
ssh_key = Net::SSH::KeyFactory.load_data_private_key(credential_data[:private_data], datastore['key_pass'], false)
begin
credential_core = create_credential(credential_data)
credential_data[:core] = credential_core
create_credential_login(credential_data)
rescue ::StandardError => e
print_brute level: :info, ip: ip, msg: "Failed to create credential: #{e.class} #{e}"
print_brute level: :warn, ip: ip, msg: 'We do not currently support storing password protected SSH keys: https://github.com/rapid7/metasploit-framework/issues/20598'
credential_core = nil
end
if datastore['CreateSession']
if credential_core.is_a? Metasploit::Credential::Core
session_setup(result, scanner, ssh_key.fingerprint, credential_core.private_id)
else
session_setup(result, scanner, ssh_key.fingerprint, nil)
end
cred_id = credential_core.is_a?(Metasploit::Credential::Core) ? credential_core.private_id : nil
session_setup(result, scanner, ssh_key.public_key.fingerprint, cred_id)
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 = '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
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
if datastore['VERBOSE']
print_brute :level => :verror, :ip => ip, :msg => "Could not connect: #{result.proof}"
print_brute level: :verror, ip: ip, msg: "Could not connect: #{result.proof}"
end
scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed?
invalidate_login(credential_data)
:abort
when Metasploit::Model::Login::Status::INCORRECT
if datastore['VERBOSE']
print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'"
print_brute level: :verror, ip: ip, msg: "Failed: '#{result.credential}'"
end
invalidate_login(credential_data)
scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed?
@@ -224,7 +228,7 @@ class MetasploitModule < Msf::Auxiliary
# Override CredentialCollection#has_privates?
def has_privates?
!@key_data.empty?
@key_data.present?
end
def realm
@@ -235,49 +239,62 @@ class MetasploitModule < Msf::Auxiliary
@error_list = []
@key_data = Set.new
unless @private_key.present? || @key_path.present?
raise RuntimeError, "No key path or key provided"
end
if @key_path.present?
if File.directory?(@key_path)
@key_files ||= Dir.entries(@key_path).reject { |f| f =~ /^\x2e|\x2epub$/ }
@key_files.each do |f|
begin
data = read_key(File.join(@key_path, f))
@key_data << data if valid_key?(data)
rescue StandardError => e
@error_list << "#{File.join(@key_path, f)}: #{e}"
end
end
elsif File.file?(@key_path)
begin
data = read_key(@key_path)
@key_data << data if valid_key?(data)
rescue StandardError => e
@error_list << "#{@key_path} could not be read, #{e}"
end
else
raise RuntimeError, "Invalid key path"
end
end
if @private_key.present?
data = Net::SSH::KeyFactory.load_data_private_key(@private_key, @password, false).to_s
if valid_key?(data)
@key_data << data
else
raise RuntimeError, "Invalid private key"
end
results = validate_private_key(@private_key)
elsif @key_path.present?
results = validate_key_path(@key_path)
else
@error_list << 'No key path or key provided'
raise RuntimeError, 'No key path or key provided'
end
!@key_data.empty?
if results[:key_data].present?
@key_data.merge(results[:key_data])
else
@error_list.concat(results[:error_list]) if results[:error_list].present?
end
@key_data.present?
end
def valid_key?(key_data)
!!(key_data.match(/BEGIN [RECD]SA PRIVATE KEY/) && !key_data.match(/Proc-Type:.*ENCRYPTED/))
def validate_private_key(private_key)
key_data = Set.new
error_list = []
begin
if Net::SSH::KeyFactory.load_data_private_key(private_key, @password, false).present?
key_data << private_key
end
rescue StandardError => e
error_list << "Error validating private key: #{e}"
end
{key_data: key_data, error_list: error_list}
end
def validate_key_path(key_path)
key_data = Set.new
error_list = []
if File.file?(key_path)
key_files = [key_path]
elsif File.directory?(key_path)
key_files = Dir.entries(key_path).reject { |f| f =~ /^\x2e|\x2epub$/ }.map { |f| File.join(key_path, f) }
else
return {key_data: nil, error: "#{key_path} Invalid key path"}
end
key_files.each do |f|
begin
if read_key(f).present?
key_data << File.read(f)
end
rescue StandardError => e
error_list << "#{f}: #{e}"
end
end
{key_data: key_data, error_list: error_list}
end
def each
prepended_creds.each { |c| yield c }
@@ -307,7 +324,7 @@ class MetasploitModule < Msf::Auxiliary
def read_key(file_path)
@cache ||= {}
@cache[file_path] ||= Net::SSH::KeyFactory.load_data_private_key(File.read(file_path), password, false, key_path).to_s
@cache[file_path] ||= Net::SSH::KeyFactory.load_private_key(file_path, password, false)
@cache[file_path]
end
end
@@ -1,5 +1,6 @@
require 'spec_helper'
require 'metasploit/framework/login_scanner/ssh'
require 'metasploit/framework/credential_collection'
RSpec.describe Metasploit::Framework::LoginScanner::SSH do
let(:public) { 'root' }
@@ -48,7 +49,8 @@ RSpec.describe Metasploit::Framework::LoginScanner::SSH do
}
let(:detail_group) {
[ pub_blank, pub_pub, pub_pri]
Metasploit::Framework::CredentialCollection.new()
# [ pub_blank, pub_pub, pub_pri]
}
subject(:ssh_scanner) {
@@ -145,6 +147,7 @@ RSpec.describe Metasploit::Framework::LoginScanner::SSH do
:proxy => factory,
:append_all_supported_algorithms => true,
:auth_methods => ['password','keyboard-interactive'],
:passphrase => nil,
:password => private,
:non_interactive => true,
:verify_host_key => :never