Back to Intelligence

CVE-2026-104736: Critical LearnPress WordPress PHP Object Injection (CVSS 9.8) — Detection, Hunting, and Remediation Guide

SA
Security Arsenal Team
October 17, 2026
10 min read

NVD has published CVE-2026-104736, a CVSS 9.8 (CRITICAL), network-exploitable vulnerability in the LearnPress plugin for WordPress, one of the most widely deployed learning management system (LMS) plugins in the ecosystem. All versions up to and including 4.4.10 are vulnerable.

The flaw is a PHP Object Injection reachable by unauthenticated attackers through the checkout AJAX workflow. The checkout-email request parameter is passed through sanitize_text_field() — a WordPress sanitization function that does not strip PHP serialized object syntax — is persisted into the LearnPress session table, and is later fed into unserialize() without an allowed_classes restriction via LP_Helper::maybe_unserialize() during LP_Session_Handler::get() on subsequent requests.

If you operate WordPress sites running LearnPress — for corporate training portals, customer education platforms, or higher-ed deployments — treat this as an urgent remediation item. Unauthenticated, network-reachable deserialization bugs are exactly the class of vulnerability that gets weaponized into automated exploitation within days of disclosure.


Technical Analysis

Affected Component and Versions

ItemDetail
CVECVE-2026-104736
CVSS9.8 (Critical) — network vector, unauthenticated
ProductLearnPress plugin for WordPress
Affected versions≤ 4.4.10
Vulnerability classCWE-502 — Deserialization of Untrusted Data (PHP Object Injection)
Authentication requiredNone

Attack Chain — From a Defender's Perspective

Understanding the two-stage nature of this bug is essential, because it changes where your detection opportunities live:

  1. Stage 1 — Injection (write): An attacker submits a crafted checkout-email value during the LearnPress checkout AJAX action. The input passes through sanitize_text_field(). Critically, that function sanitizes HTML and strips octets, but it does not reject or neutralize PHP serialized object syntax such as O:8:"ClassName":.... The attacker-controlled serialized payload is written to the LearnPress session table in the WordPress database.

  2. Stage 2 — Deserialization (trigger): On a subsequent request, LP_Session_Handler::get() retrieves the stored session value and passes it to LP_Helper::maybe_unserialize(), which calls unserialize() with no allowed_classes restriction. At this point, PHP instantiates whatever class the attacker serialized. If a suitable POP (Property-Oriented Programming) gadget chain exists anywhere in the codebase — WordPress core, LearnPress itself, or any co-installed plugin or theme — magic methods such as __wakeup(), __destruct(), or __toString() fire, leading to arbitrary code execution, file writes, or data exfiltration.

Why This Matters Even Without a Known Gadget Chain

PHP Object Injection severity is often initially downplayed with "no known POP chain in the component itself." That reasoning is dangerous in WordPress environments specifically:

  • The average WordPress site runs dozens of plugins and themes. Any class with exploitable magic methods loaded at deserialization time becomes a candidate gadget — and the attacker gets to fingerprint your plugin inventory trivially.
  • Publicly documented gadget chains exist in numerous popular WordPress plugins and libraries (e.g., anything bundling PHPMailer, Guzzle, or legacy PSR-compatible autoloaders), and security tooling like PHPGGC has commoditized chain generation.
  • The attack surface is unauthenticated and reachable over the network — the worst combination for a CMS plugin.

Exploitation Status

At the time of writing, CVE-2026-104736 is newly published through NVD with no confirmed in-the-wild exploitation or CISA KEV listing. However, WordPress plugin vulnerabilities with a 9.8 score, unauthenticated reachability, and a clearly documented injection path historically attract mass scanning and automated exploitation quickly. Do not wait for a KEV listing to act — verify your LearnPress version today.


Detection & Response

The two-stage design gives defenders two distinct detection planes: the HTTP request carrying serialized payload syntax in the checkout-email parameter, and downstream behavioral anomalies from the web server process if deserialization is triggered with a working gadget.

Sigma Rules

