经过一周时间的 log4j2 RCE 事件的发酵,事情也变也越来越复杂和有趣,就连 log4j 官方紧急发布了 2.15.0 版本之后没有过多久,又发声明说 2.15.0 版本也没有完全解决问题,然后进而继续发布了 2.16.0 版本。刚刚熬夜费劲升级了 2.15.0 版本的小伙伴们是不是心中一万个小羊驼奔过。然而在网络上也有各种文章和博客进行激烈的讨论,例如高版本的 JDK 也可以避免,开启 formatMsgNolookups 的方案,还有log4j1 可以幸免于难等等各种说法。在这里笔者针对这些讨论详细介绍一下,JDK 高版本是否可以避免,开启 formatMsgNolookups 的方案是否可行,log4j1 是否真的可以幸免于难。
JDK高版本是否可以避免问题
在上一篇文章里,我们以 RMI 下载远程的恶意代码的方式介绍如何复现 RCE,这里有一个前提就是被害方的 JVM 里打开了 RMI/LDAP 等协议的 truseURLCodebase 这个属性设置。由于在 java8u121 以上的版本中这些属性默认就是没有开启的,所以网上有了这样的讨论,那么事实我们真的就没有办法了么?被害方的机器环境里得到 RIM 的 reference 对象之后首先会在本地尝试加载该对象指定的类,如果没有就去该对象中指定的远程地址中去加载,那些属性只是在远程加载类的时候起作用。那么如果被害方的机器环境能找到一些可以运行危险代码的类,这样就可以绕过 java 版本的问题了, 原理如下图:
根据上面的原理,还真有这样的类可以被利用,这些类就是:
1. org.apache.naming.factory.BeanFactory
2. javax.el.ELProcessor
而这两个类就在 spring boot 的内置 tomcat 服务器里可以找到,惊不惊喜,意不意外,是不是顿时觉得自己又中招了。大名鼎鼎的 spring boot,有几个 java 应用没有使用这个框架呢,下面展示了内置 tomcat 服务器中的这些类:
下面来介绍如何利用这两个类触发 log4j 的 RCE漏洞和原理,RMI 服务端和触发的客户端程序例子如下图:
- 在构造 reference 对象的时候指定这两个类,然后最后一个参数可以为空,因为最后一个参数是远程地址,这里我们要的是本地寻找。
- 对于 ELProcessor 类中有一个叫 eval 的方法,该方法接受一个字符串参数,然后就可以当代码运行,该例子中就是利用这个类运行危险代码 System.exit(1) 。
- 构造相关的对象来传入 fourceString 和 testFlag=eval。其中 fourceString 是必须这样写的,testFlag 可以随意的,eval 表示在指定的 ELProcessor 类中寻找参数为 String 类型的 eval 方法。
- 构造相关的对象传入 testFlag 和 危险代码 System.exit(1)。其中 testFlag 是必须要和上面步骤当中的 testFlag 对应的,System.exit(1) 表示一旦找到上面的 eval 方法就传入这样的参数,利用反射运行。
- 其中上面介绍的所有的逻辑都是被定义在 org.apache.naming.factory.BeanFactory 这个类中的,总结起来就是该类会实例化一个 ELProcessor 对象(由黑客可以控制的 reference 对象设置),然后寻找该类中一个参数类型为 String 名字叫 eval 的方法(由黑客可以控制的 reference 对象设置)。找到之后利用反射调用该方法,并传入参数 System.exit(1) (由黑客可以控制的 reference 对象设置)。这样就实现了 RCE,其中 BeanFactory 核心代码如下:
根据上面介绍,java 版本在比较高的情况下可以避免也是有前提的,那就是你的本地的 JVM 环境中没有上面两个类的引用。但是我们已经看到了,这两个类就在 spring boot 框架中的内置 tomcat 服务器中,所以还是有不保险的机会。
开启 formatMsgNolookups 是否可以
还有的方案说开启 formatMsgNolookups 就可以避免这个问题,那么情况真的是这样么。笔者在开启该设置的前提下,依然复现了问题:
- 在 log4j2 的配置文件里把日志的 pattern 配置一个可以从线程上线文中取数据的方式。这种配置应用很广的,由于服务调用在一个线程里有一串方法,这样从线程上下文中取数据就很方便,例如记录用户的名称等等。
- 黑客在线程的上下文中注入危险调用 ${jndi:rmi://127.0.0.1:1033/refobj}。
- 这种方式能够被执行的原理就是因为 formatMsgNolookups 设置只是针对日志的信息限制不做查询,但是日志的配置还是做查询的。所以一旦黑客通过上下文的方式把危险调用注入到配置中,一样会触发 RCE。
- 所以说开启 formatMsgNolookups 设置也是有前提的,那就是你的日志配置里没有在线程上下文里取数据,不给黑客注入危险调用到配置中的机会。
Log4j2 2.15.0 的隐患是什么
官网已经明确说明 2.15.0 版本有问题,那么隐患是什么呢?引用官网的一段原文如下:
- 官网已经说的很清楚了,查询功能在日记消息里默认被禁用了,但是在配置里还在。单纯就这点来说,它是和和上面介绍的 formatMsgNolookups 情况一样,但是默认的 JNDI 调的白名单是本机,这样就避免了连接黑客 host 的 rmi/ladp 服务。
- 这样就没有问题了么,试想在配置有在上下文中取值的情况下黑客注入一个在本机的 jndi 调用,那么就会去连接本机的服务。本机当然没有这个服务了,然后就会有 tcp 握手不上一直等到超时的情况。请注意默认 socket 去连接 jndi 是同步的,不是异步的。所以当前线程会被这个一直等到超时的网络握手占用,而服务器的线程是有限的,一般就几百个。如果黑客大量做这样的注入,那么整个服务的线程资源都会被这样的网络握手占用,而正常的请求却没有线程资源使用了。
- 所以说 2.15.0 版本在某些情况下还是有不保险的。
Log4j1 就可以幸免于难么
有的讨论说 log4j1 版本就可以避免这个问题的发生,但是事实真的是这样么?在下面两种情况下 log4j1 也有可能被攻击,首先是我们有向消息队列中发送日志:
- 在配置文件里配上 JMSAppender,然后再把 TopicBindingName 其中这个属性和 TopicConnectionFactoryBindingName 属性的值配置成危险调用。
- 由 JMSAppender 源码发现,会对上面步骤中的两个属性做 JNDI 查询,所以也是有风险的。
- 当然这个风险是有前提的,就是我们需要把日记发送到消息队列,然后黑客还可以修改到生产上的配置文件。所以这里一定是内部黑客,如果可以修改这个配置文件,也意味着可以修改更危险的东西,所以这是属于内部黑客的防范措施。
我们再来看看 log4j1 中的 socket server,它是可以启动一个基于 tcp 的 socket 网络服务,用来接受远端发送过来的日志:
- 从源码里我们发现这个服务是有反序列化对象的操作的,就在 82 行。然而这个反序列化读取对象的操作却直接使用了 ObjectInputStream 对象,压根就没有对一些危险的类做反序列化白名单过滤。
- 所以这也是有风险的,当然它的前提是我们有使用接收远端日志的这个服务。
目前先我们写到这里,这个漏洞自打暴露以来网络上有各种各种的讨论和方案,但是最终要的是不盲从,不跟风,不人云亦云。问题确实严重,我们需要的是实实在在分析,找到根本原因。由上面分析看每种情况都有特定的条件,我们一定要在找到每种情况的根本原因基础上,结合实际能否复现的条件来做出合理的修复方案。