#!/usr/bin/env python3 """ CVE-2026-48909 — SP LMS (com_splms) PHP Object Injection Detection Affected : JoomShaper SP LMS <= 4.1.3 Fixed : JoomShaper SP LMS >= 4.1.4 Author : Amin İsayev / Proxima Cyber Security Detects whether the lmsOrders cookie is passed to unserialize() without validation. This script only identifies the vulnerability — it does NOT exploit it and does NOT execute any code on the target. Usage: python3 CVE-2026-48909.py https://target.com """ import sys import time import base64 import requests import urllib3 urllib3.disable_warnings() # com_splms model: $cartItems = unserialize(base64_decode($cookie)) # $cartItems = (!is_array($cartItems) && !$cartItems) ? [] : $cartItems; # if(count($cartItems)) { ... } # # PROBE : base64(serialize(stdClass{})) # Vulnerable : unserialize() → stdClass → count(stdClass) → PHP 8 TypeError → HTTP 500 # Patched : cookie rejected/sanitized → empty array → count([])=0 → HTTP 200 # # BENIGN : base64("not_php_serialized_data") # Any server: unserialize() → false → cartItems=[] → HTTP 200 # # Signal: probe=500 AND benign=200 → VULNERABLE PROBE_SERIALIZED = base64.b64encode(b'O:8:"stdClass":0:{}').decode() PROBE_BENIGN = base64.b64encode(b'not_php_serialized_data').decode() CART_PATH = "/index.php?option=com_splms&view=cart" TIMEOUT = 10 def detect(base_url: str) -> tuple[bool, str]: url = base_url.rstrip("/") + CART_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", }) try: # Step 0: baseline — no cookie at all, check if endpoint is healthy r_base = session.get(url, timeout=TIMEOUT) if r_base.status_code == 404: return False, f"SP LMS cart endpoint not found (HTTP 404) — com_splms may not be installed" if r_base.status_code >= 500: return False, ( f"Endpoint already returns HTTP {r_base.status_code} without any cookie " f"— server is broken/misconfigured, cannot assess vulnerability" ) if r_base.status_code not in (200, 301, 302, 403): return False, f"Unexpected HTTP {r_base.status_code} without cookie — skipping" # Step 1: benign cookie — base64 of random non-PHP string r_benign = session.get(url, cookies={"lmsOrders": PROBE_BENIGN}, timeout=TIMEOUT) # Step 2: serialized stdClass probe t0 = time.time() r_probe = session.get(url, cookies={"lmsOrders": PROBE_SERIALIZED}, timeout=TIMEOUT) elapsed = time.time() - t0 except requests.RequestException as e: return False, f"Connection error: {e}" # Signal 1 (primary): stdClass → count() TypeError → HTTP 500 (PHP 8) # Requires benign=200 AND no-cookie=200 to avoid false positives from Mod_Security/WAF if r_probe.status_code == 500 and r_benign.status_code == 200: return True, ( f"HTTP 500 on probe vs 200 on benign " f"— unserialize() called on cookie (count(stdClass) TypeError)" ) # Signal 2: PHP error text in probe response php_indicators = ["PHP Warning", "PHP Fatal error", "TypeError", "unserialize()"] for ind in php_indicators: if ind in r_probe.text and ind not in r_benign.text: return True, f"PHP error in probe response: '{ind}'" # Signal 3: 403 vs 200 if r_probe.status_code == 403 and r_benign.status_code == 200: return True, f"HTTP 403 on probe vs 200 on benign — deserialization side-effect" # Signal 4: response body differs significantly between benign and probe len_diff = abs(len(r_probe.text) - len(r_benign.text)) if len_diff > 500 and r_probe.status_code == r_benign.status_code: return True, ( f"Response body differs {len_diff} bytes (probe vs benign) " f"— unserialize() processing the cookie" ) # Signal 5: slow response on probe only if elapsed > 5.0 and r_benign.status_code == 200: return True, f"Abnormal response time {elapsed:.2f}s on probe — deserialization delay" return False, ( f"No vulnerability indicators " f"(base={r_base.status_code}, benign={r_benign.status_code}, probe={r_probe.status_code})" ) def main(): if len(sys.argv) < 2: print(f"Usage: {sys.argv[0]} ") print(f" Example: {sys.argv[0]} https://example.com") sys.exit(1) target = sys.argv[1] print(f"[*] Target : {target}") print(f"[*] Path : {CART_PATH}") print(f"[*] Probe : lmsOrders={PROBE_SERIALIZED}") print() vulnerable, reason = detect(target) if vulnerable: print(f"[VULNERABLE] {reason}") print("[!] Update to SP LMS >= 4.1.4 immediately.") else: print(f"[NOT VULNERABLE / INCONCLUSIVE] {reason}") sys.exit(0 if not vulnerable else 1) if __name__ == "__main__": main()