6b4eb9a8e2
This change adds two new Rex exceptions and changes the local comm to raise the right one depending on the circumstances. The problem with the existing model is that failed binds and failed connections both raised the same exception. This change is backwards compatible with modules that rescue Rex::AddressInUse in additi on to Rex::ConnectionError. There were two corner cases that rescued Rex::AddressInUse specifically: 1. The 'r'-services mixin and modules caught the old exception when handling bind errors. These have been updated to use BindFailed 2. The meterpreter client had a catch for the old exception when the socket reports a bad destination (usually a network connection dropped). This has been updat ed to use InvalidDestination as that was the intention prior to this change. Since AddressInUse was part of ConnectionError, modules and mixins which caught both in the same rescue have been updated to just catch ConnectionError.
318 lines
10 KiB
Ruby
318 lines
10 KiB
Ruby
##
|
|
# This module requires Metasploit: http://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
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
|
|
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
|