|
|
|
@@ -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
|
|
|
|
|