
POCs para demonstrar CVE-2026-42167 no ProFTPD
Demonstrações de prova de conceito para CVE-2026-42167, uma vulnerabilidade de injeção de SQL
no pipeline de logging mod_sql do ProFTPD que permite a um
atacante não autenticado executar SQL arbitrário — e injetar usuários backdoor no
banco de dados de autenticação do FTP ou obter execução remota de código no
host do banco de dados.
Todos os POCs são específicos do PostgreSQL, mas os de usuário backdoor funcionam com backends MySQL e sqlite com algumas modificações na query injetada (que deixamos como exercício para o leitor).
is_escaped_text() no SQLLog do mod_sql (CVE-2026-42167, CWE-89)O mod_sql do ProFTPD registra todos os comandos FTP através do mecanismo SQLLog /
SQLNamedQuery. Ao resolver variáveis de formato como %U
(nome de usuário original) ou %{basename} (componente de nome de arquivo), o framework
chama is_escaped_text() para decidir se o escaping é necessário — e pula
o escaping por completo para qualquer valor que comece e termine com aspas simples
e não contenha aspas simples internas (ex.: '|| (SELECT 1) ||'). A
verificação é puramente sintática e
opera sobre entrada bruta controlada pelo atacante vinda da sessão FTP, portanto não consegue
distinguir "pré-escapado por código confiável" de "criado por um atacante para
parecer pré-escapado."
A prática padrão documentada envolve as variáveis de formato em aspas simples por segurança do SQL:
SQLNamedQuery log_activity INSERT "'%U', '%r', '%m'" activity_log
SQLLog * log_activity
SQLLog ERR_* log_activity
Quando um atacante fornece um valor da forma '<payload>', a substituição
produz ''<payload>'' no SQL final — os literais de string vazia fecham
as aspas ao redor e o payload é executado como SQL bruto. Com PostgreSQL
(PQexec) ou SQLite (sqlite3_exec), o suporte a consultas empilhadas significa que o
payload pode ser uma instrução INSERT, UPDATE, CREATE TABLE ou
COPY TO PROGRAM completa.
O código vulnerável está em contrib/mod_sql.c (o framework SQL compartilhado),
não em um backend específico, portanto todos os backends SQL são afetados — mas a
superfície de ataque depende de como o administrador configurou o logging. Um servidor é
explorável quando ambas as condições a seguir são verdadeiras:
O administrador definiu um SQLNamedQuery INSERT (ou UPDATE) cuja string de formato
interpola uma dessas variáveis controláveis pelo atacante envolvida
em aspas simples — ex.: "'%U', '%m'". As variáveis que vêm de
entrada do atacante são:
O bypass transforma o caminho de logging em uma primitiva de SQL arbitrário no backend. Os dois cenários de maior impacto:
Injetar um usuário backdoor com privilégios arbitrários (bypass de autenticação).
O backend de autenticação SQL do ProFTPD lê nomes de usuário, hashes de senha, uid, gid,
homedir e shell da mesma tabela users na qual o INSERT de logging agora pode
gravar. Um INSERT INTO users empilhado planta uma conta escolhida pelo atacante
— uid=0, homedir=/, senha em texto puro — e o atacante então faz login
normalmente com acesso total ao sistema de arquivos através do daemon FTP. Acessível
na pré-autenticação pelo caminho %U + SQLLog ERR_*, ou na pós-autenticação por qualquer
variável controlada pelo atacante vinculada a um comando que ele possa emitir
(ex.: %{basename} + SQLLog STOR). Funciona em PostgreSQL e SQLite
(ambos suportam consultas empilhadas).
Execução remota de código no host do banco de dados via COPY TO PROGRAM.
O COPY (SELECT …) TO PROGRAM '<cmd>' do PostgreSQL executa <cmd> através de um
shell no servidor de banco de dados. Uma injeção com consulta empilhada que emite
concede execução arbitrária de comandos do sistema operacional como o usuário
postgres do SO, o que é suficiente para exfiltração de credenciais, movimento lateral
e persistência. Acessível pelos mesmos caminhos de trigger do
caso de backdoor (pré-autenticação via ou pós-autenticação via etc.).
Específico do PostgreSQL e exige que o papel (role) de banco de dados do ProFTPD tenha privilégios
de superusuário (o pré-requisito para ) — comum em implantações single-
tenant onde o papel é o dono do banco.
Os POCs deste repositório têm como alvo especificamente o PostgreSQL — o
caso mais forte, já que PQexec() suporta consultas empilhadas e COPY TO PROGRAM fornece execução direta de comandos do SO. O bypass em si está no
framework SQL compartilhado e afeta todos os backends:
PQexec, RCE via
COPY TO PROGRAM.sqlite3_exec, executa sob PRIVS_ROOT
no worker do proftpd — a injeção de backdoor funciona da mesma forma; a primitiva
de RCE difere (não há equivalente a COPY TO PROGRAM, mas uma tabela
users gravável em um backend SQL em processo root já é suficiente).users
ou a execução de SO dentro do INSERT de logging único é mais difícil.
A manipulação com instrução única (exfiltração por subquery em um slot de VALUES,
cega baseada em tempo, cega baseada em erro) é direta; transformar isso
em uma gravação em uma tabela diferente exige contornar vários
obstáculos:
mod_sql_mysql chama mysql_real_query() sem
CLIENT_MULTI_STATEMENTS, portanto um ; INSERT INTO users … final
é rejeitado pela conexão.Na configuração PostgreSQL, os POCs demonstram dois caminhos de trigger representativos. Outras combinações da tabela acima são equivalentes em princípio:
%U + USER. SQLLog ERR_* torna isso totalmente
não autenticado.%{basename} + STOR. Exige quaisquer credenciais FTP,
mas nenhum privilégio além da capacidade de enviar um arquivo.setup/ — Configuração automatizada do ambiente. Clona a árvore de código-fonte do ProFTPD
fixada no commit contra o qual estas descobertas foram reportadas, compila o
daemon com mod_sql + mod_sql_postgres e sobe um cluster Docker Compose
(ProFTPD + PostgreSQL) com dados iniciais e a configuração vulnerável de SQLLog.
pocs/ — Cinco scripts de exploit. Todos são scripts Python autocontidos
que exigem apenas a biblioteca padrão.
preauth_user_backdoor.py — Trigger de pré-autenticação %U → usuário backdoor (uid=0,
homedir=/) injetado no banco de autenticação. Sem credenciais e sem superusuário
de banco necessários. Este POC é específico do PostgreSQL, mas o problema também pode ser explorado com backend mysql ou sqlite.preauth_user_rce.py — Trigger de pré-autenticação %U → RCE no host PostgreSQL
via COPY TO PROGRAM. Sem credenciais. Exige que o papel de banco do ProFTPD
seja um superusuário PostgreSQL.postauth_stor_backdoor.py — Trigger de pós-autenticação → usuário
backdoor injetado. Exige qualquer usuário FTP autenticado. Este POC é específico do PostgreSQL, mas o problema também pode ser explorado com backend mysql ou sqlite.Pré-requisitos: Docker, Git, Python 3.10+ e uv.
cd setup
./setup.sh
A primeira execução clona o código-fonte do ProFTPD e compila o servidor (~2-3 min). As execuções seguintes reutilizam o cache de build e iniciam em segundos.
Quando a configuração termina, ela imprime todos os detalhes do ambiente (endpoints de FTP e banco, credenciais de teste e comandos de POC prontos para colar). Siga essas instruções para executar os POCs.
Para remover o ambiente:
cd setup
./teardown.sh
| Var | Significado |
|---|
%A | string de senha do login anônimo |
%J | parâmetros do comando (tudo após o verbo) |
%S | string da mensagem de resposta (pode incluir entrada do atacante ecoada em erros) |
%U | nome de usuário original de USER (definido antes da autenticação, disponível mesmo em login com falha) |
%d | nome do diretório (último componente do caminho) |
%l | resposta ident RFC 1413 (controlada pelo atacante se ele executar um identd) |
%m | método/verbo FTP (o atacante escolhe qual comando enviar) |
%r | comando FTP completo (verbo + argumentos) |
%u | nome de usuário autenticado |
%{basename} | componente de nome de arquivo do argumento de caminho, sem prefixo de diretório |
%f, %F e %D parecem controlados pelo atacante, mas sempre resolvem para
caminhos absolutos que começam com /, portanto não conseguem satisfazer o
requisito de começar com ' do is_escaped_text().
O administrador vinculou esse SQLNamedQuery a uma diretiva SQLLog para um
comando FTP (ou classe de comando) que o atacante consegue alcançar. Os
curingas amplamente documentados SQLLog * e SQLLog ERR_* são o caso mais amplo
e o que torna o caminho via %U pré-autenticação: ERR_* dispara em um USER
com falha, então nenhuma credencial é necessária. Diretivas por comando como
SQLLog STOR cobrem qualquer usuário autenticado.
COPY TO PROGRAM%U%{basename}COPY TO PROGRAMusers ou em um arquivo no disco.%{basename}postauth_stor_rce.py — Trigger de pós-autenticação %{basename} → RCE no
host PostgreSQL. Exige qualquer usuário FTP autenticado e um papel de banco
superusuário PostgreSQL.postgres_blind_dump.py — Trigger de pré-autenticação %U → extração cega
baseada em tempo da tabela users de autenticação. Não usa consultas empilhadas, então
funciona em implantações onde o papel de banco do proftpd tem apenas privilégios
mínimos (somente INSERT na tabela de log) e os POCs de backdoor / RCE
acima falhariam de forma segura. Extrai cada byte de cada coluna —
incluindo a coluna passwd — por busca binária, um bit de cada vez,
via pg_sleep(). Espelha o que o sqlmap automatizaria, implementado manualmente, sem
dependências de terceiros.