一个称得上优秀的框架,必备的要素之一可以通过某种约定的格式读取到所运行环境中的配置信息。本文中我们就来感受下log4j2实现此项功能时的精妙设计。
-1. 关于jndi漏洞(20211212)
只能说世事无常,2018年的时候本着"多了解一些让心理有底"把log4j2的源码大体翻了一遍,没想到都2021年末了还能趁上热度。真是世事无常,谁都不知道那块云彩有雨。
下面就将笔者所了解到的这个问题相关信息做个总结,让找到这里的看官少跑点路。
-1.1 漏洞说明
本次漏洞是因为Apache Log4j2组件中 lookup扩展的实现类 JndiLookup
的设计缺陷导致,而这个类存在于 log4j-core-xxx.jar中。
如果你的项目中没有直接或间接引用log4j-core,而只是引入了log4j-api,那本次漏洞和你是没有关系的。关于这一点的证明,Apache Log4j2官方也已经给出了说明:
-1.2 解决方案:
上图中其实官方已经给出了相应的解决方案:
版本号 | 解决方案 |
---|---|
2.10 - 2.14之间的版本 | 设置 系统属性log4j2.formatMsgNoLookups 或环境变量LOG4J_FORMAT_MSG_NO_LOOKUPS |
2.7 - 2.14.1之间的版本 | 设置log4j2.xml文件中的 %m 为 %m{nolookups} |
2.10以下的版本 | 直接干掉org/apache/logging/log4j/core/lookup/JndiLookup.class 这个类 |
-1.3 漏洞检验
相关原理性链接这里就不给出了,这里只给出一个BurpLog4j2Scan - 黑盒测试的链接。
-1.4 后记
其实对这种安全性事件,笔者个人主张是尽量少参与,等事态过去了再复盘啥的。但现实是漏洞刚爆出来的时候还好,公布出来的信息都比较克制,比如会将导致漏洞的关键字符进行打码,但半天不到事态就变得不可收拾了,流量的狂欢开始,并愈演愈烈。
事态平息后,再写一篇相关的复盘总结吧。
1. 概述
“ Lookups provide a way to add values to the Log4j configuration at arbitrary places. They are a particular type of Plugin that implements the StrLookup
interface. ”
以上内容复制于log4j2的官方文档lookup - Office Site。其清晰地说明了lookup的主要功能就是提供另外一种方式以添加某些特殊的值到日志中,以最大化松散耦合地提供可配置属性供使用者以约定的格式进行调用。
2. 配置示例
以下列举了两个主要使用的位置;当然不仅仅如此,log4j2允许你在任何需要的地方使用约定格式来获取环境中的指定配置信息。
<properties>
<!-- 之后我们就可以以 ${logPath}来引用该属性值 -->
<property name="logPath">${sys:catalina.home}/xmlogs</property>
</properties>
<!-- 这里的${hostName} 是由log4j2默认提供的, 其值为程序所在的服务器的主机名 -->
<!-- 至于${thread:threadName}, 将是本次我们所提供一个自定义lookup示例 -->
<PatternLayout pattern="[${hostName}];[${thread:threadName}];[%X{user}];[$${ctx:user}];[$${date:YYYY-MM/dd}]" />
3. 继承链
在开始构建自定义lookup逻辑前,我们先来看看log4j2已经为我们提供了的各类lookup实现;这样既可以避免作些无用功,也能在我们的自定义实现中最大化地复用现有代码,站在巨人的肩膀上。
由以上类层次结构图可以看出
- log4j2提供不下十种获取所运行环境配置信息的方式,基本能满足实际运行环境中获取各类配置信息的需求。
- 我们在自定义lookup时,可以根据自身需求自由选择继承自
StrLookup
,AbstractLookup
,AbstractConfigurationAwareLookup
等等来简化我们的代码。
以上默认提供的各类lookup,其取值来源看官可以通过下面给出的引用链接中的第二个进行详细的了解,我就不再在这里赘述一遍了。
4. 自定义lookup
下面我们将自定义一个lookup,以获取记录日志的线程名。
// 从这里的注解可以看出,lookup属于一类特殊的plugin
@Plugin(name = "thread", category = StrLookup.CATEGORY)
public class ThreadLookup implements StrLookup {
@Override
public String lookup(String key) {
return Thread.currentThread().getName();
}
@Override
public String lookup(LogEvent event, String key) {
return event.getThreadName() == null ? Thread.currentThread().getName() : event
.getThreadName();
}
}
可以看到自定义lookup的操作非常简单,log4j2的设计精妙由此也可见一斑。接下来我们就需要进行一些配置工作,让log4j2知道如何使用该lookup。
<!--使用packages属性告知log4j2我们自定义的plugin所在的package; 注意多个package是以 , 进行分割 -->
<Configuration status="TRACE" monitorInterval="5"
packages="com.kanq.extend.cat.log4j2,slf4j._log4j2.classes.core.lookup">
<!--调用方式就是多了一个我们在定义plugin时声明的名称作为前缀 -->
<PatternLayout pattern="${thread:threadName}" />
</Configuration>
最后附上一张执行堆栈图,还是比较清晰的
5. 补充
接下来我们来探索一些稍微深入的内容,以及一些细节性的内容。
- 作为lookup对外门面的
Interpolator
是通过 log4j2中负责解析<properties>
节点的PropertiesPlugin
类来并入执行流程中的。具体源码可以参见PropertiesPlugin.configureSubstitutor
方法。其中注意的是,我们在<properties>
中提供的属性是以default的优先级提供给外界的。 - 作为lookup对外门面的
Interpolator
,在其构造函数中载入了所有category值为StrLookup.CATEGORY
的plugin【即包括log4j2内置的(“org.apache.logging.log4j.core” package下的),也包括用户自定义的(log4j2.xml文件中的Configuration.packages
属性值指示的package下的)】。 Interpolator
可以单独使用,但某些值可能取不到。- 获取MDC中的内容,log4j2提供了两种方式:
$${ctx:user}
或%X{user}
。
6. Links
- lookup - Office Site
- Property Substitution - Configuration - Office Site
- custom lookup – 千万注意定义plugin时的契约,这里的教训是plugin的命名要求全小写,千万别习惯成自然地弄成驼峰命名。