The first rule targets web server access logs (ingested via a Sigma-compatible pipeline that maps to the webserver category) for serialized PHP object syntax in checkout-related requests. The second targets the blast radius: web server worker processes spawning shells or script interpreters — the classic post-exploitation signature of PHP code execution.

YAML
---
title: PHP Serialized Object Syntax in WordPress Checkout Parameter
description: Detects HTTP requests to WordPress AJAX or checkout endpoints containing PHP serialized object/array syntax consistent with CVE-2026-104736 exploitation attempts against the LearnPress checkout-email parameter.
id: 3f9c2b71-8a4e-4d5c-b2a1-6e7f8d9c0a12
status: experimental
references:
  - https://nvd.nist.gov/vuln/detail/CVE-2026-104736
  - https://cwe.mitre.org/data/definitions/502.html
author: Security Arsenal
date: 2026/04/10
tags:
  - attack.initial_access
  - attack.t1190
logsource:
  category: webserver
detection:
  selection_endpoint:
    c-uri|contains:
      - 'admin-ajax.php'
      - 'lp-ajax'
      - 'checkout'
  selection_serialized:
    cs-body|contains:
      - 'O:%22:'
      - 'O:+'
      - 'checkout-email=O'
      - 'checkout-email%5D=O'
      - 'a:%7B'
      - 'checkout-email=a%3A'
  condition: selection_endpoint and selection_serialized
falsepositives:
  - Legitimate applications passing serialized data via POST bodies are rare; review any hits against known custom integrations
level: high
---
title: Web Server Worker Spawning Shell or Script Interpreter
description: Detects php-fpm, Apache, or nginx worker processes spawning shells, downloaders, or script interpreters — a post-exploitation indicator consistent with PHP Object Injection achieving code execution, such as via CVE-2026-104736.
id: 8b1d4e52-3c6f-4a9b-d7e2-1f5a9c3e7b04
status: experimental
references:
  - https://nvd.nist.gov/vuln/detail/CVE-2026-104736
  - https://attack.mitre.org/techniques/T1059/
author: Security Arsenal
date: 2026/04/10
tags:
  - attack.execution
  - attack.t1059
logsource:
  category: process_creation
  product: linux
detection:
  selection_parent:
    ParentImage|endswith:
      - '/php-fpm'
      - '/php-fpm8.1'
      - '/php-fpm8.2'
      - '/php-fpm8.3'
      - '/apache2'
      - '/httpd'
      - '/nginx'
  selection_child:
    Image|endswith:
      - '/sh'
      - '/bash'
      - '/dash'
      - '/curl'
      - '/wget'
      - '/python'
      - '/python3'
      - '/perl'
      - '/nc'
      - '/ncat'
      - '/base64'
  condition: selection_parent and selection_child
falsepositives:
  - Backup or cron plugins invoking system commands — validate against scheduled maintenance windows
level: critical

KQL — Microsoft Sentinel Hunting

This query assumes web server logs are ingested into Sentinel via Syslog/CEF or Azure diagnostics. It hunts for serialized PHP syntax in request URIs and POST data hitting AJAX/checkout endpoints, plus correlates web-host process execution for post-exploitation behavior.

KQL — Microsoft Sentinel / Defender
// Hunt for PHP serialized object injection attempts against LearnPress checkout (CVE-2026-104736)
let serialized_patterns = dynamic(["O:%22", "O:+", "checkout-email=O", "checkout-email=a%3A", "%22__PHP_Incomplete_Class", "allowed_classes"]);
let window_start = ago(14d);
union isfuzzy=true
    (CommonSecurityLog
    | where TimeGenerated > window_start
    | where RequestURL has_any ("admin-ajax.php", "lp-ajax", "checkout") or AdditionalExtensions has "checkout-email"
    | extend Payload = coalesce(RequestURL, AdditionalExtensions, "")
    | where Payload has_any (serialized_patterns)
    | project TimeGenerated, SourceIP, DestinationHostName, RequestURL, RequestMethod, AdditionalExtensions, DeviceVendor),
    (Syslog
    | where TimeGenerated > window_start
    | where SyslogMessage has_any ("admin-ajax.php", "lp-ajax") and SyslogMessage has_any (serialized_patterns)
    | project TimeGenerated, HostIP, HostName, SyslogMessage, SeverityLevel)
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), AttemptCount=count() by SourceIP, DestinationHostName
| order by AttemptCount desc;

