
Эксплойт 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) на сервере.
| Атрибут | Значение |
|---|---|
| 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) |
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, поэтому при отправке формы все поля сохраняются в исходном виде.