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.
} ,
2015-12-05 21:18:29 +00:00
'Author' = > [
2015-12-05 22:49:40 +00:00
'todb' ,
2015-12-05 21:18:29 +00:00
'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.' , '' ] ) ,
2012-06-12 15:19:01 -05:00
OptPath . new ( 'KEY_DIR' , [ false , 'Directory of several keys. Filenames must not begin with a dot in order to be read.' ] ) ,
2012-08-12 23:22:38 -05:00
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
2016-02-03 14:21:54 -08:00
def key_file
2016-05-16 17:34:12 -07:00
datastore [ 'KEY_FILE' ]
2016-02-03 14:21:54 -08:00
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 = ''
2016-05-24 13:30:12 -07:00
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
2016-05-24 13:30:12 -07:00
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 |
2016-02-03 13:50:17 -08:00
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) /
2015-12-05 21:18:29 +00:00
# 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.
2015-12-05 21:18:29 +00:00
# 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
2015-12-05 21:18:29 +00:00
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?
2015-12-05 21:18:29 +00:00
# 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 |
2015-12-05 21:18:29 +00:00
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 )
2016-02-03 14:21:54 -08:00
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
2012-01-13 13:49:21 -06:00
@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
2015-12-05 21:18:29 +00: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
2015-12-05 21:18:29 +00: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 = {
2018-08-15 07:00:45 -07:00
:auth_methods = > [ 'publickey' ] ,
:port = > port ,
:key_data = > key_data [ :public ] ,
:use_agent = > false ,
2018-08-15 14:59:52 -07:00
:config = > false ,
2018-08-15 07:00:45 -07:00
:proxy = > factory ,
2018-08-15 06:48:35 -07:00
:non_interactive = > true ,
2018-08-15 07:00:45 -07:00
: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
2012-06-12 15:19:01 -05:00
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
2012-06-12 15:19:01 -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
2012-01-13 13:49:21 -06: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 \n id \n uname -a " ) . to_s }
rescue :: Exception
end
2013-08-30 16:28:54 -05:00
2012-01-13 13:49:21 -06: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
2012-06-12 15:19:01 -05:00
:: Timeout . timeout ( 1 ) { ssh_socket . close if ssh_socket } rescue nil
2013-08-30 16:28:54 -05:00
2014-11-11 14:59:41 -06: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
2012-01-13 13:49:21 -06:00
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
2012-01-13 13:49:21 -06:00
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
2015-12-05 23:27:28 +00: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 ] )
2015-12-05 23:27:28 +00:00
private_key_present = ( key [ :data ] [ :private ] != " " ) ? 'Yes' : 'No'
2015-12-05 22:45:08 +00:00
2015-12-05 21:18:29 +00:00
# Store a note relating to the public key test
note_information = {
user : user ,
2015-12-05 23:27:28 +00:00
public_key : key [ :data ] [ :public ] ,
2015-12-05 22:45:08 +00:00
private_key : private_key_present ,
2015-12-05 23:27:28 +00:00
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 ] != " "
2015-12-05 21:18:29 +00:00
# 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
2015-12-05 21:18:29 +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
2012-01-13 13:49:21 -06:00
def existing_loot ( ltype , key_id )
2018-05-21 17:37:51 -04:00
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
2015-12-05 21:18:29 +00:00
def store_public_keyfile ( ip , user , key_id , key_data )
2012-01-13 13:49:21 -06:00
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 "
2012-01-13 13:49:21 -06:00
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 ,
2012-01-13 13:49:21 -06:00
( 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
2015-12-05 21:18:29 +00: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
2012-01-13 13:49:21 -06:00
vprint_error " #{ ip } : #{ rport } SSH - Could not connect "
2012-01-05 14:10:49 -06:00
:abort
when :connection_disconnect
2012-01-13 13:49:21 -06:00
vprint_error " #{ ip } : #{ rport } SSH - Connection timed out "
2012-01-05 14:10:49 -06:00
:abort
when :fail
2012-01-13 13:49:21 -06:00
vprint_error " #{ ip } : #{ rport } SSH - Failed: ' #{ user } ' "
2012-01-05 14:10:49 -06:00
when :missing_keyfile
2012-01-13 13:49:21 -06:00
vprint_error " #{ ip } : #{ rport } SSH - Cannot read keyfile "
2012-01-05 14:10:49 -06:00
when :no_valid_keys
2012-01-13 13:49:21 -06:00
vprint_error " #{ ip } : #{ rport } SSH - No readable keys in keyfile "
2012-01-05 14:10:49 -06:00
end
end
end
end