Merge pull request #77 from rapid7/feature/MSP-9709/ssh-pubkey

Refactor ssh_login_pubkey
bug/bundler_fix
dmaloney-r7 2014-06-18 10:53:08 -05:00
commit 8e58d0803f
4 changed files with 189 additions and 228 deletions

View File

@ -42,7 +42,7 @@ module Metasploit
# @param credential [Credential] The credential object to attmpt to login with
# @return [Metasploit::Framework::LoginScanner::Result] The LoginScanner Result object
def attempt_login(credential)
ssh_socket = nil
self.ssh_socket = nil
opt_hash = {
:auth_methods => ['publickey'],
:port => port,
@ -50,7 +50,8 @@ module Metasploit
:key_data => credential.private,
:config => false,
:verbose => verbosity,
:proxies => proxies
:proxies => proxies,
:record_auth_info => true
}
result_options = {
@ -58,7 +59,7 @@ module Metasploit
}
begin
::Timeout.timeout(connection_timeout) do
ssh_socket = Net::SSH.start(
self.ssh_socket = Net::SSH.start(
host,
credential.public,
opt_hash

View File

@ -785,7 +785,6 @@ class Db
core.private ? core.private.data : "",
core.realm ? core.realm.value : "",
core.private ? core.private.class.model_name.human : "",
core.created_at,
]
else
core.logins.each do |login|

View File

@ -53,27 +53,9 @@ class Metasploit3 < Msf::Auxiliary
datastore['RPORT']
end
def session_setup(credential, ssh_socket)
def session_setup(result, ssh_socket)
return unless ssh_socket
proof = ''
begin
Timeout.timeout(5) do
proof = ssh_socket.exec!("id\n").to_s
if(proof =~ /id=/)
proof << ssh_socket.exec!("uname -a\n").to_s
else
# Cisco IOS
if proof =~ /Unknown command or computer name/
proof = ssh_socket.exec!("ver\n").to_s
else
proof << ssh_socket.exec!("help\n?\n\n\n").to_s
end
end
end
rescue ::Exception
end
# Create a new session
conn = Net::SSH::CommandStream.new(ssh_socket, '/bin/sh', true)
@ -81,14 +63,14 @@ class Metasploit3 < Msf::Auxiliary
'USERPASS_FILE' => nil,
'USER_FILE' => nil,
'PASS_FILE' => nil,
'USERNAME' => credential.public,
'PASSWORD' => credential.private
'USERNAME' => result.credential.public,
'PASSWORD' => result.credential.private
}
info = "#{proto_from_fullname} #{credential} (#{@ip}:#{rport})"
info = "#{proto_from_fullname} #{result.credential} (#{@ip}:#{rport})"
s = start_session(self, info, merge_me, false, conn.lsock)
# Set the session platform
case proof
case result.proof
when /Linux/
s.platform = "linux"
when /Darwin/
@ -136,8 +118,6 @@ class Metasploit3 < Msf::Auxiliary
}.merge(service_data)
create_credential_login(login_data)
end
def run_host(ip)
@ -157,7 +137,6 @@ class Metasploit3 < Msf::Auxiliary
scanner = Metasploit::Framework::LoginScanner::SSH.new(
host: ip,
port: rport,
proxies: datastore["PROXIES"],
cred_details: cred_collection,
stop_on_success: datastore['STOP_ON_SUCCESS'],
connection_timeout: datastore['SSH_TIMEOUT'],
@ -168,13 +147,13 @@ class Metasploit3 < Msf::Auxiliary
when :success
print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}' '#{result.proof.to_s.gsub(/[\r\n\e\b\a]/, ' ')}'"
do_report(ip,rport,result)
session_setup(result.credential, scanner.ssh_socket)
session_setup(result, scanner.ssh_socket)
:next_user
when :connection_error
print_brute :level => :verror, :ip => ip, :msg => "Could not connect"
scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed?
:abort
when :fail
when :failed
print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'"
scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed?
else

View File

@ -5,6 +5,7 @@
require 'msf/core'
require 'net/ssh'
require 'metasploit/framework/login_scanner/ssh_key'
class Metasploit3 < Msf::Auxiliary
@ -13,7 +14,7 @@ class Metasploit3 < Msf::Auxiliary
include Msf::Auxiliary::Report
include Msf::Auxiliary::CommandShell
attr_accessor :ssh_socket, :good_credentials, :good_key, :good_key_data
attr_accessor :ssh_socket, :good_key
def initialize
super(
@ -40,7 +41,7 @@ class Metasploit3 < Msf::Auxiliary
register_options(
[
Opt::RPORT(22),
OptPath.new('KEY_FILE', [false, 'Filename of one or several cleartext private keys.'])
OptPath.new('KEY_PATH', [true, 'Filename or directory of cleartext private keys. Filenames beginning with a dot, or ending in ".pub" will be skipped.']),
], self.class
)
@ -48,14 +49,12 @@ class Metasploit3 < Msf::Auxiliary
[
OptBool.new('SSH_DEBUG', [ false, 'Enable SSH debugging output (Extreme verbosity!)', false]),
OptString.new('SSH_KEYFILE_B64', [false, 'Raw data of an unencrypted SSH public key. This should be used by programmatic interfaces to this module only.', '']),
OptPath.new('KEY_DIR', [false, 'Directory of several cleartext private keys. Filenames must not begin with a dot, or end in ".pub" in order to be read.']),
OptInt.new('SSH_TIMEOUT', [ false, 'Specify the maximum time to negotiate a SSH session', 30])
]
)
deregister_options('RHOST','PASSWORD','PASS_FILE','BLANK_PASSWORDS','USER_AS_PASS')
deregister_options('RHOST','PASSWORD','PASS_FILE','BLANK_PASSWORDS','USER_AS_PASS','USERPASS_FILE')
@good_credentials = {}
@good_key = ''
@strip_passwords = true
@ -138,213 +137,196 @@ class Metasploit3 < Msf::Auxiliary
return cleartext_keys
end
def do_login(ip,user,port)
if datastore['KEY_FILE'] and File.readable?(datastore['KEY_FILE'])
keys = read_keyfile(datastore['KEY_FILE'])
cleartext_keys = pull_cleartext_keys(keys)
msg = "#{ip}:#{rport} SSH - Trying #{cleartext_keys.size} cleartext key#{(cleartext_keys.size > 1) ? "s" : ""} per user."
elsif datastore['SSH_KEYFILE_B64'] && !datastore['SSH_KEYFILE_B64'].empty?
keys = read_keyfile(:keyfile_b64)
cleartext_keys = pull_cleartext_keys(keys)
msg = "#{ip}:#{rport} SSH - Trying #{cleartext_keys.size} cleartext key#{(cleartext_keys.size > 1) ? "s" : ""} per user (read from datastore)."
elsif datastore['KEY_DIR']
return :missing_keyfile unless(File.directory?(key_dir) && File.readable?(key_dir))
unless @key_files
@key_files = Dir.entries(key_dir).reject {|f| f =~ /^\x2e/ || f =~ /\x2epub$/}
end
these_keys = @key_files.map {|f| File.join(key_dir,f)}
keys = read_keyfile(these_keys)
cleartext_keys = pull_cleartext_keys(keys)
msg = "#{ip}:#{rport} SSH - Trying #{cleartext_keys.size} cleartext key#{(cleartext_keys.size > 1) ? "s" : ""} per user."
else
return :missing_keyfile
end
unless @alerted_with_msg
print_status msg
@alerted_with_msg = true
end
cleartext_keys.each_with_index do |key_data,key_idx|
opt_hash = {
:auth_methods => ['publickey'],
:msframework => framework,
:msfmodule => self,
:port => port,
:key_data => key_data,
:disable_agent => true,
:config => false,
:record_auth_info => true,
:proxies => datastore['Proxies']
}
opt_hash.merge!(:verbose => :debug) if datastore['SSH_DEBUG']
begin
::Timeout.timeout(datastore['SSH_TIMEOUT']) do
self.ssh_socket = Net::SSH.start(
ip,
user,
opt_hash
)
end
rescue Rex::ConnectionError, Rex::AddressInUse
return :connection_error
rescue Net::SSH::Disconnect, ::EOFError
return :connection_disconnect
rescue ::Timeout::Error
return :connection_disconnect
rescue Net::SSH::AuthenticationFailed
# Try, try, again
if @key_files
vprint_error "#{ip}:#{rport} SSH - Failed authentication, trying key #{@key_files[key_idx+1]}"
else
vprint_error "#{ip}:#{rport} SSH - Failed authentication, trying key #{key_idx+1}"
end
next
rescue Net::SSH::Exception => e
return [:fail,nil] # For whatever reason.
end
break
end
def session_setup(result, ssh_socket)
return unless ssh_socket
if self.ssh_socket
self.good_key = self.ssh_socket.auth_info[:pubkey_id]
self.good_key_data = self.ssh_socket.options[:key_data]
proof = ''
begin
Timeout.timeout(5) do
proof = self.ssh_socket.exec!("id\n").to_s
if(proof =~ /id=/)
proof << self.ssh_socket.exec!("uname -a\n").to_s
else
# Cisco IOS
if proof =~ /Unknown command or computer name/
proof = self.ssh_socket.exec!("ver\n").to_s
else
proof << self.ssh_socket.exec!("help\n?\n\n\n").to_s
end
end
end
rescue ::Exception
end
# Create a new session from the socket
conn = Net::SSH::CommandStream.new(ssh_socket, '/bin/sh', true)
# Create a new session from the socket, then dump it.
conn = Net::SSH::CommandStream.new(self.ssh_socket, '/bin/sh', true)
self.ssh_socket = nil
# Clean up the stored data - need to stash the keyfile into
# a datastore for later reuse.
merge_me = {
'USERPASS_FILE' => nil,
'USER_FILE' => nil,
'PASS_FILE' => nil,
'USERNAME' => user
}
if datastore['KEY_FILE'] and !datastore['KEY_FILE'].empty?
keyfile = File.open(datastore['KEY_FILE'], "rb") {|f| f.read(f.stat.size)}
merge_me.merge!(
'SSH_KEYFILE_B64' => [keyfile].pack("m*").gsub("\n",""),
'KEY_FILE' => nil
)
end
s = start_session(self, "SSH #{user}:#{self.good_key} (#{ip}:#{port})", merge_me, false, conn.lsock)
# Set the session platform
case proof
when /Linux/
s.platform = "linux"
when /Darwin/
s.platform = "osx"
when /SunOS/
s.platform = "solaris"
when /BSD/
s.platform = "bsd"
when /HP-UX/
s.platform = "hpux"
when /AIX/
s.platform = "aix"
when /Win32|Windows/
s.platform = "windows"
when /Unknown command or computer name/
s.platform = "cisco-ios"
end
return [:success, proof]
else
return [:fail, nil]
end
end
def do_report(ip, port, user, proof)
return unless framework.db.active
keyfile_path = store_keyfile(ip,user,self.good_key,self.good_key_data)
cred_hash = {
:host => ip,
:port => datastore['RPORT'],
:sname => 'ssh',
:user => user,
:pass => keyfile_path,
:type => "ssh_key",
:proof => "KEY=#{self.good_key}, PROOF=#{proof}",
:duplicate_ok => true,
:active => true
# Clean up the stored data - need to stash the keyfile into
# a datastore for later reuse.
merge_me = {
'USERPASS_FILE' => nil,
'USER_FILE' => nil,
'PASS_FILE' => nil,
'USERNAME' => result.credential.public,
'SSH_KEYFILE_B64' => [result.credential.private].pack("m*").gsub("\n",""),
'KEY_PATH' => nil
}
this_cred = report_auth_info(cred_hash)
end
def existing_loot(ltype, key_id)
framework.db.loots(myworkspace).find_all_by_ltype(ltype).select {|l| l.info == key_id}.first
end
info = "SSH #{result.credential.public}:#{ssh_socket.auth_info[:pubkey_id]} (#{ip}:#{rport})"
s = start_session(self, info, merge_me, false, conn.lsock)
def store_keyfile(ip,user,key_id,key_data)
safe_username = user.gsub(/[^A-Za-z0-9]/,"_")
case key_data
when /BEGIN RSA PRIVATE/m
ktype = "rsa"
when /BEGIN DSA PRIVATE/m
ktype = "dsa"
else
ktype = nil
# Set the session platform
case result.proof
when /Linux/
s.platform = "linux"
when /Darwin/
s.platform = "osx"
when /SunOS/
s.platform = "solaris"
when /BSD/
s.platform = "bsd"
when /HP-UX/
s.platform = "hpux"
when /AIX/
s.platform = "aix"
when /Win32|Windows/
s.platform = "windows"
when /Unknown command or computer name/
s.platform = "cisco-ios"
end
return unless ktype
ltype = "host.unix.ssh.#{user}_#{ktype}_private"
keyfile = existing_loot(ltype, key_id)
return keyfile.path if keyfile
keyfile_path = store_loot(
ltype,
"application/octet-stream", # Text, but always want to mime-type attach it
ip,
(key_data + "\n"),
"#{safe_username}_#{ktype}.key",
key_id
)
return keyfile_path
s
end
def do_report(ip, port, result)
service_data = {
address: ip,
port: port,
service_name: 'ssh',
protocol: 'tcp',
workspace_id: myworkspace_id
}
credentail_data = {
module_fullname: self.fullname,
origin_type: :service,
private_data: result.credential.private,
private_type: :ssh_key,
username: result.credential.public,
}.merge(service_data)
credential_core = create_credential(credentail_data)
login_data = {
core: credential_core,
last_attempted_at: DateTime.now,
status: Metasploit::Credential::Login::Status::SUCCESSFUL,
}.merge(service_data)
create_credential_login(login_data)
end
def run_host(ip)
print_status("#{ip}:#{rport} SSH - Testing Cleartext Keys")
# Since SSH collects keys and tries them all on one authentication session, it doesn't
# make sense to iteratively go through all the keys individually. So, ignore the pass variable,
# and try all available keys for all users.
each_user_pass do |user,pass|
ret,proof = do_login(ip,user,rport)
case ret
if datastore["USER_FILE"].blank? && datastore["USERNAME"].blank?
# Ghetto abuse of the way OptionValidateError expects an array of
# option names instead of a string message like every sane
# subclass of Exception.
raise OptionValidateError, ["At least one of USER_FILE or USERNAME must be given"]
end
keys = KeyCollection.new(
key_path: datastore['KEY_PATH'],
user_file: datastore['USER_FILE'],
username: datastore['USERNAME'],
)
print_brute :level => :vstatus, :ip => ip, :msg => "Testing #{keys.key_data.count} keys"
scanner = Metasploit::Framework::LoginScanner::SSHKey.new(
host: ip,
port: rport,
cred_details: keys,
stop_on_success: datastore['STOP_ON_SUCCESS'],
connection_timeout: datastore['SSH_TIMEOUT'],
)
scanner.scan! do |result|
case result.status
when :success
print_brute :level => :good, :msg => "Success: '#{user}':'#{self.good_key}' '#{proof.to_s.gsub(/[\r\n\e\b\a]/, ' ')}'"
do_report(ip, rport, user, proof)
print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential.public}' '#{result.proof.to_s.gsub(/[\r\n\e\b\a]/, ' ')}'"
do_report(ip,rport,result)
session_setup(result, scanner.ssh_socket)
:next_user
when :connection_error
vprint_error "#{ip}:#{rport} SSH - Could not connect"
print_brute :level => :verror, :ip => ip, :msg => "Could not connect"
scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed?
:abort
when :connection_disconnect
vprint_error "#{ip}:#{rport} SSH - Connection timed out"
:abort
when :fail
vprint_error "#{ip}:#{rport} SSH - Failed: '#{user}'"
when :missing_keyfile
vprint_error "#{ip}:#{rport} SSH - Cannot read keyfile."
when :no_valid_keys
vprint_error "#{ip}:#{rport} SSH - No cleartext keys in keyfile."
when :failed
print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'"
scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed?
else
scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed?
end
end
end
class KeyCollection
attr_accessor :key_data
def initialize(opts={})
@username = opts[:username]
@user_file = opts[:user_file]
@key_path = opts.fetch(:key_path)
valid!
end
def realm
nil
end
def valid!
@key_data = Set.new
if File.directory?(@key_path)
@key_files ||= Dir.entries(@key_path).reject { |f| f =~ /^\x2e|\x2epub$/ }
@key_files.each do |f|
data = read_key(File.join(@key_path, f))
@key_data << data if valid_key?(data)
end
elsif File.file?(@key_path)
data = read_key(@key_path)
@key_data << data if valid_key?(data)
else
raise RuntimeError, "No key path"
end
end
def valid_key?(key_data)
!!(key_data.match(/BEGIN [RD]SA PRIVATE KEY/) && !key_data.match(/Proc-Type:.*ENCRYPTED/))
end
def each
if @user_file.present?
File.open(@user_file, 'rb') do |user_fd|
user_fd.each_line do |user_from_file|
user_from_file.chomp!
each_key do |key_data|
yield Metasploit::Framework::Credential.new(public: user_from_file, private: key_data, realm: realm)
end
end
end
end
if @username.present?
each_key do |key_data|
yield Metasploit::Framework::Credential.new(public: @username, private: key_data, realm: realm)
end
end
end
def each_key
@key_data.each do |data|
yield data
end
end
def read_key(filename)
@cache ||= {}
unless @cache[filename]
data = File.open(filename, 'rb') { |fd| fd.read(fd.stat.size) }
#if data.match
@cache[filename] = data
end
@cache[filename]
end
end
end