Skip to content

Commit 2de3623

Browse files
committed
Combine ssh_login and ssh_login_pubkey modules
1 parent 205221f commit 2de3623

File tree

3 files changed

+272
-341
lines changed

3 files changed

+272
-341
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: 161 additions & 7 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
[ '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,21 +62,33 @@ 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)
6269

6370
# Create a new session
6471
sess = Msf::Sessions::SshCommandShellBind.new(scanner.ssh_socket)
6572

73+
auth_type_options = if used_key
74+
{
75+
'PASSWORD' => nil
76+
}
77+
else
78+
{
79+
'PASSWORD' => result.credential.private,
80+
'PRIVATE_KEY' => nil,
81+
'KEY_FILE' => nil
82+
}
83+
end
84+
6685
merge_me = {
6786
'USERPASS_FILE' => nil,
6887
'USER_FILE' => nil,
6988
'PASS_FILE' => nil,
70-
'USERNAME' => result.credential.public,
71-
'PASSWORD' => result.credential.private
72-
}
89+
'USERNAME' => result.credential.public
90+
}.merge(auth_type_options)
91+
7392
s = start_session(self, nil, merge_me, false, sess.rstream, sess)
7493
self.sockets.delete(scanner.ssh_socket.transport.socket)
7594

@@ -92,6 +111,35 @@ def run_host(ip)
92111
@ip = ip
93112
print_brute :ip => ip, :msg => 'Starting bruteforce'
94113

114+
if datastore['USER_FILE'].blank? && datastore['USERNAME'].blank? && datastore['USERPASS_FILE'].blank?
115+
validation_reason = 'At least one of USER_FILE, USERPASS_FILE or USERNAME must be given'
116+
raise Msf::OptionValidateError.new(
117+
{
118+
'USER_FILE' => validation_reason,
119+
'USERNAME' => validation_reason,
120+
'USERPASS_FILE' => validation_reason
121+
}
122+
)
123+
end
124+
125+
unless attempt_password_login? || attempt_pubkey_login?
126+
validation_reason = 'At least one of KEY_PATH, PRIVATE_KEY or PASSWORD must be given'
127+
raise Msf::OptionValidateError.new(
128+
{
129+
'KEY_PATH' => validation_reason,
130+
'PRIVATE_KEY' => validation_reason,
131+
'PASSWORD' => validation_reason
132+
}
133+
)
134+
end
135+
136+
do_login_creds(ip) if attempt_password_login?
137+
do_login_pubkey(ip) if attempt_pubkey_login?
138+
end
139+
140+
def do_login_creds(ip)
141+
print_status("#{ip}:#{rport} SSH - Testing User/Pass combinations")
142+
95143
cred_collection = build_credential_collection(
96144
username: datastore['USERNAME'],
97145
password: datastore['PASSWORD']
@@ -130,7 +178,7 @@ def run_host(ip)
130178

131179
if datastore['CreateSession']
132180
begin
133-
session_setup(result, scanner)
181+
session_setup(result, scanner, used_key: false)
134182
rescue StandardError => e
135183
elog('Failed to setup the session', error: e)
136184
print_brute :level => :error, :ip => ip, :msg => "Failed to setup the session - #{e.class} #{e.message}"
@@ -159,4 +207,110 @@ def run_host(ip)
159207
end
160208
end
161209
end
210+
211+
def do_login_pubkey(ip)
212+
print_status("#{ip}:#{rport} SSH - Testing Cleartext Keys")
213+
214+
keys = Metasploit::Framework::KeyCollection.new(
215+
key_path: datastore['KEY_PATH'],
216+
password: datastore['KEY_PASS'],
217+
user_file: datastore['USER_FILE'],
218+
username: datastore['USERNAME'],
219+
private_key: datastore['PRIVATE_KEY']
220+
)
221+
222+
unless keys.valid?
223+
print_error('Files that failed to be read:')
224+
keys.error_list.each do |err|
225+
print_line("\t- #{err}")
226+
end
227+
end
228+
229+
keys = prepend_db_keys(keys)
230+
231+
key_count = keys.key_data.count
232+
key_sources = []
233+
unless datastore['KEY_PATH'].blank?
234+
key_sources.append(datastore['KEY_PATH'])
235+
end
236+
237+
unless datastore['PRIVATE_KEY'].blank?
238+
key_sources.append('PRIVATE_KEY')
239+
end
240+
241+
print_brute level: :vstatus, ip: ip, msg: "Testing #{key_count} #{'key'.pluralize(key_count)} from #{key_sources.join(' and ')}"
242+
scanner = Metasploit::Framework::LoginScanner::SSH.new(
243+
configure_login_scanner(
244+
host: ip,
245+
port: rport,
246+
cred_details: keys,
247+
stop_on_success: datastore['STOP_ON_SUCCESS'],
248+
bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
249+
proxies: datastore['Proxies'],
250+
connection_timeout: datastore['SSH_TIMEOUT'],
251+
framework: framework,
252+
framework_module: self,
253+
skip_gather_proof: !datastore['GatherProof']
254+
)
255+
)
256+
257+
scanner.verbosity = :debug if datastore['SSH_DEBUG']
258+
259+
scanner.scan! do |result|
260+
credential_data = result.to_h
261+
credential_data.merge!(
262+
module_fullname: self.fullname,
263+
workspace_id: myworkspace_id
264+
)
265+
case result.status
266+
when Metasploit::Model::Login::Status::SUCCESSFUL
267+
print_brute level: :good, ip: ip, msg: "Success: '#{result.proof.to_s.gsub(/[\r\n\e\b\a]/, ' ')}'"
268+
print_brute level: :vgood, ip: ip, msg: "#{result.credential}', ' ')}'"
269+
begin
270+
credential_core = create_credential(credential_data)
271+
credential_data[:core] = credential_core
272+
create_credential_login(credential_data)
273+
rescue ::StandardError => e
274+
print_brute level: :info, ip: ip, msg: "Failed to create credential: #{e.class} #{e}"
275+
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'
276+
end
277+
278+
if datastore['CreateSession']
279+
session_setup(result, scanner, used_key: true)
280+
end
281+
if datastore['GatherProof'] && scanner.get_platform(result.proof) == 'unknown'
282+
msg = 'While a session may have opened, it may be bugged. If you experience issues with it, re-run this module with'
283+
msg << " 'set gatherproof false'. Also consider submitting an issue at github.com/rapid7/metasploit-framework with"
284+
msg << ' device details so it can be handled in the future.'
285+
print_brute level: :error, ip: ip, msg: msg
286+
end
287+
:next_user
288+
when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
289+
if datastore['VERBOSE']
290+
print_brute level: :verror, ip: ip, msg: "Could not connect: #{result.proof}"
291+
end
292+
scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed?
293+
invalidate_login(credential_data)
294+
:abort
295+
when Metasploit::Model::Login::Status::INCORRECT
296+
if datastore['VERBOSE']
297+
print_brute level: :verror, ip: ip, msg: "Failed: '#{result.credential}'"
298+
end
299+
invalidate_login(credential_data)
300+
scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed?
301+
else
302+
invalidate_login(credential_data)
303+
scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed?
304+
end
305+
end
306+
end
307+
308+
def attempt_pubkey_login?
309+
datastore['KEY_PATH'].present? || datastore['PRIVATE_KEY'].present?
310+
end
311+
312+
def attempt_password_login?
313+
datastore['PASSWORD'].present? || datastore['PASS_FILE'].present? || datastore['USERPASS_FILE'].present?
314+
end
315+
162316
end

0 commit comments

Comments
 (0)