README.md
Rendering markdown...
#!/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]} <target_url>")
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()