您现在的位置是:首页 > 文章详情

CVE-2021-45105:Log4j2 拒绝服务漏洞分析

日期:2021-12-22点击:558

报告编号:B6-2021-122201

报告来源:360CERT

报告作者:360CERT

更新日期:2021-12-22

漏洞简述

2021 年 12 月 20 日,360CERT 监测发现Apache 官方发布了Apache Log4j的风险通告,漏洞编号为CVE-2021-45105,漏洞等级:高危,漏洞评分:7.5。目前官方已发布安全版本。这是最近在 Log4J 中发现的第三个安全漏洞。

Apache Log4j 是一个基于 Java 语言开源的日志框架,已于 2015 年 8 月 5 日停止维护。Log4j2 是其重构升级版本,引入了大量丰富的特性,可以控制日志信息输送的目的地为控制台、文件、GUI 组件等,并通过定义每一条日志信息的级别,使其能更加细致地控制日志的生成过程。

对此,360CERT 建议广大用户及时将Apache Log4j升级到最新版本。与此同时,请做好资产自查以及预防工作,以免遭受黑客攻击。

风险等级

360CERT 对该漏洞的评定结果如下

评定方式 等级
威胁等级 高危
影响面 有限
攻击者价值
利用难度
360CERT 评分 7.5

影响版本

组件 影响版本 安全版本
Apache Log4j 2.0-alpha1 ~ 2.16.0 2.17.0 , 2.12.3 , 2.3.1

漏洞详情

该漏洞需要在特定的配置下才能进行利用,当PatternLayout使用$${ctx:xxx}context上下文获取用户输入的时候才能进行利用。相关配置参考:

https://logging.apache.org/log4j/2.x/manual/lookups.html

测试的配置文件为:

 <?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d %p %c{1.} [%t] $${ctx:loginId} %m%n"/> </Console> </Appenders> <Loggers> <Root level="error"> <AppenderRef ref="Console"/> </Root> </Loggers> </Configuration>

在初始化Logger对象的时候,PatternParser会解析配置文件,解析之后将${ctx:loginId}赋值给LiteralPatternConverter

接着看到PatternLayout#toSerializable方法,遍历converter

 @Override public StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) { for (LogEventPatternConverter converter : converters) { converter.format(event, buffer); } return buffer; }

MessagePatternConverter已经删除了lookupconverter

 if (LOOKUPS.equalsIgnoreCase(option) || NOLOOKUPS.equalsIgnoreCase(option)) { LOGGER.info("The {} option will be ignored. Message Lookups are no longer supported.", option); }

将目光放在LiteralPatternConverter,依然会对表达式进行解析,解析的是literal,也就是${ctx:loginId}

 public void format(final LogEvent event, final StringBuilder toAppendTo) { toAppendTo.append(substitute ? config.getStrSubstitutor().replace(event, literal) : literal); } 

跟到replace里,调用StrSubstitutor#substitute

解析逻辑

这里会提取${到第一个}之间的字符串,ctx:loginId,接着调用StrSubstitutor#resolveVariable

 String varValue = resolveVariable(event, varName, buf, startPos, endPos); ... protected String resolveVariable(final LogEvent event, final String variableName, final StringBuilder buf, final int startPos, final int endPos) { final StrLookup resolver = getVariableResolver(); if (resolver == null) { return null; } return resolver.lookup(event, variableName); }

lookup里尝试获取协议,这里是没有jndi协议的,默认情况下已经不提供了。

这里是调用ContextMapLookup#lookup,这里调用event.getContextData()

 public String lookup(final LogEvent event, final String key) { return event.getContextData().getValue(key); }

这里的contextData就是官方通告所说的攻击者需要能控制Thread Context Map里面的内容,可以参考官方文档:

https://logging.apache.org/log4j/2.x/manual/thread-context.html

我们看到 3,put的是session里的loginId,而配置文件里是${ctx:loginId}key是一样的,于是在调用event.getContextData().getValue(key)时,就会提取到攻击者输入的loginId赋值给varValue。 如果varValue不为null,继续解析。

 String varValue = resolveVariable(event, varName, buf, startPos, endPos); if (varValue == null) { varValue = varDefaultValue; } ... if (varValue != null) { // recursive replace final int varLen = varValue.length(); buf.replace(startPos, endPos, varValue); altered = true; int change = substitute(event, buf, startPos, varLen, priorVariables); ... } 

substitute递归解析攻击者输入的内容,在checkCyclicSubstitution方法里:

 private void checkCyclicSubstitution(final String varName, final List<String> priorVariables) { if (!priorVariables.contains(varName)) { return; } final StringBuilder buf = new StringBuilder(BUF_SIZE); buf.append("Infinite loop in property interpolation of "); buf.append(priorVariables.remove(0)); buf.append(": "); appendWithSeparators(buf, priorVariables, "->"); throw new IllegalStateException(buf.toString()); }

这里priorVariablesvarName的获取规则,以 DOS 漏洞的 poc 为例,递归完之后,退出递归的第一层,得到的是::-${::-j}

这里会去匹配:-:-之前的部分就是varName,即:,而:-之后的就是value,即${::-j}

然后,将这一次去掉${}之前的内容存到priorVariables里,即::-${::-$${::-j}},然后调用checkCyclicSubstitution方法,检测varName是否在priorVariables里,这里就是无限递归的关键点,如果存在,那么就会抛出无限递归错误。

接着看代码

 //此时,varName是:,无法解析出协议,即varValue赋值null String varValue = resolveVariable(event, varName, buf, startPos, endPos); if (varValue == null) { //varDefaultValue 是 ${::-j} varValue = varDefaultValue; } if (varValue != null) { // recursive replace final int varLen = varValue.length(); buf.replace(startPos, endPos, varValue); altered = true; //继续递归,注意这里和之前递归提取`${}`内部字符串是不一样的,传入了赋值的priorVariables,递归`${}`内部的字符串时,priorVariables不传,也就是null int change = substitute(event, buf, startPos, varLen, priorVariables); change = change + (varLen - (endPos - startPos)); pos += change; bufEnd += change; lengthChange += change; chars = getChars(buf); // in case buffer was altered } 

于是很明显了,在继续递归之后,在解析::-j的时候也会解析出varName:,而priorVariables里已经有:了,就会抛出无限递归的错误。

多次请求后,会导致 log4j 日志记录进程崩溃,无法继续日志记录。

漏洞修复

在 2.17.0 版本中对该漏洞进行了修复,diff 如下:

跟一下isRecursiveEvaluationAllowed方法。

默认为false,即不会再进行递归解析。

时间线

2021-12-20 360CERT 发布预警通告

2021-12-22 360CERT 发布分析报告

参考链接

1、 CVE-2021-45105:Apache Log4j 拒绝服务漏洞通告

https://cert.360.cn/warning/detail?id=4e4427335861d0c101b5311c79f87217

2、 https://logging.apache.org/log4j/2.x/security.html

原文链接:https://www.oschina.net/news/175042
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章