Someone left the mailroom door open, but the vault behind it only opens if you happen to find the right key already lying on a desk inside
CVE-2026-104736 is an unauthenticated PHP Object Injection in the LearnPress WordPress LMS plugin (ThimPress), affecting all versions through 4.4.10. The flaw sits in the checkout AJAX action: user input from the checkout-email request parameter passes through sanitize_text_field() — which does *not* strip serialized PHP object syntax — gets stored in the LearnPress session table, and is later fed to LP_Helper::maybe_unserialize() → native unserialize() without an allowed_classes restriction by LP_Session_Handler::get() on subsequent requests. An unauthenticated remote attacker can instantiate arbitrary PHP objects and trigger their magic methods (__wakeup, __destruct, __toString). The plugin has roughly 70,000+ active installations according to wordpress.org, predominantly education and online-course sites. Version 4.4.11, released October 16, 2026, fixes the issue.
The vendor score of CRITICAL / 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) treats this as if unauthenticated RCE is a single HTTP request away. That overstates reality. PHP Object Injection is a *primitive*, not an exploit — it grants object instantiation, but achieving RCE requires a suitable POP (Property-Oriented Programming) gadget chain already loaded on the target. The most famous WordPress core chain (WP_HTML_Token, PHPGGC WordPress/RCE/2) was patched in WordPress 6.4.2 in December 2023; we are now on WordPress 7.1.3, so the vast majority of maintained installs lack that chain. RCE depends entirely on whether the target site's specific plugin/theme stack supplies an alternative chain — a condition the CVSS vector's AC:L completely ignores. The correct attack complexity is AC:H, which alone drops the CVSS to approximately 8.1 before any further contextual adjustments. Without a chain, the realistic ceiling is DoS, limited file operations, or information disclosure via magic-method side effects — serious, but not 9.8-level.
5 steps from start to impact.
Reach the checkout AJAX endpoint
/wp-admin/admin-ajax.php?action=learnpress_checkout with the checkout-email parameter containing a serialized PHP object payload (e.g., O:21:"Some_Gadget_Class":1:{...}). No login, no nonce, no CAPTCHA is required. The endpoint is exposed on every LearnPress install with checkout enabled.- LearnPress ≤ 4.4.10 installed and active
- WordPress site reachable over HTTP/HTTPS
- Checkout AJAX action registered (default)
- None — endpoint is unauthenticated and always registered when the plugin is active
O:[0-9]+:") in POST bodies. Wordfence, Sucuri, and Cloudflare managed rulesets typically include PHP deserialization signatures.Payload survives sanitize_text_field() and is stored
sanitize_text_field() strips HTML tags and extra whitespace but does not strip PHP serialized object syntax. The payload is stored intact into the wp_learnpress_sessions table tied to the visitor's session cookie. This is a blind write — the attacker gets no direct feedback about storage success, but the operation is deterministic.- Step 1 succeeded
- Database is writable (standard operation)
- Some hardened WAFs inspect POST body content and block serialized object strings before they reach PHP
- ModSecurity CRS rule 942150 (PHP injection) may catch the pattern
session_value column.Stored payload is deserialized on next request
LP_Session_Handler::get() reads the session data and calls LP_Helper::maybe_unserialize(), which internally calls PHP's native unserialize() without an allowed_classes restriction. This instantiates the attacker-controlled object and triggers magic methods (__wakeup, __destruct).- Step 2 succeeded
- A second HTTP request arrives with the same session cookie
- None — this is automatic on the next page load
unserialize() calls can flag untrusted input.Gadget chain pivots to RCE (conditional)
call_user_func(), system(), file_put_contents()). The WordPress core WP_HTML_Token chain (PHPGGC WordPress/RCE/2) is only available on WP 6.4.0–6.4.1 and has been patched since December 2023. On modern WordPress 7.x, the attacker must find an alternative chain. PHPGGC and custom research are the primary toolsets for chain discovery.- Step 3 succeeded
- A usable POP gadget chain exists in the site's loaded classes
- The chain reaches a dangerous sink (code execution, file write, etc.)
- WordPress core chain patched since WP 6.4.2 — nearly 3 years ago
- Third-party chains are site-specific and unpredictable
- Many popular plugins have been audited and had chains removed
- Attacker may need to fingerprint the target's plugin stack first
.php files in webroot.Post-exploitation: site takeover
wp-config.php database credentials, dump user tables (student PII, payment info if stored), or pivot to the underlying server if PHP runs with elevated privileges. The blast radius is the individual WordPress installation and its database — not fleet-scale unless the host runs multiple WordPress sites on the same user/database server.- Step 4 succeeded (RCE achieved)
- Containerized or chrooted WordPress installs limit lateral movement
- Managed WordPress hosts (WP Engine, Kinsta, Pantheon) sandbox each site
- Database credentials may be scoped to a single database
admin-ajax.php where the body contains PHP serialized object patterns (O:[0-9]+:", a:[0-9]+:{). ModSecurity CRS rule 942150 covers PHP injection signatures. Wordfence premium likely has virtual patching for this CVE already. This blocks the injection at step 1, preventing payload storage entirely.remove_action('wp_ajax_nopriv_learnpress_checkout', ...) in a must-use plugin, or deactivate LearnPress entirely if the site is in maintenance. This eliminates the attack surface completely./wp-admin/admin-ajax.php at the reverse proxy or web server level. While this doesn't block exploitation, it slows automated scanning and buys time. Combined with WAF rules, this provides defense-in-depth.- Upgrading WordPress core alone does not help. The vulnerability is in the LearnPress plugin code (
LP_Session_Handler::get()andLP_Helper::maybe_unserialize()), not in WordPress core. Updating WordPress to 7.1.3 patches the *core gadget chain* but does NOT fix the injection primitive. - sanitize_text_field() is not a mitigation. The code already calls it — the problem is that this function was never designed to strip serialized PHP object syntax. It handles HTML entities and whitespace, not deserialization attacks.
- Disabling XML-RPC or REST API is irrelevant. The vulnerable endpoint is the WordPress AJAX handler (
admin-ajax.php), not XML-RPC or REST. These are separate attack surfaces.
The supporting signals.
| In-the-wild exploitation | Not confirmed. Not listed in CISA KEV. No GreyNoise or Shadowserver tags observed as of 2026-10-18. Disclosed only 1 day ago (Oct 17, 2026), so mass scanning has not yet materialized but should be expected within days given PoC availability. |
|---|---|
| Proof-of-concept | Available — partial. BIBILZERODAY published an advisory-level PoC on Sploitus (2026-10-16) with full exploit scripts gated behind a Telegram channel (@LatestExploits). SecureWithUmer/CVE-2026-PoCs on GitHub aggregates references. The injection primitive is trivially reproducible; weaponized RCE depends on gadget chain availability per target. |
| EPSS | 0.00000 (floor value). Expected given the CVE was published yesterday. This will rise as scanners begin probing. Historical EPSS for similar unauth WordPress object injection CVEs typically reaches 0.15–0.40 within 30 days. |
| CISA KEV status | Not listed as of 2026-10-18. |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H — vendor-assigned. noisgate assessment: AC should be H (gadget chain dependency), which would yield ~8.1 before contextual adjustments. The vector's AV:N/PR:N/UI:N portion is accurate — the injection is trivially reachable. |
| Affected versions | LearnPress ≤ 4.4.10 (all versions). The plugin has existed since 2015 with a long history of PHP Object Injection issues (prior CVEs in the same class). |
| Fixed version | 4.4.11 — released October 16, 2026 via WordPress.org plugin repository. Auto-update eligible. |
| Installed base & exposure | 70,000+ active installations per wordpress.org. These are predominantly internet-facing educational/LMS sites. Every install with the checkout action enabled (default) exposes the vulnerable endpoint. |
| Disclosure timeline | Identified 2026-10-02 → Fix released 2026-10-16 → Public disclosure 2026-10-17. Coordinated through Wordfence PRISM (automated vulnerability finder). |
| Reporting researcher / org | Wordfence PRISM finder (automated discovery). No individual researcher credited. |
Sources.
- NVD — CVE-2026-104736
- VulDB — LearnPress Checkout AJAX Deserialization
- Wordfence Threat Intel — LearnPress Vulnerabilities
- BIBILZERODAY PoC on Sploitus
- WordPress.org — LearnPress Plugin Page
- Patchstack — PHP Object Injection in WordPress
- Wordfence — WP_HTML_Token POP Chain Advisory (WP 6.4.2)
- Ostorlab — WP_HTML_Token POP Chain Technical Analysis
Why this verdict
- Gadget chain dependency is the critical friction the vendor ignored. PHP Object Injection is a *primitive*, not a complete exploit. Achieving RCE — the impact the 9.8 score assumes — requires a POP gadget chain loaded in the target's PHP autoloader. The only known WordPress core chain (
WP_HTML_Token) has been patched since WordPress 6.4.2 (December 2023). On WordPress 7.1.3 (current), no core chain exists. Alternative chains from third-party plugins are site-specific and unpredictable. This makes the true attack complexity AC:H, not AC:L, which alone drops the base CVSS to ~8.1. - No confirmed exploitation and floor-level EPSS. The CVE was disclosed yesterday with EPSS at 0.00000 and no CISA KEV listing. While PoC material exists (BIBILZERODAY advisory), the weaponized exploit requires per-target chain identification. Mass exploitation is unlikely without a universally available gadget chain — a fundamentally different risk profile than a true unauth RCE.
- Install base is modest and non-critical infrastructure. 70,000 LearnPress sites are predominantly education portals and small course marketplaces, not enterprise backbone systems. Compromise yields site-level access (student PII, course data, potentially payment info), not domain/fleet/supply-chain impact.
- Role multiplier: LearnPress runs on WordPress web servers. (a) *Low-value role:* dev/staging WordPress instances — chain succeeds identically, blast radius is trivial. (b) *Typical role:* production LMS site serving courses — chain succeeds if gadget available, blast radius is single-site takeover + student PII exfiltration. (c) *High-value role:* WordPress is NOT canonically a high-value-role component (not an IdP, DC, hypervisor, CI/CD, backup, CA, or network edge). A WordPress LMS site does not meet the ≥10% high-value-role threshold. Even in the worst case (large university LMS with regulated student data), the blast radius is tenant-level, not domain/fleet/supply-chain. No floor override triggered.
- PoC availability and internet-facing surface keep this solidly HIGH. Despite the friction, the injection itself is trivially weaponizable, the endpoint is unauthenticated, and every LearnPress install is internet-facing by nature. Scanner adoption is imminent. The 70K install base *will* be probed. Even without RCE, object injection can achieve DoS or data destruction through magic method side effects.
Why not higher?
A CRITICAL rating would require either confirmed in-the-wild exploitation or a reliable, universally-available RCE chain. Neither exists. The gadget chain dependency is a genuine, material barrier — not a theoretical one. The WordPress core chain has been patched for nearly three years, and no replacement universal chain has been published. The install base is modest (70K) and confined to non-critical web application tiers, not identity, infrastructure, or supply-chain roles. The EPSS and KEV data corroborate that this is not yet an active threat.
Why not lower?
MEDIUM would understate the risk. The injection primitive is unauthenticated, remotely reachable with zero user interaction, and the endpoint is enabled by default on every LearnPress install. PoC material is already circulating. Historical precedent shows WordPress PHP Object Injection vulns attract rapid scanner adoption — the WooCommerce and Contact Form Entries chains were weaponized within 2 weeks of disclosure. Even without a universal chain, targeted attackers can fingerprint plugin stacks and find site-specific chains. The 70K internet-facing install base guarantees broad exposure.
Crowdsourced verification payload.
Run this script on the target WordPress host (or any machine with read access to the WordPress installation directory) as a user with read permissions on the plugin files. Example: bash check_cve_2026_104736.sh /var/www/html where the argument is the WordPress root directory. No elevated privileges required — just filesystem read access.
#!/usr/bin/env bash
# check_cve_2026_104736.sh — Detect CVE-2026-104736 in LearnPress
# Usage: bash check_cve_2026_104736.sh /path/to/wordpress
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -euo pipefail
WP_ROOT="${1:?Usage: $0 /path/to/wordpress}"
PLUGIN_FILE="${WP_ROOT}/wp-content/plugins/learnpress/learnpress.php"
if [[ ! -f "$PLUGIN_FILE" ]]; then
echo "UNKNOWN — LearnPress plugin not found at ${PLUGIN_FILE}"
exit 2
fi
# Extract version from plugin header
VERSION=$(grep -i '^[[:space:]]*\*[[:space:]]*Version:' "$PLUGIN_FILE" | head -1 | sed 's/.*Version:[[:space:]]*//' | tr -d '[:space:]')
if [[ -z "$VERSION" ]]; then
echo "UNKNOWN — Could not parse LearnPress version from ${PLUGIN_FILE}"
exit 2
fi
echo "Detected LearnPress version: ${VERSION}"
# Compare versions using sort -V
FIXED="4.4.11"
LOWEST=$(printf '%s\n%s' "$VERSION" "$FIXED" | sort -V | head -1)
if [[ "$VERSION" == "$FIXED" || "$LOWEST" == "$FIXED" ]]; then
# VERSION >= FIXED
echo "PATCHED — LearnPress ${VERSION} is >= ${FIXED}"
exit 0
else
echo "VULNERABLE — LearnPress ${VERSION} is below ${FIXED} (CVE-2026-104736)"
# Secondary check: look for the vulnerable unserialize call
SESSION_FILE="${WP_ROOT}/wp-content/plugins/learnpress/inc/class-lp-session-handler.php"
if [[ -f "$SESSION_FILE" ]]; then
if grep -q 'maybe_unserialize' "$SESSION_FILE" 2>/dev/null; then
echo " CONFIRMED: LP_Session_Handler still uses maybe_unserialize() without allowed_classes."
fi
fi
exit 1
fi