Сергей Попов
Администратор
- 30.12.2015
- 6 282
- 6 992
- Специализация
- OSINT
- Веб-безопасность
- Статус верификации
- ✓ Verified
20 мая 2026 года Drupal Security Team выкатила SA-CORE-2026-004. Через 48 часов CISA втянула CVE-2026-9082 в каталог Known Exploited Vulnerabilities, а несколько security-вендоров зафиксировали массовые попытки эксплуатации. Я поднял Drupal 11.3.9 с PostgreSQL 16 в Docker в день публикации и полез в патч-дифф - три файла, три правки, и они закрывают анонимную SQL-инъекцию с прямым путём до remote code execution. EPSS-скор 0.8832 (top 1% среди всех CVE), решение CISA SSVC -
Act с пометкой automatable: yes. Это не «пропатчим на следующей неделе». Это «пропатчим до обеда».Зачем атакующему Drupal на PostgreSQL
Drupal - это государственные порталы, университеты, enterprise-контент, финансовые платформы. Анонимная SQL-инъекция в такой системе - не дефейс ради лулзов. Это прямой доступ к базе: пользовательские сессии, хеши паролей, административные учётки, конфиденциальный контент. По разным оценкам, менее 5% установок Drupal крутятся на PostgreSQL, но в абсолютных числах - тысячи интернет-доступных сайтов, сконцентрированных в госсекторе, образовании и крупном бизнесе. Как раз там, где данные стоят дороже всего.Kill chain выглядит так:
- Recon - fingerprinting Drupal, определение СУБД (PostgreSQL vs MySQL)
- Initial Access - Exploit Public-Facing Application (T1190) через JSON:API или
/user/login - Credential Access - извлечение хешей из
users_field_data, сессионных токенов - Privilege Escalation - промоушен аккаунта до admin через UPDATE (T1068)
- Execution -
COPY FROM PROGRAMдля выполнения системных команд (T1059.004, Unix Shell) - Persistence - запись веб-шелла через админку или через SQL (T1505.003, Web Shell)
- Collection -
settings.phpс кредами БД, ключи API, конфигурация кэша (T1005)
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H - сетевой доступ, низкая сложность, привилегии не нужны, действие пользователя не нужно, полная компрометация конфиденциальности, целостности и доступности. CISA Vulnrichment: technical impact total, автоматизируемость yes. Drupal обновила собственный risk score до 23 из 25 после обнаружения эксплуатации in the wild. Данные CrowdSec к 25 мая показали десятки уникальных атакующих IP - фаза ранней эксплуатации, когда у защитников ещё есть окно.Корневая причина: как ключи массива стали SQL-синтаксисом
CVE-2026-9082 (CWE-89, OWASP A03:2021 - Injection) - не классическая конкатенация ввода в SQL-строку. Баг сидит глубже - в абстракционном слое Entity Query API, который по дизайну должен защищать от инъекций через prepared statements и разделение SQL-синтаксиса и данных. Должен - но не защитил.Работает если:
- Drupal использует PostgreSQL как бэкенд базы данных
- Атакуемое условие Entity Query принимает массив (оператор
IN) - Сравнение case-insensitive (по умолчанию для большинства текстовых полей)
- Версия Drupal Core: от 8.9.0 до 10.4.10 (не включая), от 10.5.0 до 10.5.10 (не включая), от 10.6.0 до 10.6.9 (не включая), от 11.0.0 до 11.1.10 (не включая), от 11.2.0 до 11.2.12 (не включая), от 11.3.0 до 11.3.10 (не включая)
- Бэкенд MySQL, MariaDB или SQLite - уязвимый код-путь специфичен для PostgreSQL-драйвера
- Drupal 7 - другая архитектура Entity Query
- Версия уже пропатчена (10.4.10 / 10.5.10 / 10.6.9 / 11.1.10 / 11.2.12 / 11.3.10)
core/modules/pgsql/src/EntityQuery/Condition.php. PostgreSQL по умолчанию case-sensitive, и Drupal оборачивает сравнения в LOWER() для единообразного поведения с MySQL. Для оператора IN с массивом значений цикл translateCondition строит SQL-placeholder из ключей массива PHP:
PHP:
// Уязвимый паттерн (до патча, упрощённо)
$where_prefix = str_replace('.', '_', $condition['real_field']);
foreach ($condition['value'] as $key => $value) {
$where_id = $where_prefix . $key; // $key из ввода атакующего
$condition['where'] .= 'LOWER(:' . $where_id . '),';
$condition['where_args'][':' . $where_id] = $value;
}
$condition['value'] - числовой массив [0 => 'admin'], ключ 0 безопасен: placeholder получается :field_name0. Но если передать ассоциативный массив ['0||(SELECT version())' => 'x'], ключ целиком вклеивается в SQL-текст как часть идентификатора placeholder-а. Значения биндятся через PDO - а вот имя placeholder-а становится исполняемым SQL-синтаксисом. PostgreSQL парсит || как оператор конкатенации строк, подзапрос в скобках выполняется на сервере СУБД. Красота.Патч элегантен до зевоты: ключи сбрасываются в числовую последовательность
[0, 1, 2, ...] до попадания в translateCondition. Три строки кода, три файла. MySQL и SQLite никогда не входили в уязвимый цикл - их путь IN использует Connection::expandArguments() с внутренним счётчиком для имён placeholder-ов, а не пользовательские ключи.Два вектора эксплуатации CVE-2026-9082 без аутентификации
Анализ публичных PoC и патч-диффа показывает два независимых анонимных пути до уязвимогоtranslateCondition. Оба используют стандартную JSON content negotiation Drupal и заканчиваются в том же PostgreSQL-специфичном цикле.JSON:API - error-based SQL injection через GET-запрос
[Применимо: внешний пентест, Drupal 9+/10.x/11.x с включённым JSON:API (включён по умолчанию)]JSON:API экспонирует endpoints вида
/jsonapi/node/{type} и поддерживает фильтрацию через query parameters. Для эксплуатации нужен хотя бы один node указанного типа (article, page - есть на большинстве сайтов). Инъекция идёт через ключ параметра filter[...][value]:
Код:
GET /jsonapi/node/article?
filter[f][condition][path]=title&
filter[f][condition][operator]=IN&
filter[f][condition][value][0]=legit&
filter[f][condition][value][0||(SELECT version())]=x
IN запускает ветку с массивом значений. Ключ 0||(SELECT version()) попадает в $where_id без санитизации. PostgreSQL парсит || как конкатенацию, подзапрос SELECT version() выполняется. Ответ - HTTP 500 с PostgreSQL-ошибкой, содержащей полный текст SQL-запроса: классический error-based extraction. Инъекция несуществующего столбца (SELECT BADABOOM) возвращает column "badaboom" does not exist - прямое подтверждение, что произвольный SQL выполняется.На Exploit-DB опубликован EDB-52608 (автор cardosource, 2026-06-01) - «Drupal Core 10.5.5 - Error-Based SQL Injection», подтверждающий этот вектор. Nuclei-темплейт
CVE-2026-9082.yaml из projectdiscovery/nuclei-templates автоматизирует первичную проверку.Когда вектор НЕ работает: JSON:API отключён администратором. На Drupal 9+ модуль включён по умолчанию, так что это редкость - но бывает. Зато второй вектор через
/user/login работает независимо./user/login - blind SQLi через POST
[Применимо: внешний пентест, любой Drupal 8.9+ на PostgreSQL, включая минимальные инсталляции]Endpoint
/user/login?_format=json - часть core-модуля user, работает на дефолтной установке. Контроллер парсит JSON-тело через Symfony JsonEncoder, который декодирует JSON-объекты в ассоциативные PHP-массивы. Передаём name как объект - ключи попадают в Entity Query для поиска пользователя:
JSON:
POST /user/login?_format=json
Content-Type: application/json
{"name":{"0":"x","0||(SELECT 1)":"x"},"pass":"x"}
Sorry, unrecognized username or password) приходит 500 - смена кода подтверждает инъекцию, но для вытаскивания данных нужна boolean-based blind техника.Рабочий подход из публичных PoC - divide-by-zero gadget через
CASE WHEN. Инъекция вида 0||1/(SELECT CASE WHEN (SELECT name FROM users_field_data WHERE uid=1) LIKE 'admin%' THEN 0 END) создаёт деление на ноль при истинном условии (500) и NULL при ложном (400). Перебираем посимвольно - LIKE 'a%', LIKE 'ad%', LIKE 'adm%' - и вытаскиваем имя администратора, хеш пароля (pass из users_field_data), любые другие данные из базы.Ограничение: blind extraction - медленная история. На один символ - минимум один HTTP-запрос. Для полного хеша (Drupal использует
phpass с $S$ префиксом, ~55 символов) - десятки запросов. Без автоматизации скриптом тут делать нечего.Эскалация до RCE: COPY FROM PROGRAM на PostgreSQL
📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Когда RCE не работает
- PostgreSQL-пользователь без
SUPERUSERи без ролиpg_execute_server_program-COPY FROM PROGRAMвернёт permission denied. На hardened-инсталляциях Drupal-пользователь БД обычно ограниченSELECT/INSERT/UPDATE/DELETEбез системных привилегий - Managed PostgreSQL (AWS RDS, GCP Cloud SQL, Azure Database for PostgreSQL) -
COPY FROM PROGRAMзапрещён на уровне платформы, RCE через этот примитив невозможен - SELinux / AppArmor в enforcing mode ограничивают доступные команды даже для SUPERUSER
- Docker-контейнер без
--privileged- RCE ограничен контейнером, но pivot через внутреннюю сеть Docker-кластера всё ещё возможен
users_field_data для смены роли на admin - вопрос одного запроса.Fingerprinting и стенд для воспроизведения
Требования к окружению:- Docker 20+ и docker-compose
- RAM: 2 GB минимум
- Drupal 11.3.9 или ниже (до патча), образ
drupal:11.3.9 - PostgreSQL 14+ (тестировалось на 16)
- curl или Burp Suite для отправки запросов
docker-compose.yml: в settings.php драйвер базы должен быть pgsql. Официальный Docker-образ Drupal по умолчанию тянет MySQL - нужно явно сконфигурировать PostgreSQL при установке через drush site:install --db-url=pgsql://user:pass@db/drupal. Я на этом потерял полчаса, пока не перечитал дефолтный конфиг.Fingerprinting PostgreSQL на целевом Drupal (до эксплуатации):
Определить СУБД до отправки exploit-а - обязательный шаг recon. PostgreSQL-специфичные индикаторы:
- Ошибки Drupal при невалидных запросах могут содержать
SQLSTATEкоды специфичные для PostgreSQL (42703- undefined column,42601- syntax error) или упоминаниеPDO::PGSQL - Тайминг: инъекция с
pg_sleep(5)через JSON:API вызовет задержку только на PostgreSQL. На MySQL аналог -SLEEP(5), на SQLite - отсутствует - Nuclei-темплейт
CVE-2026-9082.yamlавтоматизирует проверку: шлёт crafted запрос и ищет PostgreSQL-специфичные маркеры в ответе - При доступе к файловой системе (LFI, backup, SSH):
grep -r 'pgsql' sites/default/settings.php
/user/login доступен на любой свежей инсталляции - обе точки входа открыты для fingerprinting.Детект и индикаторы компрометации
CrowdSec зафиксировал probing-активность в первые дни после публикации. К концу мая - десятки уникальных IP атакующих. Что искать:В access-логах веб-сервера:
- GET-запросы к
/jsonapi/node/*с символами||,SELECT,COPY,pg_sleepв ключах query parameters (URL-encoded:%7C%7C=||) - POST к
/user/login?_format=jsonс HTTP 500 вместо стандартных 400/401 - аномальное соотношение 500/400 на этом endpoint - Необычные JSON-тела в POST к
/user/loginгдеname- объект, а не строка
log_statement = 'all' или log_min_error_statement = 'error'):- Запросы с
COPY FROM PROGRAMот Drupal-пользователя SELECTвнутриLOWER(...)конструкций с нетипичными подзапросами- Ошибки
division by zeroилиundefined columnв контексте Entity Query
- PHP exceptions с трассировкой через
EntityQueryиCondition::compile()→translateCondition PDOExceptionс SQL-дампом, содержащим пользовательский ввод в именах placeholder-ов
||, SELECT, CASE WHEN, pg_sleep. Если ваш WAF так не умеет - самое время проверить.Пропатченные версии: 10.4.10, 10.5.10, 10.6.9, 11.1.10, 11.2.12, 11.3.10. Для end-of-life веток (Drupal 8.9, 9.x) выпущены экстренные патчи, но эти ветки содержат другие незакрытые уязвимости - обновление до поддерживаемой ветки обязательно (OWASP A06:2021 - Vulnerable and Outdated Components).
CVE-2026-9082 - история не про Drupal конкретно, а про ложную уверенность в query builder-ах. Разработчики и пентестеры годами жили в модели «значения биндятся через PDO - инъекции нет». Drupal Database API - абстракция с placeholder-ами, prepared statements и separation of concerns. И всё равно инъекция возникла, потому что структурные данные - ключи массива - повлияли на SQL-синтаксис, а не на значения. Этот паттерн не уникален: ORM-ы в Django, Laravel, Rails оперируют не только значениями, но и именами полей, направлениями сортировки, операторами - каждый из них потенциальный injection point, который
sqlmap --forms не найдёт, потому что инъекция не в значении параметра, а в его имени. На пентестах CMS я теперь системно фаззю ключи массивов в каждом API-эндпоинте, не только значения. Скоринг CVSS без контекста актива - бесполезная метрика: анонимный доступ, CISA KEV, EPSS в top 1%, public PoC - этого достаточно для немедленного реагирования, не дожидаясь согласования в скоринг-комитете. Если хочешь повторить цепочку от placeholder injection до shell в контролируемой инфре - похожий сценарий с SQLi в CMS на PostgreSQL разобран на HackerLab.