Proof-of-concept demonstrations for CVE-2026-42167, a SQL injection
vulnerability in ProFTPD's mod_sql logging pipeline that allows an
unauthenticated attacker to execute arbitrary SQL — and to inject backdoor users into
the FTP authentication database or achieve remote code execution on the
database host.
All POCs are PostgreSQL-specific, but backdoor user ones work with MySQL and sqlite backends with some modifications to the injected query (which we're leaving as an exercise for the reader).
- This vulnerability was discovered by ZeroPath Research. Our technical blog contains more details.
ProFTPD's mod_sql logs every FTP command through the SQLLog /
SQLNamedQuery mechanism. When resolving format variables like %U
(original username) or %{basename} (filename component), the framework
calls is_escaped_text() to decide whether escaping is needed — and skips
escaping entirely for any value that starts and ends with a single quote
and contains no internal single quotes (e.g. '|| (SELECT 1) ||'). The
check is purely syntactic and
runs on raw, attacker-controlled input from the FTP session, so it cannot
distinguish "pre-escaped by trusted code" from "crafted by an attacker to
look pre-escaped."
The standard documented pattern wraps format variables in single quotes for SQL safety:
SQLNamedQuery log_activity INSERT "'%U', '%r', '%m'" activity_log
SQLLog * log_activity
SQLLog ERR_* log_activity
When an attacker supplies a value of the form '<payload>', the substitution
produces ''<payload>'' in the final SQL — the empty string literals close
the surrounding quotes and the payload runs as raw SQL. With PostgreSQL
(PQexec) or SQLite (sqlite3_exec) the stacked-query support means the
payload can be a full INSERT, UPDATE, CREATE TABLE, or
COPY TO PROGRAM statement.
The vulnerable code is in contrib/mod_sql.c (the shared SQL framework),
not in any specific backend, so all SQL backends are affected — but the
attack surface depends on how the admin has configured logging. A server is
exploitable when both of the following are true:
-
The admin has defined a
SQLNamedQuery INSERT(orUPDATE) whose format string interpolates one of these attacker-controllable variables wrapped in single quotes — e.g."'%U', '%m'". The variables that come from attacker input are:Var Meaning %Aanonymous-login password string %Jcommand parameters (everything after the verb) %Sresponse message string (may include attacker input echoed back in errors) %Uoriginal username from USER(set before auth, available even on failed login)%ddirectory name (last path component) %lRFC 1413 ident response (attacker-controlled if they run identd) %mFTP method/verb (attacker chooses which command to send) %rfull FTP command (verb + args) %uauthenticated username %{basename}filename component of the path argument, no directory prefix %f,%F, and%Dlook attacker-controlled but always resolve to absolute paths beginning with/, so they cannot match theis_escaped_text()start-with-'requirement. -
The admin has bound that
SQLNamedQueryto aSQLLogdirective for an FTP command (or command class) the attacker can reach. The widely documentedSQLLog *andSQLLog ERR_*wildcards are the broadest case and what makes the%Upath pre-auth:ERR_*fires on a failedUSER, so no credentials are needed. Per-command directives likeSQLLog STORcover any authenticated user.
The bypass turns the logging path into an arbitrary-SQL primitive on the backend. The two highest-impact scenarios:
-
Inject a backdoor user with arbitrary privileges (auth bypass). ProFTPD's SQL auth backend reads usernames, password hashes, uid, gid, homedir, and shell from the same
userstable the logging INSERT can now write to. A stackedINSERT INTO usersplants an attacker-chosen account —uid=0,homedir=/, plaintext password — and the attacker then logs in normally with full filesystem access through the FTP daemon. Reachable pre-auth via the%U+SQLLog ERR_*path, or post-auth via any attacker-controlled variable bound to a command the attacker can issue (e.g.%{basename}+SQLLog STOR). Works on PostgreSQL and SQLite (both support stacked queries). -
Remote code execution on the database host via
COPY TO PROGRAM. PostgreSQL'sCOPY (SELECT …) TO PROGRAM '<cmd>'runs<cmd>through a shell on the database server. A stacked-query injection that issuesCOPY TO PROGRAMgives arbitrary OS command execution as the postgres OS user, which is enough for credential exfiltration, lateral movement, and persistence. Reachable through the same trigger paths as the backdoor case (pre-auth via%Uor post-auth via%{basename}etc.). PostgreSQL-specific, and requires the ProFTPD DB role to have superuser privileges (the prerequisite forCOPY TO PROGRAM) — common in single- tenant deployments where the role is the DB owner.
The POCs in this repository target PostgreSQL specifically — the
strongest case, since PQexec() supports stacked queries and COPY TO PROGRAM provides direct OS command execution. The bypass itself is in
the shared SQL framework and affects all backends:
- PostgreSQL (covered here): stacked queries via
PQexec, RCE viaCOPY TO PROGRAM. - SQLite: stacked queries via
sqlite3_exec, runs underPRIVS_ROOTin the proftpd worker — backdoor injection works the same way; RCE primitive differs (noCOPY TO PROGRAMequivalent, but writableuserstable on a root-process SQL backend is plenty). - MySQL: the bypass fires the same way, but reaching the
userstable or OS execution from inside the single logging INSERT is harder. Single-statement manipulation (subquery exfil in a VALUES slot, time-based blind, error-based blind) is straightforward; turning that into a write to a different table requires getting around several obstacles:mod_sql_mysqlcallsmysql_real_query()withoutCLIENT_MULTI_STATEMENTS, so a trailing; INSERT INTO users …is rejected by the connection.- An attacker would have to figure out how to work around this
limitation to write to the
userstable or a file on disk.
Within the PostgreSQL setup, the POCs demonstrate two representative trigger paths. Other combinations from the table above are equivalent in principle:
- Pre-auth via
%U+USER.SQLLog ERR_*makes this fully unauthenticated. - Post-auth via
%{basename}+STOR. Requires any FTP credentials but no privileges beyond the ability to upload a file.
-
setup/— Automated environment setup. Clones the ProFTPD source tree pinned to the commit these findings were reported against, builds the daemon withmod_sql+mod_sql_postgres, and stands up a Docker Compose cluster (ProFTPD + PostgreSQL) with seed data and the vulnerable SQLLog configuration. -
pocs/— Five exploit scripts. All are self-contained Python scripts requiring only the standard library.preauth_user_backdoor.py— Pre-auth%Utrigger → backdoor user (uid=0, homedir=/) injected into the auth DB. No credentials, no DB superuser required. This POC is PostgreSQL-specific, but the issue can be exploited with either mysql or sqlite backend as well.preauth_user_rce.py— Pre-auth%Utrigger → RCE on the PostgreSQL host viaCOPY TO PROGRAM. No credentials. Requires the ProFTPD DB role to be a PostgreSQL superuser.postauth_stor_backdoor.py— Post-auth%{basename}trigger → backdoor user injected. Requires any authenticated FTP user. This POC is PostgreSQL-specific, but the issue can be exploited with either mysql or sqlite backend as well.postauth_stor_rce.py— Post-auth%{basename}trigger → RCE on the PostgreSQL host. Requires any authenticated FTP user and a PostgreSQL superuser DB role.postgres_blind_dump.py— Pre-auth%Utrigger → time-based blind extraction of the authuserstable. Doesn't use stacked queries, so works on deployments where the proftpd DB role has only minimal privileges (justINSERTon the log table) and the backdoor / RCE POCs above would fail closed. Pulls every byte of every column — including thepasswdcolumn — by binary-searching one bit at a time viapg_sleep(). Mirrors what sqlmap would automate, hand-rolled with no third-party dependencies.
Prerequisites: Docker, Git, Python 3.10+, and uv.
cd setup
./setup.shThe first run clones the ProFTPD source and builds the server (~2-3 min). Subsequent runs reuse the build cache and start in seconds.
When setup completes, it prints the full environment details (FTP and DB endpoints, test credentials, and ready-to-paste POC commands). Follow those instructions to run the POCs.
To tear down:
cd setup
./teardown.sh