Сергей Попов

Администратор
30.12.2015
6 283
6 992
Специализация
  1. OSINT
  2. Веб-безопасность
Статус верификации
  1. ✓ Verified
Расколотый керамический цилиндр в форме бивня, символизирующего PostgreSQL, лежит на чёрном антистатическом коврике. Внутри видны слои SQL-запросов, в трещине застряло тонкое хирургическое лезвие с...


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, а сам процесс от публикации advisory до рабочего PoC я описывал в гайде по CVE эксплойт разработке. EPSS-скор 0.8832 (top 1% среди всех CVE), решение CISA SSVC - Act с пометкой automatable: yes. Это не «пропатчим на следующей неделе». Это «пропатчим до обеда».

Зачем атакующему Drupal на PostgreSQL​

Drupal - это государственные порталы, университеты, enterprise-контент, финансовые платформы. Анонимная SQL-инъекция в такой системе - не дефейс ради лулзов. Это прямой доступ к базе: пользовательские сессии, хеши паролей, административные учётки, конфиденциальный контент. По разным оценкам, менее 5% установок Drupal крутятся на PostgreSQL, но в абсолютных числах - тысячи интернет-доступных сайтов, сконцентрированных в госсекторе, образовании и крупном бизнесе. Как раз там, где данные стоят дороже всего.

Kill chain выглядит так:
  1. Recon - fingerprinting Drupal, определение СУБД (PostgreSQL vs MySQL)
  2. Initial Access - Exploit Public-Facing Application (T1190) через JSON:API или /user/login
  3. Credential Access - извлечение хешей из users_field_data, сессионных токенов
  4. Privilege Escalation - промоушен аккаунта до admin через UPDATE (T1068)
  5. Execution - COPY FROM PROGRAM для выполнения системных команд (T1059.004, Unix Shell)
  6. Persistence - запись веб-шелла через админку или через SQL (T1505.003, Web Shell)
  7. Collection - settings.php с кредами БД, ключи API, конфигурация кэша (T1005)
NVD оценивает уязвимость как CVSS 9.8 CRITICAL: 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"}
Extraction тут сложнее: endpoint не возвращает SQL-ошибку в теле ответа. Вместо стандартного 400 (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-кластера всё ещё возможен
Даже без RCE, SQL-инъекция с полным доступом к базе Drupal - game over для приложения. Сессии, хеши, контент, роли - всё у атакующего. UPDATE на 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
JSON:API включён по умолчанию на Drupal 9+, endpoint /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 - объект, а не строка
В логах PostgreSQL (log_statement = 'all' или log_min_error_statement = 'error'):
  • Запросы с COPY FROM PROGRAM от Drupal-пользователя
  • SELECT внутри LOWER(...) конструкций с нетипичными подзапросами
  • Ошибки division by zero или undefined column в контексте Entity Query
В Drupal watchdog:
  • PHP exceptions с трассировкой через EntityQuery и Condition::compile()translateCondition
  • PDOException с SQL-дампом, содержащим пользовательский ввод в именах placeholder-ов
Отдельная боль - WAF. Стандартные правила могут не поймать эту инъекцию: payload сидит в ключе HTTP-параметра, а не в значении. Типовые WAF-правила фильтруют SQL-конструкции в значениях, но ключи проверяют далеко не всегда. Правило должно анализировать полный query string, включая имена параметров, на паттерны ||, 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.
 
Последнее редактирование:
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

Похожие темы

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🧭 Навигатор · ИБ 2026
Не знаешь, какой трек твой?
5 направлений ИБ, реальные зарплаты и точка входа для каждого — в одном треде.
JuniorSenior+
100K → 600K+ ₽ /мес
Открыть навигатор →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab