Files
metasploit-gs/modules/auxiliary/scanner/ssh/ssh_identify_pubkeys.rb
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

389 lines
13 KiB
Ruby
Raw Normal View History

2012-01-05 14:10:49 -06:00
##
2017-07-24 06:26:21 -07:00
# This module requires Metasploit: https://metasploit.com/download
2013-10-15 13:50:46 -05:00
# Current source: https://github.com/rapid7/metasploit-framework
2012-01-05 14:10:49 -06:00
##
require 'net/ssh'
2012-01-08 22:28:37 -06:00
require 'sshkey' # TODO: Actually include this!
2016-09-19 15:20:35 -05:00
require 'net/ssh/pubkey_verifier'
2012-01-05 14:10:49 -06:00
2016-03-08 14:02:44 +01:00
class MetasploitModule < Msf::Auxiliary
2012-01-05 14:10:49 -06:00
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::AuthBrute
include Msf::Auxiliary::Report
2016-06-28 15:23:12 -05:00
include Msf::Exploit::Remote::SSH
2013-08-30 16:28:54 -05:00
2012-01-05 14:10:49 -06:00
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.
2013-08-30 16:28:54 -05:00
2012-01-05 14:10:49 -06:00
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.
2013-08-30 16:28:54 -05:00
2012-01-05 14:10:49 -06:00
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' => [
2015-12-05 22:49:40 +00:00
'todb',
'hdm',
'Stuart Morgan <stuart.morgan[at]mwrinfosecurity.com>', # Reworked the storage (db, credentials, notes, loot) only
],
2012-01-05 14:10:49 -06:00
'License' => MSF_LICENSE
)
2013-08-30 16:28:54 -05:00
2012-01-05 14:10:49 -06:00
register_options(
[
Opt::RPORT(22),
2016-09-13 17:42:31 -05:00
OptPath.new('KEY_FILE', [true, 'Filename of one or several cleartext public keys.'])
2016-02-03 13:20:30 -08:00
]
2012-01-05 14:10:49 -06:00
)
2013-08-30 16:28:54 -05:00
2012-01-05 14:10:49 -06:00
register_advanced_options(
[
OptBool.new('SSH_DEBUG', [ false, 'Enable SSH debugging output (Extreme verbosity!)', false]),
2012-01-07 01:13:23 -06:00
OptBool.new('SSH_BYPASS', [ false, 'Verify that authentication was not bypassed when keys are found', false]),
2012-01-05 14:10:49 -06:00
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])
2012-01-05 14:10:49 -06:00
]
)
2013-08-30 16:28:54 -05:00
2016-02-03 13:20:30 -08:00
deregister_options(
2023-12-13 17:00:19 +00:00
'PASSWORD','PASS_FILE','BLANK_PASSWORDS','USER_AS_PASS', 'USERPASS_FILE', 'DB_ALL_PASS', 'DB_ALL_CREDS'
2016-02-03 13:20:30 -08:00
)
2013-08-30 16:28:54 -05:00
2012-01-05 14:10:49 -06:00
@good_credentials = {}
@good_key = ''
@strip_passwords = true
2013-08-30 16:28:54 -05:00
2012-01-05 14:10:49 -06:00
end
2013-08-30 16:28:54 -05:00
2012-01-05 14:10:49 -06:00
def key_dir
2016-05-16 17:34:12 -07:00
datastore['KEY_DIR']
2012-01-05 14:10:49 -06:00
end
2013-08-30 16:28:54 -05:00
def key_file
2016-05-16 17:34:12 -07:00
datastore['KEY_FILE']
end
2012-01-05 14:10:49 -06:00
def rport
datastore['RPORT']
end
2013-08-30 16:28:54 -05:00
2012-01-05 14:10:49 -06:00
def ip
datastore['RHOST']
end
2013-08-30 16:28:54 -05:00
2012-01-05 14:10:49 -06:00
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|
2012-01-05 14:10:49 -06:00
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)}
2012-01-05 14:10:49 -06:00
end
keys = []
this_key = []
in_key = false
keyfile.split("\n").each do |line|
if /(?<key>ssh-(?:dss|rsa)\s+.*)/ =~ line
keys << key
2012-01-05 14:10:49 -06:00
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
2013-08-30 16:28:54 -05:00
2012-01-08 22:28:37 -06:00
# Validates that the key isn't total garbage, and converts PEM formatted
# keys to SSH formatted keys.
2012-01-05 14:10:49 -06:00
def validate_keys(keys)
keepers = []
keys.each do |key|
if key =~ /ssh-(dss|rsa)/
# A public key has been provided
keepers << { :public => key, :private => "" }
2012-01-05 14:10:49 -06:00
next
2015-12-05 22:49:40 +00:00
else
# Use the mighty SSHKey library from James Miller to convert them on the fly.
# This is where a PRIVATE key has been provided
2012-01-08 22:28:37 -06:00
ssh_version = SSHKey.new(key).ssh_public_key rescue nil
keepers << { :public => ssh_version, :private => key } if ssh_version
2012-01-08 22:28:37 -06:00
next
2012-01-05 14:10:49 -06:00
end
2013-08-30 16:28:54 -05:00
2012-01-05 14:10:49 -06:00
# 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 test
keepers << { :public => key, :private => "" }
2012-01-05 14:10:49 -06:00
end
if keepers.empty?
print_error "#{ip}:#{rport} SSH - No valid keys found"
end
return keepers.uniq
end
2013-08-30 16:28:54 -05:00
2012-01-05 14:10:49 -06:00
def pull_cleartext_keys(keys)
cleartext_keys = []
keys.each do |key|
next unless key[:public]
next if key[:private] =~ /Proc-Type:.*ENCRYPTED/
this_key = { :public => key[:public].gsub(/\x0d/,""), :private => key[:private] }
2012-01-05 14:10:49 -06:00
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
2013-08-30 16:28:54 -05:00
2012-01-05 14:10:49 -06:00
def do_login(ip, port, user)
2013-08-30 16:28:54 -05:00
2016-05-24 13:28:08 -07:00
if key_file && File.readable?(key_file)
keys = read_keyfile(key_file)
2012-01-05 14:10:49 -06:00
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/}
2012-01-05 14:10:49 -06:00
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
2013-08-30 16:28:54 -05:00
2012-01-05 14:10:49 -06:00
unless @alerted_with_msg
print_status msg
@alerted_with_msg = true
end
2013-08-30 16:28:54 -05:00
2012-01-05 14:10:49 -06:00
cleartext_keys.each_with_index do |key_data,key_idx|
2013-08-30 16:28:54 -05:00
key_info = ""
if key_data[:public] =~ /ssh\-(rsa|dss)\s+([^\s]+)\s+(.*)/
2012-01-05 14:10:49 -06:00
key_info = "- #{$3.strip}"
end
2013-08-30 16:28:54 -05:00
2016-06-28 15:23:12 -05:00
factory = ssh_socket_factory
2012-01-05 14:10:49 -06:00
opt_hash = {
:auth_methods => ['publickey'],
:port => port,
:key_data => key_data[:public],
:use_agent => false,
2018-08-15 14:59:52 -07:00
:config => false,
:proxy => factory,
:non_interactive => true,
:verify_host_key => :never
2012-01-05 14:10:49 -06:00
}
2013-08-30 16:28:54 -05:00
2012-01-05 14:10:49 -06:00
opt_hash.merge!(:verbose => :debug) if datastore['SSH_DEBUG']
2013-08-30 16:28:54 -05:00
2012-01-05 14:10:49 -06:00
begin
ssh_socket = nil
2016-09-19 15:20:35 -05:00
success = false
verifier = Net::SSH::PubkeyVerifier.new(ip,user,opt_hash)
::Timeout.timeout(datastore['SSH_TIMEOUT']) do
success = verifier.verify
ssh_socket = verifier.connection
end
2013-08-30 16:28:54 -05:00
if datastore['SSH_BYPASS'] and ssh_socket
2012-01-07 01:13:23 -06:00
data = nil
2013-08-30 16:28:54 -05:00
print_status("#{ip}:#{rport} SSH - User #{user} is being tested for authentication bypass...")
2013-08-30 16:28:54 -05:00
2012-01-07 01:13:23 -06:00
begin
::Timeout.timeout(5) { data = ssh_socket.exec!("help\nid\nuname -a").to_s }
rescue ::Exception
end
2013-08-30 16:28:54 -05:00
print_brute(:level => :good, :msg => "User #{user} successfully bypassed authentication: #{data.inspect} ") if data
2012-01-07 01:13:23 -06:00
end
2013-08-30 16:28:54 -05:00
::Timeout.timeout(1) { ssh_socket.close if ssh_socket } rescue nil
2013-08-30 16:28:54 -05:00
rescue Rex::ConnectionError
2012-01-05 14:10:49 -06:00
return :connection_error
rescue Net::SSH::Disconnect, ::EOFError
return :connection_disconnect
rescue Net::SSH::AuthenticationFailed
2016-01-04 13:26:03 -06:00
rescue Net::SSH::Exception
2012-01-05 14:10:49 -06:00
return [:fail,nil] # For whatever reason.
end
2013-08-30 16:28:54 -05:00
2016-09-19 15:20:35 -05:00
unless success
2012-01-05 14:10:49 -06:00
if @key_files
print_brute :level => :verror, :msg => "User #{user} does not accept key #{@key_files[key_idx+1]} #{key_info}"
2012-01-05 14:10:49 -06:00
else
print_brute :level => :verror, :msg => "User #{user} does not accept key #{key_idx+1} #{key_info}"
2012-01-05 14:10:49 -06:00
end
2016-09-19 15:20:35 -05:00
return [:fail,nil]
2012-01-05 14:10:49 -06:00
end
2013-08-30 16:28:54 -05:00
2016-09-19 15:20:35 -05:00
key = verifier.key
key_fingerprint = key.fingerprint
user = verifier.user
private_key_present = (key_data[:private] != "") ? 'Yes' : 'No'
print_brute :level => :good, :msg => "Public key accepted: '#{user}' with key '#{key_fingerprint}' (Private Key: #{private_key_present}) #{key_info}"
key_hash = {
data: key_data,
key: key,
info: key_info
}
do_report(ip, rport, user, key_hash)
2012-01-05 14:10:49 -06:00
end
end
2013-08-30 16:28:54 -05:00
def do_report(ip, port, user, key)
2012-01-08 22:28:37 -06:00
return unless framework.db.active
2015-12-05 22:49:40 +00:00
2016-01-04 13:26:03 -06:00
store_public_keyfile(ip,user,key[:fingerprint],key[:data][:public])
private_key_present = (key[:data][:private]!="") ? 'Yes' : 'No'
2015-12-05 22:45:08 +00:00
# Store a note relating to the public key test
note_information = {
user: user,
public_key: key[:data][:public],
2015-12-05 22:45:08 +00:00
private_key: private_key_present,
info: key[:info]
2015-12-05 22:49:40 +00:00
}
2015-12-05 21:19:34 +00:00
report_note(host: ip, port: port, type: "ssh.publickey.accepted", data: note_information, update: :unique_data)
2015-12-05 22:49:40 +00:00
2015-12-05 22:45:08 +00:00
if key[:data][:private] != ""
# Store these keys in loot
private_keyfile_path = store_private_keyfile(ip,user,key[:fingerprint],key[:data][:private])
# Use the proper credential method to store credentials that we have
service_data = {
address: ip,
port: port,
service_name: 'ssh',
protocol: 'tcp',
workspace_id: myworkspace_id
}
credential_data = {
module_fullname: self.fullname,
origin_type: :service,
private_data: key[:data][:private],
private_type: :ssh_key,
username: key[:key][:user],
}.merge(service_data)
2015-12-05 22:49:40 +00:00
login_data = {
core: create_credential(credential_data),
last_attempted_at: DateTime.now,
status: Metasploit::Model::Login::Status::SUCCESSFUL,
proof: private_keyfile_path
}.merge(service_data)
create_credential_login(login_data)
end
2012-01-08 22:28:37 -06:00
end
2013-08-30 16:28:54 -05:00
def existing_loot(ltype, key_id)
framework.db.loots(workspace: myworkspace).where(ltype: ltype).select {|l| l.info == key_id}.first
2012-01-08 22:28:37 -06:00
end
2013-08-30 16:28:54 -05:00
def store_public_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"
2012-01-08 22:28:37 -06:00
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
2012-06-06 00:36:17 -05:00
ip,
(key_data + "\n"),
"#{safe_username}_#{ktype}.pub",
key_id
)
return keyfile_path
2012-01-08 22:28:37 -06:00
end
2013-08-30 16:28:54 -05:00
def store_private_keyfile(ip,user,key_id,key_data)
safe_username = user.gsub(/[^A-Za-z0-9]/,"_")
ktype = key_data.match(/-----BEGIN ([RD]SA) (?:PRIVATE|PUBLIC) KEY-----/)[1].downcase rescue nil
return unless ktype
ltype = "host.unix.ssh.#{user}_#{ktype}_private"
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}.private",
key_id
)
return keyfile_path
end
2012-06-06 00:36:17 -05:00
def run_host(ip)
2016-09-13 17:42:31 -05:00
# 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.
2012-01-05 14:10:49 -06:00
each_user_pass do |user,pass|
2016-01-04 13:26:03 -06:00
ret, _ = do_login(ip, rport, user)
2012-01-05 14:10:49 -06:00
case ret
when :connection_error
vprint_error "#{ip}:#{rport} SSH - Could not connect"
2012-01-05 14:10:49 -06:00
:abort
when :connection_disconnect
vprint_error "#{ip}:#{rport} SSH - Connection timed out"
2012-01-05 14:10:49 -06:00
:abort
when :fail
vprint_error "#{ip}:#{rport} SSH - Failed: '#{user}'"
2012-01-05 14:10:49 -06:00
when :missing_keyfile
vprint_error "#{ip}:#{rport} SSH - Cannot read keyfile"
2012-01-05 14:10:49 -06:00
when :no_valid_keys
vprint_error "#{ip}:#{rport} SSH - No readable keys in keyfile"
2012-01-05 14:10:49 -06:00
end
end
end
end