最近,多数读者可能已经了解到了一些关于 Log4j 漏洞的信息,这个漏洞自从 12 月 9 日披露出来之后,不断给系统管理员增加烦恼。这个漏洞比较容易利用,可以用来进行远程代码执行,而且整个互联网上的服务器中到处都有这个问题。把它称为近年来被披露的最糟糕的漏洞之一,并不算夸张。从某种意义上来说,从 Log4j 中得到的教训并没有什么新的内容,但这个漏洞确实进一步明确地突出了自由软件生态系统中的一些问题。
出了什么问题?
有很多文章详细描述了这个 bug 的机制,以及如何利用,在这个 页面上就收集了很多。简而言之:Log4j™是一个 Java 日志软件包,由 Apache 软件基金会注册了商标并进行发布。它已经被包含进了许多其他项目,在互联网上到处都可以找到 Log4j。事实上,根据这篇文章,Log4j 已经被下载了超过 2800 万次(仅仅统计了过去四个月里的情况)并且是将近 7000 个其他项目的依赖项。因此,Log4j 中的漏洞很可能就会成为碰巧使用了它的那些其他系统中的漏洞。
正如 Apache 软件基金会在 6 月份自豪地发出的一条 推文所说,它甚至也存在于火星上的灵巧号直升机之中。
通常情况下,人们认为一个日志工具应该只是把感兴趣的数据接收下来并可靠地记录下来。Log4j 似乎做到了这一点,但它也做了一些可以说是任何日志系统都不应该做的事情: 它主动地对要记录的数据进行解释、并相应地采取行动 。它可以做的事情之一就是查询远程服务器的数据,并将其纳入日志信息中。例如,它可以从LDAP服务器获取信息并加入到日志中,这样在人们希望在日志中加入某个用户的详细账户信息的数据时,这个功能就会很有用。
然而,事实上远程目录服务器可以提供更多形式的数据,比如说可以提供一个序列化处理(serialized)的 Java 对象,被后续重新组装起来进行执行。这就把这个功能变成了一个向正在运行的应用程序注入代码的一种方式,而本来这个日志工具只是希望记录一些数据而已。为了利用这个漏洞,攻击者需要做两件事:
架设一个精心构建好的运行相关协议的服务器,确保被攻击的目标系统能访问到这个服务器。LDAP 似乎是目前的攻击首选协议,但其他协议也同样可能受到攻击。对 LWN 服务器的日志进行筛选后,就看到也有尝试利用 DNS 协议的。
说服要攻击的目标系统来记录一个由攻击者提供的包含特殊“咒语”的字符串,这样就能从恶意服务器中加载和执行对象了。
上面的第二步看起来困难,其实很容易。许多系统都会乐意记录用户所提供的数据。这些恶意字符串可能是伪装成最终会出现在日志中的用户名,而另一个流行的选择是伪装成浏览器的用户代理字符串。一旦目标上钩,记录了恶意字符串,游戏就结束了。
换句话说,这是一个 对来自互联网所提供的未经清理的数据直接使用 的典型案例,后果可想而知。在任何一个正常审查过程中都应该发现这个问题。请注意,恶意字符串也可以是由前端软件传递给内部系统的,然后内部系统可能决定要不要记录。换句话说,仅仅是不直接暴露在互联网上,并不一定就能对这个脆弱的系统实现充分的防御。每一个使用 Log4j 的系统都需要进行修复,要么升级,要么就采用上面链接中提供的文章中所说的某种其他缓解措施。请注意,最初的修复措施已被证明不足以解决 Log4j 的所有问题,用户需要不断保持这个工具的最新更新。
对这个漏洞的响应是迅速而且强烈。一些评论家断言“开源已被打破”。之前如果没有看过xkcd #2347的话,现在碰到的其实就是这种情况。我们的社区是否像某些人所说的那样出现了严重的问题?简而言之,这一幕之中似乎展示了开源项目的两个广为人知的缺点,分别跟 依赖性 和 维护者 有关。
太多依赖
在自由软件的早期,根本就没有什么自由代码(free code),所以几乎所有内容都要从零开始写。因此,在那个时候,几乎没有什么(脆弱的)软件包可供人们免费下载和使用,所以每个项目的安全漏洞都是自己写出来的。社区就面临这一挑战,即使在过去那些人们都比较单纯的日子里,也经常看到安全问题。
我在这一领域浸淫的时间有多长(比我愿意承认的时间还要长),那么开发者和学术界谈论可重用软件的好处就有多久。多年来,人们的这个梦想确实已经实现了。许多语言的社区已经为各种常见的(和不常见的)工作积累了大量的模块。编写程序往往只是需要找到正确的模块并将它们正确地组合在一起。用来从模块库自动下载的这些接口,使得获取所需模块(以及它们进一步所依赖的模块)的过程都自动化了。对于我们这些很久以前就习惯了不断重复运行 ./configure
然后手动安装下一个尚未就绪的依赖模块的人来说,现代化的开发环境简直就是对我们过去做法的碾压。当初我们的困难,现在已经不复存在了。
这对我们的社区来说是一个巨大的成就。我们已经创造了可以在其中快乐工作的开发环境,它允许我们以几十年前完全无法想象到的生产力水平来继续工作。但是,这里潜伏着一个问题:这种结构使得项目很容易积累对外部模块的依赖,而每个模块都可能带来一些自身的风险。事实上你是在将互联网上的一段随机代码直接导入到你自己的程序之中,这可能会出现很多种后果。也许其中的某个模块可能干脆就是一个公开用来进行恶意攻击的模块(就像 event-stream 事件),也许这个模块就是直接消失了(left-pad 案例),或者它仅仅是不再受到重视和维护了,就像 Log4j 这次的情况。
当一个人所使用的东西非常重视质量时,人们往往会去使用已知的品牌。Log4j 是在 Apache 软件基金会的品牌下开发的,因此人们认为这能代表着它有着很好的质量,并且也在积极维护之中。不过,外表经常是可以欺骗人的。我们都不用提 Apache OpenOffice,它一直在持续被人们下载和使用,尽管多年来几乎完全没有针对它所进行的 开发工作了。不过,OpenOffice 的用户很欣慰地获悉(根据该项目 2021 年 10 月的报告)OpenOffice 终于提出了新的使命的宣言草案。Log4j 比它还是要更活跃一些,但它仍然是依赖于无偿工作的维护者的个人空闲时间的工作。无论是否有 Apache 品牌,这个被广泛依赖的项目都没有人付费来支持维护工作。
但是,哪怕今后品牌真的能代表着更高的可靠性,可是很难在拥有数百个依赖库之后仍然保持稳定。一个依赖库,在被采用时可能看起来很稳固、维护得很好,但是在一两年后看起来就不那么吸引人了。但是这些缺乏良好维护的项目往往不会引起人们的注意,直到出了严重的问题才为人所知。这种项目的用户可能不会意识到风险在增加,直到最终发生问题时才知道,已经为时已晚了。我们的工具使得添加依赖关系变得很容易(甚至我们可能都没有意识到有这些依赖);但在评估现有的依赖模块的当前维护状态是否良好这个方面,工具却没有提供多大的帮助。
维护者
还有一个相关的问题,就是对这些人们所严重依赖的项目的开发和维护工作缺乏支持。过去常说的自由软件和自由小狗(free software and a free puppy)之间的比较仍然是正确的:小狗当然很好,但如果有人不注意的话,它们肯定会在地毯上撒尿、咬坏你的鞋子。很容易就能利用自由软件的免费的特性,来加入大量功能强大的代码,但每一个依赖项其实都是一只小狗,需要有人看管。
作为一个社区来说,我们获取小狗的能力远比训练它们的能力要强。各个商业公司总会很高兴地把我们所提供的软件收下来,但是却不觉得有必要对他们系统中最关键的组件给出一些贡献和支持。事实上,我们都是这样做的,没有人可以对他们所依赖的每一个项目都给出支持。我们从自由软件中得到的东西远远多于我们可能投入的东西,这当然是一件好事。
尽管如此,在我们的生态系统中的企业一方事实上也不应该这么快就把自由软件的这些好处视为理所当然的。如果一个公司在自由软件的基础上建立了一个重要的产品或服务,那么该公司就应该确保该软件得到了良好的支持,在有必要的时候就应该站出来使之成为现实。一般来说,这样做总是正确的做法,但这远远不是一种利他主义(altruistic act)的行为。否则的话,就会不断地出现类似 Log4j 这样的危机。正如目前许多公司都意识到的,这种危机都会付出昂贵的代价。
“站出来” 就意味着像支持开发者一样支持维护者。最严重的问题往往出在维护者这一边。即使是像 Linux 内核这样的项目,有成千上万的开发人员正在为他们所做的工作而获得报酬,但却 很难找到对维护者的支持。公司最好的观点也是认为维护者的工作就是一些开销;最坏的观点是认为支持维护者就是在帮助竞争对手;而通常的观点来说都认为这是别人的责任。很少有公司会对他们的员工担任维护者这个工作给出奖励,所以他们中的许多人最终都是在用自己的时间来做这些工作。而工作的成果也许会有数百万次下载,而这个维护工作其实是在某人的空闲时间内完成的(如果它真的完成了的话)。
这些问题并不是自由软件所特有的。也不是没有过发现某个专有软件并不像所宣称的那样得到很好的支持。自由软件,至少,即使是在其创造者并不配合的情况下,也可以得到修复。但是,我们社区创造的大量软件使其中的一些问题变得更糟糕。有大量的代码需要维护,而许多用户却没有动力去帮助实现这一目标。我们大概会在未来某个时间点能处理好这些问题,但现在还完全不清楚该如何处理。在这之前,我们只能将继续向火星(以及更远的地方)部署那些缺乏支持的软件。