
POCs для демонстрации CVE-2026-42167 в ProFTPD
Демонстрации proof-of-concept для CVE-2026-42167 — уязвимости SQL-инъекции
в конвейере журналирования mod_sql в ProFTPD, которая позволяет
неаутентифицированному атакующему выполнять произвольный SQL — а также
внедрять бэкдор-пользователей в базу данных аутентификации FTP или достигать
удалённого выполнения кода на хосте базы данных.
Все POC ориентированы на PostgreSQL, но эксплойты для бэкдор-пользователей работают и с бэкендами MySQL и sqlite с некоторыми изменениями во внедряемом запросе (которые мы оставляем читателю в качестве упражнения).
is_escaped_text() в SQLLog модуля mod_sql (CVE-2026-42167, CWE-89)Модуль mod_sql в ProFTPD журналирует каждую FTP-команду через механизм
SQLLog / SQLNamedQuery. При разрешении переменных формата, таких как %U
(исходное имя пользователя) или %{basename} (компонент имени файла), фреймворк
вызывает is_escaped_text(), чтобы решить, нужно ли экранирование — и пропускает
экранирование полностью для любого значения, которое начинается и заканчивается
одинарной кавычкой и не содержит внутренних одинарных кавычек (например,
'|| (SELECT 1) ||'). Проверка чисто синтаксическая и
выполняется над необработанными данными, контролируемыми атакующим, из FTP-сессии,
поэтому она не может отличить «заранее экранировано доверенным кодом» от
«сконструировано атакующим так, чтобы выглядеть заранее экранированным».
Стандартный документированный шаблон оборачивает переменные формата в одинарные кавычки для безопасности SQL:
SQLNamedQuery log_activity INSERT "'%U', '%r', '%m'" activity_log
SQLLog * log_activity
SQLLog ERR_* log_activity
Когда атакующий передаёт значение вида '<payload>', подстановка
даёт ''<payload>'' в итоговом SQL — литералы пустых строк закрывают
окружающие кавычки, и payload выполняется как сырой SQL. Благодаря поддержке
составных запросов в PostgreSQL (PQexec) или SQLite (sqlite3_exec) payload
может быть полноценным оператором INSERT, UPDATE, CREATE TABLE или
COPY TO PROGRAM.
Уязвимый код находится в contrib/mod_sql.c (общий SQL-фреймворк),
а не в каком-то конкретном бэкенде, поэтому затронуты все SQL-бэкенды — но
поверхность атаки зависит от того, как администратор настроил журналирование. Сервер
эксплуатируем, когда истинны оба следующих условия:
Администратор определил SQLNamedQuery INSERT (или UPDATE), строка формата
которого интерполирует одну из этих переменных, контролируемых атакующим, обёрнутую
в одинарные кавычки — например, "'%U', '%m'". Переменные, поступающие из
ввода атакующего:
| Var | Значение |
|---|---|
%A | строка пароля анонимного входа |
%J | параметры команды (всё после глагола) |
%S | строка ответного сообщения (может содержать ввод атакующего, отражённый в ошибках) |
%U | исходное имя пользователя из USER (устанавливается до аутентификации, доступно даже при неудачном входе) |
%d | имя каталога (последний компонент пути) |
%l | ответ идентификации RFC 1413 (контролируется атакующим, если он запустит identd) |
%m | метод/глагол FTP (атакующий выбирает, какую команду отправить) |
%r | полная FTP-команда (глагол + аргументы) |
%u | имя аутентифицированного пользователя |
%{basename} | компонент имени файла из аргумента пути, без префикса каталога |
%f, %F и %D выглядят контролируемыми атакующим, но всегда разрешаются в
абсолютные пути, начинающиеся с /, поэтому не могут удовлетворить
требованию is_escaped_text() о начале с '.
Администратор привязал этот SQLNamedQuery к директиве SQLLog для FTP-команды
(или класса команд), доступной атакующему. Широко документированные
подстановочные знаки SQLLog * и SQLLog ERR_* — самый широкий случай,
и именно они делают путь через %U доступным до аутентификации: ERR_*
срабатывает при неудачном USER, поэтому учётные данные не нужны. Директивы
для конкретных команд, такие как SQLLog STOR, охватывают любого
аутентифицированного пользователя.
Обход превращает путь журналирования в примитив произвольного SQL на бэкенде. Два сценария с наибольшим влиянием:
Внедрение бэкдор-пользователя с произвольными привилегиями (обход аутентификации).
SQL-бэкенд аутентификации ProFTPD читает имена пользователей, хэши паролей, uid, gid,
домашний каталог и оболочку из той же таблицы users, в которую теперь может
писать журналирующий INSERT. Составной INSERT INTO users внедряет выбранную
атакующим учётную запись — uid=0, homedir=/, пароль в открытом виде — после чего
атакующий входит в систему обычным образом с полным доступом к файловой системе через
FTP-демон. Достижимо до аутентификации через путь %U + SQLLog ERR_* или после
аутентификации через любую контролируемую атакующим переменную, привязанную к команде,
которую атакующий может отправить (например, %{basename} + SQLLog STOR). Работает
на PostgreSQL и SQLite (оба поддерживают составные запросы).
Удалённое выполнение кода на хосте базы данных через COPY TO PROGRAM.
COPY (SELECT …) TO PROGRAM '<cmd>' в PostgreSQL выполняет <cmd> через
оболочку на сервере базы данных. Внедрение составного запроса, которое отправляет
COPY TO PROGRAM, даёт произвольное выполнение OS-команд от имени
пользователя OS postgres, чего достаточно для эксфильтрации учётных данных, бокового
перемещения и закрепления. Достижимо через те же пути срабатывания, что и в случае
с бэкдором (до аутентификации через %U или после через %{basename} и т.д.).
Специфично для PostgreSQL и требует, чтобы роль БД ProFTPD имела привилегии
суперпользователя (предпосылка для COPY TO PROGRAM) — часто встречается в
однопользовательских развёртываниях, где эта роль является владельцем БД.
POC в этом репозитории нацелены конкретно на PostgreSQL — самый
сильный случай, поскольку PQexec() поддерживает составные запросы, а COPY TO PROGRAM обеспечивает прямое выполнение OS-команд. Сам обход находится в
общем SQL-фреймворке и затрагивает все бэкенды:
PQexec, RCE через
COPY TO PROGRAM.sqlite3_exec, выполняется под PRIVS_ROOT
в рабочем процессе proftpd — внедрение бэкдора работает так же; примитив RCE
отличается (нет аналога COPY TO PROGRAM, но доступная на запись таблица users
в SQL-бэкенде, работающем от root, более чем достаточна).users или
выполнения OS-команд из одного журналирующего INSERT сложнее.
Манипуляции с одним оператором (подзапрос для эксфильтрации в слоте VALUES,
time-based blind, error-based blind) просты; превращение этого в запись в другую
таблицу требует обхода нескольких препятствий:
mod_sql_mysql вызывает mysql_real_query() без
CLIENT_MULTI_STATEMENTS, поэтому хвостовой ; INSERT INTO users …
отклоняется соединением.users или в файл на диске.В рамках настройки PostgreSQL POC демонстрируют два репрезентативных пути срабатывания. Другие комбинации из таблицы выше эквивалентны в принципе:
%U + USER. SQLLog ERR_* делает это полностью
неаутентифицированным.%{basename} + STOR. Требуются любые FTP-учётные данные,
но не более чем возможность загрузить файл.