Skip to content

Commit 8c5c395

Browse files
committed
Fix ssh login pubkey module
1 parent 8ad35c0 commit 8c5c395

File tree

5 files changed

+83
-62
lines changed

5 files changed

+83
-62
lines changed

Gemfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ GEM
327327
mutex_m
328328
railties (~> 7.0)
329329
zeitwerk
330-
metasploit-credential (6.0.16)
330+
metasploit-credential (6.0.19)
331331
bigdecimal
332332
csv
333333
drb
@@ -340,7 +340,7 @@ GEM
340340
railties
341341
rex-socket
342342
rubyntlm
343-
rubyzip
343+
rubyzip (< 3.0.0)
344344
metasploit-model (5.0.4)
345345
activemodel (~> 7.0)
346346
activesupport (~> 7.0)

lib/metasploit/framework/login_scanner/ssh.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def attempt_login(credential)
6868
:key_data => credential.private,
6969
)
7070
end
71+
opt_hash[:passphrase] = cred_details.password
7172

7273
result_options = {
7374
credential: credential

lib/msf/core/auxiliary/report_summary.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def create_credential_and_login(credential_data)
120120
# @param [Msf::Sessions::<SESSION_CLASS>] sess
121121
# @return [Msf::Sessions::<SESSION_CLASS>]
122122
def start_session(obj, info, ds_merge, crlf = false, sock = nil, sess = nil)
123-
return super unless framework.features.enabled?(Msf::FeatureManager::SHOW_SUCCESSFUL_LOGINS) && datastore['ShowSuccessfulLogins']
123+
return super unless framework.features.enabled?(Msf::FeatureManager::SHOW_SUCCESSFUL_LOGINS) && datastore['ShowSuccessfulLogins'] && @report
124124

125125
result = super
126126
@report[rhost] ||= {}

modules/auxiliary/scanner/ssh/ssh_login_pubkey.rb

Lines changed: 75 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def session_setup(result, scanner, fingerprint, cred_core_private_id)
8888
'PASS_FILE' => nil,
8989
'USERNAME' => result.credential.public,
9090
'CRED_CORE_PRIVATE_ID' => cred_core_private_id,
91-
'SSH_KEYFILE_B64' => [result.credential.private].pack("m*").gsub("\n", ""),
91+
'SSH_KEYFILE_B64' => [result.credential.private].pack('m*').gsub("\n", ''),
9292
'KEY_PATH' => nil
9393
}
9494

@@ -113,12 +113,12 @@ def session_setup(result, scanner, fingerprint, cred_core_private_id)
113113
def run_host(ip)
114114
print_status("#{ip}:#{rport} SSH - Testing Cleartext Keys")
115115

116-
if datastore["USER_FILE"].blank? && datastore["USERNAME"].blank?
117-
validation_reason = "At least one of USER_FILE or USERNAME must be given"
116+
if datastore['USER_FILE'].blank? && datastore['USERNAME'].blank?
117+
validation_reason = 'At least one of USER_FILE or USERNAME must be given'
118118
raise Msf::OptionValidateError.new(
119119
{
120-
"USER_FILE" => validation_reason,
121-
"USERNAME" => validation_reason
120+
'USER_FILE' => validation_reason,
121+
'USERNAME' => validation_reason
122122
}
123123
)
124124
end
@@ -132,7 +132,7 @@ def run_host(ip)
132132
)
133133

134134
unless keys.valid?
135-
print_error("Files that failed to be read:")
135+
print_error('Files that failed to be read:')
136136
keys.error_list.each do |err|
137137
print_line("\t- #{err}")
138138
end
@@ -150,7 +150,7 @@ def run_host(ip)
150150
key_sources.append('PRIVATE_KEY')
151151
end
152152

153-
print_brute :level => :vstatus, :ip => ip, :msg => "Testing #{key_count} #{'key'.pluralize(key_count)} from #{key_sources.join(' and ')}"
153+
print_brute level: :vstatus, ip: ip, msg: "Testing #{key_count} #{'key'.pluralize(key_count)} from #{key_sources.join(' and ')}"
154154
scanner = Metasploit::Framework::LoginScanner::SSH.new(
155155
configure_login_scanner(
156156
host: ip,
@@ -176,36 +176,40 @@ def run_host(ip)
176176
)
177177
case result.status
178178
when Metasploit::Model::Login::Status::SUCCESSFUL
179-
print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}' '#{result.proof.to_s.gsub(/[\r\n\e\b\a]/, ' ')}'"
180-
credential_core = create_credential(credential_data)
181-
credential_data[:core] = credential_core
182-
create_credential_login(credential_data)
183-
tmp_key = result.credential.private
184-
ssh_key = SSHKey.new tmp_key
179+
print_brute level: :good, ip: ip, msg: "Success: '#{result.credential}' '#{result.proof.to_s.gsub(/[\r\n\e\b\a]/, ' ')}'"
180+
ssh_key = Net::SSH::KeyFactory.load_data_private_key(credential_data[:private_data], datastore['key_pass'], false)
181+
182+
begin
183+
credential_core = create_credential(credential_data)
184+
credential_data[:core] = credential_core
185+
create_credential_login(credential_data)
186+
rescue ::StandardError => e
187+
print_brute level: :info, ip: ip, msg: "Failed to create credential: #{e.class} #{e}"
188+
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'
189+
credential_core = nil
190+
end
191+
185192
if datastore['CreateSession']
186-
if credential_core.is_a? Metasploit::Credential::Core
187-
session_setup(result, scanner, ssh_key.fingerprint, credential_core.private_id)
188-
else
189-
session_setup(result, scanner, ssh_key.fingerprint, nil)
190-
end
193+
cred_id = credential_core.is_a?(Metasploit::Credential::Core) ? credential_core.private_id : nil
194+
session_setup(result, scanner, ssh_key.public_key.fingerprint, cred_id)
191195
end
192196
if datastore['GatherProof'] && scanner.get_platform(result.proof) == 'unknown'
193-
msg = "While a session may have opened, it may be bugged. If you experience issues with it, re-run this module with"
197+
msg = 'While a session may have opened, it may be bugged. If you experience issues with it, re-run this module with'
194198
msg << " 'set gatherproof false'. Also consider submitting an issue at github.com/rapid7/metasploit-framework with"
195-
msg << " device details so it can be handled in the future."
196-
print_brute :level => :error, :ip => ip, :msg => msg
199+
msg << ' device details so it can be handled in the future.'
200+
print_brute level: :error, ip: ip, msg: msg
197201
end
198202
:next_user
199203
when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
200204
if datastore['VERBOSE']
201-
print_brute :level => :verror, :ip => ip, :msg => "Could not connect: #{result.proof}"
205+
print_brute level: :verror, ip: ip, msg: "Could not connect: #{result.proof}"
202206
end
203207
scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed?
204208
invalidate_login(credential_data)
205209
:abort
206210
when Metasploit::Model::Login::Status::INCORRECT
207211
if datastore['VERBOSE']
208-
print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'"
212+
print_brute level: :verror, ip: ip, msg: "Failed: '#{result.credential}'"
209213
end
210214
invalidate_login(credential_data)
211215
scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed?
@@ -224,7 +228,7 @@ class KeyCollection < Metasploit::Framework::CredentialCollection
224228

225229
# Override CredentialCollection#has_privates?
226230
def has_privates?
227-
!@key_data.empty?
231+
@key_data.present?
228232
end
229233

230234
def realm
@@ -235,49 +239,62 @@ def valid?
235239
@error_list = []
236240
@key_data = Set.new
237241

238-
unless @private_key.present? || @key_path.present?
239-
raise RuntimeError, "No key path or key provided"
242+
if @private_key.present?
243+
results = validate_private_key(@private_key)
244+
elsif @key_path.present?
245+
results = validate_key_path(@key_path)
246+
else
247+
@error_list << 'No key path or key provided'
248+
raise RuntimeError, 'No key path or key provided'
240249
end
241250

242-
if @key_path.present?
243-
if File.directory?(@key_path)
244-
@key_files ||= Dir.entries(@key_path).reject { |f| f =~ /^\x2e|\x2epub$/ }
245-
@key_files.each do |f|
246-
begin
247-
data = read_key(File.join(@key_path, f))
248-
@key_data << data if valid_key?(data)
249-
rescue StandardError => e
250-
@error_list << "#{File.join(@key_path, f)}: #{e}"
251-
end
252-
end
253-
elsif File.file?(@key_path)
254-
begin
255-
data = read_key(@key_path)
256-
@key_data << data if valid_key?(data)
257-
rescue StandardError => e
258-
@error_list << "#{@key_path} could not be read, #{e}"
259-
end
260-
else
261-
raise RuntimeError, "Invalid key path"
262-
end
251+
if results[:key_data].present?
252+
@key_data.merge(results[:key_data])
253+
else
254+
@error_list.concat(results[:error_list]) if results[:error_list].present?
263255
end
264256

265-
if @private_key.present?
266-
data = Net::SSH::KeyFactory.load_data_private_key(@private_key, @password, false).to_s
267-
if valid_key?(data)
268-
@key_data << data
269-
else
270-
raise RuntimeError, "Invalid private key"
257+
@key_data.present?
258+
end
259+
260+
def validate_private_key(private_key)
261+
key_data = Set.new
262+
error_list = []
263+
begin
264+
if Net::SSH::KeyFactory.load_data_private_key(private_key, @password, false).present?
265+
key_data << private_key
271266
end
267+
rescue StandardError => e
268+
error_list << "Error validating private key: #{e}"
272269
end
273-
274-
!@key_data.empty?
270+
{key_data: key_data, error_list: error_list}
275271
end
276272

277-
def valid_key?(key_data)
278-
!!(key_data.match(/BEGIN [RECD]SA PRIVATE KEY/) && !key_data.match(/Proc-Type:.*ENCRYPTED/))
273+
def validate_key_path(key_path)
274+
key_data = Set.new
275+
error_list = []
276+
277+
if File.file?(key_path)
278+
key_files = [key_path]
279+
elsif File.directory?(key_path)
280+
key_files = Dir.entries(key_path).reject { |f| f =~ /^\x2e|\x2epub$/ }.map { |f| File.join(key_path, f) }
281+
else
282+
return {key_data: nil, error: "#{key_path} Invalid key path"}
283+
end
284+
285+
key_files.each do |f|
286+
begin
287+
if read_key(f).present?
288+
key_data << File.read(f)
289+
end
290+
rescue StandardError => e
291+
error_list << "#{f}: #{e}"
292+
end
293+
end
294+
{key_data: key_data, error_list: error_list}
279295
end
280296

297+
281298
def each
282299
prepended_creds.each { |c| yield c }
283300

@@ -307,7 +324,7 @@ def each_key
307324

308325
def read_key(file_path)
309326
@cache ||= {}
310-
@cache[file_path] ||= Net::SSH::KeyFactory.load_data_private_key(File.read(file_path), password, false, key_path).to_s
327+
@cache[file_path] ||= Net::SSH::KeyFactory.load_private_key(file_path, password, false)
311328
@cache[file_path]
312329
end
313330
end

spec/lib/metasploit/framework/login_scanner/ssh_spec.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'spec_helper'
22
require 'metasploit/framework/login_scanner/ssh'
3+
require 'metasploit/framework/credential_collection'
34

45
RSpec.describe Metasploit::Framework::LoginScanner::SSH do
56
let(:public) { 'root' }
@@ -48,7 +49,8 @@
4849
}
4950

5051
let(:detail_group) {
51-
[ pub_blank, pub_pub, pub_pri]
52+
Metasploit::Framework::CredentialCollection.new()
53+
# [ pub_blank, pub_pub, pub_pri]
5254
}
5355

5456
subject(:ssh_scanner) {
@@ -145,6 +147,7 @@
145147
:proxy => factory,
146148
:append_all_supported_algorithms => true,
147149
:auth_methods => ['password','keyboard-interactive'],
150+
:passphrase => nil,
148151
:password => private,
149152
:non_interactive => true,
150153
:verify_host_key => :never

0 commit comments

Comments
 (0)