##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'net/ssh'
require 'metasploit/framework/login_scanner/ssh'
require 'metasploit/framework/credential_collection'
require 'sshkey'
require 'net/ssh/command_stream'
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::Sessions::CreateSessionOptions
include Msf::Auxiliary::ReportSummary
attr_accessor :ssh_socket, :good_key
def initialize
super(
'Name' => 'SSH Public Key Login Scanner',
'Description' => %q{
This module will test ssh logins on a range of machines using
a defined private key file, 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.
Key files may be a single private key, or several private keys in a single
directory. Only a single passphrase is supported however, so it must either
be shared between subject keys or only belong to a single one.
},
'Author' => ['todb', 'RageLtMan'],
'License' => MSF_LICENSE
)
register_options(
[
Opt::RPORT(22),
OptPath.new('KEY_PATH', [false, 'Filename or directory of cleartext private keys. Filenames beginning with a dot, or ending in ".pub" will be skipped. Duplicate private keys will be ignored.']),
OptString.new('KEY_PASS', [false, 'Passphrase for SSH private key(s)']),
OptString.new('PRIVATE_KEY', [false, 'The string value of the private key that will be used. If you are using MSFConsole, this value should be set as file:PRIVATE_KEY_PATH. OpenSSH, RSA, DSA, and ECDSA private keys are supported.'])
], self.class
)
register_advanced_options(
[
Opt::Proxies,
OptBool.new('SSH_DEBUG', [false, 'Enable SSH debugging output (Extreme verbosity!)', false]),
OptString.new('SSH_KEYFILE_B64', [false, 'Raw data of an unencrypted SSH public key. This should be used by programmatic interfaces to this module only.', '']),
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])
]
)
deregister_options(
'PASSWORD','PASS_FILE','BLANK_PASSWORDS','USER_AS_PASS','USERPASS_FILE',
'DB_ALL_CREDS', 'DB_ALL_PASS', 'DB_SKIP_EXISTING'
)
@good_key = ''
@strip_passwords = true
end
def rport
datastore['RPORT']
end
def ip
datastore['RHOST']
end
def session_setup(result, scanner, fingerprint, cred_core_private_id)
return unless scanner.ssh_socket
# Create a new session
sess = Msf::Sessions::SshCommandShellBind.new(scanner.ssh_socket)
# Clean up the stored data - need to stash the keyfile into
# a datastore for later reuse.
merge_me = {
'USERPASS_FILE' => nil,
'USER_FILE' => nil,
'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",""),
'KEY_PATH' => nil
}
s = start_session(self, nil, merge_me, false, sess.rstream, sess)
self.sockets.delete(scanner.ssh_socket.transport.socket)
# Set the session platform
s.platform = scanner.get_platform(result.proof)
# Create database host information
host_info = {host: scanner.host}
unless s.platform == 'unknown'
host_info[:os_name] = s.platform
end
report_host(host_info)
s
end
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"
raise Msf::OptionValidateError.new(
{
"USER_FILE" => validation_reason,
"USERNAME" => validation_reason
}
)
end
keys = KeyCollection.new(
key_path: datastore['KEY_PATH'],
password: datastore['KEY_PASS'],
user_file: datastore['USER_FILE'],
username: datastore['USERNAME'],
private_key: datastore['PRIVATE_KEY']
)
unless keys.valid?
print_error("Files that failed to be read:")
keys.error_list.each do |err|
print_line("\t- #{err}")
end
end
keys = prepend_db_keys(keys)
key_count = keys.key_data.count
key_sources = []
unless datastore['KEY_PATH'].blank?
key_sources.append(datastore['KEY_PATH'])
end
unless datastore['PRIVATE_KEY'].blank?
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 ')}"
scanner = Metasploit::Framework::LoginScanner::SSH.new(
configure_login_scanner(
host: ip,
port: rport,
cred_details: keys,
stop_on_success: datastore['STOP_ON_SUCCESS'],
bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
proxies: datastore['Proxies'],
connection_timeout: datastore['SSH_TIMEOUT'],
framework: framework,
framework_module: self,
skip_gather_proof: !datastore['GatherProof']
)
)
scanner.verbosity = :debug if datastore['SSH_DEBUG']
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_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
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
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
if datastore['VERBOSE']
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}'"
end
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
end
end
class KeyCollection < Metasploit::Framework::CredentialCollection
attr_accessor :key_data
attr_accessor :key_path
attr_accessor :private_key
attr_accessor :error_list
# Override CredentialCollection#has_privates?
def has_privates?
!@key_data.empty?
end
def realm
nil
end
def valid?
@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
end
!@key_data.empty?
end
def valid_key?(key_data)
!!(key_data.match(/BEGIN [RECD]SA PRIVATE KEY/) && !key_data.match(/Proc-Type:.*ENCRYPTED/))
end
def each
prepended_creds.each { |c| yield c }
if @user_file.present?
File.open(@user_file, 'rb') do |user_fd|
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, private_type: :ssh_key)
end
end
end
end
if @username.present?
each_key do |key_data|
yield Metasploit::Framework::Credential.new(public: @username, private: key_data, realm: realm, private_type: :ssh_key)
end
end
end
def each_key
@key_data.each do |data|
yield data
end
end
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]
end
end
end
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation