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 ] ,
@@ -36,7 +40,10 @@ def initialize
3640
3741 register_options (
3842 [
39- Opt ::RPORT ( 22 )
43+ Opt ::RPORT ( 22 ) ,
44+ 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.' ] ) ,
45+ OptString . new ( 'KEY_PASS' , [ false , 'Passphrase for SSH private key(s)' ] ) ,
46+ 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.' ] )
4047 ] , self . class
4148 )
4249
@@ -54,7 +61,7 @@ def rport
5461 datastore [ 'RPORT' ]
5562 end
5663
57- def session_setup ( result , scanner )
64+ def session_setup ( result , scanner , used_key : false )
5865 return unless scanner . ssh_socket
5966
6067 platform = scanner . get_platform ( result . proof )
@@ -66,9 +73,24 @@ def session_setup(result, scanner)
6673 'USERPASS_FILE' => nil ,
6774 'USER_FILE' => nil ,
6875 'PASS_FILE' => nil ,
69- 'USERNAME' => result . credential . public ,
70- 'PASSWORD' => result . credential . private
76+ 'USERNAME' => result . credential . public
7177 }
78+ if used_key
79+ merge_me . merge! (
80+ {
81+ 'PASSWORD' => nil
82+ }
83+ )
84+ else
85+ merge_me . merge! (
86+ {
87+ 'PASSWORD' => result . credential . private ,
88+ 'PRIVATE_KEY' => nil ,
89+ 'KEY_FILE' => nil
90+ }
91+ )
92+ end
93+
7294 s = start_session ( self , nil , merge_me , false , sess . rstream , sess )
7395 self . sockets . delete ( scanner . ssh_socket . transport . socket )
7496
@@ -91,6 +113,35 @@ def run_host(ip)
91113 @ip = ip
92114 print_brute :ip => ip , :msg => 'Starting bruteforce'
93115
116+ if datastore [ 'USER_FILE' ] . blank? && datastore [ 'USERNAME' ] . blank? && datastore [ 'USERPASS_FILE' ] . blank?
117+ validation_reason = 'At least one of USER_FILE, USERPASS_FILE or USERNAME must be given'
118+ raise Msf ::OptionValidateError . new (
119+ {
120+ 'USER_FILE' => validation_reason ,
121+ 'USERNAME' => validation_reason ,
122+ 'USERPASS_FILE' => validation_reason
123+ }
124+ )
125+ end
126+
127+ unless attempt_password_login? || attempt_pubkey_login?
128+ validation_reason = 'At least one of KEY_PATH, PRIVATE_KEY or PASSWORD must be given'
129+ raise Msf ::OptionValidateError . new (
130+ {
131+ 'KEY_PATH' => validation_reason ,
132+ 'PRIVATE_KEY' => validation_reason ,
133+ 'PASSWORD' => validation_reason
134+ }
135+ )
136+ end
137+
138+ do_login_creds ( ip ) if attempt_password_login?
139+ do_login_pubkey ( ip ) if attempt_pubkey_login?
140+ end
141+
142+ def do_login_creds ( ip )
143+ print_status ( "#{ ip } :#{ rport } SSH - Testing User/Pass combinations" )
144+
94145 cred_collection = build_credential_collection (
95146 username : datastore [ 'USERNAME' ] ,
96147 password : datastore [ 'PASSWORD' ]
@@ -129,7 +180,7 @@ def run_host(ip)
129180
130181 if datastore [ 'CreateSession' ]
131182 begin
132- session_setup ( result , scanner )
183+ session_setup ( result , scanner , used_key : false )
133184 rescue StandardError => e
134185 elog ( 'Failed to setup the session' , error : e )
135186 print_brute :level => :error , :ip => ip , :msg => "Failed to setup the session - #{ e . class } #{ e . message } "
@@ -158,4 +209,110 @@ def run_host(ip)
158209 end
159210 end
160211 end
212+
213+ def do_login_pubkey ( ip )
214+ print_status ( "#{ ip } :#{ rport } SSH - Testing Cleartext Keys" )
215+
216+ keys = Metasploit ::Framework ::KeyCollection . new (
217+ key_path : datastore [ 'KEY_PATH' ] ,
218+ password : datastore [ 'KEY_PASS' ] ,
219+ user_file : datastore [ 'USER_FILE' ] ,
220+ username : datastore [ 'USERNAME' ] ,
221+ private_key : datastore [ 'PRIVATE_KEY' ]
222+ )
223+
224+ unless keys . valid?
225+ print_error ( 'Files that failed to be read:' )
226+ keys . error_list . each do |err |
227+ print_line ( "\t - #{ err } " )
228+ end
229+ end
230+
231+ keys = prepend_db_keys ( keys )
232+
233+ key_count = keys . key_data . count
234+ key_sources = [ ]
235+ unless datastore [ 'KEY_PATH' ] . blank?
236+ key_sources . append ( datastore [ 'KEY_PATH' ] )
237+ end
238+
239+ unless datastore [ 'PRIVATE_KEY' ] . blank?
240+ key_sources . append ( 'PRIVATE_KEY' )
241+ end
242+
243+ print_brute level : :vstatus , ip : ip , msg : "Testing #{ key_count } #{ 'key' . pluralize ( key_count ) } from #{ key_sources . join ( ' and ' ) } "
244+ scanner = Metasploit ::Framework ::LoginScanner ::SSH . new (
245+ configure_login_scanner (
246+ host : ip ,
247+ port : rport ,
248+ cred_details : keys ,
249+ stop_on_success : datastore [ 'STOP_ON_SUCCESS' ] ,
250+ bruteforce_speed : datastore [ 'BRUTEFORCE_SPEED' ] ,
251+ proxies : datastore [ 'Proxies' ] ,
252+ connection_timeout : datastore [ 'SSH_TIMEOUT' ] ,
253+ framework : framework ,
254+ framework_module : self ,
255+ skip_gather_proof : !datastore [ 'GatherProof' ]
256+ )
257+ )
258+
259+ scanner . verbosity = :debug if datastore [ 'SSH_DEBUG' ]
260+
261+ scanner . scan! do |result |
262+ credential_data = result . to_h
263+ credential_data . merge! (
264+ module_fullname : self . fullname ,
265+ workspace_id : myworkspace_id
266+ )
267+ case result . status
268+ when Metasploit ::Model ::Login ::Status ::SUCCESSFUL
269+ print_brute level : :good , ip : ip , msg : "Success: '#{ result . proof . to_s . gsub ( /[\r \n \e \b \a ]/ , ' ' ) } '"
270+ print_brute level : :vgood , ip : ip , msg : "#{ result . credential } ', ' ')}'"
271+ begin
272+ credential_core = create_credential ( credential_data )
273+ credential_data [ :core ] = credential_core
274+ create_credential_login ( credential_data )
275+ rescue ::StandardError => e
276+ print_brute level : :info , ip : ip , msg : "Failed to create credential: #{ e . class } #{ e } "
277+ 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'
278+ end
279+
280+ if datastore [ 'CreateSession' ]
281+ session_setup ( result , scanner , used_key : true )
282+ end
283+ if datastore [ 'GatherProof' ] && scanner . get_platform ( result . proof ) == 'unknown'
284+ msg = 'While a session may have opened, it may be bugged. If you experience issues with it, re-run this module with'
285+ msg << " 'set gatherproof false'. Also consider submitting an issue at github.com/rapid7/metasploit-framework with"
286+ msg << ' device details so it can be handled in the future.'
287+ print_brute level : :error , ip : ip , msg : msg
288+ end
289+ :next_user
290+ when Metasploit ::Model ::Login ::Status ::UNABLE_TO_CONNECT
291+ if datastore [ 'VERBOSE' ]
292+ print_brute level : :verror , ip : ip , msg : "Could not connect: #{ result . proof } "
293+ end
294+ scanner . ssh_socket . close if scanner . ssh_socket && !scanner . ssh_socket . closed?
295+ invalidate_login ( credential_data )
296+ :abort
297+ when Metasploit ::Model ::Login ::Status ::INCORRECT
298+ if datastore [ 'VERBOSE' ]
299+ print_brute level : :verror , ip : ip , msg : "Failed: '#{ result . credential } '"
300+ end
301+ invalidate_login ( credential_data )
302+ scanner . ssh_socket . close if scanner . ssh_socket && !scanner . ssh_socket . closed?
303+ else
304+ invalidate_login ( credential_data )
305+ scanner . ssh_socket . close if scanner . ssh_socket && !scanner . ssh_socket . closed?
306+ end
307+ end
308+ end
309+
310+ def attempt_pubkey_login?
311+ datastore [ 'KEY_PATH' ] . present? || datastore [ 'PRIVATE_KEY' ] . present?
312+ end
313+
314+ def attempt_password_login?
315+ datastore [ 'PASSWORD' ] . present? || datastore [ 'PASS_FILE' ] . present? || datastore [ 'USERPASS_FILE' ] . present?
316+ end
317+
161318end
0 commit comments