复习完 JNDI 之后想专门过来学一下 log4j
demo 实现与具体组件分析
0x01 环境搭建
在分析漏洞之前 ,log4j 的具体工作流程和组成很有必要去了解和熟悉
先把测试环境搭建好
依赖
1 | <dependency> |
log4j 的配置和其他很多组件很像,都是通过获取一个配置文件中的内容来进行具体配置的。这里我们用 xml 配置文件来写 log4j 的配置
通常分为两个部分:1.设置日志信息输出目的地 2.定义 logger,也就是定位我们需要打日志的包中
1 |
|
然后写个测试类
1 | import org.apache.logging.log4j.LogManager; |
运行一下就能打印出很多信息
然后在当前项目下应该会有一个 logs 文件夹,用来存放日志信息
0x02 log4j2 功能组件分析
1x01 日志记录/触发点—-AbstractLogger
通常情况下我们是使用LogManager.getLogger()
方法来获取一个 logger 对象,然后通过调用 logger 对象的 debug/info/error/warn/fatal/trace/log 等方法记录日志等信息
在这些方法中,都会先使用org.apache.logging.log4j.spi.AbstractLogger#logIfEnabled
的若干重载方法,根据当前配置文件中的配置信息中记录的日志等级,来判断是否需要输出 console 以及日志记录文件,log4j 中的日志记录等级默认如下: ALL < DEBUG < INFO < WARN < ERROR < FATAL < OFF ,然后默认输出的是 WARN/ERROR/FATAL 等级的日志信息,我们也可以在配置文件中修改配置日志输出等级:
具体是在设置 logger 的时候,我们指定它的 level 等级即可,比如我们上面环境实现的等级设置为 TRACE 等级
当然也可以用代码直接去配置
1 | LoggerContext ctx = (LoggerContext) LogManager.getContext(false); |
logger 本身是一个接口,我们待会分析漏洞的入口就是它的一个抽象实现类 AbstractLogger 开始的,只要调用了 info,error,warn 等方法都可以被作为漏洞的触发点。不同点是配置的输出等级不同
1x02 消息格式化—-MessagePatternConverter
log4j2 采用org.apache.logging.log4j.core.pattern.MessagePatternConverter
来对日志信息进行处理
先看一下初始化
在初始化MessagePatternConverter 过程中会从 Properties 和 options 获取配置来判断是否需要调用 lookups 功能
其中FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS
的获取是通过工具类的 getBooleanProperty
方法来获取的
他这里的默认值传进来是 false,然后在默认 options 为空的情况下,loadNoLookups
方法运算出来为-1,所以Constants.FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS || noLookupsIdx >= 0
运算出来为 false,noLookups
的默认值就为 false,也就是 lookups 功能默认是开启的
继续看下面的方法loadMessageRenderer
方法通过 options 中的字符配置来获取相应的模板渲染器
format 方法有两段 if 判断的内容
第一段 if 判断的内容是先从 event 中获取到 message,然后判断 message 的类型是否为 StringBuilderFormattable,之后就是渲染的具体内容
第二段 if 判断的内容是重点,在一个正常请求的情况下:config 获取不为空,并且 noLookups 默认为 false 的情况下,标志着我们可以通过 lookups 功能来进行字符串解析
然后这里的解析功能的重点是在 config.getStrSubstitutor().replace(event, value)
中
1x03 字符替换—-StrSubstitutor
log4j2 中提供的 lookup 功能的字符替换的关键处理类就是StrSubstitutor
,位于org.apache.logging.log4j.core.lookup.StrSubstitutor
,现在来看它的具体内容
光是默认属性配置就需要重点注意,默认的PREFIX
前缀是${
,默认的后缀}
,默认赋值分隔符DEFAULT_VALUE_DELIMITER_STRING
StrSubstitutor 中的关键方法是substitute
,也是整个 lookup 功能的核心,用来递归替换相应的字符
由于该方法的内容较多,这里我们只截取关键性内容
方法的开头先把各个前后缀以及内容的匹配器加载上
然后通过 while 循环来找前缀,这里找的前缀是最开始的前缀
找完前缀再找后缀,不过在找后缀的 while 循环中,又判断了是否替换变量中的值,如果替换,则再匹配一次前缀,如果找到了前缀,则 continue 跳出循环,再走一次找后缀的逻辑,比如说这个${${}}这种情况
后续的逻辑中,主要是针对DEFAULT_VALUE_DELIMITER_STRING
以及ESCAPE_DELIMITER_STRING
进行,通过多个 if/else 来匹配:-
和:\-
这里就不一一分析代码了,这里其实就是对两个标识符的功能的描述:
:-
是一个赋值关键字,如果程序处理到${aaaa:-bbbb}
这样的字符串,处理的结果将会是bbbb
,:-
关键字将会被截取掉,而之前的字符串都会被舍弃掉。:\-
是转义的:-
,如果一个用a:b
表示的键值对的 key a 中包含:
,则需要使用转义来配合处理,例如${aaa:\\-bbb:-ccc}
,代表 key 是aaa:bbb
,value 是ccc
在没有匹配到变量赋值或者匹配结束后,将会调用resolveVariable
方法来解析满足 lookup 功能的语法,并执行相应的 lookup
我们跟进到 resolveVariable 方法
他这里会先获取到variableResolver
,然后调用其 lookup 方法进行处理。实际上variableResolver
是一个代理类Interpolator
,之后再深入了解
分析这些内容,我相信此时应该能感受到StrSubstitutor
的substitute
方法的重要性,它是直接解析 payload 的处理方法,通过在这里下断点/Hook 点,能够最直接的分析到 payload 的处理,以及日后的防御处理
1x04 lookup 处理—-Interpolator
刚才提到,variableResolver 实际上是一个代理类org.apache.logging.log4j.core.lookup.Interpolator
,他来代理所有的 StrLookup 实现类,也就是说我们在调用 Lookup 的时候,都是由Interpolator
处理和分发的
看一下默认结构
在 interpolator 的构造方法中,它创建了一个strLookupMap
键值对表,对一些默认情况的 Lookup 查询进行了装载,这里的 JNDI-Lookup 查询是通过下面的 try catch 块实现的
再看最为关键的 lookup 方法具体的内容
它通过:
作为表示符,用来分隔 Lookup 关键字和参数,从 strLookup 中根据分割出来的关键字匹配到相应的处理类,并调用其 Lookup 方法
log4j 的漏洞触发是通过jndi:
关键字来触发 JNDI 注入漏洞的,jndi:
关键字对应的处理类是org.apache.logging.log4j.core.lookup.JndiLookup
,我们跟进查看具体它的 lookup 方法是如何被调用的
先是获取了 jndiManager
,然后再调用 jndiManager
的 lookup 方法
我们先跟进查看jndiManager
是如何被创建的
最终跟进到 AbstractManager
的 getManager
方法中,通过 JndiManagerFactory 来创建
不过这个 JndiManagerFactory 本身就是在 JndiManager 的子类中
具体看 createManager 的内容
实例化的的时候带着InitialContext
创建的,看到这个InitialContext
就想到后续的一系列调用
而 JndiManager 的 lookup 调用方法就是直接调InitialContext
,并且参数 name 是我们可以控制的
最终 sink 点也是在这
漏洞分析
0x01 漏洞复现
影响版本:2.x <= log4j <= 2.15.0-rc1
先把 Ldap 服务器本地开一下
1 | import com.sun.jndi.rmi.registry.ReferenceWrapper; |
恶意类的内容是 runtime 执行一个计算器
然后看 exp
1 | import org.apache.logging.log4j.LogManager; |
执行完毕
0x02 流程分析
调试前的心里准备:
首先我们要明确,log4j2 漏洞的触发点是在字符串处理阶段触发的,所以在流程调试前期的到日志打印的流程都是不需要跟进的,明确我们要首先跟进到 MessagePatternConverter 的字符转化 format 方法中,然后记住如果有分支已经要返回出去打印了就不需要跟进了
在 info 处打个断点
由于AbstractLogger 的日志功能方法有若干重载方法,以及各种数据处理,师傅们可以参考我的调用栈,避免不必要的时间浪费
最近的调用可以打在 PatternLayout
类的 toText
方法中
然后跟进到 toSerializable 中,到这就是熟悉的 format 方法了,但是还没到MessagePatternConverter
类,所以还需要继续深入跟进
而且这里会有几次循环调用,并且由于信息类型不同,调用的Pattern 就不同,直到第八次循环(可能 exp 或者版本不同循环次数也不同?)
才是我们想要的 MessagePatternConverter 的 format 方法,跟进
这里 format 的内容就不讲了,直接到 Substitutor 的字符串处理逻辑即可
跟进到 replace 方法
这里获取了 StringBuilder 之后开始调用substitute
关于substitute
的整体逻辑我们在StrSubstitutor 的详细功能分析中已经分析过了,这里直接到下一阶段的跟进点,也就是将字符串经过前后缀获取以及删减,最终在没有匹配到变量赋值或者匹配结束后resolveVariable
方法来解析满足 lookup 功能的语法,并执行相应的 lookup 逻辑
这里流程循环较多,因为字符较多,他又会根据解析程度再次循环截取处理
师傅们可参考的我这边的进度来调
来到resolveVariable
,继续跟进
之前也提到过variableResolver
实际上是一个代理类 interpolator(StrLookup 键值对表中的所有功能的 lookup 功能都是通过它来代理),所以这里会直接调进Interpolator
的 lookup
继续跟进,也是从 strLookupMap 中获取到 JndiLookup 之后开始调用其 lookup 了
继续跟进到JndiLookup 的 lookup
由于创建JndiManager 时自带构建 initialContext,并且这里的调用 JndiManager 的 lookup 就是调用 InitialContext 的 lookup,之后就是熟悉的 JNDI 注入的原始的流程了
基本流程 sink 分析结束
(师傅们如果在看我这篇文章学习的话可以先尝试把 log4j2 功能组件的分析先了解深一点,后续跟进调试会轻松很多)
高版本绕过分析
0x01 rc1 及绕过
1x01 安全更新点分析 1
在漏洞披露后,log4j2 官方发布了 log4j-2.15.0-rc1 安全更新包,经过师傅们进一步分析,在开启 lookup 配置后可以被绕过 (危害性较低,一般都不会再去开启 lookup 功能了)
回忆一下漏洞之初的样子,我们在MessagePatternConverter 中是默认开启了 lookup 选项的,但是从 rc1 之后,它的源码如下:
然后再看看之前的构造方法
构造方法中赋值的操作被拆分了,并且多了很多子类,将之前的功能模块化了
也就是说在 newInstance 实例化方法中,他会根据判断条件来选择返回出哪一个 Converter,假如说我们还想跟进到之前的流程就必须要走到LookupMessagePatternConverter
子类中LookupMessagePatternConverter
返回的条件是 lookups && config != null
,lookups 的获取就是新增的安全更新点,不再提供从 properties 中获取 lookups 配置的选项,而是直接从 loadLookups 方法中获取
这里 LOOKUPS 值为”lookups”字符串,options 默认为空,那么也就进不去 if 判断,默认 return false,lookup 功能默认不开启
1x02 安全更新点 2
还有一个最直接的就是在 jndiManager 的 lookup 方法上,以及jndiManager 的创建上
具体内容如下:
不再使用 InitialContext,而是使用子类 InitialDirContext,并为其添加白名单 JNDI 协议、白名单主机名、白名单类名 ,其中 permanentAllowedHosts 是本地 IP,permanentAllowedClasses 是八大基础数据类型加 Character,permanentAllowedProtocols 包含 java/ldap/ldaps。
关键函数 lookup 上也加了限制,不过在某些版本上 catch 块中没有 return,逻辑判断失误,之后依然可以调 context 的 lookup,所以只要想办法直接走到 catch 块中即可
不过我们这边更新的版本没有。。。。
所以这里就复现不出来,其实这里的区别就是 rc1 和 rc2 的区别了,之后的版本由于默认 lookup 功能被关闭了,甚至直接移除了日志对 lookup 功能的支持,所以研究的价值不大了
个人总结
这次的学习并不涉及 payload 绕过等操作,比如关键字截取,嵌套,带外等操作,感兴趣的师傅可以去
https://su18.org/post/log4j2/#%E5%89%8D%E8%A8%80 su18 师傅的博客
https://drun1baby.top/2022/08/09/Log4j2%E5%A4%8D%E7%8E%B0/#Log4j2-%E5%A4%8D%E7%8E%B0 drunkbaby 师傅的博客
log4j 分析暂告一段落了,从 JNDI 的复习开始,有了一种新的感觉,新的学习的感觉,之前听队里的师傅说之前的漏洞调流程的时候要能够自己调出来,我并没有将这个事在自己心里强度,只是默认自己该这么做,也觉得自己现在可以有这样的能力,但还是缺少很多感觉,要走的路还有很长