Skip to content

Commit 95e62cf

Browse files
committed
Combine ssh_login and ssh_login_pubkey modules
1 parent b545def commit 95e62cf

File tree

3 files changed

+274
-337
lines changed

3 files changed

+274
-337
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
module Metasploit::Framework
2+
class KeyCollection < Metasploit::Framework::CredentialCollection
3+
attr_accessor :key_data
4+
attr_accessor :key_path
5+
attr_accessor :private_key
6+
attr_accessor :error_list
7+
attr_accessor :ssh_keyfile_b64
8+
9+
# Override CredentialCollection#has_privates?
10+
def has_privates?
11+
@key_data.present?
12+
end
13+
14+
def realm
15+
nil
16+
end
17+
18+
def valid?
19+
@error_list = []
20+
@key_data = Set.new
21+
22+
if @private_key.present?
23+
results = validate_private_key(@private_key)
24+
elsif @key_path.present?
25+
results = validate_key_path(@key_path)
26+
else
27+
@error_list << 'No key path or key provided'
28+
raise RuntimeError, 'No key path or key provided'
29+
end
30+
31+
if results[:key_data].present?
32+
@key_data.merge(results[:key_data])
33+
else
34+
@error_list.concat(results[:error_list]) if results[:error_list].present?
35+
end
36+
37+
@key_data.present?
38+
end
39+
40+
def validate_private_key(private_key)
41+
key_data = Set.new
42+
error_list = []
43+
begin
44+
if Net::SSH::KeyFactory.load_data_private_key(private_key, @password, false).present?
45+
key_data << private_key
46+
end
47+
rescue StandardError => e
48+
error_list << "Error validating private key: #{e}"
49+
end
50+
{key_data: key_data, error_list: error_list}
51+
end
52+
53+
def validate_key_path(key_path)
54+
key_data = Set.new
55+
error_list = []
56+
57+
if File.file?(key_path)
58+
key_files = [key_path]
59+
elsif File.directory?(key_path)
60+
key_files = Dir.entries(key_path).reject { |f| f =~ /^\x2e|\x2epub$/ }.map { |f| File.join(key_path, f) }
61+
else
62+
return {key_data: nil, error: "#{key_path} Invalid key path"}
63+
end
64+
65+
key_files.each do |f|
66+
begin
67+
if read_key(f).present?
68+
key_data << File.read(f)
69+
end
70+
rescue StandardError => e
71+
error_list << "#{f}: #{e}"
72+
end
73+
end
74+
{key_data: key_data, error_list: error_list}
75+
end
76+
77+
78+
def each
79+
prepended_creds.each { |c| yield c }
80+
81+
if @user_file.present?
82+
File.open(@user_file, 'rb') do |user_fd|
83+
user_fd.each_line do |user_from_file|
84+
user_from_file.chomp!
85+
each_key do |key_data|
86+
yield Metasploit::Framework::Credential.new(public: user_from_file, private: key_data, realm: realm, private_type: :ssh_key)
87+
end
88+
end
89+
end
90+
end
91+
92+
if @username.present?
93+
each_key do |key_data|
94+
yield Metasploit::Framework::Credential.new(public: @username, private: key_data, realm: realm, private_type: :ssh_key)
95+
end
96+
end
97+
end
98+
99+
def each_key
100+
@key_data.each do |data|
101+
yield data
102+
end
103+
end
104+
105+
def read_key(file_path)
106+
@cache ||= {}
107+
@cache[file_path] ||= Net::SSH::KeyFactory.load_private_key(file_path, password, false)
108+
@cache[file_path]
109+
end
110+
end
111+
end

modules/auxiliary/scanner/ssh/ssh_login.rb

Lines changed: 163 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
require 'net/ssh/command_stream'
88
require 'metasploit/framework/login_scanner/ssh'
99
require 'metasploit/framework/credential_collection'
10+
require 'metasploit/framework/key_collection'
1011

1112
class 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+
161318
end

0 commit comments

Comments
 (0)