Skip to content

Commit 3990c02

Browse files
committed
Land rapid7#6318, updates for ssh_identify_pubkeys
2 parents 58c0472 + 6f01df3 commit 3990c02

File tree

1 file changed

+87
-34
lines changed

1 file changed

+87
-34
lines changed

modules/auxiliary/scanner/ssh/ssh_identify_pubkeys.rb

Lines changed: 87 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,16 @@ def initialize
2929
this module will record authorized public keys and hosts so you can
3030
track your process.
3131
32-
3332
Key files may be a single public (unencrypted) key, or several public
3433
keys concatenated together as an ASCII text file. Non-key data should be
3534
silently ignored. Private keys will only utilize the public key component
3635
stored within the key file.
3736
},
38-
'Author' => ['todb', 'hdm'],
37+
'Author' => [
38+
'todb',
39+
'hdm',
40+
'Stuart Morgan <stuart.morgan[at]mwrinfosecurity.com>', # Reworked the storage (db, credentials, notes, loot) only
41+
],
3942
'License' => MSF_LICENSE
4043
)
4144

@@ -116,11 +119,14 @@ def validate_keys(keys)
116119
keepers = []
117120
keys.each do |key|
118121
if key =~ /ssh-(dss|rsa)/
119-
keepers << key
122+
# A public key has been provided
123+
keepers << { :public => key, :private => "" }
120124
next
121-
else # Use the mighty SSHKey library from James Miller to convert them on the fly.
125+
else
126+
# Use the mighty SSHKey library from James Miller to convert them on the fly.
127+
# This is where a PRIVATE key has been provided
122128
ssh_version = SSHKey.new(key).ssh_public_key rescue nil
123-
keepers << ssh_version if ssh_version
129+
keepers << { :public => ssh_version, :private => key } if ssh_version
124130
next
125131
end
126132

@@ -130,8 +136,8 @@ def validate_keys(keys)
130136
next unless key =~ /\n-----END [RD]SA (PRIVATE|PUBLIC) KEY-----\x0d?\x0a?$/m
131137
# Shouldn't have binary.
132138
next unless key.scan(/[\x00-\x08\x0b\x0c\x0e-\x1f\x80-\xff]/).empty?
133-
# Add more tests to taste.
134-
keepers << key
139+
# Add more tests to test
140+
keepers << { :public => key, :private => "" }
135141
end
136142
if keepers.empty?
137143
print_error "#{ip}:#{rport} SSH - No valid keys found"
@@ -142,9 +148,9 @@ def validate_keys(keys)
142148
def pull_cleartext_keys(keys)
143149
cleartext_keys = []
144150
keys.each do |key|
145-
next unless key
146-
next if key =~ /Proc-Type:.*ENCRYPTED/
147-
this_key = key.gsub(/\x0d/,"")
151+
next unless key[:public]
152+
next if key[:private] =~ /Proc-Type:.*ENCRYPTED/
153+
this_key = { :public => key[:public].gsub(/\x0d/,""), :private => key[:private] }
148154
next if cleartext_keys.include? this_key
149155
cleartext_keys << this_key
150156
end
@@ -182,26 +188,26 @@ def do_login(ip, port, user)
182188
@alerted_with_msg = true
183189
end
184190

191+
185192
cleartext_keys.each_with_index do |key_data,key_idx|
186-
key_info = ""
187193

188-
if key_data =~ /ssh\-(rsa|dss)\s+([^\s]+)\s+(.*)/
194+
key_info = ""
195+
if key_data[:public] =~ /ssh\-(rsa|dss)\s+([^\s]+)\s+(.*)/
189196
key_info = "- #{$3.strip}"
190197
end
191198

192-
193199
accepted = []
194200
opt_hash = {
195201
:auth_methods => ['publickey'],
196202
:msframework => framework,
197203
:msfmodule => self,
198204
:port => port,
199-
:key_data => key_data,
205+
:key_data => key_data[:public],
200206
:disable_agent => true,
201207
:record_auth_info => true,
202208
:skip_private_keys => true,
203209
:config =>false,
204-
:accepted_key_callback => Proc.new {|key| accepted << key },
210+
:accepted_key_callback => Proc.new {|key| accepted << { :data => key_data, :key => key, :info => key_info } },
205211
:proxies => datastore['Proxies']
206212
}
207213

@@ -231,7 +237,7 @@ def do_login(ip, port, user)
231237
rescue Net::SSH::Disconnect, ::EOFError
232238
return :connection_disconnect
233239
rescue Net::SSH::AuthenticationFailed
234-
rescue Net::SSH::Exception => e
240+
rescue Net::SSH::Exception
235241
return [:fail,nil] # For whatever reason.
236242
end
237243

@@ -244,35 +250,64 @@ def do_login(ip, port, user)
244250
end
245251

246252
accepted.each do |key|
247-
print_brute :level => :good, :msg => "Accepted: '#{user}' with key '#{key[:fingerprint]}' #{key_info}"
248-
do_report(ip, rport, user, key, key_data)
253+
private_key_present = (key[:data][:private]!="") ? 'Yes' : 'No'
254+
print_brute :level => :good, :msg => "Public key accepted: '#{user}' with key '#{key[:key][:fingerprint]}' (Private Key: #{private_key_present}) #{key_info}"
255+
do_report(ip, rport, user, key)
249256
end
250257
end
251258
end
252259

253-
def do_report(ip, port, user, key, key_data)
260+
def do_report(ip, port, user, key)
254261
return unless framework.db.active
255-
keyfile_path = store_keyfile(ip,user,key[:fingerprint],key_data)
256-
cred_hash = {
257-
:host => ip,
258-
:port => rport,
259-
:sname => 'ssh',
260-
:user => user,
261-
:pass => keyfile_path,
262-
:source_type => "user_supplied",
263-
:type => 'ssh_pubkey',
264-
:proof => "KEY=#{key[:fingerprint]}",
265-
:duplicate_ok => true,
266-
:active => true
262+
263+
store_public_keyfile(ip,user,key[:fingerprint],key[:data][:public])
264+
private_key_present = (key[:data][:private]!="") ? 'Yes' : 'No'
265+
266+
# Store a note relating to the public key test
267+
note_information = {
268+
user: user,
269+
public_key: key[:data][:public],
270+
private_key: private_key_present,
271+
info: key[:info]
267272
}
268-
this_cred = report_auth_info(cred_hash)
273+
report_note(host: ip, port: port, type: "ssh.publickey.accepted", data: note_information, update: :unique_data)
274+
275+
if key[:data][:private] != ""
276+
# Store these keys in loot
277+
private_keyfile_path = store_private_keyfile(ip,user,key[:fingerprint],key[:data][:private])
278+
279+
# Use the proper credential method to store credentials that we have
280+
service_data = {
281+
address: ip,
282+
port: port,
283+
service_name: 'ssh',
284+
protocol: 'tcp',
285+
workspace_id: myworkspace_id
286+
}
287+
288+
credential_data = {
289+
module_fullname: self.fullname,
290+
origin_type: :service,
291+
private_data: key[:data][:private],
292+
private_type: :ssh_key,
293+
username: key[:key][:user],
294+
}.merge(service_data)
295+
296+
login_data = {
297+
core: create_credential(credential_data),
298+
last_attempted_at: DateTime.now,
299+
status: Metasploit::Model::Login::Status::SUCCESSFUL,
300+
proof: private_keyfile_path
301+
}.merge(service_data)
302+
create_credential_login(login_data)
303+
end
269304
end
270305

271306
def existing_loot(ltype, key_id)
272307
framework.db.loots(myworkspace).where(ltype: ltype).select {|l| l.info == key_id}.first
273308
end
274309

275-
def store_keyfile(ip,user,key_id,key_data)
310+
def store_public_keyfile(ip,user,key_id,key_data)
276311
safe_username = user.gsub(/[^A-Za-z0-9]/,"_")
277312
ktype = key_data.match(/ssh-(rsa|dss)/)[1] rescue nil
278313
return unless ktype
@@ -291,12 +326,30 @@ def store_keyfile(ip,user,key_id,key_data)
291326
return keyfile_path
292327
end
293328

329+
def store_private_keyfile(ip,user,key_id,key_data)
330+
safe_username = user.gsub(/[^A-Za-z0-9]/,"_")
331+
ktype = key_data.match(/-----BEGIN ([RD]SA) (?:PRIVATE|PUBLIC) KEY-----/)[1].downcase rescue nil
332+
return unless ktype
333+
ltype = "host.unix.ssh.#{user}_#{ktype}_private"
334+
keyfile = existing_loot(ltype, key_id)
335+
return keyfile.path if keyfile
336+
keyfile_path = store_loot(
337+
ltype,
338+
"application/octet-stream", # Text, but always want to mime-type attach it
339+
ip,
340+
(key_data + "\n"),
341+
"#{safe_username}_#{ktype}.private",
342+
key_id
343+
)
344+
return keyfile_path
345+
end
346+
294347
def run_host(ip)
295348
# Since SSH collects keys and tries them all on one authentication session, it doesn't
296349
# make sense to iteratively go through all the keys individually. So, ignore the pass variable,
297350
# and try all available keys for all users.
298351
each_user_pass do |user,pass|
299-
ret, proof = do_login(ip, rport, user)
352+
ret, _ = do_login(ip, rport, user)
300353
case ret
301354
when :connection_error
302355
vprint_error "#{ip}:#{rport} SSH - Could not connect"

0 commit comments

Comments
 (0)