// Correlate: web server worker spawning suspicious child processes (post-deserialization code execution)
DeviceProcessEvents
| where TimeGenerated > ago(7d)
| where InitiatingProcessFileName has_any ("php-fpm", "apache2", "httpd", "nginx", "php-cgi")
| where FileName in~ ("sh", "bash", "dash", "curl", "wget", "nc", "ncat", "python", "python3", "perl")
| project TimeGenerated, DeviceName, InitiatingProcessFileName, FileName, ProcessCommandLine, AccountName, InitiatingProcessCommandLine
| order by TimeGenerated desc;

Velociraptor VQL — Fleet-Wide LearnPress Version Triage

The fastest way to scope exposure across a fleet of web hosts is to enumerate installed LearnPress versions. WordPress plugins declare their version in the main plugin file header and readme.txt. This artifact globs common document roots, extracts the version string, and flags anything at or below 4.4.10.

VQL — Velociraptor
-- Artifact: Custom.Hunt.LearnPressVersionTriage
-- Scope: identify LearnPress installations vulnerable to CVE-2026-104736 (<= 4.4.10)

LET plugin_files = SELECT FullPath AS PluginFile,
       read_file(filename=FullPath, length=4096) AS Header
FROM glob(globs=[
    '/var/www/*/wp-content/plugins/learnpress/learnpress.php',
    '/var/www/html/**/wp-content/plugins/learnpress/learnpress.php',
    '/srv/www/**/wp-content/plugins/learnpress/learnpress.php',
    '/home/*/public_html/wp-content/plugins/learnpress/learnpress.php',
    'C:/inetpub/**/wp-content/plugins/learnpress/learnpress.php'
])

LET parsed = SELECT PluginFile,
       parse_string_with_regex(string=Header,
           regex='Version:\\s*([0-9]+\\.[0-9]+\\.[0-9]+)') AS VersionMatch
FROM plugin_files

SELECT PluginFile,
       VersionMatch[1] AS InstalledVersion,
       if(condition=VersionMatch[1] =~ '^(4\\.[0-3]\\.|4\\.4\\.([0-9]|10)$|[0-3]\\.)',
          then='VULNERABLE - CVE-2026-104736',
          else='Review - verify patch level') AS RiskStatus,
       timestamp(epoch=now()) AS ScanTime
FROM parsed
WHERE VersionMatch

Remediation and Verification Script

Use this Bash script on Linux web hosts to locate every LearnPress installation, report its version, and flag vulnerable instances. It is read-only by design — pair it with your deployment pipeline to push the patched plugin version.

Bash / Shell
#!/usr/bin/env bash
# CVE-2026-104736 — LearnPress version triage across common WordPress document roots
# Flags any LearnPress install <= 4.4.10 as vulnerable

set -euo pipefail

DOCROOTS=("/var/www" "/srv/www" "/home" "/opt/wordpress")
VULN=0

version_lte_4410() {
    # Returns 0 (true) if $1 <= 4.4.10
    [ "$(printf '%s\n4.4.10\n' "$1" | sort -V | head -n1)" = "$1" ] && [ "$1" != "4.4.11" ]
}

