Skip to content

Commit b29aba1

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

File tree

3 files changed

+266
-334
lines changed

3 files changed

+266
-334
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: 155 additions & 3 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

@@ -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+
161313
end

0 commit comments

Comments
 (0)