2018 年 9 月 10 日,Webmin 把一台后来确认已遭入侵的开发构建服务器换成新装 CentOS 7 的机器。新操作系统是干净的,从旧服务器备份中复制过来的构建目录却已遭篡改。其中,修改过的 password_change.cgi 仍藏着后门,后续官方软件包继续带上了这个文件。[1]
这起事件于 2019 年 8 月公开,表现为 Webmin 中一个可用 root 权限远程执行命令的漏洞。Webmin 是管理类 Unix 系统的 Web 界面。它留下的长久教训越过了“保护构建服务器”这一层:重装机器与恢复可变工作目录的信任方向相反。前者清除不可信状态,后者保留着将其重新带回的风险。
这一差别在本案中造成了格外严重的后果,因为用户常把三个不同对象合称为“发行版”,而它们实际互相矛盾:
- Webmin 提交至 GitHub 的源代码没有恶意代码。
- 用于组装发行包的本地目录含有恶意代码。
- 经项目官方 SourceForge 下载路径提供的软件包继承了该本地目录的状态。[1][2][5]
上方照片拍摄的是 2009 年 Wikimedia 位于坦帕的服务器,并非 Webmin 的构建主机。[7] 线缆、端口、风扇、电源和机箱给出了有用的物理尺度。构建服务器并非从源码指向下载文件的一支中性箭头;它是一台保存状态的计算机,备份既能忠实保存预期输入,也能原样留住未经审查的状态。
一个脚本先后被修改两次
Webmin 的复盘从 2018 年 4 月开始。当时,一名身份不明的攻击者利用了开发构建服务器,并修改 password_change.cgi。这次改动把攻击者指定的输入放进 Perl 的 qx// 执行结构,其内容会经由 shell 运行。在 Webmin 1.890 中,这条路径可在默认配置下绕过身份认证触发,并以 root 权限执行命令。[1][5]
据项目方说明,攻击者还把该文件的时间戳调回过去,使这次改动没有出现在当时工作流程使用的差异对比中。后来,项目用 GitHub 中已提交的版本恢复了该文件。到了 2018 年 7 月,文件再次遭到修改。第二种恶意形式进入了 Webmin 1.900 以及此后直至 1.920 的软件包,不过它受一项配置条件限制:只有启用过期密码修改功能,相关漏洞路径才可访问。[1]
版本差异在这里直接影响暴露程度。不同版本的 Webmin 安装对应不同的暴露状态:
- 来自受影响官方软件包路径的 1.890: 默认安装即可遭到远程利用。
- 来自该路径的 1.900 至 1.920: 启用过期密码修改功能时存在漏洞。
- 使用对应的干净 GitHub 源码完成的构建: 没有证据显示其中含有注入构建目录的恶意代码。[1][3][5]
CVE Program 的 CVE-2019-15107 记录将漏洞描述为:攻击者通过 password_change.cgi 的 old 参数实施命令注入。Webmin 安全页面至今仍在事故说明旁列出 CVE-2019-15231;CVE Program 目前已把后一个编号标为重复项并拒绝收录,同时引导读者查阅 CVE-2019-15107。[1][3][4] 披露后的编号记录有所调整,默认配置即可利用的 1.890 软件包与后来受配置条件限制的软件包之间,操作层面的差别始终存在。
干净的代码仓库无法证明发行包同样干净
公共代码仓库无法复现这个漏洞。Webmin 开发者 Joe Cooper 告诉 The Register,可利用代码从未进入项目的 GitHub 仓库。因此,只查这些文件会让调查人员得到安全无虞的印象,用户实际下载的软件包却仍含有恶意代码。[5]
将这处错位概括成“GitHub 归档包正常、SourceForge 归档包恶意”,同样失真。2019 年 5 月,也就是公开披露前三个月,一名用户在邮件列表中询问 GitHub 与 SourceForge 的归档包为何差异如此之大。项目作者 Jamie Cameron 回答说,SourceForge 软件包才是官方发行版,GitHub 自动取得的源码树没有经过正确打包。[2] 正常的发行工作本来就会带来预期差异:生成文件、选定模块、元数据和其他打包产物都会改变制品,因此,制品是否完全相同不足以充当有效的检验标准。
由此留下了危险的权威断层。代码仓库界定了经审查的源码,却没有完整规定最终交付的制品。一个长期存在的本地目录补上了缺失的转换过程。这个目录既可修改,又对发行结果具有决定权,攻击者因而能够改变发行包,同时避开任何会进入审查视野的恶意提交。
现有记录只把 SourceForge 定位为分发场所,没有将其确定为入侵点。维护者的说明把入侵位置指向 Webmin 的构建基础设施,受影响的软件包随后经正常的官方渠道发布。[1][5] 熟悉的 HTTPS 下载 URL 只能验证由哪个站点提供这些字节,无法证明这些字节经过怎样的组装过程,因此不足以提供完整的来源保证。
这起事件把四种需要分别检验的保证拆分开来:
- 源码完整性: 经审查的提交是否包含预期代码?
- 构建输入完整性: 任务开始时是否只使用已声明的源码、依赖和工具?
- 制品完整性: 软件包是否确由该任务产出,且没有混入可变工作目录的残留?
- 发布授权: 获准的身份是否将这份确切制品纳入正式发布并完成签名?
Webmin 的 Git 历史为第一项提供了依据。在第二、第三项继承这份可信依据之前,本地打包流程已经截断了链条。
干净机器的迁移保留了错误状态
2018 年 9 月 10 日,旧构建服务器退役并被替换。从备份复制现有构建目录,在运维上有其道理,因为这保住了持续发布所需的整套工具和流程。这一做法也把该目录当作业务数据处理,没有将其视为潜在的事故证据。恶意文件就此进入新环境,攻击者早先的入侵结果已经足以完成这次跨环境传播。[1]
这是整篇事后复盘中最尖锐的技术环节。全新安装只改变操作系统层的信任状态,恢复回来的应用状态仍需单独核验。构建工作目录、缓存、软件包暂存目录、编译器包装程序、CI 插件和签名材料全都位于操作系统层之上。只要其来源未经确认,“恢复”就成了一项需要验证的新输入,无法作为恢复工作的终点。
这次迁移也压缩了后续取证空间。Cameron 告诉 The Register,原机器已经退役,能说明攻击者如何进入的日志也随之丢失。项目能够还原文件何时出现、何处有差异以及如何传播,却无法确定最初的访问路径。[5] 任何更具体的攻击过程描述都会超出现有记录。
更安全的恢复方式需要同时以两种互不相容的方式处理旧工作目录;即使迁移发生时,它还只是日后才被证实为事故证据的材料:
- 将其保留为证据: 创建只读快照,记录哈希值,并保存日志以供对比。
- 拒绝将其用作构建输入: 从经过认证的提交和审查过的构建定义出发,创建全新的空工作目录。
同一批字节不能从证据通道直接搬进生产通道。备份服务于业务连续性,却无法证明保存的状态早于入侵。
公开披露显出了两种不同的影响范围
一份公开利用代码于 2019 年 8 月 10 日在 DEF CON 出现。Webmin 维护者表示,他们在 8 月 17 日得知此事,随即移除恶意代码并发布 Webmin 1.930。[1][5] Ars Technica 报道,受影响版本的下载次数超过 900,000。这个数字只统计分发量,无法证明每次下载都转化成暴露的安装实例,也无法证明每台暴露主机都已遭利用。[6]
因此,响应工作需要建立两份清单。
第一份是制品清单:安装的是哪个版本、从哪个渠道取得、采用什么配置?对于尚未遭入侵的系统,升级到 1.930 会移除已知漏洞代码。第二份是主机入侵清单:这条远程命令路径是否曾在一项可从互联网访问、拥有 root 权限的管理服务上被触发?若某台主机无法排除已遭利用,单独替换一个 Webmin 软件包无法撤销已经执行的命令、已经安装的持久化手段或已经读取的凭据。同期独立分析因此把完整重建主机和轮换凭据列为审慎的恢复措施。[6]
Webmin 项目自身的响应集中在发行流程上。构建过程改为只使用已提交到 GitHub 的代码,停止使用本地同步目录;项目轮换了旧构建系统能够接触的密码和密钥,并审计此前一年间的 GitHub 提交,查找其他可疑改动。[1] 三项措施分别对应三类风险:未声明的构建状态、凭据暴露风险,以及未经审查或带有恶意内容的源码改动。
把删除写进发行设计
对于小型项目,一套复杂到无人能够维护的供应链平台没有实际意义。发行路径本身应当允许每次构建前删除其中的重要状态。
一套可靠的最低配置如下:
- 将获批标签解析为确切提交,并记录该提交 ID。
- 创建新工作目录;禁止复用或恢复上一次任务的工作树。
- 从已经固定版本或经过完整性检查的引用获取已声明的依赖与工具链。
- 构建软件包,并记录输入清单和输出摘要。
- 再做一次比对:条件允许时重新构建;否则至少解包制品,逐项核对它与源码树之间每一处预期差异。
- 由另行管理的发布身份对最终摘要签名,或批准以该摘要所指制品发布;发布操作锁定摘要,不接受工作节点上的任意路径。
- 将清单、日志、签名和制品保存在工作节点管理权限覆盖范围之外。
可复现性会加强这条链,但无法取代源码审查。若恶意提交本身就是声明的输入,两套独立构建环境都会如实复现它。另一方面,归档文件含有时间戳或生成的元数据时,逐字节比对也需要先做归一化处理。有效的控制来自经解释、可核验的源码与发布字节对应关系,一条例行命令本身没有同等效力。
现实条件同样有限。一个由两名维护者组成的项目未必配有独立发布工程师、硬件签名设备或两套隔离的构建环境。它仍可让工作节点保持临时性,把发布 token 隔离在拉取请求任务之外,要求使用经过审查的标签,清点打包文件,并把签名校验和保存在软件包托管方无法静默改写的位置。这些规模不大的控制措施直接针对 Webmin 事件中的传播方式。
补救措施可以用一项具体测试来验证:删除发布工作节点,在空环境中克隆获批提交,再执行已声明的构建。若产出相同且可以解释的制品,同时发布权由另一身份掌握,那么,从备份复制旧工作目录便失去了延续这一攻击路径的条件。
在已知传播链条里,后门进入 Webmin 新服务器不需要第二次巧妙入侵。一次普通的恢复操作,继承了早先入侵留下的结果。这正是本案的价值所在:危险步骤看起来只是在维持连续性。可信发行的起点,是决定哪些状态值得保留下来。
来源
- Webmin,“Security”——维护者对受影响版本、2018 年 4 月与 7 月的修改、9 月构建服务器替换、2019 年 8 月的响应及构建流程变更所作的说明。
- Webmin 邮件列表,“Huge difference between sourceforge release and github release”,2019 年 5 月 11–12 日——同期讨论确认 SourceForge 归档包是官方打包发行版,GitHub 归档包则因正常原因存在差异。
- CVE Program,“CVE-2019-15107”——规范记录,描述通过
password_change.cgi的old参数实施的命令注入。 - CVE Program,“CVE-2019-15231”——遭拒绝的重复记录,其中说明应改用 CVE-2019-15107。
- Thomas Claburn,The Register,“Dear planet Earth: Patch Webmin now”,2019 年 8 月 19 日——同期独立报道,收录开发者对构建路径、恢复目录、Perl 注入、漏洞披露和取证限制的说明。
- Dan Goodin,Ars Technica,“The year-long rash of supply chain attacks against open source is getting worse”,2019 年 8 月 21 日——独立分析软件包的传播范围、配置条件与主机恢复事项。
- RobH,“Wmf sdtpa servers 2009-01-20 34.jpg”,Wikimedia Commons——本文所用 2009 年 1 月真实服务器集群照片的来源页面。