echo "[*] Scanning for LearnPress installations..."
for root in "${DOCROOTS[@]}"; do
    [ -d "$root" ] || continue
    while IFS= read -r pluginfile; do
        ver=$(grep -m1 -E '^[[:space:]]*\*?[[:space:]]*Version:' "$pluginfile" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
        [ -z "${ver:-}" ] && ver="UNKNOWN"
        site=$(echo "$pluginfile" | awk -F'/wp-content/' '{print $1}')
        if [ "$ver" != "UNKNOWN" ] && version_lte_4410 "$ver"; then
            echo "[VULNERABLE] $site — LearnPress $ver (CVE-2026-104736)"
            VULN=$((VULN+1))
        else
            echo "[OK] $site — LearnPress $ver"
        fi
    done < <(find "$root" -type f -path '*/wp-content/plugins/learnpress/learnpress.php' 2>/dev/null)
done

# Check for evidence of serialized payload injection in access logs (last 7 days)
echo ""
echo "[*] Checking access logs for serialized object injection patterns in checkout-email..."
LOGDIRS=("/var/log/nginx" "/var/log/apache2" "/var/log/httpd")
for ld in "${LOGDIRS[@]}"; do
    [ -d "$ld" ] || continue
    hits=$(zgrep -hE 'checkout-email=([0-9]*O|a%3A|O%3A|O:|a:)' "$ld"/*.log* 2>/dev/null | wc -l || true)
    if [ "${hits:-0}" -gt 0 ]; then
        echo "[ALERT] $hits suspicious request(s) found in $ld — investigate immediately"
        zgrep -hE 'checkout-email=([0-9]*O|a%3A|O%3A|O:|a:)' "$ld"/*.log* 2>/dev/null | tail -n 20
    else
        echo "[CLEAN] No serialized-payload patterns in $ld"
    fi
done

echo ""
echo "[*] Summary: $VULN vulnerable LearnPress installation(s) found."
[ "$VULN" -gt 0 ] && exit 1 || exit 0

Remediation

  1. Update LearnPress immediately. Upgrade to the latest release beyond 4.4.10. Verify the update via the WordPress admin dashboard (Plugins → Installed Plugins) or WP-CLI: wp plugin update learnpress && wp plugin get learnpress --field=version. Confirm the version string after updating — a stalled auto-update is a common false sense of closure.

  2. Audit for prior exploitation before assuming you are clean. Because the payload is persisted in the LearnPress session table, check the database for injected serialized strings: query the LearnPress session table for rows whose values match O:% or a:% patterns with class names not present in your codebase. Review access logs for checkout-email parameters containing serialized syntax (the script above automates this).

  3. Restrict unserialize gadget exposure. Where PHP configuration permits, ensure unserialize() is not globally invoked with attacker input elsewhere. Defense-in-depth: run a current PHP version (8.1+), and consider deploying a WAF rule blocking request bodies containing O:, a:, or C: serialized prefixes in POST parameters to admin-ajax.php and lp-ajax endpoints — with tuned exceptions for legitimate integrations.

  4. Reduce the POP chain surface. Remove unused plugins and themes — every dormant class with a magic method is a potential deserialization gadget. This is cheap, high-yield hardening that pays off against the entire class of WordPress object-injection bugs, not just this CVE.

  5. Segment and monitor web workloads. Web server processes should never spawn interactive shells. Alert on it (Sigma rule #2 above). Egress filtering on web hosts limits the value of code execution for staging second payloads.

  6. Track the advisory. Monitor the NVD entry at https://nvd.nist.gov/vuln/detail/CVE-2026-104736 and the LearnPress changelog/Wordfence intelligence feed for updates on exploitation activity and any KEV addition, which would trigger emergency-change remediation timelines under most compliance frameworks.

Compliance Framing

For organizations under PCI-DSS or HIPAA, an unauthenticated RCE-adjacent flaw in a public-facing application handling checkout flows sits squarely in "critical vulnerability — expedited remediation" territory (PCI-DSS Req. 6.2 / 6.3 timelines; HIPAA Security Rule risk management obligations). Document your patch date, version verification, and hunt results — auditors will ask, and IR scoping will be far cheaper if you can demonstrate the environment was patched before any exploitation window.

Related Resources

Security Arsenal Penetration Testing Services AlertMonitor Platform Book a SOC Assessment vulnerability-management Intel Hub

Is your security operations ready?

Get a free SOC assessment or see how AlertMonitor cuts through alert noise with automated triage.